feat: integrate the create-wdio package (#14618)
* chore: create sub-package of `create-wdio` * docs: copy and adjustment for changing repository * chore: copy a code of create-wdio * chore: update compiler plugin * chore: adjust npm script * chore(wdio-cli): delete the code copied to `create-wdio` * chore: remove compiler plugin for @wdio/cli * docs: update root readme * chore: update package.json * style: apply format rule * fix: remove unnecessary export `installPackages` * test: remove unnecessary mocking in wdio-cli --------- Co-authored-by: Christian Bromann <git@bromann.dev>
This commit is contained in:
parent
6464dc7d4a
commit
3f1e7ef93a
98 changed files with 4646 additions and 2702 deletions
|
|
@ -134,6 +134,7 @@ __Did you build a WebdriverIO service or reporter?__ That's awesome! Please add
|
|||
|
||||
### Others
|
||||
|
||||
- [create-wdio](https://github.com/webdriverio/webdriverio/tree/main/packages/create-wdio) - A CLI and utility to install and setup a WebdriverIO
|
||||
- [eslint-plugin-wdio](https://github.com/webdriverio/webdriverio/tree/main/packages/eslint-plugin-wdio) - Eslint rules for WebdriverIO
|
||||
- [@wdio/smoke-test-reporter](https://github.com/webdriverio/webdriverio/tree/main/packages/wdio-smoke-test-reporter) - A WebdriverIO utility to smoke test reporters for internal testing purposes
|
||||
- [@wdio/smoke-test-service](https://github.com/webdriverio/webdriverio/tree/main/packages/wdio-smoke-test-service) - A WebdriverIO utility to smoke test services for internal testing purposes
|
||||
|
|
|
|||
19
__mocks__/commander.ts
Normal file
19
__mocks__/commander.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { vi } from 'vitest'
|
||||
import * as path from 'node:path'
|
||||
|
||||
const command: Record<string, unknown> = {}
|
||||
command.name = vi.fn().mockReturnValue(command)
|
||||
command.version = vi.fn().mockReturnValue(command)
|
||||
command.usage = vi.fn().mockReturnValue(command)
|
||||
command.arguments = vi.fn().mockReturnValue(command)
|
||||
command.action = vi.fn((cb) => {
|
||||
cb(`${path.sep}foo${path.sep}bar${path.sep}someProjectName`)
|
||||
return command
|
||||
})
|
||||
command.option = vi.fn().mockReturnValue(command)
|
||||
command.allowUnknownOption = vi.fn().mockReturnValue(command)
|
||||
command.on = vi.fn().mockReturnValue(command)
|
||||
command.parse = vi.fn().mockReturnValue(command)
|
||||
command.opts = vi.fn().mockReturnValue('foobar')
|
||||
|
||||
export const Command = vi.fn().mockReturnValue(command)
|
||||
|
|
@ -60,7 +60,7 @@ const packages = (
|
|||
*/
|
||||
const esmPlugins: Record<string, Plugin[]> = {
|
||||
'@wdio/protocols': [generateTypes()],
|
||||
'@wdio/cli': [copyEJSTemplates()],
|
||||
'create-wdio': [copyEJSTemplates('create-wdio')],
|
||||
'webdriverio': [externalScripts()]
|
||||
}
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -109,11 +109,12 @@ export function generateDts(absWorkingDir: string, pkg: PackageJson): Plugin {
|
|||
}
|
||||
|
||||
/**
|
||||
* Plugin to copy EJS templates from the `@wdio/cli` package to build directory
|
||||
* Plugin to copy EJS templates from specified package to build directory
|
||||
* @param {string} pkgName Name of the package to copy
|
||||
* @returns {Plugin} an Esbuild plugin
|
||||
*/
|
||||
export function copyEJSTemplates() {
|
||||
const cliPackage = path.resolve(__dirname, '..', '..', '..', 'packages', 'wdio-cli')
|
||||
export function copyEJSTemplates(pkgName: string): Plugin {
|
||||
const cliPackage = path.resolve(__dirname, '..', '..', '..', 'packages', pkgName)
|
||||
return copy({
|
||||
resolveFrom: cliPackage,
|
||||
assets: [{
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@
|
|||
"test": "run-s test:eslint test:ejslint test:typings test:unit:run test:smoke test:component",
|
||||
"test:depcheck": "tsx ./scripts/depcheck.ts",
|
||||
"test:eslint": "eslint --cache packages examples scripts tests",
|
||||
"test:ejslint": "ejslint packages/wdio-cli/src/templates/**/*",
|
||||
"test:ejslint": "ejslint packages/wdio-cli/src/templates/**/* packages/create-wdio/src/templates/**/*",
|
||||
"test:interop": "pnpm -r --filter=@wdio/test-interop-cjs run test",
|
||||
"test:typings": "pnpm install && run-p test:typings:*",
|
||||
"test:typings:webdriver": "cd tests/typings/webdriver && npx tsc --skipLibCheck",
|
||||
|
|
|
|||
4
packages/create-wdio/.npmignore
Normal file
4
packages/create-wdio/.npmignore
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
src
|
||||
tests
|
||||
tsconfig.json
|
||||
tsconfig.prod.json
|
||||
20
packages/create-wdio/LICENSE
Normal file
20
packages/create-wdio/LICENSE
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
Copyright (c) OpenJS Foundation and other contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
69
packages/create-wdio/README.md
Normal file
69
packages/create-wdio/README.md
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
WebdriverIO Starter Toolkit [](https://github.com/webdriverio/webdriverio/blob/main/CONTRIBUTING.md)
|
||||
===========================
|
||||
|
||||
<img alt="Logo" align="right" src="https://webdriver.io/assets/images/robot-3677788dd63849c56aa5cb3f332b12d5.svg" width="20%" />
|
||||
|
||||
One command to create a fresh WebdriverIO project or add WebdriverIO to an existing project.
|
||||
|
||||
- [Get Started Guide](https://webdriver.io/docs/gettingstarted) - How to get started with WebdriverIO
|
||||
- [Supported Options](#supported-options) - command line parameters
|
||||
|
||||
`create-wdio` works on macOS, Windows, and Linux.<br>
|
||||
If something doesn’t work, please [file an issue](https://github.com/webdriverio/webdriverio/issues/new).<br>
|
||||
If you have questions or need help, please ask in our [Discord Support channel](https://discord.webdriver.io).
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/webdriverio/webdriverio/main/packages/create-wdio/assets/demo.gif" alt="Example" />
|
||||
</p>
|
||||
|
||||
## Usage
|
||||
|
||||
To install a WebdriverIO project, you may choose one of the following methods:
|
||||
|
||||
#### npx
|
||||
|
||||
```sh
|
||||
npx create-wdio@latest ./e2e
|
||||
```
|
||||
|
||||
_[`npx`](https://medium.com/@maybekatz/introducing-npx-an-npm-package-runner-55f7d4bd282b) is a package runner tool that comes with npm 5.2+ and higher, see [instructions for older npm versions](https://gist.github.com/gaearon/4064d3c23a77c74a3614c498a8bb1c5f)_
|
||||
|
||||
#### npm
|
||||
|
||||
```sh
|
||||
npm init wdio@latest ./e2e
|
||||
```
|
||||
|
||||
_[`npm init <initializer>`](https://docs.npmjs.com/cli/v10/commands/npm-init) is available in npm 6+_
|
||||
|
||||
#### yarn
|
||||
|
||||
```sh
|
||||
yarn create wdio@latest ./e2e
|
||||
```
|
||||
|
||||
_[`yarn create <starter-kit-package>`](https://yarnpkg.com/lang/en/docs/cli/create/) is available in Yarn 0.25+_
|
||||
|
||||
#### pnpm
|
||||
|
||||
```sh
|
||||
pnpm create wdio ./e2e
|
||||
```
|
||||
|
||||
_[`pnpm create <starter-kit-package>`](https://pnpm.io/cli/create) is available in pnpm v7+_
|
||||
|
||||
It will create a directory called `e2e` inside the current folder.
|
||||
Then it will run the configuration wizard that will help you set-up your framework.
|
||||
|
||||
|
||||
## Supported Options
|
||||
|
||||
You can pass the following command line flags to modify the bootstrap mechanism:
|
||||
|
||||
* `--dev` - Install all packages as `devDependencies` (default: `true`)
|
||||
* `--yes` - Will fill in all config defaults without prompting (default: `false`)
|
||||
* `--npm-tag` - use a specific NPM tag for `@wdio/cli` package (default: `latest`)
|
||||
|
||||
----
|
||||
|
||||
For more information on WebdriverIO see the [homepage](https://webdriver.io).
|
||||
BIN
packages/create-wdio/assets/demo.gif
Normal file
BIN
packages/create-wdio/assets/demo.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1,005 KiB |
9
packages/create-wdio/bin/wdio.js
Executable file
9
packages/create-wdio/bin/wdio.js
Executable file
|
|
@ -0,0 +1,9 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
import { run } from '../build/index.js'
|
||||
|
||||
run()
|
||||
80
packages/create-wdio/package.json
Normal file
80
packages/create-wdio/package.json
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
{
|
||||
"name": "create-wdio",
|
||||
"version": "9.0.0",
|
||||
"description": "Install and setup a WebdriverIO project with all its dependencies in a single run",
|
||||
"author": "WebdriverIO Team <mail@webdriver.io>",
|
||||
"homepage": "https://github.com/webdriverio/webdriverio/tree/main/packages/create-wdio",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./build/index.d.ts",
|
||||
"import": "./build/index.js",
|
||||
"importSource": "./src/index.ts"
|
||||
},
|
||||
"./config/cli": {
|
||||
"types": "./build/config/cli.d.ts",
|
||||
"import": "./build/config/cli.js",
|
||||
"importSource": "./src/config/cli.ts"
|
||||
},
|
||||
"./install/cli": {
|
||||
"types": "./build/install/cli.d.ts",
|
||||
"import": "./build/install/cli.js",
|
||||
"importSource": "./src/install/cli.ts"
|
||||
},
|
||||
"./utils": {
|
||||
"types": "./build/utils/index.d.ts",
|
||||
"import": "./build/utils/index.js",
|
||||
"importSource": "./src/utils/index.ts"
|
||||
}
|
||||
},
|
||||
"types": "./build/index.d.ts",
|
||||
"typeScriptVersion": "3.8.3",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/webdriverio/webdriverio.git",
|
||||
"directory": "packages/create-wdio"
|
||||
},
|
||||
"keywords": [
|
||||
"webdriverio",
|
||||
"create-wdio",
|
||||
"wdio",
|
||||
"installer",
|
||||
"e2e"
|
||||
],
|
||||
"bugs": {
|
||||
"url": "https://github.com/webdriverio/webdriverio/issues"
|
||||
},
|
||||
"bin": {
|
||||
"create-wdio": "./bin/wdio.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^14.0.0",
|
||||
"cross-spawn": "^7.0.3",
|
||||
"ejs": "^3.1.10",
|
||||
"execa": "^9.6.0",
|
||||
"import-meta-resolve": "^4.1.0",
|
||||
"inquirer": "^12.7.0",
|
||||
"normalize-package-data": "^7.0.0",
|
||||
"read-pkg-up": "^10.1.0",
|
||||
"recursive-readdir": "^2.2.3",
|
||||
"semver": "^7.6.3",
|
||||
"type-fest": "^4.41.0",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cross-spawn": "^6.0.6",
|
||||
"@types/ejs": "^3.1.5",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/normalize-package-data": "^2.4.4",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@types/yargs": "^17.0.33"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
61
packages/create-wdio/src/cli/config.ts
Normal file
61
packages/create-wdio/src/cli/config.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import fss from 'node:fs'
|
||||
import type { Argv } from 'yargs'
|
||||
|
||||
import { CLI_EPILOGUE
|
||||
} from '../constants.js'
|
||||
import {
|
||||
parseAnswers,
|
||||
runConfigCommand,
|
||||
} from './utils.js'
|
||||
import type { ConfigCommandArguments } from '../types.js'
|
||||
|
||||
let hasYarnLock = false
|
||||
try {
|
||||
fss.accessSync('yarn.lock')
|
||||
hasYarnLock = true
|
||||
} catch {
|
||||
hasYarnLock = false
|
||||
}
|
||||
|
||||
export const command = 'config'
|
||||
export const desc = 'Initialize WebdriverIO and setup configuration in your current project.'
|
||||
|
||||
export const cmdArgs = {
|
||||
yarn: {
|
||||
type: 'boolean',
|
||||
desc: 'Install packages via Yarn package manager.',
|
||||
default: hasYarnLock
|
||||
},
|
||||
yes: {
|
||||
alias: 'y',
|
||||
desc: 'will fill in all config defaults without prompting',
|
||||
type: 'boolean',
|
||||
default: false
|
||||
},
|
||||
npmTag: {
|
||||
alias: 't',
|
||||
desc: 'define NPM tag to use for WebdriverIO related packages',
|
||||
type: 'string',
|
||||
default: 'latest'
|
||||
}
|
||||
} as const
|
||||
|
||||
export const builder = (yargs: Argv) => {
|
||||
return yargs
|
||||
.options(cmdArgs)
|
||||
.epilogue(CLI_EPILOGUE)
|
||||
.help()
|
||||
}
|
||||
|
||||
export async function handler(argv: ConfigCommandArguments, runConfigCmd = runConfigCommand) {
|
||||
const parsedAnswers = await parseAnswers(argv.yes)
|
||||
await runConfigCmd(parsedAnswers, argv.npmTag)
|
||||
return {
|
||||
success: true,
|
||||
parsedAnswers,
|
||||
installedPackages: parsedAnswers.packagesToInstall.map((pkg) => pkg.split('--')[0])
|
||||
}
|
||||
}
|
||||
|
||||
export { missingConfigurationPrompt, canAccessConfigPath } from './utils.js'
|
||||
export { formatConfigFilePaths } from '../utils.js'
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
|
||||
|
|
@ -10,12 +9,14 @@ import {
|
|||
findInConfig,
|
||||
addServiceDeps,
|
||||
convertPackageHashToObject,
|
||||
formatConfigFilePaths,
|
||||
detectPackageManager
|
||||
} from '../utils.js'
|
||||
import { installPackages } from '../install.js'
|
||||
import { formatConfigFilePaths, canAccessConfigPath, missingConfigurationPrompt } from './config.js'
|
||||
import { missingConfigurationPrompt } from './config.js'
|
||||
import { SUPPORTED_PACKAGES, CLI_EPILOGUE } from '../constants.js'
|
||||
import type { InstallCommandArguments, SupportedPackage } from '../types.js'
|
||||
import { canAccessConfigPath } from './utils.js'
|
||||
|
||||
const supportedInstallations = {
|
||||
runner: SUPPORTED_PACKAGES.runner.map(({ value }) => convertPackageHashToObject(value)),
|
||||
|
|
@ -65,8 +66,10 @@ export async function handler(argv: InstallCommandArguments) {
|
|||
*/
|
||||
if (!Object.keys(supportedInstallations).includes(type)) {
|
||||
console.log(`Type ${type} is not supported.`)
|
||||
if (process.env.WDIO_UNIT_TESTS) {
|
||||
return
|
||||
}
|
||||
process.exit(0)
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -79,9 +82,10 @@ export async function handler(argv: InstallCommandArguments) {
|
|||
`Available options for a ${type} are:\n` +
|
||||
`- ${options.join('\n- ')}`
|
||||
)
|
||||
if (process.env.WDIO_UNIT_TESTS) {
|
||||
return
|
||||
}
|
||||
process.exit(0)
|
||||
// keep return for unit test purposes
|
||||
return
|
||||
}
|
||||
|
||||
const defaultPath = path.resolve(process.cwd(), 'wdio.conf')
|
||||
|
|
@ -95,8 +99,6 @@ export async function handler(argv: InstallCommandArguments) {
|
|||
return handler(argv)
|
||||
} catch {
|
||||
process.exit(1)
|
||||
// keep return for unit test purposes
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -107,8 +109,6 @@ export async function handler(argv: InstallCommandArguments) {
|
|||
if (match && match[0].includes(name)) {
|
||||
console.log(`The ${type} ${name} is already part of your configuration.`)
|
||||
process.exit(0)
|
||||
// keep return for unit test purposes
|
||||
return
|
||||
}
|
||||
|
||||
const selectedPackage = supportedInstallations[type].find(({ short }) => short === name) as SupportedPackage
|
||||
|
|
@ -122,8 +122,6 @@ export async function handler(argv: InstallCommandArguments) {
|
|||
|
||||
if (!success) {
|
||||
process.exit(1)
|
||||
// keep return for unit test purposes
|
||||
return
|
||||
}
|
||||
|
||||
console.log(`Package "${selectedPackage.package}" installed successfully.`)
|
||||
|
|
@ -1,57 +1,70 @@
|
|||
import fs from 'node:fs/promises'
|
||||
import fss from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import fs from 'node:fs/promises'
|
||||
import inquirer from 'inquirer'
|
||||
import type { Argv } from 'yargs'
|
||||
import { configHelperSuccessMessage, CONFIG_HELPER_SERENITY_BANNER, SUPPORTED_CONFIG_FILE_EXTENSION, CONFIG_HELPER_INTRO, isNuxtProject, SUPPORTED_PACKAGES } from '../constants.js'
|
||||
import type { ParsedAnswers } from '../types.js'
|
||||
import { createPackageJSON, setupTypeScript, npmInstall, createWDIOConfig, createWDIOScript, runAppiumInstaller, convertPackageHashToObject, getAnswers, getPathForFileGeneration, getProjectProps, getProjectRoot, getSerenityPackages } from '../utils.js'
|
||||
|
||||
import {
|
||||
CONFIG_HELPER_INTRO, CLI_EPILOGUE, SUPPORTED_PACKAGES,
|
||||
configHelperSuccessMessage, isNuxtProject, SUPPORTED_CONFIG_FILE_EXTENSION, CONFIG_HELPER_SERENITY_BANNER,
|
||||
} from '../constants.js'
|
||||
import {
|
||||
convertPackageHashToObject, getAnswers, getPathForFileGeneration, getProjectProps,
|
||||
getProjectRoot, createPackageJSON, setupTypeScript, npmInstall,
|
||||
createWDIOConfig, createWDIOScript, runAppiumInstaller, getSerenityPackages
|
||||
} from '../utils.js'
|
||||
import type { ConfigCommandArguments, ParsedAnswers } from '../types.js'
|
||||
export async function runConfigCommand(parsedAnswers: ParsedAnswers, npmTag: string) {
|
||||
console.log('\n')
|
||||
|
||||
let hasYarnLock = false
|
||||
try {
|
||||
fss.accessSync('yarn.lock')
|
||||
hasYarnLock = true
|
||||
} catch {
|
||||
hasYarnLock = false
|
||||
await createPackageJSON(parsedAnswers)
|
||||
await setupTypeScript(parsedAnswers)
|
||||
await npmInstall(parsedAnswers, npmTag)
|
||||
await createWDIOConfig(parsedAnswers)
|
||||
await createWDIOScript(parsedAnswers)
|
||||
|
||||
/**
|
||||
* print success message
|
||||
*/
|
||||
console.log(
|
||||
configHelperSuccessMessage({
|
||||
projectRootDir: parsedAnswers.projectRootDir,
|
||||
runScript: parsedAnswers.serenityAdapter ? 'serenity' : 'wdio',
|
||||
extraInfo: parsedAnswers.serenityAdapter ? CONFIG_HELPER_SERENITY_BANNER : ''
|
||||
}),
|
||||
)
|
||||
|
||||
await runAppiumInstaller(parsedAnswers)
|
||||
}
|
||||
|
||||
export const command = 'config'
|
||||
export const desc = 'Initialize WebdriverIO and setup configuration in your current project.'
|
||||
/**
|
||||
* Helper utility used in `run` and `install` command to create config if none exist
|
||||
* @param {string} command to be executed by user
|
||||
* @param {string} configPath the path to a wdio.conf.[js/ts] file
|
||||
* @param {Function} runConfigCmd runConfig method to be replaceable for unit testing
|
||||
*/
|
||||
export async function missingConfigurationPrompt(command: string, configPath: string, runConfigCmd: Function = runConfigCommand) {
|
||||
const message = (
|
||||
`Could not execute "${command}" due to missing configuration, file ` +
|
||||
`"${path.parse(configPath).name}[.js/.ts]" not found! ` +
|
||||
'Would you like to create one?'
|
||||
)
|
||||
|
||||
export const cmdArgs = {
|
||||
yarn: {
|
||||
type: 'boolean',
|
||||
desc: 'Install packages via Yarn package manager.',
|
||||
default: hasYarnLock
|
||||
},
|
||||
yes: {
|
||||
alias: 'y',
|
||||
desc: 'will fill in all config defaults without prompting',
|
||||
type: 'boolean',
|
||||
const { config } = await inquirer.prompt({
|
||||
type: 'confirm',
|
||||
name: 'config',
|
||||
message,
|
||||
default: false
|
||||
},
|
||||
npmTag: {
|
||||
alias: 't',
|
||||
desc: 'define NPM tag to use for WebdriverIO related packages',
|
||||
type: 'string',
|
||||
default: 'latest'
|
||||
}
|
||||
} as const
|
||||
})
|
||||
|
||||
export const builder = (yargs: Argv) => {
|
||||
return yargs
|
||||
.options(cmdArgs)
|
||||
.epilogue(CLI_EPILOGUE)
|
||||
.help()
|
||||
/**
|
||||
* don't exit if running unit tests
|
||||
*/
|
||||
if (!config) {
|
||||
/* istanbul ignore next */
|
||||
console.log(`No WebdriverIO configuration found in "${process.cwd()}"`)
|
||||
|
||||
/* istanbul ignore next */
|
||||
if (!process.env.WDIO_UNIT_TESTS) {
|
||||
process.exit(0)
|
||||
}
|
||||
return configPath
|
||||
}
|
||||
|
||||
const parsedAnswers = await parseAnswers(false)
|
||||
await runConfigCmd(parsedAnswers, 'latest')
|
||||
return configPath
|
||||
}
|
||||
|
||||
export const parseAnswers = async function (yes: boolean): Promise<ParsedAnswers> {
|
||||
|
|
@ -145,59 +158,13 @@ export const parseAnswers = async function (yes: boolean): Promise<ParsedAnswers
|
|||
}
|
||||
}
|
||||
|
||||
export async function runConfigCommand(parsedAnswers: ParsedAnswers, npmTag: string) {
|
||||
console.log('\n')
|
||||
|
||||
await createPackageJSON(parsedAnswers)
|
||||
await setupTypeScript(parsedAnswers)
|
||||
await npmInstall(parsedAnswers, npmTag)
|
||||
await createWDIOConfig(parsedAnswers)
|
||||
await createWDIOScript(parsedAnswers)
|
||||
|
||||
/**
|
||||
* print success message
|
||||
*/
|
||||
console.log(
|
||||
configHelperSuccessMessage({
|
||||
projectRootDir: parsedAnswers.projectRootDir,
|
||||
runScript: parsedAnswers.serenityAdapter ? 'serenity' : 'wdio',
|
||||
extraInfo: parsedAnswers.serenityAdapter ? CONFIG_HELPER_SERENITY_BANNER : ''
|
||||
}),
|
||||
)
|
||||
|
||||
await runAppiumInstaller(parsedAnswers)
|
||||
}
|
||||
|
||||
export async function handler(argv: ConfigCommandArguments, runConfigCmd = runConfigCommand) {
|
||||
const parsedAnswers = await parseAnswers(argv.yes)
|
||||
await runConfigCmd(parsedAnswers, argv.npmTag)
|
||||
return {
|
||||
success: true,
|
||||
parsedAnswers,
|
||||
installedPackages: parsedAnswers.packagesToInstall.map((pkg) => pkg.split('--')[0])
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper utility used in `run` and `install` command to format a provided config path,
|
||||
* giving it back as an absolute path, and a version without the file extension
|
||||
* @param config the initially given file path to the WDIO config file
|
||||
*/
|
||||
export async function formatConfigFilePaths(config: string) {
|
||||
const fullPath = path.isAbsolute(config)
|
||||
? config
|
||||
: path.join(process.cwd(), config)
|
||||
const fullPathNoExtension = fullPath.substring(0, fullPath.lastIndexOf(path.extname(fullPath)))
|
||||
return { fullPath, fullPathNoExtension }
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper utility used in `run` and `install` command to check whether a config file currently exists
|
||||
* @param configPathNoExtension the file path to the WDIO config file without extension
|
||||
* @param configPath the file path to the WDIO config file that is checked first if set
|
||||
* @returns {string} the path to the config file that exists, otherwise undefined
|
||||
*/
|
||||
export async function canAccessConfigPath(configPathNoExtension:string, configPath?:string) {
|
||||
export async function canAccessConfigPath(configPathNoExtension:string, configPath?:string): Promise<string|undefined> {
|
||||
return new Promise<string | undefined>((resolve, reject)=>{
|
||||
if (configPath) {
|
||||
fs.access(configPath).then(()=>resolve(configPath), reject)
|
||||
|
|
@ -213,42 +180,3 @@ export async function canAccessConfigPath(configPathNoExtension:string, configPa
|
|||
))
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper utility used in `run` and `install` command to create config if none exist
|
||||
* @param {string} command to be executed by user
|
||||
* @param {string} configPath the path to a wdio.conf.[js/ts] file
|
||||
* @param {boolean} useYarn parameter set to true if yarn is used
|
||||
* @param {Function} runConfigCmd runConfig method to be replaceable for unit testing
|
||||
*/
|
||||
export async function missingConfigurationPrompt(command: string, configPath: string, runConfigCmd = runConfigCommand) {
|
||||
const message = (
|
||||
`Could not execute "${command}" due to missing configuration, file ` +
|
||||
`"${path.parse(configPath).name}[.js/.ts]" not found! ` +
|
||||
'Would you like to create one?'
|
||||
)
|
||||
|
||||
const { config } = await inquirer.prompt({
|
||||
type: 'confirm',
|
||||
name: 'config',
|
||||
message,
|
||||
default: false
|
||||
})
|
||||
|
||||
/**
|
||||
* don't exit if running unit tests
|
||||
*/
|
||||
if (!config) {
|
||||
/* istanbul ignore next */
|
||||
console.log(`No WebdriverIO configuration found in "${process.cwd()}"`)
|
||||
|
||||
/* istanbul ignore next */
|
||||
if (!process.env.WDIO_UNIT_TESTS) {
|
||||
process.exit(0)
|
||||
}
|
||||
return configPath
|
||||
}
|
||||
|
||||
const parsedAnswers = await parseAnswers(false)
|
||||
await runConfigCmd(parsedAnswers, 'latest')
|
||||
return configPath
|
||||
}
|
||||
1
packages/create-wdio/src/config/cli.ts
Normal file
1
packages/create-wdio/src/config/cli.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * as config from '../cli/config.js'
|
||||
737
packages/create-wdio/src/constants.ts
Normal file
737
packages/create-wdio/src/constants.ts
Normal file
|
|
@ -0,0 +1,737 @@
|
|||
import { convertPackageHashToObject, detectPackageManager, getDefaultFiles, detectCompiler, getProjectRoot } from './utils.js'
|
||||
import pkg from '../package.json' with { type:'json' }
|
||||
import path from 'node:path'
|
||||
import fs from 'node:fs'
|
||||
import type { Questionnair } from './types.js'
|
||||
import chalk from 'chalk'
|
||||
|
||||
export { pkg }
|
||||
|
||||
export const colorItBold = chalk.bold.rgb(234, 89, 6)
|
||||
export const colorIt = chalk.rgb(234, 89, 6)
|
||||
export type PACKAGE_MANAGER = 'npm' | 'pnpm' | 'yarn' | 'bun'
|
||||
|
||||
export const DEFAULT_NPM_TAG = 'latest'
|
||||
export const ASCII_ROBOT = `
|
||||
-:...........................-:.
|
||||
+ +
|
||||
\`\` + \`...\` \`...\` + \`
|
||||
./+/ + .:://:::\` \`::///::\` \` + ++/.
|
||||
.+oo+ + /:+ooo+-/ /-+ooo+-/ ./ + +oo+.
|
||||
-ooo+ + /-+ooo+-/ /-+ooo+-/ .: + +ooo.
|
||||
-+o+ + \`::///:-\` \`::///::\` + +o+-
|
||||
\`\`. /. \`\`\`\`\` \`\`\`\`\` .: .\`\`
|
||||
.----------------------------.
|
||||
\`-::::::::::::::::::::::::::::::::::::::::-\`
|
||||
.+oooo/:------------------------------:/oooo+.
|
||||
\`.--/oooo- :oooo/--.\`
|
||||
.::-\`\`:oooo\` .oooo-\`\`-::.
|
||||
./-\` -oooo\`--.: :.-- .oooo- \`-/.
|
||||
-/\` \`-/oooo////////////////////////////////////oooo/.\` \`/-
|
||||
\`+\` \`/+oooooooooooooooooooooooooooooooooooooooooooooooo+:\` .+\`
|
||||
-/ +o/.:oooooooooooooooooooooooooooooooooooooooooooo:-/o/ +.
|
||||
-/ .o+ -oooosoooososssssooooo------------------:oooo- \`oo\` +.
|
||||
-/ .o+ -oooodooohyyssosshoooo\` .oooo- oo. +.
|
||||
-/ .o+ -oooodooysdooooooyyooo\` \`.--.\`\` .:::-oooo- oo. +.
|
||||
-/ .o+ -oooodoyyodsoooooyyooo.//-..-:/:.\`.//.\`./oooo- oo. +.
|
||||
-/ .o+ -oooohsyoooyysssysoooo+-\` \`-:::. .oooo- oo. +.
|
||||
-/ .o+ -ooooosooooooosooooooo+//////////////////oooo- oo. +.
|
||||
-/ .o+ -oooooooooooooooooooooooooooooooooooooooooooo- oo. +.
|
||||
-/ .o+ -oooooooooooooooooooooooooooooooooooooooooooo- oo. +.
|
||||
-+////o+\` -oooo---:///:----://::------------------:oooo- \`oo////+-
|
||||
+ooooooo/\`-oooo\`\`:-\`\`\`.:\`.:.\`.+/- .::::::::::\` .oooo-\`+ooooooo+
|
||||
oooooooo+\`-oooo\`-- \`/\` .:+ -/-\`/\` .:::::::::: .oooo-.+oooooooo
|
||||
+-/+://-/ -oooo-\`:\`.o-\`:.:-\`\`\`\`.: .///:\`\`\`\`\`\` -oooo-\`/-//:+:-+
|
||||
: :..--:-:.+ooo+/://o+/-.-:////:-....-::::-....--/+ooo+.:.:--.-- /
|
||||
- /./\`-:-\` .:///+/ooooo/+///////////////+++ooooo/+///:. .-:.\`+./ :
|
||||
:-:/. :\`ooooo\`/\` .:.ooooo : ./---
|
||||
:\`ooooo\`/\` .:.ooooo :
|
||||
:\`ooooo./\` .:-ooooo :
|
||||
:\`ooooo./\` .:-ooooo :
|
||||
\`...:-+++++:/. ./:+++++-:...\`
|
||||
:-.\`\`\`\`\`\`\`\`/../ /.-:\`\`\`\`\`\`\`\`.:-
|
||||
-/::::::::://:/+ \`+/:+::::::::::+.
|
||||
:oooooooooooo++/ +++oooooooooooo-
|
||||
`
|
||||
|
||||
export const PROGRAM_TITLE = `
|
||||
${colorItBold('Webdriver.IO')}
|
||||
${colorIt('Next-gen browser and mobile automation')}
|
||||
${colorIt('test framework for Node.js')}
|
||||
`
|
||||
|
||||
export const UNSUPPORTED_NODE_VERSION = (
|
||||
'⚠️ Unsupported Node.js Version Error ⚠️\n' +
|
||||
`You are using Node.js ${process.version} which is too old to be used with WebdriverIO.\n` +
|
||||
'Please update to Node.js v20 to continue.\n'
|
||||
)
|
||||
|
||||
export const INSTALL_COMMAND: Record<PACKAGE_MANAGER, string> = {
|
||||
npm: 'install',
|
||||
pnpm: 'add',
|
||||
yarn: 'add',
|
||||
bun: 'install'
|
||||
} as const
|
||||
|
||||
export const EXECUTER: Record<PACKAGE_MANAGER, string> = {
|
||||
npm: 'npx',
|
||||
pnpm: 'pnpm',
|
||||
yarn: 'yarn',
|
||||
bun: 'bunx'
|
||||
} as const
|
||||
|
||||
export const EXECUTE_COMMAND: Record<PACKAGE_MANAGER, string> = {
|
||||
npm: '',
|
||||
pnpm: 'exec',
|
||||
yarn: 'exec',
|
||||
bun: ''
|
||||
} as const
|
||||
|
||||
export const DEV_FLAG: Record<PACKAGE_MANAGER, string> = {
|
||||
npm: '--save-dev',
|
||||
pnpm: '--save-dev',
|
||||
yarn: '--dev',
|
||||
bun: '--dev'
|
||||
} as const
|
||||
|
||||
export const SUPPORTED_PACKAGE_MANAGERS = Object.keys(INSTALL_COMMAND) as PACKAGE_MANAGER[]
|
||||
// transport from
|
||||
export const CONFIG_HELPER_INTRO = `
|
||||
===============================
|
||||
🤖 WDIO Configuration Wizard 🧙
|
||||
===============================
|
||||
`
|
||||
|
||||
export const CLI_EPILOGUE = `Documentation: https://webdriver.io\n@wdio/cli (v${pkg.version})`
|
||||
|
||||
/**
|
||||
* We have to use a string hash for value because InquirerJS default values do not work if we have
|
||||
* objects as a `value` to be stored from the user's answers.
|
||||
*/
|
||||
export const SUPPORTED_PACKAGES = {
|
||||
runner: [
|
||||
{ name: 'E2E Testing - of Web or Mobile Applications', value: '@wdio/local-runner$--$local$--$e2e' },
|
||||
{ name: 'Component or Unit Testing - in the browser\n > https://webdriver.io/docs/component-testing', value: '@wdio/browser-runner$--$browser$--$component' },
|
||||
{ name: 'Desktop Testing - of Electron Applications\n > https://webdriver.io/docs/desktop-testing/electron', value: '@wdio/local-runner$--$local$--$electron' },
|
||||
{ name: 'Desktop Testing - of MacOS Applications\n > https://webdriver.io/docs/desktop-testing/macos', value: '@wdio/local-runner$--$local$--$macos' },
|
||||
{ name: 'VS Code Extension Testing\n > https://webdriver.io/docs/vscode-extension-testing', value: '@wdio/local-runner$--$local$--$vscode' },
|
||||
{ name: 'Roku Testing - of OTT apps running on RokuOS\n > https://webdriver.io/docs/wdio-roku-service', value: '@wdio/local-runner$--$local$--$roku' }
|
||||
],
|
||||
framework: [
|
||||
{ name: 'Mocha (https://mochajs.org/)', value: '@wdio/mocha-framework$--$mocha' },
|
||||
{ name: 'Mocha with Serenity/JS (https://serenity-js.org/)', value: '@serenity-js/webdriverio$--$@serenity-js/webdriverio$--$mocha' },
|
||||
{ name: 'Jasmine (https://jasmine.github.io/)', value: '@wdio/jasmine-framework$--$jasmine' },
|
||||
{ name: 'Jasmine with Serenity/JS (https://serenity-js.org/)', value: '@serenity-js/webdriverio$--$@serenity-js/webdriverio$--$jasmine' },
|
||||
{ name: 'Cucumber (https://cucumber.io/)', value: '@wdio/cucumber-framework$--$cucumber' },
|
||||
{ name: 'Cucumber with Serenity/JS (https://serenity-js.org/)', value: '@serenity-js/webdriverio$--$@serenity-js/webdriverio$--$cucumber' },
|
||||
],
|
||||
reporter: [
|
||||
{ name: 'spec', value: '@wdio/spec-reporter$--$spec', checked: true },
|
||||
{ name: 'dot', value: '@wdio/dot-reporter$--$dot' },
|
||||
{ name: 'junit', value: '@wdio/junit-reporter$--$junit' },
|
||||
{ name: 'allure', value: '@wdio/allure-reporter$--$allure' },
|
||||
{ name: 'sumologic', value: '@wdio/sumologic-reporter$--$sumologic' },
|
||||
{ name: 'concise', value: '@wdio/concise-reporter$--$concise' },
|
||||
{ name: 'json', value: '@wdio/json-reporter$--$json' },
|
||||
// external
|
||||
{ name: 'reportportal', value: 'wdio-reportportal-reporter$--$reportportal' },
|
||||
{ name: 'video', value: 'wdio-video-reporter$--$video' },
|
||||
{ name: 'cucumber-json', value: 'wdio-cucumberjs-json-reporter$--$cucumberjs-json' },
|
||||
{ name: 'mochawesome', value: 'wdio-mochawesome-reporter$--$mochawesome' },
|
||||
{ name: 'timeline', value: 'wdio-timeline-reporter$--$timeline' },
|
||||
{ name: 'html-nice', value: 'wdio-html-nice-reporter$--$html-nice' },
|
||||
{ name: 'slack', value: '@moroo/wdio-slack-reporter$--$slack' },
|
||||
{ name: 'teamcity', value: 'wdio-teamcity-reporter$--$teamcity' },
|
||||
{ name: 'delta', value: '@delta-reporter/wdio-delta-reporter-service$--$delta' },
|
||||
{ name: 'testrail', value: '@wdio/testrail-reporter$--$testrail' },
|
||||
{ name: 'light', value: 'wdio-light-reporter$--$light' },
|
||||
{ name: 'wdio-json-html-reporter', value: 'wdio-json-html-reporter$--$jsonhtml' }
|
||||
],
|
||||
plugin: [
|
||||
{ name: 'wait-for: utilities that provide functionalities to wait for certain conditions till a defined task is complete.\n > https://www.npmjs.com/package/wdio-wait-for', value: 'wdio-wait-for$--$wait-for' },
|
||||
{ name: 'angular-component-harnesses: support for Angular component test harnesses\n > https://www.npmjs.com/package/@badisi/wdio-harness', value: '@badisi/wdio-harness$--$harness' },
|
||||
{ name: 'Testing Library: utilities that encourage good testing practices laid down by dom-testing-library.\n > https://testing-library.com/docs/webdriverio-testing-library/intro', value: '@testing-library/webdriverio$--$testing-library' }
|
||||
],
|
||||
service: [
|
||||
// internal or community driver services
|
||||
{ name: 'visual', value: '@wdio/visual-service$--$visual' },
|
||||
{ name: 'vite', value: 'wdio-vite-service$--$vite' },
|
||||
{ name: 'nuxt', value: 'wdio-nuxt-service$--$nuxt' },
|
||||
{ name: 'firefox-profile', value: '@wdio/firefox-profile-service$--$firefox-profile' },
|
||||
{ name: 'gmail', value: 'wdio-gmail-service$--$gmail' },
|
||||
{ name: 'sauce', value: '@wdio/sauce-service$--$sauce' },
|
||||
{ name: 'testingbot', value: '@wdio/testingbot-service$--$testingbot' },
|
||||
{ name: 'browserstack', value: '@wdio/browserstack-service$--$browserstack' },
|
||||
{ name: 'lighthouse', value: '@wdio/lighthouse-service$--$lighthouse' },
|
||||
{ name: 'vscode', value: 'wdio-vscode-service$--$vscode' },
|
||||
{ name: 'electron', value: 'wdio-electron-service$--$electron' },
|
||||
{ name: 'appium', value: '@wdio/appium-service$--$appium' },
|
||||
// external
|
||||
{ name: 'eslinter-service', value: 'wdio-eslinter-service$--$eslinter' },
|
||||
{ name: 'lambdatest', value: 'wdio-lambdatest-service$--$lambdatest' },
|
||||
{ name: 'zafira-listener', value: 'wdio-zafira-listener-service$--$zafira-listener' },
|
||||
{ name: 'reportportal', value: 'wdio-reportportal-service$--$reportportal' },
|
||||
{ name: 'docker', value: 'wdio-docker-service$--$docker' },
|
||||
{ name: 'ui5', value: 'wdio-ui5-service$--$ui5' },
|
||||
{ name: 'wiremock', value: 'wdio-wiremock-service$--$wiremock' },
|
||||
{ name: 'ng-apimock', value: 'wdio-ng-apimock-service$--$ng-apimock' },
|
||||
{ name: 'slack', value: 'wdio-slack-service$--$slack' },
|
||||
{ name: 'cucumber-viewport-logger', value: 'wdio-cucumber-viewport-logger-service$--$cucumber-viewport-logger' },
|
||||
{ name: 'intercept', value: 'wdio-intercept-service$--$intercept' },
|
||||
{ name: 'docker', value: 'wdio-docker-service$--$docker' },
|
||||
{ name: 'novus-visual-regression', value: 'wdio-novus-visual-regression-service$--$novus-visual-regression' },
|
||||
{ name: 'rerun', value: 'wdio-rerun-service$--$rerun' },
|
||||
{ name: 'winappdriver', value: 'wdio-winappdriver-service$--$winappdriver' },
|
||||
{ name: 'ywinappdriver', value: 'wdio-ywinappdriver-service$--$ywinappdriver' },
|
||||
{ name: 'performancetotal', value: 'wdio-performancetotal-service$--$performancetotal' },
|
||||
{ name: 'cleanuptotal', value: 'wdio-cleanuptotal-service$--$cleanuptotal' },
|
||||
{ name: 'aws-device-farm', value: 'wdio-aws-device-farm-service$--$aws-device-farm' },
|
||||
{ name: 'ms-teams', value: 'wdio-ms-teams-service$--$ms-teams' },
|
||||
{ name: 'tesults', value: 'wdio-tesults-service$--$tesults' },
|
||||
{ name: 'azure-devops', value: '@gmangiapelo/wdio-azure-devops-service$--$azure-devops' },
|
||||
{ name: 'google-Chat', value: 'wdio-google-chat-service$--$google-chat' },
|
||||
{ name: 'qmate-service', value: '@sap_oss/wdio-qmate-service$--$qmate-service' },
|
||||
{ name: 'robonut', value: 'wdio-robonut-service$--$robonut' },
|
||||
{ name: 'qunit', value: 'wdio-qunit-service$--$qunit' },
|
||||
{ name: 'roku', value: 'wdio-roku-service$--$roku' },
|
||||
{ name: 'obsidian', value: 'wdio-obsidian-service$--$obsidian' }
|
||||
]
|
||||
}
|
||||
|
||||
export const configHelperSuccessMessage = ({ projectRootDir, runScript, extraInfo = '' }: { projectRootDir: string, runScript: string, extraInfo: string }) => `
|
||||
🤖 Successfully setup project at ${ projectRootDir } 🎉
|
||||
|
||||
Join our Discord Community Server and instantly find answers to your issues or queries. Or just join and say hi 👋!
|
||||
🔗 https://discord.webdriver.io
|
||||
|
||||
Visit the project on GitHub to report bugs 🐛 or raise feature requests 💡:
|
||||
🔗 https://github.com/webdriverio/webdriverio
|
||||
${ extraInfo }
|
||||
To run your tests, execute:
|
||||
$ cd ${ projectRootDir }
|
||||
$ npm run ${ runScript }
|
||||
`
|
||||
export const isNuxtProject = [
|
||||
path.join(process.cwd(), 'nuxt.config.js'),
|
||||
path.join(process.cwd(), 'nuxt.config.ts'),
|
||||
path.join(process.cwd(), 'nuxt.config.mjs'),
|
||||
path.join(process.cwd(), 'nuxt.config.mts')
|
||||
].map((p) => {
|
||||
try {
|
||||
fs.accessSync(p)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}).some(Boolean)
|
||||
|
||||
export const SUPPORTED_CONFIG_FILE_EXTENSION = ['js', 'ts', 'mjs', 'mts', 'cjs', 'cts']
|
||||
|
||||
export const CONFIG_HELPER_SERENITY_BANNER = `
|
||||
Learn more about Serenity/JS:
|
||||
🔗 https://serenity-js.org/
|
||||
🔗 https://serenity-js.org/handbook/test-runners/webdriverio/
|
||||
`
|
||||
|
||||
export function usesSerenity (answers: Questionnair) {
|
||||
return answers.framework.includes('serenity-js')
|
||||
}
|
||||
|
||||
enum ProtocolOptions {
|
||||
HTTPS = 'https',
|
||||
HTTP = 'http'
|
||||
}
|
||||
|
||||
export enum BackendChoice {
|
||||
Local = 'On my local machine',
|
||||
Experitest = 'In the cloud using Experitest',
|
||||
Saucelabs = 'In the cloud using Sauce Labs',
|
||||
Browserstack = 'In the cloud using BrowserStack',
|
||||
OtherVendors = 'In the cloud using Testingbot or LambdaTest or a different service',
|
||||
Grid = 'I have my own Selenium cloud'
|
||||
}
|
||||
export enum RegionOptions {
|
||||
US = 'us',
|
||||
EU = 'eu'
|
||||
}
|
||||
|
||||
export enum ElectronBuildToolChoice {
|
||||
ElectronForge = 'Electron Forge (https://www.electronforge.io/)',
|
||||
ElectronBuilder = 'electron-builder (https://www.electron.build/)',
|
||||
SomethingElse = 'Something else'
|
||||
}
|
||||
|
||||
export const SUPPORTED_BROWSER_RUNNER_PRESETS = [
|
||||
{ name: 'Lit (https://lit.dev/)', value: '$--$' },
|
||||
{ name: 'Vue.js (https://vuejs.org/)', value: '@vitejs/plugin-vue$--$vue' },
|
||||
{ name: 'Svelte (https://svelte.dev/)', value: '@sveltejs/vite-plugin-svelte$--$svelte' },
|
||||
{ name: 'SolidJS (https://www.solidjs.com/)', value: 'vite-plugin-solid$--$solid' },
|
||||
{ name: 'StencilJS (https://stenciljs.com/)', value: '$--$stencil' },
|
||||
{ name: 'React (https://reactjs.org/)', value: '@vitejs/plugin-react$--$react' },
|
||||
{ name: 'Preact (https://preactjs.com/)', value: '@preact/preset-vite$--$preact' },
|
||||
{ name: 'Other', value: null }
|
||||
]
|
||||
|
||||
function isBrowserRunner (answers: Questionnair) {
|
||||
return answers.runner === SUPPORTED_PACKAGES.runner[1].value
|
||||
}
|
||||
function getTestingPurpose (answers: Questionnair) {
|
||||
return convertPackageHashToObject(answers.runner).purpose as 'e2e' | 'electron' | 'component' | 'vscode' | 'macos' | 'roku'
|
||||
}
|
||||
|
||||
export const TESTING_LIBRARY_PACKAGES: Record<string, string> = {
|
||||
react: '@testing-library/react',
|
||||
preact: '@testing-library/preact',
|
||||
vue: '@testing-library/vue',
|
||||
svelte: '@testing-library/svelte',
|
||||
solid: 'solid-testing-library'
|
||||
}
|
||||
|
||||
export const E2E_ENVIRONMENTS = [
|
||||
{ name: 'Web - web applications in the browser', value: 'web' },
|
||||
{ name: 'Mobile - native, hybrid and mobile web apps, on Android or iOS', value: 'mobile' }
|
||||
]
|
||||
|
||||
export const MOBILE_ENVIRONMENTS = [
|
||||
{ name: 'Android - native, hybrid and mobile web apps, tested on emulators and real devices\n > using UiAutomator2 (https://www.npmjs.com/package/appium-uiautomator2-driver)', value: 'android' },
|
||||
{ name: 'iOS - applications on iOS, iPadOS, and tvOS\n > using XCTest (https://appium.github.io/appium-xcuitest-driver)', value: 'ios' }
|
||||
]
|
||||
export const BROWSER_ENVIRONMENTS = [
|
||||
{ name: 'Chrome', value: 'chrome', checked: true },
|
||||
{ name: 'Firefox', value: 'firefox' },
|
||||
{ name: 'Safari', value: 'safari' },
|
||||
{ name: 'Microsoft Edge', value: 'MicrosoftEdge' }
|
||||
]
|
||||
|
||||
function selectDefaultService (serviceNames: string | string[]) {
|
||||
serviceNames = Array.isArray(serviceNames) ? serviceNames : [serviceNames]
|
||||
return SUPPORTED_PACKAGES.service
|
||||
/* istanbul ignore next */
|
||||
.filter(({ name }) => serviceNames.includes(name))
|
||||
.map(({ value }) => value)
|
||||
}
|
||||
|
||||
function prioServiceOrderFor (serviceNamesParam: string | string[]) {
|
||||
const serviceNames = Array.isArray(serviceNamesParam) ? serviceNamesParam : [serviceNamesParam]
|
||||
let services = SUPPORTED_PACKAGES.service
|
||||
for (const serviceName of serviceNames) {
|
||||
const index = services.findIndex(({ name }) => name === serviceName)
|
||||
services = [services[index], ...services.slice(0, index), ...services.slice(index + 1)]
|
||||
}
|
||||
|
||||
return services
|
||||
}
|
||||
|
||||
export const QUESTIONNAIRE = [{
|
||||
type: 'list',
|
||||
name: 'runner',
|
||||
message: 'What type of testing would you like to do?',
|
||||
choices: SUPPORTED_PACKAGES.runner
|
||||
}, {
|
||||
type: 'list',
|
||||
name: 'preset',
|
||||
message: 'Which framework do you use for building components?',
|
||||
choices: SUPPORTED_BROWSER_RUNNER_PRESETS,
|
||||
// only ask if there are more than 1 runner to pick from
|
||||
when: /* istanbul ignore next */ isBrowserRunner
|
||||
}, {
|
||||
type: 'confirm',
|
||||
name: 'installTestingLibrary',
|
||||
message: 'Do you like to use Testing Library (https://testing-library.com/) as test utility?',
|
||||
default: true,
|
||||
// only ask if there are more than 1 runner to pick from
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => (
|
||||
isBrowserRunner(answers) &&
|
||||
/**
|
||||
* Only show if Testing Library has an add-on for framework
|
||||
*/
|
||||
answers.preset && TESTING_LIBRARY_PACKAGES[convertPackageHashToObject(answers.preset!).short]
|
||||
)
|
||||
}, {
|
||||
type: 'list',
|
||||
name: 'electronBuildTool',
|
||||
message: 'Which tool are you using to build your Electron app?',
|
||||
choices: Object.values(ElectronBuildToolChoice),
|
||||
when: /* instanbul ignore next */ (answers: Questionnair) => getTestingPurpose(answers) === 'electron'
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'electronAppBinaryPath',
|
||||
message: 'What is the path to the binary of your built Electron app?',
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => getTestingPurpose(answers) === 'electron' && (answers.electronBuildTool === ElectronBuildToolChoice.SomethingElse)
|
||||
}, {
|
||||
type: 'list',
|
||||
name: 'backend',
|
||||
message: 'Where is your automation backend located?',
|
||||
choices: Object.values(BackendChoice),
|
||||
when: /* instanbul ignore next */ (answers: Questionnair) => getTestingPurpose(answers) === 'e2e'
|
||||
}, {
|
||||
type: 'list',
|
||||
name: 'e2eEnvironment',
|
||||
message: 'Which environment you would like to automate?',
|
||||
choices: E2E_ENVIRONMENTS,
|
||||
default: 'web',
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => getTestingPurpose(answers) === 'e2e'
|
||||
}, {
|
||||
type: 'list',
|
||||
name: 'mobileEnvironment',
|
||||
message: 'Which mobile environment you\'d like to automate?',
|
||||
choices: MOBILE_ENVIRONMENTS,
|
||||
when: /* instanbul ignore next */ (answers: Questionnair) => (
|
||||
getTestingPurpose(answers) === 'e2e' &&
|
||||
answers.e2eEnvironment === 'mobile'
|
||||
)
|
||||
}, {
|
||||
type: 'checkbox',
|
||||
name: 'browserEnvironment',
|
||||
message: 'With which browser should we start?',
|
||||
choices: BROWSER_ENVIRONMENTS,
|
||||
when: /* instanbul ignore next */ (answers: Questionnair) => (
|
||||
getTestingPurpose(answers) === 'e2e' &&
|
||||
answers.e2eEnvironment === 'web'
|
||||
)
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'hostname',
|
||||
message: 'What is the host address of that cloud service?',
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => answers.backend && answers.backend.indexOf('different service') > -1
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'port',
|
||||
message: 'What is the port on which that service is running?',
|
||||
default: '80',
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => answers.backend && answers.backend.indexOf('different service') > -1
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'expEnvAccessKey',
|
||||
message: 'Access key from Experitest Cloud',
|
||||
default: 'EXPERITEST_ACCESS_KEY',
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => answers.backend === BackendChoice.Experitest
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'expEnvHostname',
|
||||
message: 'Environment variable for cloud url',
|
||||
default: 'example.experitest.com',
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => answers.backend === BackendChoice.Experitest
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'expEnvPort',
|
||||
message: 'Environment variable for port',
|
||||
default: '443',
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => answers.backend === BackendChoice.Experitest
|
||||
}, {
|
||||
type: 'list',
|
||||
name: 'expEnvProtocol',
|
||||
message: 'Choose a protocol for environment variable',
|
||||
default: ProtocolOptions.HTTPS,
|
||||
choices: Object.values(ProtocolOptions),
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => (
|
||||
answers.backend === BackendChoice.Experitest &&
|
||||
answers.expEnvPort !== '80' &&
|
||||
answers.expEnvPort !== '443'
|
||||
)
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'env_user',
|
||||
message: 'Environment variable for username',
|
||||
default: 'LT_USERNAME',
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => (
|
||||
answers.backend && answers.backend.indexOf('LambdaTest') > -1 &&
|
||||
answers.hostname!.indexOf('lambdatest.com') > -1
|
||||
)
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'env_key',
|
||||
message: 'Environment variable for access key',
|
||||
default: 'LT_ACCESS_KEY',
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => (
|
||||
answers.backend && answers.backend.indexOf('LambdaTest') > -1 &&
|
||||
answers.hostname!.indexOf('lambdatest.com') > -1
|
||||
)
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'env_user',
|
||||
message: 'Environment variable for username',
|
||||
default: 'BROWSERSTACK_USERNAME',
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => answers.backend === BackendChoice.Browserstack
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'env_key',
|
||||
message: 'Environment variable for access key',
|
||||
default: 'BROWSERSTACK_ACCESS_KEY',
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => answers.backend === BackendChoice.Browserstack
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'env_user',
|
||||
message: 'Environment variable for username',
|
||||
default: 'SAUCE_USERNAME',
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => answers.backend === BackendChoice.Saucelabs
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'env_key',
|
||||
message: 'Environment variable for access key',
|
||||
default: 'SAUCE_ACCESS_KEY',
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => answers.backend === BackendChoice.Saucelabs
|
||||
}, {
|
||||
type: 'list',
|
||||
name: 'region',
|
||||
message: 'In which region do you want to run your Sauce Labs tests in?',
|
||||
choices: Object.values(RegionOptions),
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => answers.backend === BackendChoice.Saucelabs
|
||||
}, {
|
||||
type: 'confirm',
|
||||
name: 'useSauceConnect',
|
||||
message: (
|
||||
'Are you testing a local application and need Sauce Connect to be set-up?\n' +
|
||||
'Read more on Sauce Connect at: https://docs.saucelabs.com/secure-connections/#sauce-connect-proxy'
|
||||
),
|
||||
default: isNuxtProject,
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => (
|
||||
answers.backend === BackendChoice.Saucelabs &&
|
||||
!isNuxtProject
|
||||
)
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'hostname',
|
||||
message: 'What is the IP or URI to your Selenium standalone or grid server?',
|
||||
default: 'localhost',
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => answers.backend && answers.backend.toString().indexOf('own Selenium cloud') > -1
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'port',
|
||||
message: 'What is the port which your Selenium standalone or grid server is running on?',
|
||||
default: '4444',
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => answers.backend && answers.backend.toString().indexOf('own Selenium cloud') > -1
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'path',
|
||||
message: 'What is the path to your browser driver or grid server?',
|
||||
default: '/',
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => answers.backend && answers.backend.toString().indexOf('own Selenium cloud') > -1
|
||||
}, {
|
||||
type: 'list',
|
||||
name: 'framework',
|
||||
message: 'Which framework do you want to use?',
|
||||
choices: /* instanbul ignore next */ (answers: Questionnair) => {
|
||||
/**
|
||||
* browser runner currently supports only Mocha framework
|
||||
*/
|
||||
if (isBrowserRunner(answers)) {
|
||||
return SUPPORTED_PACKAGES.framework.slice(0, 1)
|
||||
}
|
||||
/**
|
||||
* Serenity tests don't come with proper ElectronJS example files
|
||||
*/
|
||||
if (getTestingPurpose(answers) === 'electron') {
|
||||
return SUPPORTED_PACKAGES.framework.filter(
|
||||
({ value }) => !value.startsWith('@serenity-js')
|
||||
)
|
||||
}
|
||||
|
||||
return SUPPORTED_PACKAGES.framework
|
||||
}
|
||||
}, {
|
||||
type: 'confirm',
|
||||
name: 'isUsingTypeScript',
|
||||
message: 'Do you want to use Typescript to write tests?',
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => {
|
||||
/**
|
||||
* StencilJS only supports TypeScript - use the default
|
||||
*/
|
||||
if (answers.preset?.includes('stencil')) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
default: /* istanbul ignore next */ (answers: Questionnair) => answers.preset?.includes('stencil') || detectCompiler(answers)
|
||||
}, {
|
||||
type: 'confirm',
|
||||
name: 'generateTestFiles',
|
||||
message: 'Do you want WebdriverIO to autogenerate some test files?',
|
||||
default: true,
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => {
|
||||
/**
|
||||
* we only have examples for Mocha and Jasmine
|
||||
*/
|
||||
if (['vscode', 'electron', 'macos'].includes(getTestingPurpose(answers)) && answers.framework.includes('cucumber')) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'specs',
|
||||
message: 'What should be the location of your spec files?',
|
||||
default: /* istanbul ignore next */ (answers: Questionnair) => {
|
||||
const pattern = isBrowserRunner(answers) ? 'src/**/*.test' : 'test/specs/**/*'
|
||||
return getDefaultFiles(answers, pattern)
|
||||
},
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => answers.generateTestFiles && /(mocha|jasmine)/.test(answers.framework)
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'specs',
|
||||
message: 'What should be the location of your feature files?',
|
||||
default: (answers: Questionnair) => getDefaultFiles(answers, 'features/**/*.feature'),
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => answers.generateTestFiles && answers.framework.includes('cucumber')
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'stepDefinitions',
|
||||
message: 'What should be the location of your step definitions?',
|
||||
default: (answers: Questionnair) => getDefaultFiles(answers, 'features/step-definitions/steps'),
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => answers.generateTestFiles && answers.framework.includes('cucumber')
|
||||
}, {
|
||||
type: 'confirm',
|
||||
name: 'usePageObjects',
|
||||
message: 'Do you want to use page objects (https://martinfowler.com/bliki/PageObject.html)?',
|
||||
default: true,
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => (
|
||||
answers.generateTestFiles &&
|
||||
/**
|
||||
* page objects aren't common for component testing
|
||||
*/
|
||||
!isBrowserRunner(answers) &&
|
||||
/**
|
||||
* and also not needed when running VS Code tests since the service comes with
|
||||
* its own page object implementation, nor when running Electron or MacOS tests
|
||||
*/
|
||||
!['vscode', 'electron', 'macos'].includes(getTestingPurpose(answers)) &&
|
||||
/**
|
||||
* Serenity/JS generates Lean Page Objects by default, so there's no need to ask about it
|
||||
* See https://serenity-js.org/handbook/web-testing/page-objects-pattern/
|
||||
*/
|
||||
!usesSerenity(answers)
|
||||
)
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'pages',
|
||||
message: 'Where are your page objects located?',
|
||||
default: /* istanbul ignore next */ (answers: Questionnair) => (
|
||||
answers.framework.match(/(mocha|jasmine)/)
|
||||
? getDefaultFiles(answers, 'test/pageobjects/**/*')
|
||||
: getDefaultFiles(answers, 'features/pageobjects/**/*')
|
||||
),
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => answers.generateTestFiles && answers.usePageObjects
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'serenityLibPath',
|
||||
message: 'What should be the location of your Serenity/JS Screenplay Pattern library?',
|
||||
default: /* istanbul ignore next */ async (answers: Questionnair) => {
|
||||
const projectRootDir = await getProjectRoot(answers)
|
||||
const specsDir = path.resolve(projectRootDir, path.dirname(answers.specs || '').replace(/\*\*$/, ''))
|
||||
return path.resolve(specsDir, '..', 'serenity')
|
||||
},
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => answers.generateTestFiles && usesSerenity(answers)
|
||||
}, {
|
||||
type: 'checkbox',
|
||||
name: 'reporters',
|
||||
message: 'Which reporter do you want to use?',
|
||||
choices: SUPPORTED_PACKAGES.reporter
|
||||
}, {
|
||||
type: 'checkbox',
|
||||
name: 'plugins',
|
||||
message: 'Do you want to add a plugin to your test setup?',
|
||||
choices: SUPPORTED_PACKAGES.plugin,
|
||||
default: []
|
||||
}, {
|
||||
type: 'confirm',
|
||||
name: 'includeVisualTesting',
|
||||
message: 'Would you like to include Visual Testing to your setup? For more information see https://webdriver.io/docs/visual-testing!',
|
||||
default: false,
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => {
|
||||
/**
|
||||
* visual testing mostly makes sense for e2e and component tests
|
||||
*/
|
||||
return ['e2e', 'component'].includes(getTestingPurpose(answers))
|
||||
}
|
||||
}, {
|
||||
type: 'checkbox',
|
||||
name: 'services',
|
||||
message: 'Do you want to add a service to your test setup?',
|
||||
choices: (answers: Questionnair) => {
|
||||
const services: string[] = []
|
||||
if (answers.backend === BackendChoice.Browserstack) {
|
||||
services.push('browserstack')
|
||||
} else if (answers.backend === BackendChoice.Saucelabs) {
|
||||
services.push('sauce')
|
||||
}
|
||||
if (answers.e2eEnvironment === 'mobile') {
|
||||
services.push('appium')
|
||||
}
|
||||
if (getTestingPurpose(answers) === 'e2e' && isNuxtProject) {
|
||||
services.push('nuxt')
|
||||
}
|
||||
|
||||
if (getTestingPurpose(answers) === 'vscode') {
|
||||
return [SUPPORTED_PACKAGES.service.find(({ name }) => name === 'vscode')]
|
||||
} else if (getTestingPurpose(answers) === 'electron') {
|
||||
return [SUPPORTED_PACKAGES.service.find(({ name }) => name === 'electron')]
|
||||
} else if (getTestingPurpose(answers) === 'macos') {
|
||||
return [SUPPORTED_PACKAGES.service.find(({ name }) => name === 'appium')]
|
||||
} else if (getTestingPurpose(answers) === 'roku') {
|
||||
return [SUPPORTED_PACKAGES.service.find(({ name }) => name === 'roku')]
|
||||
}
|
||||
return prioServiceOrderFor(services)
|
||||
},
|
||||
default: (answers: Questionnair) => {
|
||||
const defaultServices: string[] = []
|
||||
if (answers.backend === BackendChoice.Browserstack) {
|
||||
defaultServices.push('browserstack')
|
||||
} else if (answers.backend === BackendChoice.Saucelabs) {
|
||||
defaultServices.push('sauce')
|
||||
}
|
||||
if (answers.e2eEnvironment === 'mobile' || getTestingPurpose(answers) === 'macos') {
|
||||
defaultServices.push('appium')
|
||||
}
|
||||
if (getTestingPurpose(answers) === 'vscode') {
|
||||
defaultServices.push('vscode')
|
||||
} else if (getTestingPurpose(answers) === 'electron') {
|
||||
defaultServices.push('electron')
|
||||
} else if (getTestingPurpose(answers) === 'roku') {
|
||||
defaultServices.push('roku')
|
||||
}
|
||||
if (isNuxtProject) {
|
||||
defaultServices.push('nuxt')
|
||||
}
|
||||
if (answers.includeVisualTesting) {
|
||||
defaultServices.push('visual')
|
||||
}
|
||||
return selectDefaultService(defaultServices)
|
||||
}
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'outputDir',
|
||||
message: 'In which directory should the xunit reports get stored?',
|
||||
default: './',
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => answers.reporters.includes('junit')
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'outputDir',
|
||||
message: 'In which directory should the json reports get stored?',
|
||||
default: './',
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => answers.reporters.includes('json')
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'outputDir',
|
||||
message: 'In which directory should the mochawesome json reports get stored?',
|
||||
default: './',
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => answers.reporters.includes('mochawesome')
|
||||
}, {
|
||||
type: 'confirm',
|
||||
name: 'npmInstall',
|
||||
message: () => `Do you want me to run \`${detectPackageManager()} install\``,
|
||||
default: true
|
||||
}]
|
||||
|
||||
export const COMMUNITY_PACKAGES_WITH_TS_SUPPORT = [
|
||||
'wdio-electron-service',
|
||||
'wdio-vscode-service',
|
||||
'wdio-nuxt-service',
|
||||
'wdio-vite-service',
|
||||
'wdio-gmail-service',
|
||||
'wdio-roku-service',
|
||||
'wdio-obsidian-service'
|
||||
]
|
||||
|
||||
export const DEPENDENCIES_INSTALLATION_MESSAGE = `
|
||||
To install dependencies, execute:
|
||||
%s
|
||||
`
|
||||
135
packages/create-wdio/src/index.ts
Normal file
135
packages/create-wdio/src/index.ts
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { execSync } from 'node:child_process'
|
||||
|
||||
import chalk from 'chalk'
|
||||
import semver from 'semver'
|
||||
import { Command } from 'commander'
|
||||
import { resolve } from 'import-meta-resolve'
|
||||
|
||||
import { runProgram, getPackageVersion } from './utils.js'
|
||||
import {
|
||||
ASCII_ROBOT, PROGRAM_TITLE, UNSUPPORTED_NODE_VERSION, DEFAULT_NPM_TAG,
|
||||
INSTALL_COMMAND, DEV_FLAG, SUPPORTED_PACKAGE_MANAGERS, EXECUTER, EXECUTE_COMMAND
|
||||
} from './constants.js'
|
||||
import type { ProgramOpts } from './types.js'
|
||||
|
||||
const WDIO_COMMAND = 'wdio'
|
||||
let projectDir: string | undefined
|
||||
|
||||
export async function run(operation = createWebdriverIO) {
|
||||
const version = await getPackageVersion()
|
||||
|
||||
/**
|
||||
* print program ASCII art
|
||||
*/
|
||||
if (!(process.argv.includes('--version') || process.argv.includes('-v'))) {
|
||||
console.log(ASCII_ROBOT, PROGRAM_TITLE)
|
||||
}
|
||||
|
||||
/**
|
||||
* ensure right Node.js version is used
|
||||
*/
|
||||
const unsupportedNodeVersion = !semver.satisfies(process.version, '>=18.18.0')
|
||||
if (unsupportedNodeVersion) {
|
||||
console.log(chalk.yellow(UNSUPPORTED_NODE_VERSION))
|
||||
return
|
||||
}
|
||||
|
||||
const program = new Command(WDIO_COMMAND)
|
||||
.version(version, '-v, --version')
|
||||
.arguments('[project-path]')
|
||||
.usage(`${chalk.green('[project-path]')} [options]`)
|
||||
.action(name => (projectDir = name))
|
||||
|
||||
.option('-t, --npm-tag <tag>', 'Which NPM version you like to install, e.g. @next', DEFAULT_NPM_TAG)
|
||||
.option('-y, --yes', 'will fill in all config defaults without prompting', false)
|
||||
.option('-d, --dev', 'Install all packages as into devDependencies', true)
|
||||
|
||||
.allowUnknownOption()
|
||||
.on('--help', () => console.log())
|
||||
.parse(process.argv)
|
||||
|
||||
return operation(program.opts())
|
||||
}
|
||||
|
||||
/**
|
||||
* detects the package manager that was used
|
||||
* uses the environment variable `npm_config_user_agent` to detect the package manager
|
||||
* falls back to `npm` if no package manager could be detected
|
||||
*/
|
||||
function detectPackageManager() {
|
||||
if (!process.env.npm_config_user_agent) {
|
||||
return 'npm'
|
||||
}
|
||||
const detectedPM = process.env.npm_config_user_agent.split('/')[0].toLowerCase()
|
||||
|
||||
const matchedPM = SUPPORTED_PACKAGE_MANAGERS.find(pm => pm.toLowerCase() === detectedPM)
|
||||
|
||||
return matchedPM || 'npm'
|
||||
}
|
||||
|
||||
export async function createWebdriverIO(opts: ProgramOpts) {
|
||||
const npmTag = opts.npmTag.startsWith('@') ? opts.npmTag : `@${opts.npmTag}`
|
||||
const root = path.resolve(process.cwd(), projectDir || '')
|
||||
|
||||
/**
|
||||
* find package manager that was used to create project
|
||||
*/
|
||||
const pm = detectPackageManager()
|
||||
|
||||
const hasPackageJson = await fs.access(path.resolve(root, 'package.json')).then(() => true).catch(() => false)
|
||||
if (!hasPackageJson) {
|
||||
await fs.mkdir(root, { recursive: true })
|
||||
await fs.writeFile(path.resolve(root, 'package.json'), JSON.stringify({
|
||||
name: root.substring(root.lastIndexOf(path.sep) + 1),
|
||||
type: 'module'
|
||||
}, null, 2))
|
||||
}
|
||||
|
||||
const cliInstalled = await isCLIInstalled(root)
|
||||
if (!cliInstalled) {
|
||||
console.log(`\nInstalling ${chalk.bold('@wdio/cli')} to initialize project...`)
|
||||
const args = [INSTALL_COMMAND[pm]]
|
||||
if (opts.dev) {
|
||||
args.push(DEV_FLAG[pm])
|
||||
}
|
||||
args.push(`@wdio/cli${npmTag}`)
|
||||
await runProgram(pm, args, { cwd: root, stdio: 'ignore' })
|
||||
console.log(chalk.green.bold('✔ Success!'))
|
||||
}
|
||||
|
||||
return runProgram(EXECUTER[pm], [
|
||||
EXECUTE_COMMAND[pm],
|
||||
WDIO_COMMAND,
|
||||
'config',
|
||||
...(opts.yes ? ['--yes'] : []),
|
||||
...(opts.npmTag ? ['--npm-tag', opts.npmTag] : [])
|
||||
].filter(i => !!i), { cwd: root })
|
||||
}
|
||||
|
||||
async function isCLIInstalled(path: string) {
|
||||
try {
|
||||
// can be replaced with import.meta.resolve('@wdio/cli', new URL(`file:///${root}`).href) in the future
|
||||
// check if the cli is installed in the project
|
||||
resolve('@wdio/cli', pathToFileURL(path).href)
|
||||
return true
|
||||
} catch {
|
||||
// check of the cli is installed globally
|
||||
// wrap in try/catch as it can fail on Windows
|
||||
try {
|
||||
const output = execSync('npm ls -g', {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['ignore', 'pipe', 'ignore']
|
||||
})
|
||||
if (output.includes('@wdio/cli')) {
|
||||
return true
|
||||
}
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
import { execa } from 'execa'
|
||||
import { detectPackageManager } from './utils.js'
|
||||
import type { PM } from './types.js'
|
||||
import type { PACKAGE_MANAGER } from './constants.js'
|
||||
|
||||
const installCommand: Record<PM, string> = {
|
||||
const installCommand: Record<PACKAGE_MANAGER, string> = {
|
||||
npm: 'install',
|
||||
pnpm: 'add',
|
||||
yarn: 'add',
|
||||
bun: 'install'
|
||||
}
|
||||
|
||||
const devFlag: Record<PM, string> = {
|
||||
const devFlag: Record<PACKAGE_MANAGER, string> = {
|
||||
npm: '--save-dev',
|
||||
pnpm: '--save-dev',
|
||||
yarn: '--dev',
|
||||
|
|
@ -42,7 +42,7 @@ export async function installPackages (cwd: string, packages: string[], dev: boo
|
|||
return true
|
||||
}
|
||||
|
||||
export function getInstallCommand (pm: PM, packages: string[], dev: boolean) {
|
||||
export function getInstallCommand (pm: PACKAGE_MANAGER, packages: string[], dev: boolean) {
|
||||
const devParam = dev ? devFlag[pm] : ''
|
||||
return `${pm} ${installCommand[pm]} ${packages.join(' ')} ${devParam}`
|
||||
}
|
||||
1
packages/create-wdio/src/install/cli.ts
Normal file
1
packages/create-wdio/src/install/cli.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * as install from '../cli/install.js'
|
||||
107
packages/create-wdio/src/types.ts
Normal file
107
packages/create-wdio/src/types.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import type { BackendChoice, RegionOptions, ElectronBuildToolChoice, } from './constants.js'
|
||||
import type { PackageJson as typeFestPackageJson } from 'type-fest'
|
||||
import type { Package as normalizePackage } from 'normalize-package-data'
|
||||
export type NormalizedPackageJson = PackageJson & normalizePackage
|
||||
export type PackageJson = typeFestPackageJson
|
||||
|
||||
export interface ProgramOpts {
|
||||
info: boolean
|
||||
dev: boolean
|
||||
yes: boolean
|
||||
npmTag: string
|
||||
}
|
||||
|
||||
export interface Questionnair {
|
||||
runner: string
|
||||
preset?: string
|
||||
installTestingLibrary?: boolean
|
||||
electronAppBinaryPath?: string
|
||||
electronBuildTool?: ElectronBuildToolChoice
|
||||
electronBuilderConfigPath?: string
|
||||
backend?: BackendChoice
|
||||
hostname?: string
|
||||
port?: string
|
||||
path?: string
|
||||
expEnvAccessKey?: string
|
||||
expEnvHostname?: string
|
||||
expEnvPort?: string
|
||||
expEnvProtocol?: 'http' | 'https'
|
||||
|
||||
env_user?: string
|
||||
|
||||
env_key?: string
|
||||
region?: RegionOptions
|
||||
useSauceConnect?: boolean
|
||||
framework: string
|
||||
specs?: string
|
||||
stepDefinitions?: string
|
||||
generateTestFiles: boolean
|
||||
usePageObjects?: boolean
|
||||
pages?: string
|
||||
isUsingTypeScript: boolean
|
||||
reporters: string[]
|
||||
services: string[]
|
||||
serenityLibPath?: string
|
||||
plugins: string[]
|
||||
outputDir?: string
|
||||
includeVisualTesting: boolean
|
||||
npmInstall: boolean
|
||||
createPackageJSON?: boolean
|
||||
projectRootCorrect?: boolean
|
||||
projectRoot?: string
|
||||
e2eEnvironment?: 'web' | 'mobile'
|
||||
mobileEnvironment?: 'android' | 'ios'
|
||||
browserEnvironment?: ('chrome' | 'firefox' | 'safari' | 'microsoftedge')[]
|
||||
}
|
||||
|
||||
export interface ParsedAnswers extends Omit<Questionnair, 'runner' | 'framework' | 'reporters' | 'services' | 'plugins'> {
|
||||
rawAnswers: Questionnair
|
||||
runner: 'local' | 'browser'
|
||||
projectName: string
|
||||
framework: string
|
||||
purpose: string
|
||||
reporters: string[]
|
||||
plugins: string[]
|
||||
services: string[]
|
||||
packagesToInstall: string[]
|
||||
isUsingTypeScript: boolean
|
||||
serenityAdapter: string | false
|
||||
esmSupport: boolean
|
||||
isSync: boolean
|
||||
_async: string
|
||||
_await: string
|
||||
projectRootDir: string
|
||||
destSpecRootPath: string
|
||||
destPageObjectRootPath: string
|
||||
destStepRootPath: string;
|
||||
destSerenityLibRootPath: string
|
||||
relativePath: string
|
||||
hasRootTSConfig: boolean
|
||||
tsConfigFilePath: string
|
||||
tsProject: string
|
||||
wdioConfigPath: string
|
||||
}
|
||||
|
||||
export interface ConfigCommandArguments {
|
||||
yarn: boolean
|
||||
yes: boolean
|
||||
npmTag: string
|
||||
}
|
||||
|
||||
export interface SupportedPackage {
|
||||
package: string
|
||||
short: string
|
||||
purpose: string
|
||||
}
|
||||
|
||||
export interface ProjectProps {
|
||||
esmSupported: boolean
|
||||
path: string
|
||||
packageJson: NormalizedPackageJson
|
||||
}
|
||||
|
||||
export interface InstallCommandArguments {
|
||||
config?: string
|
||||
type: 'service' | 'reporter' | 'framework' | 'plugin'
|
||||
name: string
|
||||
}
|
||||
887
packages/create-wdio/src/utils.ts
Normal file
887
packages/create-wdio/src/utils.ts
Normal file
|
|
@ -0,0 +1,887 @@
|
|||
import url from 'node:url'
|
||||
import path from 'node:path'
|
||||
import util, { promisify } from 'node:util'
|
||||
import fs from 'node:fs/promises'
|
||||
import { execSync, } from 'node:child_process'
|
||||
import readDir from 'recursive-readdir'
|
||||
|
||||
import { $ } from 'execa'
|
||||
import { readPackageUp } from 'read-pkg-up'
|
||||
|
||||
import { EjsHelpers } from './templates/EjsHelpers.js'
|
||||
|
||||
import inquirer from 'inquirer'
|
||||
|
||||
import ejs from 'ejs'
|
||||
import type { SpawnOptions } from 'node:child_process'
|
||||
|
||||
import spawn from 'cross-spawn'
|
||||
|
||||
import { COMMUNITY_PACKAGES_WITH_TS_SUPPORT, DEPENDENCIES_INSTALLATION_MESSAGE, pkg, SUPPORTED_PACKAGE_MANAGERS, QUESTIONNAIRE, TESTING_LIBRARY_PACKAGES, usesSerenity } from './constants.js'
|
||||
import type { ParsedAnswers, ProjectProps, Questionnair, SupportedPackage } from './types.js'
|
||||
import chalk from 'chalk'
|
||||
import { getInstallCommand, installPackages } from './install.js'
|
||||
|
||||
const NPM_COMMAND = /^win/.test(process.platform) ? 'npm.cmd' : 'npm'
|
||||
|
||||
const __dirname = path.dirname(url.fileURLToPath(import.meta.url))
|
||||
|
||||
process.on('SIGINT', () => printAndExit(undefined, 'SIGINT'))
|
||||
|
||||
const TEMPLATE_ROOT_DIR = path.join(__dirname, 'templates', 'exampleFiles')
|
||||
|
||||
export function runProgram (command: string, args: string[], options: SpawnOptions) {
|
||||
const child = spawn(command, args, { stdio: 'inherit', ...options })
|
||||
return new Promise<void>((resolve, rejects) => {
|
||||
let error: Error
|
||||
child.on('error', (e) => (error = e))
|
||||
child.on('close', (code, signal) => {
|
||||
if (code !== 0) {
|
||||
const errorMessage = (error && error.message) || `Error calling: ${command} ${args.join(' ')}`
|
||||
printAndExit(errorMessage, signal)
|
||||
return rejects(errorMessage)
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export async function getPackageVersion() {
|
||||
try {
|
||||
const pkgJsonPath = path.join(__dirname, '..', 'package.json')
|
||||
const pkg = JSON.parse((await fs.readFile(pkgJsonPath)).toString())
|
||||
return `v${pkg.version}`
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
function printAndExit (error?: string, signal?: NodeJS.Signals | null) {
|
||||
if (signal === 'SIGINT') {
|
||||
console.log('\n\nGoodbye 👋')
|
||||
} else {
|
||||
console.log(`\n\n⚠️ Ups, something went wrong${error ? `: ${error}` : ''}!`)
|
||||
}
|
||||
|
||||
process.exit(1)
|
||||
}
|
||||
// transported from cli
|
||||
export function convertPackageHashToObject(pkg: string, hash = '$--$'): SupportedPackage {
|
||||
const [p, short, purpose] = pkg.split(hash)
|
||||
return { package: p, short, purpose }
|
||||
}
|
||||
|
||||
export async function getAnswers(yes: boolean): Promise<Questionnair> {
|
||||
if (yes) {
|
||||
const ignoredQuestions = ['e2eEnvironment']
|
||||
const filteredQuestionaire = QUESTIONNAIRE.filter((question) => !ignoredQuestions.includes(question.name))
|
||||
const answers = {} as Questionnair
|
||||
for (const question of filteredQuestionaire) {
|
||||
/**
|
||||
* set nothing if question doesn't apply
|
||||
*/
|
||||
if (question.when && !question.when(answers)) {
|
||||
continue
|
||||
}
|
||||
|
||||
Object.assign(answers, {
|
||||
[question.name]: typeof question.default !== 'undefined'
|
||||
/**
|
||||
* set default value if existing
|
||||
*/
|
||||
? typeof question.default === 'function'
|
||||
? await question.default(answers)
|
||||
: question.default
|
||||
: question.choices && question.choices.length
|
||||
/**
|
||||
* pick first choice, select value if it exists
|
||||
*/
|
||||
? typeof question.choices === 'function'
|
||||
? (question.choices(answers)[0] as unknown as { value: unknown }).value
|
||||
? (question.choices(answers)[0] as unknown as { value: unknown }).value
|
||||
: question.choices(answers)[0]
|
||||
: (question.choices[0] as { value: unknown }).value
|
||||
? question.type === 'checkbox'
|
||||
? [(question.choices[0] as { value: unknown }).value]
|
||||
: (question.choices[0] as { value: unknown }).value
|
||||
: question.choices[0]
|
||||
: {}
|
||||
})
|
||||
}
|
||||
/**
|
||||
* some questions have async defaults
|
||||
*/
|
||||
answers.isUsingTypeScript = await answers.isUsingTypeScript
|
||||
answers.specs = await answers.specs
|
||||
answers.pages = await answers.pages
|
||||
return answers
|
||||
}
|
||||
|
||||
const projectProps = await getProjectProps(process.cwd())
|
||||
const isProjectExisting = Boolean(projectProps)
|
||||
const nameInPackageJsonIsNotCreateWdioDefault = projectProps?.packageJson?.name !== 'my-new-project'
|
||||
const projectName = projectProps?.packageJson?.name ? ` named "${projectProps.packageJson.name}"` : ''
|
||||
|
||||
const projectRootQuestions = [
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'createPackageJSON',
|
||||
default: true,
|
||||
message: `Couldn't find a package.json in "${process.cwd()}" or any of the parent directories, do you want to create one?`,
|
||||
when: !isProjectExisting
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'projectRootCorrect',
|
||||
default: true,
|
||||
message: `A project${projectName} was detected at "${projectProps?.path}", correct?`,
|
||||
when: isProjectExisting && nameInPackageJsonIsNotCreateWdioDefault
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'projectRoot',
|
||||
message: 'What is the project root for your test project?',
|
||||
default: projectProps?.path,
|
||||
// only ask if there are more than 1 runner to pick from
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => isProjectExisting && nameInPackageJsonIsNotCreateWdioDefault && !answers.projectRootCorrect
|
||||
}
|
||||
]
|
||||
|
||||
// @ts-ignore
|
||||
const answers = await inquirer.prompt(projectRootQuestions)
|
||||
if (answers.createPackageJSON) {
|
||||
answers.projectRoot = process.cwd()
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
return inquirer.prompt(QUESTIONNAIRE, answers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive project properties
|
||||
* @returns {@type ProjectProps} if a package.json can be found in cwd or parent directories, otherwise undefined
|
||||
* which means that a new project can be created
|
||||
*/
|
||||
export async function getProjectProps(cwd = process.cwd()): Promise<ProjectProps | undefined> {
|
||||
try {
|
||||
const { packageJson, path: packageJsonPath } = await readPackageUp({ cwd }) || {}
|
||||
if (!packageJson || !packageJsonPath) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return {
|
||||
esmSupported: (
|
||||
packageJson.type === 'module' ||
|
||||
typeof packageJson.module === 'string'
|
||||
),
|
||||
packageJson,
|
||||
path: path.dirname(packageJsonPath)
|
||||
}
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
export function getPathForFileGeneration(answers: Questionnair, projectRootDir: string) {
|
||||
const specAnswer = answers.specs || ''
|
||||
const stepDefinitionAnswer = answers.stepDefinitions || ''
|
||||
const pageObjectAnswer = answers.pages || ''
|
||||
|
||||
const destSpecRootPath = generatePathFromAnswer(specAnswer, projectRootDir).replace(/\*\*$/, '')
|
||||
const destStepRootPath = generatePathFromAnswer(stepDefinitionAnswer, projectRootDir)
|
||||
const destPageObjectRootPath = answers.usePageObjects
|
||||
? generatePathFromAnswer(pageObjectAnswer, projectRootDir).replace(/\*\*$/, '')
|
||||
: ''
|
||||
const destSerenityLibRootPath = usesSerenity(answers)
|
||||
? path.resolve(projectRootDir, answers.serenityLibPath || 'serenity')
|
||||
: ''
|
||||
const relativePath = (answers.generateTestFiles && answers.usePageObjects)
|
||||
? !(convertPackageHashToObject(answers.framework).short === 'cucumber')
|
||||
? path.relative(destSpecRootPath, destPageObjectRootPath)
|
||||
: path.relative(destStepRootPath, destPageObjectRootPath)
|
||||
: ''
|
||||
|
||||
return {
|
||||
destSpecRootPath: destSpecRootPath,
|
||||
destStepRootPath: destStepRootPath,
|
||||
destPageObjectRootPath: destPageObjectRootPath,
|
||||
destSerenityLibRootPath: destSerenityLibRootPath,
|
||||
relativePath: relativePath.replaceAll(path.sep, '/')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a valid file path from answers provided.
|
||||
* @param answers The answer from which a file path is to be generated.
|
||||
* @param projectRootDir The root directory of the project.
|
||||
* @returns filePath
|
||||
*/
|
||||
function generatePathFromAnswer(answers:string, projectRootDir:string):string {
|
||||
return path.resolve(
|
||||
projectRootDir, path.dirname(answers) === '.' ? path.resolve(answers) : path.dirname(answers))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get project root directory based on questionaire answers
|
||||
* @param answers questionaire answers
|
||||
* @param projectProps project properties received via `getProjectProps`
|
||||
* @returns project root path
|
||||
*/
|
||||
export async function getProjectRoot (parsedAnswers?: Questionnair) {
|
||||
if (parsedAnswers?.createPackageJSON && parsedAnswers.projectRoot) {
|
||||
return parsedAnswers.projectRoot
|
||||
}
|
||||
|
||||
const root = (await getProjectProps())?.path
|
||||
if (!root) {
|
||||
throw new Error('Could not find project root directory with a package.json')
|
||||
}
|
||||
|
||||
return !parsedAnswers || parsedAnswers.projectRootCorrect
|
||||
? root
|
||||
: parsedAnswers.projectRoot || process.cwd()
|
||||
}
|
||||
|
||||
/**
|
||||
* create package.json if not already existing
|
||||
*/
|
||||
export async function createPackageJSON(parsedAnswers: ParsedAnswers) {
|
||||
const packageJsonExists = await fs.access(path.resolve(process.cwd(), 'package.json')).then(() => true, () => false)
|
||||
|
||||
// Use the existing package.json if it already exists.
|
||||
if (packageJsonExists) {
|
||||
return
|
||||
}
|
||||
|
||||
// If a user said no to creating a package.json, but it doesn't exist, abort.
|
||||
if (parsedAnswers.createPackageJSON === false) {
|
||||
/* istanbul ignore if */
|
||||
if (!packageJsonExists) {
|
||||
console.log(`No WebdriverIO configuration found in "${parsedAnswers.wdioConfigPath}"`)
|
||||
return !process.env.WDIO_UNIT_TESTS && process.exit(0)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Only create if the user gave explicit permission to
|
||||
if (parsedAnswers.createPackageJSON) {
|
||||
console.log(`Creating a ${chalk.bold('package.json')} for the directory...`)
|
||||
await fs.writeFile(path.resolve(process.cwd(), 'package.json'), JSON.stringify({
|
||||
name: 'webdriverio-tests',
|
||||
version: '0.0.0',
|
||||
private: true,
|
||||
license: 'ISC',
|
||||
type: 'module',
|
||||
dependencies: {},
|
||||
devDependencies: {}
|
||||
}, null, 2))
|
||||
console.log(chalk.green(chalk.bold('✔ Success!\n')))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* set up TypeScript if it is desired but not installed
|
||||
*/
|
||||
export async function setupTypeScript(parsedAnswers: ParsedAnswers) {
|
||||
/**
|
||||
* don't create a `tsconfig.json` if user doesn't want to use TypeScript
|
||||
*/
|
||||
if (!parsedAnswers.isUsingTypeScript) {
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
* don't set up TypeScript if a `tsconfig.json` already exists but ensure we install `tsx`
|
||||
* as it is a requirement for running tests with TypeScript
|
||||
*/
|
||||
if (parsedAnswers.hasRootTSConfig) {
|
||||
return
|
||||
}
|
||||
|
||||
console.log('Setting up TypeScript...')
|
||||
const frameworkPackage = convertPackageHashToObject(parsedAnswers.rawAnswers.framework)
|
||||
const servicePackages = parsedAnswers.rawAnswers.services.map((service) => convertPackageHashToObject(service))
|
||||
const serenityTypes = parsedAnswers.serenityAdapter === 'jasmine' ? ['jasmine'] : []
|
||||
|
||||
const types = [
|
||||
'node',
|
||||
...(parsedAnswers.framework === 'jasmine' ? ['jasmine'] : []),
|
||||
'@wdio/globals/types',
|
||||
...(parsedAnswers.framework === 'jasmine' ? ['expect-webdriverio/jasmine'] : ['expect-webdriverio']),
|
||||
...(parsedAnswers.serenityAdapter ? serenityTypes : [frameworkPackage.package]),
|
||||
...(parsedAnswers.runner === 'browser' ? ['@wdio/browser-runner'] : []),
|
||||
...servicePackages
|
||||
.filter(service => (
|
||||
/**
|
||||
* given that we know that all "official" services have
|
||||
* typescript support we only include them
|
||||
*/
|
||||
service.package.startsWith('@wdio') ||
|
||||
/**
|
||||
* also include community maintained packages with known
|
||||
* support for TypeScript
|
||||
*/
|
||||
COMMUNITY_PACKAGES_WITH_TS_SUPPORT.includes(service.package)
|
||||
)).map(service => service.package)
|
||||
]
|
||||
|
||||
const preset = getPreset(parsedAnswers)
|
||||
const config = {
|
||||
compilerOptions: {
|
||||
// compiler
|
||||
moduleResolution: 'node',
|
||||
module: !parsedAnswers.esmSupport ? 'commonjs' : 'ESNext',
|
||||
target: 'es2022',
|
||||
lib: ['es2022', 'dom'],
|
||||
types,
|
||||
skipLibCheck: true,
|
||||
// bundler
|
||||
noEmit: true,
|
||||
allowImportingTsExtensions: true,
|
||||
resolveJsonModule: true,
|
||||
isolatedModules: true,
|
||||
// linting
|
||||
strict: true,
|
||||
noUnusedLocals: true,
|
||||
noUnusedParameters: true,
|
||||
noFallthroughCasesInSwitch: true,
|
||||
...Object.assign(
|
||||
preset === 'lit'
|
||||
? {
|
||||
experimentalDecorators: true,
|
||||
useDefineForClassFields: false
|
||||
}
|
||||
: {},
|
||||
preset === 'react'
|
||||
? {
|
||||
jsx: 'react-jsx'
|
||||
}
|
||||
: {},
|
||||
preset === 'preact'
|
||||
? {
|
||||
jsx: 'react-jsx',
|
||||
jsxImportSource: 'preact'
|
||||
}
|
||||
: {},
|
||||
preset === 'solid'
|
||||
? {
|
||||
jsx: 'preserve',
|
||||
jsxImportSource: 'solid-js'
|
||||
}
|
||||
: {},
|
||||
preset === 'stencil'
|
||||
? {
|
||||
experimentalDecorators: true,
|
||||
jsx: 'react',
|
||||
jsxFactory: 'h',
|
||||
jsxFragmentFactory: 'Fragment'
|
||||
}
|
||||
: {}
|
||||
)
|
||||
},
|
||||
include: preset === 'svelte'
|
||||
? ['src/**/*.d.ts', 'src/**/*.ts', 'src/**/*.js', 'src/**/*.svelte']
|
||||
: preset === 'vue'
|
||||
? ['src/**/*.ts', 'src/**/*.d.ts', 'src/**/*.tsx', 'src/**/*.vue']
|
||||
: ['test', 'wdio.conf.ts']
|
||||
}
|
||||
|
||||
if (parsedAnswers.framework === 'cucumber') {
|
||||
config.include.push('features')
|
||||
}
|
||||
|
||||
await fs.mkdir(path.dirname(parsedAnswers.tsConfigFilePath), { recursive: true })
|
||||
await fs.writeFile(
|
||||
parsedAnswers.tsConfigFilePath,
|
||||
JSON.stringify(config, null, 4)
|
||||
)
|
||||
|
||||
console.log(chalk.green(chalk.bold('✔ Success!\n')))
|
||||
}
|
||||
|
||||
/**
|
||||
* run npm install only if required by the user
|
||||
*/
|
||||
const SEP = '\n- '
|
||||
export async function npmInstall(parsedAnswers: ParsedAnswers, npmTag: string) {
|
||||
const servicePackages = parsedAnswers.rawAnswers.services.map((service) => convertPackageHashToObject(service))
|
||||
const presetPackage = convertPackageHashToObject(parsedAnswers.rawAnswers.preset || '')
|
||||
|
||||
/**
|
||||
* install Testing Library dependency if desired
|
||||
*/
|
||||
if (parsedAnswers.installTestingLibrary && TESTING_LIBRARY_PACKAGES[presetPackage.short]) {
|
||||
parsedAnswers.packagesToInstall.push(
|
||||
TESTING_LIBRARY_PACKAGES[presetPackage.short],
|
||||
'@testing-library/jest-dom'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* add helper package for Solidjs testing
|
||||
*/
|
||||
if (presetPackage.short === 'solid') {
|
||||
parsedAnswers.packagesToInstall.push('solid-js')
|
||||
}
|
||||
|
||||
/**
|
||||
* add visual service if user selected support for it
|
||||
*/
|
||||
if (parsedAnswers.includeVisualTesting) {
|
||||
parsedAnswers.packagesToInstall.push('@wdio/visual-service')
|
||||
}
|
||||
|
||||
/**
|
||||
* add dependency for Lit testing
|
||||
*/
|
||||
const preset = getPreset(parsedAnswers)
|
||||
if (preset === 'lit') {
|
||||
parsedAnswers.packagesToInstall.push('lit')
|
||||
}
|
||||
|
||||
/**
|
||||
* add dependency for Stencil testing
|
||||
*/
|
||||
if (preset === 'stencil') {
|
||||
parsedAnswers.packagesToInstall.push('@stencil/core')
|
||||
}
|
||||
|
||||
/**
|
||||
* add helper for React rendering when not using Testing Library
|
||||
*/
|
||||
if (presetPackage.short === 'react') {
|
||||
parsedAnswers.packagesToInstall.push('react')
|
||||
if (!parsedAnswers.installTestingLibrary) {
|
||||
parsedAnswers.packagesToInstall.push('react-dom')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* add Jasmine types if necessary
|
||||
*/
|
||||
if (parsedAnswers.framework === 'jasmine' && parsedAnswers.isUsingTypeScript) {
|
||||
parsedAnswers.packagesToInstall.push('@types/jasmine')
|
||||
}
|
||||
|
||||
/**
|
||||
* add Appium mobile drivers if desired
|
||||
*/
|
||||
if (parsedAnswers.purpose === 'macos') {
|
||||
parsedAnswers.packagesToInstall.push('appium-mac2-driver')
|
||||
}
|
||||
if (parsedAnswers.mobileEnvironment === 'android') {
|
||||
parsedAnswers.packagesToInstall.push('appium-uiautomator2-driver')
|
||||
}
|
||||
if (parsedAnswers.mobileEnvironment === 'ios') {
|
||||
parsedAnswers.packagesToInstall.push('appium-xcuitest-driver')
|
||||
}
|
||||
|
||||
/**
|
||||
* add packages that are required by services
|
||||
*/
|
||||
addServiceDeps(servicePackages, parsedAnswers.packagesToInstall)
|
||||
|
||||
/**
|
||||
* update package version if CLI is a pre release
|
||||
*/
|
||||
parsedAnswers.packagesToInstall = specifyVersionIfNeeded(parsedAnswers.packagesToInstall, pkg.version, npmTag)
|
||||
|
||||
const cwd = await getProjectRoot(parsedAnswers)
|
||||
const pm = detectPackageManager()
|
||||
if (parsedAnswers.npmInstall) {
|
||||
console.log(`Installing packages using ${pm}:${SEP}${parsedAnswers.packagesToInstall.join(SEP)}`)
|
||||
const success = await installPackages(cwd, parsedAnswers.packagesToInstall, true)
|
||||
if (success) {
|
||||
console.log(chalk.green(chalk.bold('✔ Success!\n')))
|
||||
}
|
||||
} else {
|
||||
const installationCommand = getInstallCommand(pm, parsedAnswers.packagesToInstall, true)
|
||||
console.log(util.format(DEPENDENCIES_INSTALLATION_MESSAGE, installationCommand))
|
||||
}
|
||||
}
|
||||
|
||||
function getPreset (parsedAnswers: ParsedAnswers) {
|
||||
const isUsingFramework = typeof parsedAnswers.preset === 'string'
|
||||
return isUsingFramework ? (parsedAnswers.preset || 'lit') : ''
|
||||
}
|
||||
export function addServiceDeps(names: SupportedPackage[], packages: string[], update = false) {
|
||||
/**
|
||||
* install Appium if it is not installed globally if `@wdio/appium-service`
|
||||
* was selected for install
|
||||
*/
|
||||
if (names.some(({ short }) => short === 'appium')) {
|
||||
const result = execSync('appium --version || echo APPIUM_MISSING', { stdio: 'pipe' }).toString().trim()
|
||||
if (result === 'APPIUM_MISSING') {
|
||||
packages.push('appium')
|
||||
} else if (update) {
|
||||
|
||||
console.log(
|
||||
'\n=======',
|
||||
'\nUsing globally installed appium', result,
|
||||
'\nPlease add the following to your wdio.conf.js:',
|
||||
"\nappium: { command: 'appium' }",
|
||||
'\n=======\n')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function createWDIOConfig(parsedAnswers: ParsedAnswers) {
|
||||
try {
|
||||
console.log('Creating a WebdriverIO config file...')
|
||||
const tplPath = path.resolve(__dirname, 'templates', 'wdio.conf.tpl.ejs')
|
||||
const renderedTpl = await renderFile(tplPath, {
|
||||
answers: parsedAnswers,
|
||||
_: new EjsHelpers({ useEsm: parsedAnswers.esmSupport, useTypeScript: parsedAnswers.isUsingTypeScript })
|
||||
})
|
||||
await fs.writeFile(parsedAnswers.wdioConfigPath, renderedTpl)
|
||||
console.log(chalk.green(chalk.bold('✔ Success!\n')))
|
||||
|
||||
if (parsedAnswers.generateTestFiles) {
|
||||
console.log('Autogenerate test files...')
|
||||
await generateTestFiles(parsedAnswers)
|
||||
console.log(chalk.green(chalk.bold('✔ Success!\n')))
|
||||
}
|
||||
} catch (err) {
|
||||
throw new Error(`⚠️ Couldn't write config file: ${(err as Error).stack}`)
|
||||
}
|
||||
}
|
||||
|
||||
export const renderFile = promisify(ejs.renderFile) as (path: string, data: Record<string, unknown>) => Promise<string>
|
||||
|
||||
/* c8 ignore start */
|
||||
export async function createWDIOScript(parsedAnswers: ParsedAnswers) {
|
||||
const rootDir = await getProjectRoot(parsedAnswers)
|
||||
const pathToWdioConfig = `./${path.join('.', parsedAnswers.wdioConfigPath.replace(rootDir, ''))}`
|
||||
|
||||
const wdioScripts = {
|
||||
'wdio': `wdio run ${ pathToWdioConfig }`,
|
||||
}
|
||||
|
||||
const serenityScripts = {
|
||||
'serenity': 'failsafe serenity:clean wdio serenity:report',
|
||||
'serenity:clean': 'rimraf target',
|
||||
'wdio': `wdio run ${ pathToWdioConfig }`,
|
||||
'serenity:report': 'serenity-bdd run',
|
||||
}
|
||||
|
||||
const scripts = parsedAnswers.serenityAdapter ? serenityScripts : wdioScripts
|
||||
|
||||
for (const [script, command] of Object.entries(scripts)) {
|
||||
|
||||
const args = ['pkg', 'set', `scripts.${ script }=${ command }`]
|
||||
|
||||
try {
|
||||
console.log(`Adding ${chalk.bold(`"${ script }"`)} script to package.json`)
|
||||
await runProgram(NPM_COMMAND, args, { cwd: parsedAnswers.projectRootDir })
|
||||
} catch (err) {
|
||||
const [preArgs, scriptPath] = args.join(' ').split('=')
|
||||
console.error(
|
||||
`⚠️ Couldn't add script to package.json: "${(err as Error).message}", you can add it manually ` +
|
||||
`by running:\n\n\t${NPM_COMMAND} ${preArgs}="${scriptPath}"`
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
console.log(chalk.green(chalk.bold('✔ Success!')))
|
||||
return true
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
export async function runAppiumInstaller(parsedAnswers: ParsedAnswers) {
|
||||
if (parsedAnswers.e2eEnvironment !== 'mobile') {
|
||||
return
|
||||
}
|
||||
|
||||
const answer = await inquirer.prompt({
|
||||
name: 'continueWithAppiumSetup',
|
||||
message: 'Continue with Appium setup using appium-installer (https://github.com/AppiumTestDistribution/appium-installer)?',
|
||||
type: 'confirm',
|
||||
default: true
|
||||
})
|
||||
|
||||
if (!answer.continueWithAppiumSetup) {
|
||||
return console.log(
|
||||
'Ok! You can learn more about setting up mobile environments in the ' +
|
||||
'Appium docs at https://appium.io/docs/en/latest/quickstart/'
|
||||
)
|
||||
}
|
||||
|
||||
return $({ stdio: 'inherit' })`npx appium-installer`
|
||||
}
|
||||
export function getSerenityPackages(answers: Questionnair): string[] {
|
||||
const framework = convertPackageHashToObject(answers.framework)
|
||||
|
||||
if (framework.package !== '@serenity-js/webdriverio') {
|
||||
return []
|
||||
}
|
||||
|
||||
const packages: Record<string, Array<string | false>> = {
|
||||
cucumber: [
|
||||
'@cucumber/cucumber',
|
||||
'@serenity-js/cucumber',
|
||||
],
|
||||
mocha: [
|
||||
'@serenity-js/mocha',
|
||||
'mocha',
|
||||
],
|
||||
jasmine: [
|
||||
'@serenity-js/jasmine',
|
||||
'jasmine',
|
||||
],
|
||||
common: [
|
||||
'@serenity-js/assertions',
|
||||
'@serenity-js/console-reporter',
|
||||
'@serenity-js/core',
|
||||
'@serenity-js/rest',
|
||||
'@serenity-js/serenity-bdd',
|
||||
'@serenity-js/web',
|
||||
'npm-failsafe',
|
||||
'rimraf',
|
||||
]
|
||||
}
|
||||
|
||||
if (answers.isUsingTypeScript) {
|
||||
packages.mocha.push('@types/mocha')
|
||||
packages.jasmine.push('@types/jasmine')
|
||||
packages.common.push('@types/node')
|
||||
}
|
||||
|
||||
return [
|
||||
...packages[framework.purpose],
|
||||
...packages.common,
|
||||
].filter(Boolean).sort() as string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* detect the package manager that was used
|
||||
* uses the environment variable `npm_config_user_agent` to detect the package manager
|
||||
* falls back to `npm` if no package manager could be detected
|
||||
*/
|
||||
export function detectPackageManager() {
|
||||
if (!process.env.npm_config_user_agent) {
|
||||
return 'npm'
|
||||
}
|
||||
const detectedPM = process.env.npm_config_user_agent.split('/')[0].toLowerCase()
|
||||
|
||||
const matchedPM = SUPPORTED_PACKAGE_MANAGERS.find(pm => pm.toLowerCase() === detectedPM)
|
||||
|
||||
return matchedPM || 'npm'
|
||||
}
|
||||
|
||||
export async function getDefaultFiles(answers: Questionnair, pattern: string) {
|
||||
const rootDir = await getProjectRoot(answers)
|
||||
const presetPackage = convertPackageHashToObject(answers.preset || '')
|
||||
const isJSX = TSX_BASED_FRAMEWORKS.includes(presetPackage.short || '')
|
||||
const val = pattern.endsWith('.feature')
|
||||
? path.join(rootDir, pattern)
|
||||
: answers?.isUsingTypeScript
|
||||
? `${path.join(rootDir, pattern)}.ts${isJSX ? 'x' : ''}`
|
||||
: `${path.join(rootDir, pattern)}.js${isJSX ? 'x' : ''}`
|
||||
return val
|
||||
}
|
||||
|
||||
const TSX_BASED_FRAMEWORKS = ['react', 'preact', 'solid', 'stencil']
|
||||
|
||||
/**
|
||||
* detect if project has a compiler file
|
||||
*/
|
||||
export async function detectCompiler(answers: Questionnair) {
|
||||
// obviously there is no compiler, if there is no package.json
|
||||
if (answers.createPackageJSON) {
|
||||
return false
|
||||
}
|
||||
|
||||
const root = await getProjectRoot(answers)
|
||||
const hasRootTSConfig = await fs.access(path.resolve(root, 'tsconfig.json')).then(() => true, () => false)
|
||||
return hasRootTSConfig
|
||||
}
|
||||
|
||||
const VERSION_REGEXP = /(\d+)\.(\d+)\.(\d+)-(alpha|beta|)\.(\d+)\+(.+)/g
|
||||
/**
|
||||
* Ensure core WebdriverIO packages have the same version as cli so that if someone
|
||||
* installs `@wdio/cli@next` and runs the wizard, all related packages have the same version.
|
||||
* running `matchAll` to a version like "8.0.0-alpha.249+4bc237701", results in:
|
||||
* ['8.0.0-alpha.249+4bc237701', '8', '0', '0', 'alpha', '249', '4bc237701']
|
||||
*/
|
||||
export function specifyVersionIfNeeded(packagesToInstall: string[], version: string, npmTag: string) {
|
||||
const { value } = version.matchAll(VERSION_REGEXP).next()
|
||||
const [major, minor, patch, tagName, build] = (value || []).slice(1, -1) // drop commit bit
|
||||
return packagesToInstall.map((p) => {
|
||||
if (
|
||||
(p.startsWith('@wdio') && p !== '@wdio/visual-service') ||
|
||||
['webdriver', 'webdriverio'].includes(p)
|
||||
) {
|
||||
const tag = major && npmTag === 'latest'
|
||||
? `^${major}.${minor}.${patch}-${tagName}.${build}`
|
||||
: npmTag
|
||||
return `${p}@${tag}`
|
||||
}
|
||||
return p
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* generate test files based on CLI answers
|
||||
*/
|
||||
export async function generateTestFiles(answers: ParsedAnswers) {
|
||||
if (answers.serenityAdapter) {
|
||||
return generateSerenityExamples(answers)
|
||||
}
|
||||
|
||||
if (answers.runner === 'local') {
|
||||
return generateLocalRunnerTestFiles(answers)
|
||||
}
|
||||
|
||||
return generateBrowserRunnerTestFiles(answers)
|
||||
}
|
||||
|
||||
/* c8 ignore start */
|
||||
async function generateSerenityExamples(answers: ParsedAnswers): Promise<void> {
|
||||
const templateDirectories = Object.entries({
|
||||
[answers.projectRootDir]: path.join(TEMPLATE_ROOT_DIR, 'serenity-js', 'common', 'config'),
|
||||
[answers.destSpecRootPath]: path.join(TEMPLATE_ROOT_DIR, 'serenity-js', answers.serenityAdapter as string),
|
||||
[answers.destSerenityLibRootPath]: path.join(TEMPLATE_ROOT_DIR, 'serenity-js', 'common', 'serenity'),
|
||||
})
|
||||
|
||||
await Promise.all(templateDirectories.map(async ([destinationRootDir, templateRootDir]) => {
|
||||
const pathsToTemplates = await readDir(templateRootDir)
|
||||
|
||||
await Promise.all(pathsToTemplates.map(async (pathToTemplate) => {
|
||||
const extension = answers.isUsingTypeScript ? '.ts' : '.js'
|
||||
const destination = path.join(destinationRootDir, path.relative(templateRootDir, pathToTemplate))
|
||||
.replace(/\.ejs$/, '')
|
||||
.replace(/\.ts$/, extension)
|
||||
|
||||
const contents = await renderFile(
|
||||
pathToTemplate,
|
||||
{ answers, _: new EjsHelpers({ useEsm: answers.esmSupport, useTypeScript: answers.isUsingTypeScript }) },
|
||||
)
|
||||
|
||||
await fs.mkdir(path.dirname(destination), { recursive: true })
|
||||
await fs.writeFile(destination, contents)
|
||||
}))
|
||||
}))
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
async function generateLocalRunnerTestFiles(answers: ParsedAnswers) {
|
||||
const testFiles = answers.framework === 'cucumber'
|
||||
? [path.join(TEMPLATE_ROOT_DIR, 'cucumber')]
|
||||
: [path.join(TEMPLATE_ROOT_DIR, 'mochaJasmine')]
|
||||
|
||||
if (answers.usePageObjects) {
|
||||
testFiles.push(path.join(TEMPLATE_ROOT_DIR, 'pageobjects'))
|
||||
}
|
||||
|
||||
const files = (await Promise.all(testFiles.map((dirPath) => readDir(
|
||||
dirPath,
|
||||
[(file, stats) => !stats.isDirectory() && !(file.endsWith('.ejs') || file.endsWith('.feature'))]
|
||||
)))).reduce((cur, acc) => [...acc, ...(cur)], [])
|
||||
|
||||
await Promise.all(files.map(async (file) => {
|
||||
const renderedTpl = await renderFile(file, { answers })
|
||||
const isJSX = answers.preset && TSX_BASED_FRAMEWORKS.includes(answers.preset)
|
||||
const fileEnding = (answers.isUsingTypeScript ? '.ts' : '.js') + (isJSX ? 'x' : '')
|
||||
const destPath = (
|
||||
file.endsWith('page.js.ejs')
|
||||
? path.join(answers.destPageObjectRootPath, path.basename(file))
|
||||
: file.includes('step_definition')
|
||||
? path.join(answers.destStepRootPath, path.basename(file))
|
||||
: path.join(answers.destSpecRootPath, path.basename(file))
|
||||
).replace(/\.ejs$/, '').replace(/\.js$/, fileEnding)
|
||||
|
||||
await fs.mkdir(path.dirname(destPath), { recursive: true })
|
||||
await fs.writeFile(destPath, renderedTpl)
|
||||
}))
|
||||
}
|
||||
|
||||
export async function generateBrowserRunnerTestFiles(answers: ParsedAnswers) {
|
||||
const isUsingFramework = typeof answers.preset === 'string'
|
||||
const preset = getPreset(answers)
|
||||
const tplRootDir = path.join(TEMPLATE_ROOT_DIR, 'browser')
|
||||
await fs.mkdir(answers.destSpecRootPath, { recursive: true })
|
||||
|
||||
/**
|
||||
* render css file
|
||||
*/
|
||||
if (isUsingFramework) {
|
||||
const renderedCss = await renderFile(path.join(tplRootDir, 'Component.css.ejs'), { answers })
|
||||
await fs.writeFile(path.join(answers.destSpecRootPath, 'Component.css'), renderedCss)
|
||||
}
|
||||
|
||||
/**
|
||||
* render component file
|
||||
*/
|
||||
const testExt = `${(answers.isUsingTypeScript ? 'ts' : 'js')}${TSX_BASED_FRAMEWORKS.includes(preset) ? 'x' : ''}`
|
||||
const fileExt = ['svelte', 'vue'].includes(preset as 'svelte' | 'vue')
|
||||
? preset!
|
||||
: testExt
|
||||
if (preset) {
|
||||
const componentOutFileName = `Component.${fileExt}`
|
||||
const renderedComponent = await renderFile(path.join(tplRootDir, `Component.${preset}.ejs`), { answers })
|
||||
await fs.writeFile(path.join(answers.destSpecRootPath, componentOutFileName), renderedComponent)
|
||||
}
|
||||
|
||||
/**
|
||||
* render test file
|
||||
*/
|
||||
const componentFileName = preset ? `Component.${preset}.test.ejs` : 'standalone.test.ejs'
|
||||
const renderedTest = await renderFile(path.join(tplRootDir, componentFileName), { answers })
|
||||
await fs.writeFile(path.join(answers.destSpecRootPath, `Component.test.${testExt}`), renderedTest)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper utility used in `run` and `install` command to format a provided config path,
|
||||
* giving it back as an absolute path, and a version without the file extension
|
||||
* @param config the initially given file path to the WDIO config file
|
||||
*/
|
||||
export async function formatConfigFilePaths(config: string) {
|
||||
const fullPath = path.isAbsolute(config)
|
||||
? config
|
||||
: path.join(process.cwd(), config)
|
||||
const fullPathNoExtension = fullPath.substring(0, fullPath.lastIndexOf(path.extname(fullPath)))
|
||||
return { fullPath, fullPathNoExtension }
|
||||
}
|
||||
|
||||
export function findInConfig(config: string, type: string) {
|
||||
let regexStr = `[\\/\\/]*[\\s]*${type}s: [\\s]*\\[([\\s]*['|"]\\w*['|"],*)*[\\s]*\\]`
|
||||
|
||||
if (type === 'framework') {
|
||||
regexStr = `[\\/\\/]*[\\s]*${type}: ([\\s]*['|"]\\w*['|"])`
|
||||
}
|
||||
|
||||
const regex = new RegExp(regexStr, 'gmi')
|
||||
return config.match(regex)
|
||||
}
|
||||
|
||||
function buildNewConfigArray(str: string, type: string, change: string) {
|
||||
const newStr = str
|
||||
.split(`${type}s: `)[1]
|
||||
.replace(/'/g, '')
|
||||
|
||||
const newArray = newStr.match(/(\w*)/gmi)?.filter(e => !!e).concat([change]) || []
|
||||
|
||||
return str
|
||||
.replace('// ', '')
|
||||
.replace(
|
||||
new RegExp(`(${type}s: )((.*\\s*)*)`), `$1[${newArray.map(e => `'${e}'`)}]`
|
||||
)
|
||||
}
|
||||
|
||||
function buildNewConfigString(str: string, type: string, change: string) {
|
||||
return str.replace(new RegExp(`(${type}: )('\\w*')`), `$1'${change}'`)
|
||||
}
|
||||
|
||||
export function replaceConfig(config: string, type: string, name: string) {
|
||||
if (type === 'framework') {
|
||||
return buildNewConfigString(config, type, name)
|
||||
}
|
||||
|
||||
const match = findInConfig(config, type)
|
||||
if (!match || match.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const text = match.pop() || ''
|
||||
return config.replace(text, buildNewConfigArray(text, type, name))
|
||||
}
|
||||
2
packages/create-wdio/src/utils/index.ts
Normal file
2
packages/create-wdio/src/utils/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
|
||||
export { SUPPORTED_PACKAGE_MANAGERS } from '../constants.js'
|
||||
432
packages/create-wdio/tests/__snapshots__/onboarding.test.ts.snap
Normal file
432
packages/create-wdio/tests/__snapshots__/onboarding.test.ts.snap
Normal file
|
|
@ -0,0 +1,432 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`onboarding templates > Svelte > with testing-library 1`] = `
|
||||
"
|
||||
import { $, expect } from '@wdio/globals'
|
||||
import { render, fireEvent } from '@testing-library/svelte'
|
||||
|
||||
import * as matchers from '@testing-library/jest-dom/matchers'
|
||||
expect.extend(matchers)
|
||||
|
||||
import ExampleComponent from './Component.svelte'
|
||||
import './Component.css'
|
||||
|
||||
describe('Svelte Component Testing', () => {
|
||||
|
||||
it('should test component with Testing Library', async () => {
|
||||
const { getByText } = render(ExampleComponent)
|
||||
|
||||
const component = getByText(/count is 0/i)
|
||||
expect(component).toBeInTheDocument()
|
||||
|
||||
await fireEvent.click(component)
|
||||
await fireEvent.click(component)
|
||||
|
||||
expect(getByText(/count is 2/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
|
||||
it('should test component with WebdriverIO', async () => {
|
||||
render(ExampleComponent)
|
||||
|
||||
const component = await $('button*=count is')
|
||||
await expect(component).toBePresent()
|
||||
await expect(component).toHaveText('count is 0')
|
||||
|
||||
await component.click()
|
||||
await component.click()
|
||||
|
||||
await expect(component).toHaveText('count is 2')
|
||||
})
|
||||
})
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`onboarding templates > Svelte > without testing-library 1`] = `
|
||||
"
|
||||
import { $, expect } from '@wdio/globals'
|
||||
|
||||
|
||||
import ExampleComponent from './Component.svelte'
|
||||
import './Component.css'
|
||||
|
||||
describe('Svelte Component Testing', () => {
|
||||
|
||||
let container: Element
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement('div')
|
||||
document.body.appendChild(container)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
container?.remove()
|
||||
})
|
||||
|
||||
|
||||
it('should test component with WebdriverIO', async () => {
|
||||
new ExampleComponent({ target: container, props: {} })
|
||||
|
||||
const component = await $('button*=count is')
|
||||
await expect(component).toBePresent()
|
||||
await expect(component).toHaveText('count is 0')
|
||||
|
||||
await component.click()
|
||||
await component.click()
|
||||
|
||||
await expect(component).toHaveText('count is 2')
|
||||
})
|
||||
})
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`onboarding templates > Vue > with testing-library 1`] = `
|
||||
"
|
||||
import { $, expect } from '@wdio/globals'
|
||||
import { render, fireEvent } from '@testing-library/vue'
|
||||
|
||||
import * as matchers from '@testing-library/jest-dom/matchers'
|
||||
expect.extend(matchers)
|
||||
|
||||
import ExampleComponent from './Component.vue'
|
||||
import './Component.css'
|
||||
|
||||
describe('Vue Component Testing', () => {
|
||||
|
||||
it('should test component with Testing Library', async () => {
|
||||
// The render method returns a collection of utilities to query your component.
|
||||
const { getByText } = render(ExampleComponent, {
|
||||
props: { msg: 'WebdriverIO Component Testing' }
|
||||
})
|
||||
|
||||
const component = getByText(/count is 0/i)
|
||||
expect(component).toBeInTheDocument()
|
||||
|
||||
await fireEvent.click(component)
|
||||
await fireEvent.click(component)
|
||||
|
||||
expect(getByText(/count is 2/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
|
||||
it('should test component with WebdriverIO', async () => {
|
||||
render(ExampleComponent, { props: { msg: 'WebdriverIO Component Testing' } })
|
||||
|
||||
const component = await $('button*=count is')
|
||||
await expect(component).toBePresent()
|
||||
await expect(component).toHaveText('count is 0')
|
||||
|
||||
await component.click()
|
||||
await component.click()
|
||||
|
||||
await expect(component).toHaveText('count is 2')
|
||||
})
|
||||
})
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`onboarding templates > Vue > without testing-library 1`] = `
|
||||
"
|
||||
import { $, expect } from '@wdio/globals'
|
||||
import { createApp } from 'vue'
|
||||
|
||||
import ExampleComponent from './Component.vue'
|
||||
import './Component.css'
|
||||
|
||||
describe('Vue Component Testing', () => {
|
||||
|
||||
let container: Element
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement('div')
|
||||
container.setAttribute('id', 'app')
|
||||
document.body.appendChild(container)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
container?.remove()
|
||||
})
|
||||
|
||||
|
||||
it('should test component with WebdriverIO', async () => {
|
||||
createApp(ExampleComponent, { msg: 'WebdriverIO Component Testing' }).mount(container)
|
||||
|
||||
const component = await $('button*=count is')
|
||||
await expect(component).toBePresent()
|
||||
await expect(component).toHaveText('count is 0')
|
||||
|
||||
await component.click()
|
||||
await component.click()
|
||||
|
||||
await expect(component).toHaveText('count is 2')
|
||||
})
|
||||
})
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`onboarding templates > lit > without testing-library 1`] = `
|
||||
"import { html, render } from 'lit'
|
||||
import { $, expect } from '@wdio/globals'
|
||||
|
||||
import './Component.js'
|
||||
import './Component.css'
|
||||
|
||||
describe('Lit component testing', () => {
|
||||
it('should increment value on click', async () => {
|
||||
render(
|
||||
html\`<my-element id="root">WebdriverIO Component Testing</my-element>\`,
|
||||
document.body
|
||||
)
|
||||
|
||||
const button = await $('my-element').$('button')
|
||||
await expect(button).toHaveText('count is 0')
|
||||
|
||||
await button.click()
|
||||
await button.click()
|
||||
|
||||
await expect(button).toHaveText('count is 2')
|
||||
})
|
||||
})
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`onboarding templates > preact > with testing-library 1`] = `
|
||||
"
|
||||
import { expect, $ } from '@wdio/globals'
|
||||
import { h } from 'preact'
|
||||
import { render, screen, fireEvent } from '@testing-library/preact'
|
||||
|
||||
import * as matchers from '@testing-library/jest-dom/matchers'
|
||||
expect.extend(matchers)
|
||||
|
||||
import ExampleComponent from './Component'
|
||||
|
||||
describe('Preact Component Tests', () => {
|
||||
|
||||
it('should test component with Testing Library', async () => {
|
||||
render(<ExampleComponent />)
|
||||
const component = screen.getByText(/count is 0/i)
|
||||
expect(component).toBeInTheDocument()
|
||||
|
||||
await fireEvent.click(component)
|
||||
await fireEvent.click(component)
|
||||
|
||||
expect(screen.getByText(/count is 2/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
|
||||
it('should test component with WebdriverIO', async () => {
|
||||
render(<ExampleComponent />)
|
||||
|
||||
const component = await $('button*=count is')
|
||||
await expect(component).toBePresent()
|
||||
await expect(component).toHaveText('count is 0')
|
||||
|
||||
await component.click()
|
||||
await component.click()
|
||||
|
||||
await expect(component).toHaveText('count is 2')
|
||||
})
|
||||
})
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`onboarding templates > preact > without testing-library 1`] = `
|
||||
"
|
||||
import { expect, $ } from '@wdio/globals'
|
||||
import { h, render } from 'preact'
|
||||
import htm from 'https://esm.sh/htm'
|
||||
|
||||
// Initialize htm with Preact
|
||||
const html = htm.bind(h);
|
||||
|
||||
import ExampleComponent from './Component'
|
||||
|
||||
describe('Preact Component Tests', () => {
|
||||
|
||||
let container: Element
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement('div')
|
||||
document.body.appendChild(container)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
container?.remove()
|
||||
})
|
||||
|
||||
|
||||
it('should test component with WebdriverIO', async () => {
|
||||
render(html\`<\${ExampleComponent} />\`, container)
|
||||
|
||||
const component = await $('button*=count is')
|
||||
await expect(component).toBePresent()
|
||||
await expect(component).toHaveText('count is 0')
|
||||
|
||||
await component.click()
|
||||
await component.click()
|
||||
|
||||
await expect(component).toHaveText('count is 2')
|
||||
})
|
||||
})
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`onboarding templates > react > with testing-library 1`] = `
|
||||
"
|
||||
import React from 'react'
|
||||
import { expect, $ } from '@wdio/globals'
|
||||
import { render, screen, fireEvent } from '@testing-library/react'
|
||||
|
||||
import * as matchers from '@testing-library/jest-dom/matchers'
|
||||
expect.extend(matchers)
|
||||
|
||||
import ExampleComponent from './Component'
|
||||
|
||||
describe('React Component Tests', () => {
|
||||
|
||||
it('should test component with Testing Library', async () => {
|
||||
render(<ExampleComponent />)
|
||||
const component = screen.getByText(/count is 0/i)
|
||||
expect(component).toBeInTheDocument()
|
||||
|
||||
await fireEvent.click(component)
|
||||
await fireEvent.click(component)
|
||||
|
||||
expect(screen.getByText(/count is 2/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
|
||||
it('should test component with WebdriverIO', async () => {
|
||||
render(<ExampleComponent />)
|
||||
|
||||
const component = await $('button*=count is')
|
||||
await expect(component).toBePresent()
|
||||
await expect(component).toHaveText('count is 0')
|
||||
|
||||
await component.click()
|
||||
await component.click()
|
||||
|
||||
await expect(component).toHaveText('count is 2')
|
||||
})
|
||||
})
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`onboarding templates > react > without testing-library 1`] = `
|
||||
"
|
||||
import React from 'react'
|
||||
import { expect, $ } from '@wdio/globals'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
|
||||
import ExampleComponent from './Component'
|
||||
|
||||
describe('React Component Tests', () => {
|
||||
|
||||
let container: Element
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement('div')
|
||||
document.body.appendChild(container)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
container?.remove()
|
||||
})
|
||||
|
||||
|
||||
it('should test component with WebdriverIO', async () => {
|
||||
const root = createRoot(container)
|
||||
root.render(<ExampleComponent />)
|
||||
|
||||
const component = await $('button*=count is')
|
||||
await expect(component).toBePresent()
|
||||
await expect(component).toHaveText('count is 0')
|
||||
|
||||
await component.click()
|
||||
await component.click()
|
||||
|
||||
await expect(component).toHaveText('count is 2')
|
||||
})
|
||||
})
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`onboarding templates > solid > with testing-library 1`] = `
|
||||
"
|
||||
import { expect, $ } from '@wdio/globals'
|
||||
import { cleanup, render, screen, fireEvent } from 'solid-testing-library'
|
||||
|
||||
import * as matchers from '@testing-library/jest-dom/matchers'
|
||||
expect.extend(matchers)
|
||||
|
||||
import ExampleComponent from './Component'
|
||||
|
||||
describe('Preact Component Tests', () => {
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
it('should test component with Testing Library', async () => {
|
||||
render(() => <ExampleComponent />)
|
||||
const component = screen.getByText(/count is 0/i)
|
||||
expect(component).toBeInTheDocument()
|
||||
|
||||
await fireEvent.click(component)
|
||||
await fireEvent.click(component)
|
||||
|
||||
expect(screen.getByText(/count is 2/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
|
||||
it('should test component with WebdriverIO', async () => {
|
||||
render(() => <ExampleComponent />)
|
||||
|
||||
const component = await $('button*=count is')
|
||||
await expect(component).toBePresent()
|
||||
await expect(component).toHaveText('count is 0')
|
||||
|
||||
await component.click()
|
||||
await component.click()
|
||||
|
||||
await expect(component).toHaveText('count is 2')
|
||||
})
|
||||
})
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`onboarding templates > solid > without testing-library 1`] = `
|
||||
"
|
||||
import { expect, $ } from '@wdio/globals'
|
||||
import { render } from 'solid-js/web'
|
||||
|
||||
import ExampleComponent from './Component'
|
||||
|
||||
describe('Preact Component Tests', () => {
|
||||
|
||||
let container: Element
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement('div')
|
||||
document.body.appendChild(container)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
container?.remove()
|
||||
})
|
||||
|
||||
|
||||
it('should test component with WebdriverIO', async () => {
|
||||
render(<ExampleComponent />, container)
|
||||
|
||||
const component = await $('button*=count is')
|
||||
await expect(component).toBePresent()
|
||||
await expect(component).toHaveText('count is 0')
|
||||
|
||||
await component.click()
|
||||
await component.click()
|
||||
|
||||
await expect(component).toHaveText('count is 2')
|
||||
})
|
||||
})
|
||||
"
|
||||
`;
|
||||
70
packages/create-wdio/tests/__snapshots__/utils.test.ts.snap
Normal file
70
packages/create-wdio/tests/__snapshots__/utils.test.ts.snap
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`getAnswers 1`] = `
|
||||
{
|
||||
"backend": "On my local machine",
|
||||
"framework": "@wdio/mocha-framework$--$mocha",
|
||||
"generateTestFiles": true,
|
||||
"includeVisualTesting": false,
|
||||
"isUsingTypeScript": false,
|
||||
"npmInstall": true,
|
||||
"plugins": [],
|
||||
"reporters": [
|
||||
"@wdio/spec-reporter$--$spec",
|
||||
],
|
||||
"runner": "@wdio/local-runner$--$local$--$e2e",
|
||||
"services": [],
|
||||
"usePageObjects": true,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`npmInstall 1`] = `
|
||||
[
|
||||
[
|
||||
"/foo/bar",
|
||||
[
|
||||
"foo$--$bar",
|
||||
"bar$--$foo",
|
||||
"@testing-library/vue",
|
||||
"@testing-library/jest-dom",
|
||||
"@wdio/visual-service",
|
||||
"@types/jasmine",
|
||||
],
|
||||
true,
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`setupTypeScript 1`] = `
|
||||
"{
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "node",
|
||||
"module": "ESNext",
|
||||
"target": "es2022",
|
||||
"lib": [
|
||||
"es2022",
|
||||
"dom"
|
||||
],
|
||||
"types": [
|
||||
"node",
|
||||
"@wdio/globals/types",
|
||||
"expect-webdriverio",
|
||||
"foo",
|
||||
"wdio-electron-service"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": [
|
||||
"test",
|
||||
"wdio.conf.ts"
|
||||
]
|
||||
}"
|
||||
`;
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`handler 1`] = `
|
||||
{
|
||||
"installedPackages": [
|
||||
"@wdio/local-runner",
|
||||
"@wdio/mocha-framework",
|
||||
"@sveltejs/vite-plugin-svelte",
|
||||
],
|
||||
"parsedAnswers": {
|
||||
"_async": "async ",
|
||||
"_await": "await ",
|
||||
"backend": "On my local machine",
|
||||
"baseUrl": "http://localhost",
|
||||
"destPageObjectRootPath": "",
|
||||
"destSerenityLibRootPath": "",
|
||||
"destSpecRootPath": "/foo/bar",
|
||||
"destStepRootPath": "/foo/bar",
|
||||
"esmSupport": true,
|
||||
"framework": "mocha",
|
||||
"generateTestFiles": false,
|
||||
"hasRootTSConfig": false,
|
||||
"installTestingLibrary": false,
|
||||
"isSync": false,
|
||||
"isUsingTypeScript": true,
|
||||
"npmInstall": true,
|
||||
"packagesToInstall": [
|
||||
"@wdio/local-runner",
|
||||
"@wdio/mocha-framework",
|
||||
"@sveltejs/vite-plugin-svelte",
|
||||
],
|
||||
"plugins": [],
|
||||
"preset": "svelte",
|
||||
"projectName": "my-module",
|
||||
"projectRootDir": "/foo/bar",
|
||||
"purpose": undefined,
|
||||
"rawAnswers": {
|
||||
"backend": "On my local machine",
|
||||
"baseUrl": "http://localhost",
|
||||
"framework": "@wdio/mocha-framework$--$mocha",
|
||||
"generateTestFiles": false,
|
||||
"isUsingTypeScript": true,
|
||||
"npmInstall": true,
|
||||
"plugins": [],
|
||||
"preset": "@sveltejs/vite-plugin-svelte$--$svelte",
|
||||
"reporters": [],
|
||||
"runner": "@wdio/local-runner$--$local",
|
||||
"services": [],
|
||||
},
|
||||
"relativePath": "",
|
||||
"reporters": [],
|
||||
"runner": "local",
|
||||
"serenityAdapter": false,
|
||||
"services": [],
|
||||
"specs": undefined,
|
||||
"stepDefinitions": undefined,
|
||||
"tsConfigFilePath": "/foo/bar/tsconfig.json",
|
||||
"tsProject": "./tsconfig.json",
|
||||
"usePageObjects": false,
|
||||
"useSauceConnect": undefined,
|
||||
"wdioConfigPath": "/foo/bar/wdio.conf.ts",
|
||||
},
|
||||
"success": true,
|
||||
}
|
||||
`;
|
||||
101
packages/create-wdio/tests/cli/__snapshots__/utils.test.ts.snap
Normal file
101
packages/create-wdio/tests/cli/__snapshots__/utils.test.ts.snap
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`parseAnswers 1`] = `
|
||||
{
|
||||
"_async": "async ",
|
||||
"_await": "await ",
|
||||
"backend": "On my local machine",
|
||||
"baseUrl": "http://localhost",
|
||||
"destPageObjectRootPath": "/tmp/foobar",
|
||||
"destSerenityLibRootPath": "",
|
||||
"destSpecRootPath": "/tmp/foobar",
|
||||
"destStepRootPath": "/tmp/foobar",
|
||||
"esmSupport": true,
|
||||
"framework": "mocha",
|
||||
"generateTestFiles": true,
|
||||
"hasRootTSConfig": true,
|
||||
"installTestingLibrary": false,
|
||||
"isSync": false,
|
||||
"isUsingTypeScript": false,
|
||||
"npmInstall": true,
|
||||
"packagesToInstall": [
|
||||
"@wdio/local-runner",
|
||||
"@wdio/mocha-framework",
|
||||
"@sveltejs/vite-plugin-svelte",
|
||||
"@wdio/spec-reporter",
|
||||
"wdio-wait-for",
|
||||
"@wdio/sauce-service",
|
||||
],
|
||||
"pages": "/tmp/foobar/pageobjects",
|
||||
"plugins": [
|
||||
"wait-for",
|
||||
],
|
||||
"preset": "svelte",
|
||||
"projectName": "my-module",
|
||||
"projectRootDir": "/foo/bar",
|
||||
"purpose": undefined,
|
||||
"rawAnswers": {
|
||||
"backend": "On my local machine",
|
||||
"baseUrl": "http://localhost",
|
||||
"framework": "@wdio/mocha-framework$--$mocha",
|
||||
"generateTestFiles": true,
|
||||
"isUsingTypeScript": false,
|
||||
"npmInstall": true,
|
||||
"pages": "/tmp/foobar/pageobjects",
|
||||
"plugins": [
|
||||
"wdio-wait-for$--$wait-for",
|
||||
],
|
||||
"preset": "@sveltejs/vite-plugin-svelte$--$svelte",
|
||||
"reporters": [
|
||||
"@wdio/spec-reporter$--$spec",
|
||||
],
|
||||
"runner": "@wdio/local-runner$--$local",
|
||||
"services": [
|
||||
"@wdio/sauce-service$--$sauce",
|
||||
],
|
||||
"specs": "/tmp/foobar/specs",
|
||||
"stepDefinitions": "/tmp/foobar/steps",
|
||||
"usePageObjects": true,
|
||||
},
|
||||
"relativePath": "",
|
||||
"reporters": [
|
||||
"spec",
|
||||
],
|
||||
"runner": "local",
|
||||
"serenityAdapter": false,
|
||||
"services": [
|
||||
"sauce",
|
||||
],
|
||||
"specs": "./../../tmp/foobar/specs",
|
||||
"stepDefinitions": "./../../tmp/foobar/steps",
|
||||
"tsConfigFilePath": "/tmp/foobar/tsconfig.json",
|
||||
"tsProject": "./../../tmp/foobar/tsconfig.json",
|
||||
"usePageObjects": true,
|
||||
"useSauceConnect": undefined,
|
||||
"wdioConfigPath": "/foo/bar/wdio.conf.js",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`runConfigCommand 1`] = `
|
||||
[
|
||||
[
|
||||
"
|
||||
",
|
||||
],
|
||||
[
|
||||
"
|
||||
🤖 Successfully setup project at /foo/bar 🎉
|
||||
|
||||
Join our Discord Community Server and instantly find answers to your issues or queries. Or just join and say hi 👋!
|
||||
🔗 https://discord.webdriver.io
|
||||
|
||||
Visit the project on GitHub to report bugs 🐛 or raise feature requests 💡:
|
||||
🔗 https://github.com/webdriverio/webdriverio
|
||||
|
||||
To run your tests, execute:
|
||||
$ cd /foo/bar
|
||||
$ npm run wdio
|
||||
",
|
||||
],
|
||||
]
|
||||
`;
|
||||
84
packages/create-wdio/tests/cli/config.test.ts
Normal file
84
packages/create-wdio/tests/cli/config.test.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
|
||||
import { afterEach, beforeEach, expect, test, vi } from 'vitest'
|
||||
|
||||
import {
|
||||
builder,
|
||||
handler,
|
||||
} from '../../src/cli/config.js'
|
||||
import {
|
||||
getAnswers,
|
||||
} from '../../src/utils.js'
|
||||
import { BackendChoice } from '../../src/constants.js'
|
||||
|
||||
const consoleLog = console.log.bind(console)
|
||||
beforeEach(() => {
|
||||
console.log = vi.fn()
|
||||
})
|
||||
afterEach(() => {
|
||||
console.log = consoleLog
|
||||
})
|
||||
|
||||
const isUsingWindows = os.platform() === 'win32'
|
||||
|
||||
process.cwd = () => '/foo/bar'
|
||||
|
||||
vi.mock('node:fs/promises', async (orig) => ({
|
||||
...(await orig()) as any,
|
||||
default: {
|
||||
access: vi.fn().mockRejectedValue('Yay')
|
||||
}
|
||||
}))
|
||||
vi.mock('@wdio/logger', () => import(path.join(process.cwd(), '__mocks__', '@wdio/logger')))
|
||||
vi.mock('../../src/utils.js', async () => {
|
||||
const actual = await vi.importActual('../../src/utils.js') as Promise<object>
|
||||
return {
|
||||
...actual,
|
||||
getAnswers: vi.fn(),
|
||||
getProjectProps: vi.fn().mockResolvedValue({
|
||||
path: '/foo/bar',
|
||||
esmSupported: true,
|
||||
packageJson: {
|
||||
name: 'my-module'
|
||||
}
|
||||
}),
|
||||
getProjectRoot: vi.fn().mockReturnValue('/foo/bar'),
|
||||
createPackageJSON: vi.fn(),
|
||||
npmInstall: vi.fn(),
|
||||
createWDIOConfig: vi.fn(),
|
||||
createWDIOScript: vi.fn(),
|
||||
runAppiumInstaller: vi.fn()
|
||||
}
|
||||
})
|
||||
|
||||
test('builder', () => {
|
||||
const yargs = {} as any
|
||||
yargs.options = vi.fn().mockReturnValue(yargs)
|
||||
yargs.epilogue = vi.fn().mockReturnValue(yargs)
|
||||
yargs.help = vi.fn().mockReturnValue(yargs)
|
||||
builder(yargs)
|
||||
expect(yargs.options).toBeCalledTimes(1)
|
||||
expect(yargs.options).toBeCalledWith(expect.any(Object))
|
||||
expect(yargs.epilogue).toBeCalledTimes(1)
|
||||
expect(yargs.help).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
test.skipIf(isUsingWindows)('handler', async () => {
|
||||
vi.mocked(getAnswers).mockResolvedValue({
|
||||
backend: BackendChoice.Local,
|
||||
generateTestFiles: false,
|
||||
baseUrl: 'http://localhost',
|
||||
runner: '@wdio/local-runner$--$local',
|
||||
framework: '@wdio/mocha-framework$--$mocha',
|
||||
preset: '@sveltejs/vite-plugin-svelte$--$svelte',
|
||||
isUsingTypeScript: true,
|
||||
reporters: [],
|
||||
plugins: [],
|
||||
services: [],
|
||||
npmInstall: true
|
||||
} as any)
|
||||
const runConfigCmd = vi.fn()
|
||||
expect(await handler({} as any, runConfigCmd)).toMatchSnapshot()
|
||||
expect(runConfigCmd).toBeCalledTimes(1)
|
||||
})
|
||||
|
|
@ -1,11 +1,10 @@
|
|||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { vi, describe, it, expect, afterEach, beforeEach } from 'vitest'
|
||||
// @ts-expect-error mock
|
||||
import { yargs } from 'yargs'
|
||||
|
||||
import * as installCmd from '../../src/commands/install.js'
|
||||
import * as configCmd from '../../src/commands/config.js'
|
||||
import * as installCmd from '../../src/cli/install.js'
|
||||
import * as configCmd from '../../src/cli/config.js'
|
||||
import * as utils from '../../src/utils.js'
|
||||
import { installPackages } from '../../src/install.js'
|
||||
|
||||
|
|
@ -18,7 +17,6 @@ vi.mock('node:fs/promises', async (orig) => ({
|
|||
writeFile: vi.fn().mockResolvedValue({})
|
||||
}
|
||||
}))
|
||||
vi.mock('@wdio/logger', () => import(path.join(process.cwd(), '__mocks__', '@wdio/logger')))
|
||||
vi.mock('../../src/install', () => ({
|
||||
installPackages: vi.fn(),
|
||||
getInstallCommand: vi.fn().mockReturnValue('npm install foo bar --save-dev')
|
||||
|
|
@ -36,7 +34,7 @@ const findInConfigMock = vi.spyOn(utils, 'findInConfig')
|
|||
describe('Command: install', () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(console, 'log')
|
||||
vi.spyOn(process, 'exit').mockImplementation((code?: number): never => code as never)
|
||||
vi.spyOn(process, 'exit').mockImplementation(((code?: number): never => code as never) as any)
|
||||
vi.spyOn(configCmd, 'missingConfigurationPrompt').mockImplementation(() => Promise.resolve() as any)
|
||||
vi.spyOn(utils, 'addServiceDeps')
|
||||
vi.spyOn(utils, 'replaceConfig').mockReturnValueOnce({ foo: 123 } as any)
|
||||
|
|
@ -67,7 +65,7 @@ describe('Command: install', () => {
|
|||
})
|
||||
|
||||
it('should prompt missing configuration', async () => {
|
||||
vi.spyOn(configCmd, 'formatConfigFilePaths').mockReturnValue({ fullPath: '/absolute/path/to/wdio.conf.js', fullPathNoExtension: '/absolute/path/to/wdio.conf' } as any)
|
||||
vi.spyOn(utils, 'formatConfigFilePaths').mockReturnValue({ fullPath: '/absolute/path/to/wdio.conf.js', fullPathNoExtension: '/absolute/path/to/wdio.conf' } as any)
|
||||
vi.spyOn(configCmd, 'missingConfigurationPrompt').mockImplementation(() => Promise.reject())
|
||||
vi.mocked(fs.access).mockRejectedValue(new Error('Doesn\'t exist'))
|
||||
|
||||
|
|
@ -1,41 +1,14 @@
|
|||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import fs from 'node:fs/promises'
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, test, vi } from 'vitest'
|
||||
import inquirer from 'inquirer'
|
||||
|
||||
import {
|
||||
builder,
|
||||
canAccessConfigPath,
|
||||
handler,
|
||||
missingConfigurationPrompt,
|
||||
parseAnswers,
|
||||
runConfigCommand,
|
||||
} from '../../src/commands/config.js'
|
||||
import {
|
||||
createPackageJSON,
|
||||
createWDIOConfig,
|
||||
createWDIOScript,
|
||||
getAnswers,
|
||||
npmInstall,
|
||||
runAppiumInstaller,
|
||||
} from '../../src/utils.js'
|
||||
import { vi, test, expect, describe, beforeEach, it } from 'vitest'
|
||||
import { canAccessConfigPath, missingConfigurationPrompt, parseAnswers, runConfigCommand } from '../../src/cli/utils.js'
|
||||
import { createPackageJSON, npmInstall, createWDIOConfig, createWDIOScript, runAppiumInstaller, getAnswers } from '../../src/utils.js'
|
||||
import { BackendChoice } from '../../src/constants.js'
|
||||
|
||||
import inquirer from 'inquirer'
|
||||
import path from 'node:path'
|
||||
import type { Questionnair } from '../../src/types.js'
|
||||
|
||||
const consoleLog = console.log.bind(console)
|
||||
beforeEach(() => {
|
||||
console.log = vi.fn()
|
||||
})
|
||||
afterEach(() => {
|
||||
console.log = consoleLog
|
||||
})
|
||||
|
||||
const isUsingWindows = os.platform() === 'win32'
|
||||
|
||||
process.cwd = () => '/foo/bar'
|
||||
|
||||
vi.mock('node:fs/promises', async (orig) => ({
|
||||
...(await orig()) as any,
|
||||
default: {
|
||||
|
|
@ -43,7 +16,6 @@ vi.mock('node:fs/promises', async (orig) => ({
|
|||
}
|
||||
}))
|
||||
vi.mock('inquirer')
|
||||
vi.mock('@wdio/logger', () => import(path.join(process.cwd(), '__mocks__', '@wdio/logger')))
|
||||
vi.mock('../../src/utils.js', async () => {
|
||||
const actual = await vi.importActual('../../src/utils.js') as Promise<object>
|
||||
return {
|
||||
|
|
@ -65,44 +37,13 @@ vi.mock('../../src/utils.js', async () => {
|
|||
}
|
||||
})
|
||||
|
||||
test('builder', () => {
|
||||
const yargs = {} as any
|
||||
yargs.options = vi.fn().mockReturnValue(yargs)
|
||||
yargs.epilogue = vi.fn().mockReturnValue(yargs)
|
||||
yargs.help = vi.fn().mockReturnValue(yargs)
|
||||
builder(yargs)
|
||||
expect(yargs.options).toBeCalledTimes(1)
|
||||
expect(yargs.options).toBeCalledWith(expect.any(Object))
|
||||
expect(yargs.epilogue).toBeCalledTimes(1)
|
||||
expect(yargs.help).toBeCalledTimes(1)
|
||||
})
|
||||
vi.mock('../src/install', () => ({
|
||||
installPackages: vi.fn(),
|
||||
getInstallCommand: vi.fn().mockReturnValue('npm install foo bar --save-dev')
|
||||
}))
|
||||
global.console.log = vi.fn()
|
||||
|
||||
test.skipIf(isUsingWindows)('parseAnswers', async () => {
|
||||
vi.mocked(getAnswers).mockResolvedValue({
|
||||
backend: BackendChoice.Local,
|
||||
specs: '/tmp/foobar/specs',
|
||||
pages: '/tmp/foobar/pageobjects',
|
||||
generateTestFiles: true,
|
||||
usePageObjects: true,
|
||||
baseUrl: 'http://localhost',
|
||||
runner: '@wdio/local-runner$--$local',
|
||||
framework: '@wdio/mocha-framework$--$mocha',
|
||||
preset: '@sveltejs/vite-plugin-svelte$--$svelte',
|
||||
isUsingTypeScript: false,
|
||||
reporters: [
|
||||
'@wdio/spec-reporter$--$spec'
|
||||
],
|
||||
plugins: [
|
||||
'wdio-wait-for$--$wait-for'
|
||||
],
|
||||
services: [
|
||||
'@wdio/sauce-service$--$sauce'
|
||||
],
|
||||
npmInstall: true
|
||||
})
|
||||
const parsedAnswers = await parseAnswers(true)
|
||||
expect(parsedAnswers).toMatchSnapshot()
|
||||
})
|
||||
const isUsingWindows = os.platform() === 'win32'
|
||||
|
||||
test('runConfigCommand', async () => {
|
||||
await runConfigCommand({ projectRootDir: '/foo/bar' } as any, 'next')
|
||||
|
|
@ -114,64 +55,6 @@ test('runConfigCommand', async () => {
|
|||
expect(vi.mocked(console.log).mock.calls).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test.skipIf(isUsingWindows)('handler', async () => {
|
||||
vi.mocked(getAnswers).mockResolvedValue({
|
||||
backend: BackendChoice.Local,
|
||||
generateTestFiles: false,
|
||||
baseUrl: 'http://localhost',
|
||||
runner: '@wdio/local-runner$--$local',
|
||||
framework: '@wdio/mocha-framework$--$mocha',
|
||||
preset: '@sveltejs/vite-plugin-svelte$--$svelte',
|
||||
isUsingTypeScript: true,
|
||||
reporters: [],
|
||||
plugins: [],
|
||||
services: [],
|
||||
npmInstall: true
|
||||
})
|
||||
const runConfigCmd = vi.fn()
|
||||
expect(await handler({} as any, runConfigCmd)).toMatchSnapshot()
|
||||
expect(runConfigCmd).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
test('missingConfigurationPrompt does not init wizard if user does not want to', async () => {
|
||||
vi.mocked(getAnswers).mockResolvedValue({
|
||||
backend: BackendChoice.Local,
|
||||
generateTestFiles: false,
|
||||
baseUrl: 'http://localhost',
|
||||
runner: '@wdio/local-runner$--$local',
|
||||
framework: '@wdio/mocha-framework$--$mocha',
|
||||
preset: '@sveltejs/vite-plugin-svelte$--$svelte',
|
||||
reporters: [],
|
||||
plugins: [],
|
||||
services: [],
|
||||
npmInstall: true
|
||||
})
|
||||
const runConfigCmd = vi.fn()
|
||||
vi.mocked(inquirer.prompt).mockResolvedValue({})
|
||||
await missingConfigurationPrompt('config', 'foobar', runConfigCmd)
|
||||
expect(runConfigCmd).toBeCalledTimes(0)
|
||||
})
|
||||
|
||||
test('missingConfigurationPrompt does run config if user agrees', async () => {
|
||||
vi.mocked(getAnswers).mockResolvedValue({
|
||||
backend: BackendChoice.Local,
|
||||
generateTestFiles: false,
|
||||
baseUrl: 'http://localhost',
|
||||
runner: '@wdio/local-runner$--$local',
|
||||
framework: '@wdio/mocha-framework$--$mocha',
|
||||
preset: '@sveltejs/vite-plugin-svelte$--$svelte',
|
||||
reporters: [],
|
||||
plugins: [],
|
||||
services: [],
|
||||
npmInstall: true
|
||||
})
|
||||
const runConfigCmd = vi.fn()
|
||||
|
||||
vi.mocked(inquirer.prompt).mockResolvedValue({ config: true })
|
||||
await missingConfigurationPrompt('config', 'foobar', runConfigCmd)
|
||||
expect(runConfigCmd).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
describe('canAccessConfigPath', () => {
|
||||
beforeEach(()=>{
|
||||
vi.mocked(fs.access).mockClear()
|
||||
|
|
@ -212,6 +95,52 @@ describe('canAccessConfigPath', () => {
|
|||
expect(fs.access).toBeCalledWith('/foo/bar.mts')
|
||||
})
|
||||
})
|
||||
test('missingConfigurationPrompt does not init wizard if user does not want to', async () => {
|
||||
vi.mocked(getAnswers).mockResolvedValue({
|
||||
backend: BackendChoice.Local,
|
||||
generateTestFiles: false,
|
||||
baseUrl: 'http://localhost',
|
||||
runner: '@wdio/local-runner$--$local',
|
||||
framework: '@wdio/mocha-framework$--$mocha',
|
||||
preset: '@sveltejs/vite-plugin-svelte$--$svelte',
|
||||
reporters: [],
|
||||
plugins: [],
|
||||
services: [],
|
||||
npmInstall: true
|
||||
} as any)
|
||||
const runConfigCmd = vi.fn()
|
||||
vi.mocked(inquirer.prompt).mockResolvedValue({})
|
||||
await missingConfigurationPrompt('config', 'foobar', runConfigCmd)
|
||||
expect(runConfigCmd).toBeCalledTimes(0)
|
||||
})
|
||||
|
||||
test.skipIf(isUsingWindows)('parseAnswers', async () => {
|
||||
vi.mocked(getAnswers).mockResolvedValue({
|
||||
backend: BackendChoice.Local,
|
||||
specs: '/tmp/foobar/specs',
|
||||
pages: '/tmp/foobar/pageobjects',
|
||||
stepDefinitions: '/tmp/foobar/steps',
|
||||
generateTestFiles: true,
|
||||
usePageObjects: true,
|
||||
baseUrl: 'http://localhost',
|
||||
runner: '@wdio/local-runner$--$local',
|
||||
framework: '@wdio/mocha-framework$--$mocha',
|
||||
preset: '@sveltejs/vite-plugin-svelte$--$svelte',
|
||||
isUsingTypeScript: false,
|
||||
reporters: [
|
||||
'@wdio/spec-reporter$--$spec'
|
||||
],
|
||||
plugins: [
|
||||
'wdio-wait-for$--$wait-for'
|
||||
],
|
||||
services: [
|
||||
'@wdio/sauce-service$--$sauce'
|
||||
],
|
||||
npmInstall: true
|
||||
} as any)
|
||||
const parsedAnswers = await parseAnswers(true)
|
||||
expect(parsedAnswers).toMatchSnapshot()
|
||||
})
|
||||
|
||||
describe('Serenity/JS project generation', () => {
|
||||
|
||||
|
|
@ -231,7 +160,7 @@ describe('Serenity/JS project generation', () => {
|
|||
services: [],
|
||||
npmInstall: true,
|
||||
isUsingTypeScript: false,
|
||||
}
|
||||
} as any
|
||||
|
||||
it('marks serenityAdapter as false and destSerenityLibRootPath as blank if not using Serenity/JS', async () => {
|
||||
vi.mocked(getAnswers).mockResolvedValue({
|
||||
|
|
@ -456,3 +385,23 @@ describe('Serenity/JS project generation', () => {
|
|||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('missingConfigurationPrompt does run config if user agrees', async () => {
|
||||
vi.mocked(getAnswers).mockResolvedValue({
|
||||
backend: BackendChoice.Local,
|
||||
generateTestFiles: false,
|
||||
baseUrl: 'http://localhost',
|
||||
runner: '@wdio/local-runner$--$local',
|
||||
framework: '@wdio/mocha-framework$--$mocha',
|
||||
preset: '@sveltejs/vite-plugin-svelte$--$svelte',
|
||||
reporters: [],
|
||||
plugins: [],
|
||||
services: [],
|
||||
npmInstall: true
|
||||
} as any)
|
||||
const runConfigCmd = vi.fn()
|
||||
|
||||
vi.mocked(inquirer.prompt).mockResolvedValue({ config: true })
|
||||
await missingConfigurationPrompt('config', 'foobar', runConfigCmd)
|
||||
expect(runConfigCmd).toBeCalledTimes(1)
|
||||
})
|
||||
6
packages/create-wdio/tests/config/cli.test.ts
Normal file
6
packages/create-wdio/tests/config/cli.test.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { expect, test } from 'vitest'
|
||||
|
||||
test('should export config', async ()=>{
|
||||
const { config }= await import('../../src/config/cli.js')
|
||||
expect(config).toBeDefined()
|
||||
})
|
||||
21
packages/create-wdio/tests/constants.test.ts
Normal file
21
packages/create-wdio/tests/constants.test.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { describe, it, expect } from 'vitest'
|
||||
|
||||
import { convertPackageHashToObject } from '../src/utils.js'
|
||||
import { SUPPORTED_PACKAGES } from '../src/constants.js'
|
||||
|
||||
const supportedInstallations = [
|
||||
...SUPPORTED_PACKAGES.runner.map(({ value }) => convertPackageHashToObject(value)),
|
||||
...SUPPORTED_PACKAGES.plugin.map(({ value }) => convertPackageHashToObject(value)),
|
||||
...SUPPORTED_PACKAGES.service.map(({ value }) => convertPackageHashToObject(value)),
|
||||
...SUPPORTED_PACKAGES.reporter.map(({ value }) => convertPackageHashToObject(value)),
|
||||
...SUPPORTED_PACKAGES.framework.map(({ value }) => convertPackageHashToObject(value))
|
||||
]
|
||||
|
||||
describe('constants', () => {
|
||||
describe('plugin list', () => {
|
||||
it('should provide all a short name', () => {
|
||||
const pluginsWithoutShorts = supportedInstallations.filter((plugin) => !plugin.short)
|
||||
expect(pluginsWithoutShorts).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
333
packages/create-wdio/tests/index.test.ts
Normal file
333
packages/create-wdio/tests/index.test.ts
Normal file
|
|
@ -0,0 +1,333 @@
|
|||
import fs from 'node:fs/promises'
|
||||
import semver from 'semver'
|
||||
import { resolve } from 'import-meta-resolve'
|
||||
import { vi, test, expect, beforeEach, afterEach, describe, it } from 'vitest'
|
||||
import { Command } from 'commander'
|
||||
import { execSync } from 'node:child_process'
|
||||
import { execa } from 'execa'
|
||||
import { run, createWebdriverIO } from '../src/index.js'
|
||||
import { runProgram } from '../src/utils.js'
|
||||
import type { ProgramOpts } from '../src/types.js'
|
||||
import { getInstallCommand, installPackages } from '../src/install.js'
|
||||
|
||||
vi.mock('node:fs/promises', () => ({
|
||||
default: {
|
||||
access: vi.fn(),
|
||||
mkdir: vi.fn(),
|
||||
writeFile: vi.fn()
|
||||
}
|
||||
}))
|
||||
vi.mock('node:child_process', () => ({
|
||||
execSync: vi.fn()
|
||||
}))
|
||||
vi.mock('import-meta-resolve', () => ({
|
||||
resolve: vi.fn()
|
||||
}))
|
||||
vi.mock('commander')
|
||||
vi.mock('semver', () => ({
|
||||
default: {
|
||||
satisfies: vi.fn().mockReturnValue(true)
|
||||
}
|
||||
}))
|
||||
vi.mock('../src/utils.js', async (importActual) => {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
const actual = await importActual<typeof import('../src/utils.js')>()
|
||||
return {
|
||||
...actual,
|
||||
runProgram: vi.fn(),
|
||||
colorItBold: vi.fn((log) => log),
|
||||
colorIt: vi.fn((log) => log),
|
||||
getPackageVersion: vi.fn().mockReturnValue('0.1.1'),
|
||||
shouldUseYarn: vi.fn().mockReturnValue(true)
|
||||
}
|
||||
})
|
||||
|
||||
const consoleLog = console.log.bind(console)
|
||||
beforeEach(() => {
|
||||
console.log = vi.fn()
|
||||
vi.mocked(runProgram).mockClear()
|
||||
vi.mocked(resolve).mockImplementation(() => {
|
||||
throw new Error('foo')
|
||||
})
|
||||
vi.mocked(execSync).mockReturnValue(`
|
||||
├── corepack@0.20.0
|
||||
├── npm@10.2.0
|
||||
└── yarn@1.22.19
|
||||
`)
|
||||
vi.mocked(fs.access).mockResolvedValue()
|
||||
})
|
||||
|
||||
test('run', async () => {
|
||||
const op = vi.fn().mockResolvedValue({})
|
||||
await run(op)
|
||||
|
||||
expect(op).toBeCalledTimes(1)
|
||||
expect(op).toBeCalledWith('foobar')
|
||||
expect(new Command().arguments).toBeCalledWith('[project-path]')
|
||||
expect(console.log).toBeCalledTimes(1)
|
||||
expect(vi.mocked(console.log).mock.calls[0][0]).toContain('oooooooooooo')
|
||||
expect(vi.mocked(console.log).mock.calls[0][1]).toContain('Next-gen browser and mobile automation')
|
||||
})
|
||||
|
||||
test('does not run if Node.js version is too low', async () => {
|
||||
const consoleLog = vi.spyOn(console, 'log')
|
||||
vi.mocked(semver.satisfies).mockReturnValue(false)
|
||||
const op = vi.fn().mockResolvedValue({})
|
||||
await run(op)
|
||||
expect(op).toBeCalledTimes(0)
|
||||
expect(consoleLog).toBeCalledWith(expect.stringContaining('Please update to Node.js v20 to continue.'))
|
||||
})
|
||||
|
||||
test('createWebdriverIO with Yarn', async () => {
|
||||
vi.stubEnv('npm_config_user_agent', 'yarn/4.5.0 npm/? node/v20.11.0 darwin arm64')
|
||||
await createWebdriverIO({ npmTag: 'latest' } as ProgramOpts)
|
||||
expect(runProgram).toBeCalledWith(
|
||||
'yarn',
|
||||
['add', '@wdio/cli@latest'],
|
||||
expect.any(Object)
|
||||
)
|
||||
expect(runProgram).toBeCalledWith(
|
||||
'yarn',
|
||||
['exec', 'wdio', 'config', '--npm-tag', 'latest'],
|
||||
expect.any(Object)
|
||||
)
|
||||
expect(runProgram).toBeCalledTimes(2)
|
||||
expect(fs.mkdir).toBeCalledTimes(0)
|
||||
expect(fs.writeFile).toBeCalledTimes(0)
|
||||
})
|
||||
|
||||
test('createWebdriverIO with NPM', async () => {
|
||||
vi.stubEnv('npm_config_user_agent', 'npm/10.2.4 node/v20.11.0 darwin arm64 workspaces/false')
|
||||
await createWebdriverIO({ npmTag: 'latest' } as ProgramOpts)
|
||||
expect(runProgram).toBeCalledWith(
|
||||
'npm',
|
||||
['install', '@wdio/cli@latest'],
|
||||
expect.any(Object)
|
||||
)
|
||||
expect(runProgram).toBeCalledWith(
|
||||
'npx',
|
||||
['wdio', 'config', '--npm-tag', 'latest'],
|
||||
expect.any(Object)
|
||||
)
|
||||
expect(runProgram).toBeCalledTimes(2)
|
||||
expect(fs.mkdir).toBeCalledTimes(0)
|
||||
expect(fs.writeFile).toBeCalledTimes(0)
|
||||
})
|
||||
|
||||
test('createWebdriverIO with invalid agent should run npm commands', async () => {
|
||||
vi.stubEnv('npm_config_user_agent', 'invalid/10.2.4 node/v20.11.0 darwin arm64 workspaces/false')
|
||||
await createWebdriverIO({ npmTag: 'latest' } as ProgramOpts)
|
||||
expect(runProgram).toBeCalledWith(
|
||||
'npm',
|
||||
['install', '@wdio/cli@latest'],
|
||||
expect.any(Object)
|
||||
)
|
||||
expect(runProgram).toBeCalledWith(
|
||||
'npx',
|
||||
['wdio', 'config', '--npm-tag', 'latest'],
|
||||
expect.any(Object)
|
||||
)
|
||||
expect(runProgram).toBeCalledTimes(2)
|
||||
expect(fs.mkdir).toBeCalledTimes(0)
|
||||
expect(fs.writeFile).toBeCalledTimes(0)
|
||||
})
|
||||
|
||||
test('createWebdriverIO with no npm user agent should run npm commands', async () => {
|
||||
vi.stubEnv('npm_config_user_agent', '')
|
||||
await createWebdriverIO({ npmTag: 'latest' } as ProgramOpts)
|
||||
expect(runProgram).toBeCalledWith(
|
||||
'npm',
|
||||
['install', '@wdio/cli@latest'],
|
||||
expect.any(Object)
|
||||
)
|
||||
expect(runProgram).toBeCalledWith(
|
||||
'npx',
|
||||
['wdio', 'config', '--npm-tag', 'latest'],
|
||||
expect.any(Object)
|
||||
)
|
||||
expect(runProgram).toBeCalledTimes(2)
|
||||
expect(fs.mkdir).toBeCalledTimes(0)
|
||||
expect(fs.writeFile).toBeCalledTimes(0)
|
||||
})
|
||||
|
||||
test('createWebdriverIO with pnpm', async () => {
|
||||
vi.stubEnv('npm_config_user_agent', 'pnpm/9.10.0 npm/? node/v20.11.0 darwin arm64')
|
||||
await createWebdriverIO({ npmTag: 'latest' } as ProgramOpts)
|
||||
expect(runProgram).toBeCalledWith(
|
||||
'pnpm',
|
||||
['add', '@wdio/cli@latest'],
|
||||
expect.any(Object)
|
||||
)
|
||||
expect(runProgram).toBeCalledWith(
|
||||
'pnpm',
|
||||
['exec', 'wdio', 'config', '--npm-tag', 'latest'],
|
||||
expect.any(Object)
|
||||
)
|
||||
expect(runProgram).toBeCalledTimes(2)
|
||||
expect(fs.mkdir).toBeCalledTimes(0)
|
||||
expect(fs.writeFile).toBeCalledTimes(0)
|
||||
})
|
||||
|
||||
test('createWebdriverIO with bun', async () => {
|
||||
vi.stubEnv('npm_config_user_agent', 'bun/1.1.27 npm/? node/v22.6.0 darwin arm64')
|
||||
await createWebdriverIO({ npmTag: 'latest' } as ProgramOpts)
|
||||
expect(runProgram).toBeCalledWith(
|
||||
'bun',
|
||||
['install', '@wdio/cli@latest'],
|
||||
expect.any(Object)
|
||||
)
|
||||
expect(runProgram).toBeCalledWith(
|
||||
'bunx',
|
||||
['wdio', 'config', '--npm-tag', 'latest'],
|
||||
expect.any(Object)
|
||||
)
|
||||
expect(runProgram).toBeCalledTimes(2)
|
||||
expect(fs.mkdir).toBeCalledTimes(0)
|
||||
expect(fs.writeFile).toBeCalledTimes(0)
|
||||
})
|
||||
|
||||
test('creates a directory if it does not exist', async () => {
|
||||
vi.stubEnv('npm_config_user_agent', 'npm/10.2.4 node/v20.11.0 darwin arm64 workspaces/false')
|
||||
await createWebdriverIO({ npmTag: 'latest', dev: true } as ProgramOpts)
|
||||
expect(runProgram).toBeCalledWith(
|
||||
'npm',
|
||||
['install', '--save-dev', '@wdio/cli@latest'],
|
||||
expect.any(Object)
|
||||
)
|
||||
expect(runProgram).toBeCalledWith(
|
||||
'npx',
|
||||
['wdio', 'config', '--npm-tag', 'latest'],
|
||||
expect.any(Object)
|
||||
)
|
||||
expect(runProgram).toBeCalledTimes(2)
|
||||
expect(fs.mkdir).toBeCalledTimes(0)
|
||||
expect(fs.writeFile).toBeCalledTimes(0)
|
||||
})
|
||||
|
||||
test('does not install the @wdio/cli package when the @wdio/cli package is already installed in the current project', async () => {
|
||||
vi.stubEnv('npm_config_user_agent', 'npm/10.2.4 node/v20.11.0 darwin arm64 workspaces/false')
|
||||
vi.mocked(resolve).mockReturnValue('/Users/user/dev/my-monorepo/package.json')
|
||||
await createWebdriverIO({ npmTag: 'latest' } as ProgramOpts)
|
||||
expect(runProgram).toBeCalledWith(
|
||||
'npx',
|
||||
['wdio', 'config', '--npm-tag', 'latest'],
|
||||
expect.any(Object)
|
||||
)
|
||||
expect(runProgram).toBeCalledTimes(1)
|
||||
expect(fs.mkdir).toBeCalledTimes(0)
|
||||
expect(fs.writeFile).toBeCalledTimes(0)
|
||||
})
|
||||
|
||||
test('does not install the @wdio/cli package when the @wdio/cli package is already installed globally', async () => {
|
||||
vi.stubEnv('npm_config_user_agent', 'npm/10.2.4 node/v20.11.0 darwin arm64 workspaces/false')
|
||||
vi.mocked(execSync).mockReturnValue(`
|
||||
├── @wdio/cli@8.24.3
|
||||
├── corepack@0.20.0
|
||||
├── npm@10.2.0
|
||||
└── yarn@1.22.19
|
||||
`)
|
||||
await createWebdriverIO({ npmTag: 'latest' } as ProgramOpts)
|
||||
expect(runProgram).toBeCalledWith(
|
||||
'npx',
|
||||
['wdio', 'config', '--npm-tag', 'latest'],
|
||||
expect.any(Object)
|
||||
)
|
||||
expect(runProgram).toBeCalledTimes(1)
|
||||
expect(fs.mkdir).toBeCalledTimes(0)
|
||||
expect(fs.writeFile).toBeCalledTimes(0)
|
||||
})
|
||||
|
||||
test('runs the wdio config command with --yes when the yes option is set to true', async () => {
|
||||
vi.stubEnv('npm_config_user_agent', 'npm/10.2.4 node/v20.11.0 darwin arm64 workspaces/false')
|
||||
await createWebdriverIO({ npmTag: 'latest', yes: true } as ProgramOpts)
|
||||
expect(runProgram).toBeCalledWith(
|
||||
'npm',
|
||||
['install', '@wdio/cli@latest'],
|
||||
expect.any(Object)
|
||||
)
|
||||
expect(runProgram).toBeCalledWith(
|
||||
'npx',
|
||||
['wdio', 'config', '--yes', '--npm-tag', 'latest'],
|
||||
expect.any(Object)
|
||||
)
|
||||
expect(runProgram).toBeCalledTimes(2)
|
||||
expect(fs.mkdir).toBeCalledTimes(0)
|
||||
expect(fs.writeFile).toBeCalledTimes(0)
|
||||
})
|
||||
|
||||
test('does create a package.json to be used by the wdio config command when one does not exist', async () => {
|
||||
vi.stubEnv('npm_config_user_agent', 'npm/10.2.4 node/v20.11.0 darwin arm64 workspaces/false')
|
||||
vi.mocked(fs.access).mockRejectedValue(new Error('not existing'))
|
||||
await createWebdriverIO({ npmTag: 'next' } as ProgramOpts)
|
||||
expect(runProgram).toBeCalledWith(
|
||||
'npm',
|
||||
['install', '@wdio/cli@next'],
|
||||
expect.any(Object)
|
||||
)
|
||||
expect(runProgram).toBeCalledWith(
|
||||
'npx',
|
||||
['wdio', 'config', '--npm-tag', 'next'],
|
||||
expect.any(Object)
|
||||
)
|
||||
expect(runProgram).toBeCalledTimes(2)
|
||||
expect(fs.mkdir).toBeCalledTimes(1)
|
||||
expect(fs.writeFile).toBeCalledTimes(1)
|
||||
expect(fs.writeFile).toBeCalledWith(
|
||||
expect.any(String),
|
||||
JSON.stringify({ name: 'someProjectName', type: 'module' }, null, 2),
|
||||
)
|
||||
})
|
||||
|
||||
test('installs the next version when the npmTag option is set to "next"', async () => {
|
||||
vi.stubEnv('npm_config_user_agent', 'npm/10.2.4 node/v20.11.0 darwin arm64 workspaces/false')
|
||||
await createWebdriverIO({ npmTag: 'next' } as ProgramOpts)
|
||||
expect(runProgram).toBeCalledWith(
|
||||
'npm',
|
||||
['install', '@wdio/cli@next'],
|
||||
expect.any(Object)
|
||||
)
|
||||
expect(runProgram).toBeCalledWith(
|
||||
'npx',
|
||||
['wdio', 'config', '--npm-tag', 'next'],
|
||||
expect.any(Object)
|
||||
)
|
||||
expect(runProgram).toBeCalledTimes(2)
|
||||
expect(fs.mkdir).toBeCalledTimes(0)
|
||||
expect(fs.writeFile).toBeCalledTimes(0)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.mocked(fs.access).mockClear()
|
||||
vi.mocked(fs.mkdir).mockClear()
|
||||
vi.mocked(fs.writeFile).mockClear()
|
||||
vi.mocked(runProgram).mockClear()
|
||||
vi.mocked(resolve).mockClear()
|
||||
vi.mocked(execSync).mockClear()
|
||||
console.log = consoleLog
|
||||
})
|
||||
|
||||
vi.mock('execa', () => ({
|
||||
execa: vi.fn().mockResolvedValue({ stdout: 'foo', stderr: 'bar', exitCode: 0 })
|
||||
}))
|
||||
|
||||
vi.mock('detect', () => ({
|
||||
detect: vi.fn().mockResolvedValue('pnpm')
|
||||
}))
|
||||
|
||||
describe('install', () => {
|
||||
it('installPackages passing', async () => {
|
||||
expect(await installPackages('/foo/bar', ['foo', 'bar'], true))
|
||||
.toBe(true)
|
||||
})
|
||||
|
||||
it('installPackages failing', async () => {
|
||||
vi.mocked(execa).mockResolvedValue({ stdout: 'foo', stderr: 'bar', exitCode: 1 } as any)
|
||||
expect(await installPackages('/foo/bar', ['foo', 'bar'], false))
|
||||
.toBe(false)
|
||||
})
|
||||
|
||||
it('getInstallCommand', () => {
|
||||
expect(getInstallCommand('pnpm', ['foo', 'bar'], true))
|
||||
.toBe('pnpm add foo bar --save-dev')
|
||||
})
|
||||
})
|
||||
6
packages/create-wdio/tests/install/cli.test.ts
Normal file
6
packages/create-wdio/tests/install/cli.test.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { expect, test } from 'vitest'
|
||||
|
||||
test('should export install', async ()=>{
|
||||
const { install }= await import('../../src/install/cli.js')
|
||||
expect(install).toBeDefined()
|
||||
})
|
||||
736
packages/create-wdio/tests/utils.test.ts
Normal file
736
packages/create-wdio/tests/utils.test.ts
Normal file
|
|
@ -0,0 +1,736 @@
|
|||
import fs from 'node:fs/promises'
|
||||
import * as cp from 'node:child_process'
|
||||
import { vi, test, expect, beforeEach, afterEach, describe, it } from 'vitest'
|
||||
import readDir from 'recursive-readdir'
|
||||
import ejs from 'ejs'
|
||||
import { $ } from 'execa'
|
||||
import { runProgram, getPackageVersion,
|
||||
|
||||
convertPackageHashToObject,
|
||||
generateTestFiles,
|
||||
getPathForFileGeneration,
|
||||
getDefaultFiles,
|
||||
getProjectRoot,
|
||||
getAnswers,
|
||||
getProjectProps,
|
||||
createPackageJSON,
|
||||
npmInstall,
|
||||
setupTypeScript,
|
||||
createWDIOConfig,
|
||||
createWDIOScript,
|
||||
runAppiumInstaller,
|
||||
detectCompiler,
|
||||
findInConfig,
|
||||
replaceConfig,
|
||||
formatConfigFilePaths,
|
||||
|
||||
} from '../src/utils.js'
|
||||
import { parseAnswers } from '../src/cli/utils.js'
|
||||
import path from 'node:path'
|
||||
import { readPackageUp } from 'read-pkg-up'
|
||||
import type { Questionnair } from '../src/types.js'
|
||||
import inquirer from 'inquirer'
|
||||
import { installPackages } from '../src/install.js'
|
||||
|
||||
const consoleLog = console.log.bind(console)
|
||||
const processExit = process.exit.bind(process)
|
||||
|
||||
vi.mock('node:fs/promises', () => ({
|
||||
default: {
|
||||
access: vi.fn().mockRejectedValue(new Error('ENOENT')),
|
||||
mkdir: vi.fn(),
|
||||
writeFile: vi.fn().mockReturnValue(Promise.resolve())
|
||||
}
|
||||
}))
|
||||
vi.mock('child_process', () => {
|
||||
const m = {
|
||||
execSyncRes: 'APPIUM_MISSING',
|
||||
execSync: () => m.execSyncRes,
|
||||
exec: vi.fn(),
|
||||
spawn: vi.fn().mockReturnValue({ on: vi.fn().mockImplementation((ev, fn) => fn(0)) })
|
||||
}
|
||||
return m
|
||||
})
|
||||
vi.mock('inquirer')
|
||||
vi.mock('read-pkg-up')
|
||||
vi.mock('ejs')
|
||||
vi.mock('execa', () => ({
|
||||
execa: vi.fn(()=>({ stdout:'', stderr:'', exitCode:0 })),
|
||||
$: vi.fn().mockReturnValue(async (sh: string) => sh)
|
||||
}))
|
||||
vi.mock('recursive-readdir', () => ({
|
||||
default: vi.fn().mockResolvedValue([
|
||||
'/foo/bar/loo/page.js.ejs',
|
||||
'/foo/bar/example.e2e.js'
|
||||
] as any)
|
||||
}))
|
||||
vi.mock('../src/install.js', () => ({
|
||||
installPackages: vi.fn(),
|
||||
getInstallCommand: vi.fn().mockReturnValue('npm install foo bar --save-dev')
|
||||
}))
|
||||
|
||||
beforeEach(() => {
|
||||
process.exit = vi.fn() as any
|
||||
console.log = vi.fn()
|
||||
vi.mocked(readPackageUp).mockResolvedValue({
|
||||
path: '/foo/package.json',
|
||||
packageJson: {
|
||||
name: 'cool-test-module',
|
||||
type: 'module'
|
||||
}
|
||||
})
|
||||
})
|
||||
afterEach(() => {
|
||||
process.exit = processExit
|
||||
console.log = consoleLog
|
||||
})
|
||||
|
||||
test('runProgram', async () => {
|
||||
expect(await runProgram('echo', ['123'], {})).toBe(undefined)
|
||||
|
||||
await runProgram('node', ['-e', 'throw new Error(\'ups\')'], {}).catch((e) => e)
|
||||
expect(vi.mocked(console.log).mock.calls[0][0]).toMatch(/Error calling: node -e throw new Error/)
|
||||
expect(process.exit).toBeCalledTimes(1)
|
||||
|
||||
await runProgram('foobarloo', [], {}).catch((e) => e)
|
||||
|
||||
expect(vi.mocked(console.log).mock.calls[1][0]).toMatch(/spawn foobarloo ENOENT/)
|
||||
expect(process.exit).toBeCalledTimes(2)
|
||||
})
|
||||
|
||||
test('getPackageVersion', async () => {
|
||||
expect(await getPackageVersion()).toEqual(expect.any(String))
|
||||
})
|
||||
|
||||
describe('convertPackageHashToObject', () => {
|
||||
it('works with default `$--$` hash', () => {
|
||||
expect(convertPackageHashToObject('test/package-name$--$package-name')).toMatchObject({
|
||||
package: 'test/package-name',
|
||||
short: 'package-name'
|
||||
})
|
||||
})
|
||||
|
||||
it('works with custom hash', () => {
|
||||
expect(convertPackageHashToObject('test/package-name##-##package-name', '##-##')).toMatchObject({
|
||||
package: 'test/package-name',
|
||||
short: 'package-name'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('generateTestFiles', () => {
|
||||
it('Mocha with page objects', async () => {
|
||||
vi.mocked(readDir).mockResolvedValue([
|
||||
'/foo/bar/loo/page.js.ejs',
|
||||
'/foo/bar/example.e2e.js'
|
||||
] as any)
|
||||
const answers = {
|
||||
runner: 'local',
|
||||
framework: 'mocha',
|
||||
usePageObjects: true,
|
||||
generateTestFiles: true,
|
||||
destPageObjectRootPath: '/tests/page/objects/model',
|
||||
destSpecRootPath: '/tests/specs'
|
||||
}
|
||||
|
||||
await generateTestFiles(answers as any)
|
||||
|
||||
expect(readDir).toBeCalledTimes(2)
|
||||
expect(vi.mocked(readDir).mock.calls[0][0]).toContain('mocha')
|
||||
expect(vi.mocked(readDir).mock.calls[1][0]).toContain('pageobjects')
|
||||
|
||||
/**
|
||||
* test readDir callback
|
||||
*/
|
||||
const readDirCb = vi.mocked(readDir).mock.calls[0][1][0] as Function
|
||||
const stats = { isDirectory: vi.fn().mockReturnValue(false) }
|
||||
expect(readDirCb('/foo/bar.lala', stats)).toBe(true)
|
||||
expect(readDirCb('/foo/bar.js.ejs', stats)).toBe(false)
|
||||
expect(readDirCb('/foo/bar.feature', stats)).toBe(false)
|
||||
stats.isDirectory.mockReturnValue(true)
|
||||
expect(readDirCb('/foo/bar.lala', stats)).toBe(false)
|
||||
expect(readDirCb('/foo/bar.js.ejs', stats)).toBe(false)
|
||||
expect(readDirCb('/foo/bar.feature', stats)).toBe(false)
|
||||
|
||||
expect(ejs.renderFile).toBeCalledTimes(4)
|
||||
expect(ejs.renderFile).toBeCalledWith(
|
||||
'/foo/bar/loo/page.js.ejs',
|
||||
{ answers },
|
||||
expect.any(Function)
|
||||
)
|
||||
expect(ejs.renderFile).toBeCalledWith(
|
||||
'/foo/bar/example.e2e.js',
|
||||
{ answers },
|
||||
expect.any(Function)
|
||||
)
|
||||
expect(fs.mkdir).toBeCalledTimes(4)
|
||||
expect((vi.mocked(fs.writeFile).mock.calls[0][0] as string)
|
||||
.endsWith(`${path.sep}page${path.sep}objects${path.sep}model${path.sep}page.js`))
|
||||
.toBe(true)
|
||||
expect((vi.mocked(fs.writeFile).mock.calls[1][0] as string)
|
||||
.endsWith(`${path.sep}example.e2e.js`))
|
||||
.toBe(true)
|
||||
})
|
||||
|
||||
it('jasmine with page objects', async () => {
|
||||
const answers = {
|
||||
runner: 'local',
|
||||
framework: 'jasmine',
|
||||
usePageObjects: true,
|
||||
generateTestFiles: true,
|
||||
destPageObjectRootPath: '/tests/page/objects/model',
|
||||
destSpecRootPath: '/tests/specs'
|
||||
}
|
||||
|
||||
await generateTestFiles(answers as any)
|
||||
|
||||
expect(readDir).toBeCalledTimes(2)
|
||||
expect(vi.mocked(readDir).mock.calls[0][0]).toContain('mochaJasmine')
|
||||
expect(vi.mocked(readDir).mock.calls[1][0]).toContain('pageobjects')
|
||||
|
||||
/**
|
||||
* test readDir callback
|
||||
*/
|
||||
const readDirCb = vi.mocked(readDir).mock.calls[0][1][0] as Function
|
||||
const stats = { isDirectory: vi.fn().mockReturnValue(false) }
|
||||
expect(readDirCb('/foo/bar.lala', stats)).toBe(true)
|
||||
expect(readDirCb('/foo/bar.js.ejs', stats)).toBe(false)
|
||||
expect(readDirCb('/foo/bar.feature', stats)).toBe(false)
|
||||
stats.isDirectory.mockReturnValue(true)
|
||||
expect(readDirCb('/foo/bar.lala', stats)).toBe(false)
|
||||
expect(readDirCb('/foo/bar.js.ejs', stats)).toBe(false)
|
||||
expect(readDirCb('/foo/bar.feature', stats)).toBe(false)
|
||||
|
||||
expect(ejs.renderFile).toBeCalledTimes(4)
|
||||
expect(ejs.renderFile).toBeCalledWith(
|
||||
'/foo/bar/loo/page.js.ejs',
|
||||
{ answers },
|
||||
expect.any(Function)
|
||||
)
|
||||
expect(ejs.renderFile).toBeCalledWith(
|
||||
'/foo/bar/example.e2e.js',
|
||||
{ answers },
|
||||
expect.any(Function)
|
||||
)
|
||||
expect(fs.mkdir).toBeCalledTimes(4)
|
||||
expect((vi.mocked(fs.writeFile).mock.calls[0][0] as string)
|
||||
.endsWith(`${path.sep}page${path.sep}objects${path.sep}model${path.sep}page.js`))
|
||||
.toBe(true)
|
||||
expect((vi.mocked(fs.writeFile).mock.calls[1][0] as string)
|
||||
.endsWith(`${path.sep}example.e2e.js`))
|
||||
.toBe(true)
|
||||
})
|
||||
|
||||
it('Jasmine with page generation and no pageObjects', async () => {
|
||||
vi.mocked(readDir).mockResolvedValue([] as any)
|
||||
const answers = {
|
||||
runner: 'local',
|
||||
specs: './tests/e2e/**/*.js',
|
||||
framework: 'jasmine',
|
||||
generateTestFiles: false,
|
||||
usePageObjects: false
|
||||
}
|
||||
|
||||
await generateTestFiles(answers as any)
|
||||
|
||||
expect(readDir).toBeCalledTimes(1)
|
||||
expect(ejs.renderFile).toBeCalledTimes(0)
|
||||
})
|
||||
|
||||
it('Cucumber with page generation and no pageObjects', async () => {
|
||||
vi.mocked(readDir).mockResolvedValue([] as any)
|
||||
const answers = {
|
||||
runner: 'local',
|
||||
specs: './tests/e2e/**/*.js',
|
||||
framework: 'cucumber',
|
||||
generateTestFiles: false,
|
||||
usePageObjects: false,
|
||||
}
|
||||
|
||||
await generateTestFiles(answers as any)
|
||||
|
||||
expect(readDir).toBeCalledTimes(1)
|
||||
expect(ejs.renderFile).toBeCalledTimes(0)
|
||||
})
|
||||
|
||||
it('Cucumber without page objects', async () => {
|
||||
vi.mocked(readDir).mockResolvedValue([
|
||||
'/foo/bar/loo/step_definition/example.step.js',
|
||||
'/foo/bar/example.feature'
|
||||
] as any)
|
||||
const answers = {
|
||||
runner: 'local',
|
||||
specs: './tests/e2e/*.js',
|
||||
framework: 'cucumber',
|
||||
stepDefinitions: '/some/step/defs',
|
||||
usePageObjects: false,
|
||||
generateTestFiles: true,
|
||||
destSpecRootPath: '/tests/specs',
|
||||
destStepRootPath: '/tests/stepDefinitions'
|
||||
}
|
||||
await generateTestFiles(answers as any)
|
||||
|
||||
expect(readDir).toBeCalledTimes(1)
|
||||
expect(vi.mocked(readDir).mock.calls[0][0]).toContain('cucumber')
|
||||
expect(ejs.renderFile).toBeCalledTimes(2)
|
||||
expect(ejs.renderFile).toBeCalledWith(
|
||||
'/foo/bar/loo/step_definition/example.step.js',
|
||||
{ answers },
|
||||
expect.any(Function)
|
||||
)
|
||||
expect(ejs.renderFile).toBeCalledWith(
|
||||
'/foo/bar/example.feature',
|
||||
{ answers },
|
||||
expect.any(Function)
|
||||
)
|
||||
expect(fs.mkdir).toBeCalledTimes(2)
|
||||
})
|
||||
|
||||
it('Cucumber with page objects and TypeScript', async () => {
|
||||
vi.mocked(readDir).mockResolvedValue([
|
||||
'/foo/bar/loo/page.js.ejs',
|
||||
'/foo/bar/loo/step_definition/example.step.js',
|
||||
'/foo/bar/example.feature'
|
||||
] as any)
|
||||
const answers = {
|
||||
runner: 'local',
|
||||
framework: 'cucumber',
|
||||
usePageObjects: true,
|
||||
isUsingTypeScript: true,
|
||||
destStepRootPath: '/tests/stepDefinitions',
|
||||
destSpecRootPath: '/tests/specs',
|
||||
destPageObjectRootPath: '/some/page/objects',
|
||||
relativePath: '../page/object'
|
||||
}
|
||||
await generateTestFiles(answers as any)
|
||||
|
||||
expect(readDir).toBeCalledTimes(2)
|
||||
expect(vi.mocked(readDir).mock.calls[0][0]).toContain('cucumber')
|
||||
expect(ejs.renderFile).toBeCalledTimes(6)
|
||||
expect(ejs.renderFile).toBeCalledWith(
|
||||
'/foo/bar/loo/step_definition/example.step.js',
|
||||
{ answers },
|
||||
expect.any(Function)
|
||||
)
|
||||
expect(ejs.renderFile).toBeCalledWith(
|
||||
'/foo/bar/example.feature',
|
||||
{ answers },
|
||||
expect.any(Function)
|
||||
)
|
||||
expect(fs.mkdir).toBeCalledTimes(6)
|
||||
expect(
|
||||
(vi.mocked(fs.writeFile).mock.calls[0][0] as string)
|
||||
.endsWith(`${path.sep}some${path.sep}page${path.sep}objects${path.sep}page.ts`)
|
||||
).toBe(true)
|
||||
expect(
|
||||
(vi.mocked(fs.writeFile).mock.calls[2][0] as string)
|
||||
.endsWith(`${path.sep}example.feature`)
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe('findInConfig', () => {
|
||||
it('finds text for services', () => {
|
||||
const str = "services: ['foo', 'bar'],"
|
||||
|
||||
expect(findInConfig(str, 'service')).toMatchObject([
|
||||
'services: [\'foo\', \'bar\']'
|
||||
])
|
||||
})
|
||||
|
||||
it('finds text for frameworks', () => {
|
||||
const str = "framework: 'mocha'"
|
||||
|
||||
expect(findInConfig(str, 'framework')).toMatchObject([
|
||||
"framework: 'mocha'"
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatConfigFilePaths', () => {
|
||||
it('should format properly', async () => {
|
||||
|
||||
expect(await formatConfigFilePaths('/path/to/foo.js')).toMatchObject({
|
||||
fullPath:'/path/to/foo.js',
|
||||
fullPathNoExtension:'/path/to/foo'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('replaceConfig', () => {
|
||||
it('correctly changes framework', () => {
|
||||
const fakeConfig = `exports.config = {
|
||||
runner: 'local',
|
||||
specs: [
|
||||
'./test/specs/**/*.js'
|
||||
],
|
||||
framework: 'mocha',
|
||||
}`
|
||||
|
||||
expect(replaceConfig(fakeConfig, 'framework', 'jasmine')).toBe(
|
||||
`exports.config = {
|
||||
runner: 'local',
|
||||
specs: [
|
||||
'./test/specs/**/*.js'
|
||||
],
|
||||
framework: 'jasmine',
|
||||
}`
|
||||
)
|
||||
})
|
||||
|
||||
it('correctly changes service', () => {
|
||||
const fakeConfig = `exports.config = {
|
||||
runner: 'local',
|
||||
specs: [
|
||||
'./test/specs/**/*.js'
|
||||
],
|
||||
services: [],
|
||||
framework: 'mocha',
|
||||
}`
|
||||
expect(replaceConfig(fakeConfig, 'service', 'sauce')).toBe(
|
||||
`exports.config = {
|
||||
runner: 'local',
|
||||
specs: [
|
||||
'./test/specs/**/*.js'
|
||||
],
|
||||
services: ['sauce'],
|
||||
framework: 'mocha',
|
||||
}`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getPathForFileGeneration', () => {
|
||||
it('Cucumber with pageobjects default values', () => {
|
||||
const generatedPaths = getPathForFileGeneration({
|
||||
runner: 'local',
|
||||
stepDefinitions: './features/step-definitions/steps.js',
|
||||
pages: './features/pageobjects/**/*.js',
|
||||
generateTestFiles: true,
|
||||
usePageObjects: true,
|
||||
framework: '@wdio/cucumber-service$--$cucumber'
|
||||
} as any, '/foo/bar')
|
||||
expect(generatedPaths.relativePath).toEqual('../pageobjects')
|
||||
})
|
||||
|
||||
it('Cucumber with pageobjects default different path', () => {
|
||||
const generatedPaths = getPathForFileGeneration({
|
||||
runner: 'local',
|
||||
stepDefinitions: './features/step-definitions/steps.js',
|
||||
pages: './features/page/objects/**/*.js',
|
||||
generateTestFiles: true,
|
||||
usePageObjects: true,
|
||||
framework: '@wdio/cucumber-service$--$cucumber'
|
||||
} as any, '/foo/bar')
|
||||
expect(generatedPaths.relativePath).toEqual('../page/objects')
|
||||
})
|
||||
it('Cucumber with pageobjects and steps different path', () => {
|
||||
const generatedPaths = getPathForFileGeneration({
|
||||
runner: 'local',
|
||||
stepDefinitions: 'cucumber/features/steps',
|
||||
pages: 'cucumber/features/pages',
|
||||
generateTestFiles: true,
|
||||
usePageObjects: true,
|
||||
framework: '@wdio/cucumber-service$--$cucumber'
|
||||
} as any, '/foo/bar')
|
||||
expect(generatedPaths.relativePath).toEqual('')
|
||||
})
|
||||
|
||||
it('Cucumber with answer that is not a path', () => {
|
||||
const generatedPaths = getPathForFileGeneration({
|
||||
runner: 'local',
|
||||
stepDefinitions: 'y',
|
||||
pages: 'h',
|
||||
generateTestFiles: true,
|
||||
usePageObjects: true,
|
||||
framework: '@wdio/cucumber-service$--$cucumber'
|
||||
} as any, '/foo/bar')
|
||||
expect(generatedPaths.relativePath).toEqual('../h')
|
||||
})
|
||||
|
||||
it('Mocha with pageobjects default values', () => {
|
||||
const generatedPaths = getPathForFileGeneration({
|
||||
runner: 'local',
|
||||
specs: './test/specs/**/*.js',
|
||||
pages: './test/pageobjects/**/*.js',
|
||||
generateTestFiles: true,
|
||||
usePageObjects: true,
|
||||
framework: '@wdio/cucumber-service$--$mocha'
|
||||
} as any, '/foo/bar')
|
||||
expect(generatedPaths.relativePath).toEqual('../pageobjects')
|
||||
})
|
||||
|
||||
it('Mocha with pageobjects different path', () => {
|
||||
const generatedPaths = getPathForFileGeneration({
|
||||
runner: 'local',
|
||||
specs: './test/specs/files/**/*.js',
|
||||
pages: './test/pageobjects/**/*.js',
|
||||
generateTestFiles: true,
|
||||
usePageObjects: true,
|
||||
framework: '@wdio/cucumber-service$--$mocha'
|
||||
} as any, '/foo/bar')
|
||||
expect(generatedPaths.relativePath).toEqual('../../pageobjects')
|
||||
})
|
||||
|
||||
it('Do not auto generate file', () => {
|
||||
const generatedPaths = getPathForFileGeneration({
|
||||
runner: 'local',
|
||||
specs: './test/specs/files/**/*.js',
|
||||
pages: './test/pageobjects/**/*.js',
|
||||
generateTestFiles: false,
|
||||
usePageObjects: true,
|
||||
framework: '@wdio/cucumber-service$--$mocha'
|
||||
} as any, '/foo/bar')
|
||||
expect(generatedPaths.relativePath).toEqual('')
|
||||
})
|
||||
|
||||
it('Do not use PageObjects', () => {
|
||||
const generatedPaths = getPathForFileGeneration({
|
||||
runner: 'local',
|
||||
specs: './test/specs/files/**/*.js',
|
||||
pages: './test/pageobjects/**/*.js',
|
||||
generateTestFiles: true,
|
||||
usePageObjects: false,
|
||||
framework: '@wdio/cucumber-service$--$mocha'
|
||||
} as any, '/foo/bar')
|
||||
expect(generatedPaths.relativePath).toEqual('')
|
||||
})
|
||||
})
|
||||
|
||||
test('getDefaultFiles', async () => {
|
||||
const files = '/foo/bar'
|
||||
expect(await getDefaultFiles({ projectRootCorrect: false, projectRoot: '/bar', isUsingTypeScript: true } as any, files))
|
||||
.toBe(path.join('/bar', 'foo', 'bar.ts'))
|
||||
expect(await getDefaultFiles({ projectRootCorrect: false, projectRoot: '/bar', isUsingTypeScript: true, preset: 'vite-plugin-solid$--$solid' } as any, files))
|
||||
.toBe(path.join('/bar', 'foo', 'bar.tsx'))
|
||||
expect(await getDefaultFiles({ projectRootCorrect: false, projectRoot: '/bar', isUsingTypeScript: false } as any, files))
|
||||
.toBe(path.join('/bar', 'foo', 'bar.js'))
|
||||
})
|
||||
|
||||
test('original implementation of getDefaultFiles handles projectRoot with no package.json', async () => {
|
||||
const files = '/foo/bar'
|
||||
vi.mocked(readPackageUp).mockRestore()
|
||||
expect(await getDefaultFiles({ createPackageJSON: true, projectRoot: '/project-root-with-no-package.json', isUsingTypeScript: false } as any, files))
|
||||
.toBe(path.join('/project-root-with-no-package.json', 'foo', 'bar.js'))
|
||||
})
|
||||
|
||||
test('getProjectRoot', async () => {
|
||||
expect(await getProjectRoot()).toBe('/foo')
|
||||
expect(await getProjectRoot({
|
||||
projectRootCorrect: true
|
||||
} as any)).toBe('/foo')
|
||||
expect(await getProjectRoot({
|
||||
projectRootCorrect: false,
|
||||
projectRoot: '/bar/foo'
|
||||
} as any)).toBe('/bar/foo')
|
||||
})
|
||||
|
||||
test('detectCompiler', async () => {
|
||||
vi.mocked(readPackageUp).mockRestore()
|
||||
const answers = { createPackageJSON: true } as Questionnair
|
||||
expect(await detectCompiler(answers)).toBe(false)
|
||||
})
|
||||
|
||||
test('getAnswers', async () => {
|
||||
let answers = await getAnswers(true)
|
||||
delete answers.pages // delete so it doesn't fail in Windows
|
||||
delete answers.specs // delete so it doesn't fail in Windows
|
||||
expect(answers).toMatchSnapshot()
|
||||
vi.mocked(inquirer.prompt).mockReturnValue('some value' as any)
|
||||
answers = await getAnswers(false)
|
||||
delete answers.pages // delete so it doesn't fail in Windows
|
||||
delete answers.specs // delete so it doesn't fail in Windows
|
||||
expect(answers).toBe('some value')
|
||||
expect(inquirer.prompt).toBeCalledTimes(2)
|
||||
// @ts-ignore
|
||||
expect(vi.mocked(inquirer.prompt).mock.calls[0][0][0].when).toBe(false)
|
||||
vi.mocked(readPackageUp).mockResolvedValue(undefined)
|
||||
vi.mocked(inquirer.prompt).mockClear()
|
||||
expect(await getAnswers(false)).toBe('some value')
|
||||
expect(inquirer.prompt).toBeCalledTimes(2)
|
||||
// @ts-ignore
|
||||
expect(vi.mocked(inquirer.prompt).mock.calls[0][0][0].when).toBe(true)
|
||||
})
|
||||
|
||||
test('getProjectProps', async () => {
|
||||
vi.mocked(readPackageUp).mockResolvedValue(undefined)
|
||||
expect(await getProjectProps('/foo/bar')).toBe(undefined)
|
||||
expect(readPackageUp).toBeCalledWith({ cwd: '/foo/bar' })
|
||||
vi.mocked(readPackageUp).mockResolvedValue({
|
||||
path: '/foo/bar',
|
||||
packageJson: {
|
||||
name: 'cool-test-module2',
|
||||
}
|
||||
})
|
||||
expect(await getProjectProps('/foo/bar')).toEqual({
|
||||
esmSupported: false,
|
||||
packageJson: { name: 'cool-test-module2' },
|
||||
path: '/foo'
|
||||
})
|
||||
})
|
||||
|
||||
test('createPackageJSON', async () => {
|
||||
await createPackageJSON({} as any)
|
||||
expect(fs.writeFile).toBeCalledTimes(0)
|
||||
await createPackageJSON({
|
||||
createPackageJSON: true
|
||||
} as any)
|
||||
expect(console.log).toBeCalledTimes(2)
|
||||
})
|
||||
|
||||
test('npmInstall', async () => {
|
||||
const parsedAnswers = {
|
||||
rawAnswers: {
|
||||
services: ['foo$--$bar'],
|
||||
preset: 'barfoo$--$vue'
|
||||
},
|
||||
projectRootCorrect: false,
|
||||
projectRoot: '/foo/bar',
|
||||
isUsingTypeScript: true,
|
||||
framework: 'jasmine',
|
||||
installTestingLibrary: true,
|
||||
packagesToInstall: ['foo$--$bar', 'bar$--$foo'],
|
||||
npmInstall: true,
|
||||
includeVisualTesting: true
|
||||
} as any
|
||||
await npmInstall(parsedAnswers, 'next')
|
||||
expect(installPackages).toBeCalledTimes(1)
|
||||
expect(vi.mocked(installPackages).mock.calls).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('not npmInstall', async () => {
|
||||
const parsedAnswers = {
|
||||
rawAnswers: {
|
||||
services: ['foo$--$bar'],
|
||||
preset: 'barfoo$--$vue'
|
||||
},
|
||||
installTestingLibrary: true,
|
||||
packagesToInstall: ['foo$--$bar', 'bar$--$foo'],
|
||||
npmInstall: false
|
||||
} as any
|
||||
await npmInstall(parsedAnswers, 'next')
|
||||
expect(installPackages).toBeCalledTimes(0)
|
||||
expect(vi.mocked(console.log).mock.calls[0][0]).toContain('To install dependencies, execute')
|
||||
})
|
||||
|
||||
test('setupTypeScript', async () => {
|
||||
await setupTypeScript({} as any)
|
||||
expect(fs.writeFile).toBeCalledTimes(0)
|
||||
const parsedAnswers = {
|
||||
isUsingTypeScript: true,
|
||||
esmSupport: true,
|
||||
rawAnswers: {
|
||||
framework: 'foo',
|
||||
services: [
|
||||
'wdio-foobar-service$--$foobar',
|
||||
'wdio-electron-service$--$electron'
|
||||
]
|
||||
},
|
||||
packagesToInstall: [],
|
||||
tsConfigFilePath: '/foobar/tsconfig.json'
|
||||
} as any
|
||||
await setupTypeScript(parsedAnswers)
|
||||
expect(vi.mocked(fs.writeFile).mock.calls[0][1]).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('setupTypeScript does not create tsconfig.json if TypeScript was not selected', async () => {
|
||||
const parsedAnswers = {
|
||||
isUsingTypeScript: false,
|
||||
esmSupport: true,
|
||||
rawAnswers: {
|
||||
framework: 'foo',
|
||||
services: [
|
||||
'wdio-foobar-service$--$foobar',
|
||||
'wdio-electron-service$--$electron'
|
||||
]
|
||||
},
|
||||
packagesToInstall: [],
|
||||
tsConfigFilePath: '/foobar/tsconfig.json'
|
||||
} as any
|
||||
await setupTypeScript(parsedAnswers)
|
||||
expect(fs.writeFile).not.toBeCalled()
|
||||
expect(parsedAnswers.packagesToInstall).toEqual([])
|
||||
})
|
||||
|
||||
test('setupTypeScript does not create tsconfig.json if there is already one', async () => {
|
||||
const parsedAnswers = {
|
||||
isUsingTypeScript: true,
|
||||
esmSupport: true,
|
||||
rawAnswers: {
|
||||
framework: 'foo',
|
||||
services: [
|
||||
'wdio-foobar-service$--$foobar',
|
||||
'wdio-electron-service$--$electron'
|
||||
]
|
||||
},
|
||||
packagesToInstall: [],
|
||||
tsConfigFilePath: '/foobar/tsconfig.json',
|
||||
hasRootTSConfig: true
|
||||
} as any
|
||||
await setupTypeScript(parsedAnswers)
|
||||
expect(fs.writeFile).not.toBeCalled()
|
||||
})
|
||||
|
||||
describe.skip('createWDIOScript', () => {
|
||||
it('can run with success', async () => {
|
||||
const promise = createWDIOScript({ wdioConfigPath: '/foo/bar/wdio.conf.js' } as any)
|
||||
expect(await promise)
|
||||
.toBe(true)
|
||||
expect(cp.spawn).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
it('does not fail the process if spawn errors out', async () => {
|
||||
vi.mocked(cp.spawn).mockReturnValue({ on: vi.fn().mockImplementation((ev, fn) => fn(1)) } as any)
|
||||
expect(await createWDIOScript({ wdioConfigPath: '/foo/bar/wdio.conf.js' } as any))
|
||||
.toBe(false)
|
||||
expect(cp.spawn).toBeCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
test('createWDIOConfig', async () => {
|
||||
const answers = await parseAnswers(true)
|
||||
answers.destSpecRootPath = '/tests/specs'
|
||||
answers.destPageObjectRootPath = '/tests/specs'
|
||||
answers.stepDefinitions = './foo/bar'
|
||||
answers.specs = '/foo/bar/**'
|
||||
await createWDIOConfig(answers as any)
|
||||
expect(fs.writeFile).toBeCalledTimes(7)
|
||||
expect(
|
||||
vi.mocked(fs.writeFile).mock.calls[0][0]
|
||||
.toString()
|
||||
.endsWith(path.resolve('wdio.conf.js'))
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test('runAppiumInstaller', async () => {
|
||||
expect(await runAppiumInstaller({ e2eEnvironment: 'web' } as any))
|
||||
.toBe(undefined)
|
||||
expect(console.log).toBeCalledTimes(0)
|
||||
expect($).toBeCalledTimes(0)
|
||||
|
||||
vi.mocked(inquirer.prompt).mockResolvedValue({
|
||||
continueWithAppiumSetup: false
|
||||
})
|
||||
|
||||
expect(await runAppiumInstaller({ e2eEnvironment: 'mobile' } as any))
|
||||
.toBe(undefined)
|
||||
expect(console.log).toBeCalledTimes(1)
|
||||
expect($).toBeCalledTimes(0)
|
||||
|
||||
vi.mocked(inquirer.prompt).mockResolvedValue({
|
||||
continueWithAppiumSetup: true
|
||||
})
|
||||
expect(await runAppiumInstaller({ e2eEnvironment: 'mobile' } as any))
|
||||
.toEqual(['npx appium-installer'])
|
||||
expect($).toBeCalledTimes(1)
|
||||
})
|
||||
afterEach(()=>{
|
||||
vi.mocked(inquirer.prompt).mockClear()
|
||||
vi.mocked(readDir).mockClear()
|
||||
vi.mocked(ejs.renderFile).mockClear()
|
||||
vi.mocked(fs.writeFile).mockClear()
|
||||
vi.mocked(fs.mkdir).mockClear()
|
||||
vi.mocked(cp.spawn).mockClear()
|
||||
vi.mocked(installPackages).mockClear()
|
||||
})
|
||||
7
packages/create-wdio/tests/utils/index.test.ts
Normal file
7
packages/create-wdio/tests/utils/index.test.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { expect, test } from 'vitest'
|
||||
|
||||
import { SUPPORTED_PACKAGE_MANAGERS } from '../../src/utils/index.js'
|
||||
|
||||
test('should export SUPPORTED_PACKAGE_MANAGERS', async ()=>{
|
||||
expect(SUPPORTED_PACKAGE_MANAGERS).toBeDefined()
|
||||
})
|
||||
11
packages/create-wdio/tsconfig.json
Normal file
11
packages/create-wdio/tsconfig.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"extends": "../../tsconfig",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"outDir": "./build",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
||||
|
|
@ -50,6 +50,7 @@
|
|||
"async-exit-hook": "^2.0.1",
|
||||
"chalk": "^5.2.0",
|
||||
"chokidar": "^4.0.0",
|
||||
"create-wdio": "workspace:*",
|
||||
"dotenv": "^16.3.1",
|
||||
"ejs": "^3.1.9",
|
||||
"execa": "^9.2.0",
|
||||
|
|
@ -59,7 +60,6 @@
|
|||
"lodash.pickby": "^4.6.0",
|
||||
"lodash.union": "^4.6.0",
|
||||
"read-pkg-up": "^10.0.0",
|
||||
"recursive-readdir": "^2.2.3",
|
||||
"tsx": "^4.7.2",
|
||||
"webdriverio": "workspace:*",
|
||||
"yargs": "^17.7.2"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import * as config from './config.js'
|
||||
import * as install from './install.js'
|
||||
|
||||
import { config } from 'create-wdio/config/cli'
|
||||
import { install } from 'create-wdio/install/cli'
|
||||
|
||||
import * as repl from './repl.js'
|
||||
import * as run from './run.js'
|
||||
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ import type { Argv } from 'yargs'
|
|||
|
||||
import Launcher from '../launcher.js'
|
||||
import Watcher from '../watcher.js'
|
||||
import { formatConfigFilePaths, canAccessConfigPath, missingConfigurationPrompt } from './config.js'
|
||||
import { coerceOptsFor } from '../utils.js'
|
||||
import { coerceOptsFor, } from '../utils.js'
|
||||
import { CLI_EPILOGUE } from '../constants.js'
|
||||
import type { RunCommandArguments } from '../types.js'
|
||||
import { config } from 'create-wdio/config/cli'
|
||||
|
||||
export const command = 'run <configPath>'
|
||||
|
||||
|
|
@ -175,11 +175,11 @@ export async function launch(wdioConfPath: string, params: Partial<RunCommandArg
|
|||
export async function handler(argv: RunCommandArguments) {
|
||||
const { configPath = 'wdio.conf.js', ...params } = argv
|
||||
|
||||
const wdioConf = await formatConfigFilePaths(configPath)
|
||||
const confAccess = await canAccessConfigPath(wdioConf.fullPathNoExtension, wdioConf.fullPath)
|
||||
const wdioConf = await config.formatConfigFilePaths(configPath)
|
||||
const confAccess = await config.canAccessConfigPath(wdioConf.fullPathNoExtension, wdioConf.fullPath)
|
||||
if (!confAccess) {
|
||||
try {
|
||||
await missingConfigurationPrompt('run', wdioConf.fullPathNoExtension)
|
||||
await config.missingConfigurationPrompt('run', wdioConf.fullPathNoExtension)
|
||||
if (process.env.WDIO_UNIT_TESTS) {
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,59 +1,14 @@
|
|||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import module from 'node:module'
|
||||
import { HOOK_DEFINITION } from '@wdio/utils'
|
||||
import type { Options, Services, Reporters } from '@wdio/types'
|
||||
|
||||
import {
|
||||
detectCompiler,
|
||||
getDefaultFiles,
|
||||
convertPackageHashToObject,
|
||||
getProjectRoot,
|
||||
detectPackageManager,
|
||||
} from './utils.js'
|
||||
import type { Questionnair } from './types.js'
|
||||
|
||||
const require = module.createRequire(import.meta.url)
|
||||
const pkgJSON = require('../package.json')
|
||||
|
||||
export const pkg = pkgJSON
|
||||
export const CLI_EPILOGUE = `Documentation: https://webdriver.io\n@wdio/cli (v${pkg.version})`
|
||||
|
||||
export const CONFIG_HELPER_INTRO = `
|
||||
===============================
|
||||
🤖 WDIO Configuration Wizard 🧙
|
||||
===============================
|
||||
`
|
||||
|
||||
export const SUPPORTED_COMMANDS = ['run', 'install', 'config', 'repl']
|
||||
export const PMs = ['npm', 'yarn', 'pnpm', 'bun'] as const
|
||||
export const SUPPORTED_CONFIG_FILE_EXTENSION = ['js', 'ts', 'mjs', 'mts', 'cjs', 'cts']
|
||||
export const configHelperSuccessMessage = ({ projectRootDir, runScript, extraInfo = '' }: { projectRootDir: string, runScript: string, extraInfo: string }) => `
|
||||
🤖 Successfully setup project at ${ projectRootDir } 🎉
|
||||
|
||||
Join our Discord Community Server and instantly find answers to your issues or queries. Or just join and say hi 👋!
|
||||
🔗 https://discord.webdriver.io
|
||||
|
||||
Visit the project on GitHub to report bugs 🐛 or raise feature requests 💡:
|
||||
🔗 https://github.com/webdriverio/webdriverio
|
||||
${ extraInfo }
|
||||
To run your tests, execute:
|
||||
$ cd ${ projectRootDir }
|
||||
$ npm run ${ runScript }
|
||||
`
|
||||
|
||||
export const CONFIG_HELPER_SERENITY_BANNER = `
|
||||
Learn more about Serenity/JS:
|
||||
🔗 https://serenity-js.org/
|
||||
🔗 https://serenity-js.org/handbook/test-runners/webdriverio/
|
||||
`
|
||||
|
||||
export const DEPENDENCIES_INSTALLATION_MESSAGE = `
|
||||
To install dependencies, execute:
|
||||
%s
|
||||
`
|
||||
|
||||
export const NPM_INSTALL = ''
|
||||
|
||||
export const ANDROID_CONFIG = {
|
||||
platformName: 'Android',
|
||||
|
|
@ -66,616 +21,7 @@ export const IOS_CONFIG = {
|
|||
automationName: 'XCUITest',
|
||||
deviceName: 'iPhone Simulator'
|
||||
}
|
||||
|
||||
/**
|
||||
* We have to use a string hash for value because InquirerJS default values do not work if we have
|
||||
* objects as a `value` to be stored from the user's answers.
|
||||
*/
|
||||
export const SUPPORTED_PACKAGES = {
|
||||
runner: [
|
||||
{ name: 'E2E Testing - of Web or Mobile Applications', value: '@wdio/local-runner$--$local$--$e2e' },
|
||||
{ name: 'Component or Unit Testing - in the browser\n > https://webdriver.io/docs/component-testing', value: '@wdio/browser-runner$--$browser$--$component' },
|
||||
{ name: 'Desktop Testing - of Electron Applications\n > https://webdriver.io/docs/desktop-testing/electron', value: '@wdio/local-runner$--$local$--$electron' },
|
||||
{ name: 'Desktop Testing - of MacOS Applications\n > https://webdriver.io/docs/desktop-testing/macos', value: '@wdio/local-runner$--$local$--$macos' },
|
||||
{ name: 'VS Code Extension Testing\n > https://webdriver.io/docs/vscode-extension-testing', value: '@wdio/local-runner$--$local$--$vscode' },
|
||||
{ name: 'Roku Testing - of OTT apps running on RokuOS\n > https://webdriver.io/docs/wdio-roku-service', value: '@wdio/local-runner$--$local$--$roku' }
|
||||
],
|
||||
framework: [
|
||||
{ name: 'Mocha (https://mochajs.org/)', value: '@wdio/mocha-framework$--$mocha' },
|
||||
{ name: 'Mocha with Serenity/JS (https://serenity-js.org/)', value: '@serenity-js/webdriverio$--$@serenity-js/webdriverio$--$mocha' },
|
||||
{ name: 'Jasmine (https://jasmine.github.io/)', value: '@wdio/jasmine-framework$--$jasmine' },
|
||||
{ name: 'Jasmine with Serenity/JS (https://serenity-js.org/)', value: '@serenity-js/webdriverio$--$@serenity-js/webdriverio$--$jasmine' },
|
||||
{ name: 'Cucumber (https://cucumber.io/)', value: '@wdio/cucumber-framework$--$cucumber' },
|
||||
{ name: 'Cucumber with Serenity/JS (https://serenity-js.org/)', value: '@serenity-js/webdriverio$--$@serenity-js/webdriverio$--$cucumber' },
|
||||
],
|
||||
reporter: [
|
||||
{ name: 'spec', value: '@wdio/spec-reporter$--$spec', checked: true },
|
||||
{ name: 'dot', value: '@wdio/dot-reporter$--$dot' },
|
||||
{ name: 'junit', value: '@wdio/junit-reporter$--$junit' },
|
||||
{ name: 'allure', value: '@wdio/allure-reporter$--$allure' },
|
||||
{ name: 'sumologic', value: '@wdio/sumologic-reporter$--$sumologic' },
|
||||
{ name: 'concise', value: '@wdio/concise-reporter$--$concise' },
|
||||
{ name: 'json', value: '@wdio/json-reporter$--$json' },
|
||||
// external
|
||||
{ name: 'reportportal', value: 'wdio-reportportal-reporter$--$reportportal' },
|
||||
{ name: 'video', value: 'wdio-video-reporter$--$video' },
|
||||
{ name: 'cucumber-json', value: 'wdio-cucumberjs-json-reporter$--$cucumberjs-json' },
|
||||
{ name: 'mochawesome', value: 'wdio-mochawesome-reporter$--$mochawesome' },
|
||||
{ name: 'timeline', value: 'wdio-timeline-reporter$--$timeline' },
|
||||
{ name: 'html-nice', value: 'wdio-html-nice-reporter$--$html-nice' },
|
||||
{ name: 'slack', value: '@moroo/wdio-slack-reporter$--$slack' },
|
||||
{ name: 'teamcity', value: 'wdio-teamcity-reporter$--$teamcity' },
|
||||
{ name: 'delta', value: '@delta-reporter/wdio-delta-reporter-service$--$delta' },
|
||||
{ name: 'testrail', value: '@wdio/testrail-reporter$--$testrail' },
|
||||
{ name: 'light', value: 'wdio-light-reporter$--$light' },
|
||||
{ name: 'wdio-json-html-reporter', value: 'wdio-json-html-reporter$--$jsonhtml' }
|
||||
],
|
||||
plugin: [
|
||||
{ name: 'wait-for: utilities that provide functionalities to wait for certain conditions till a defined task is complete.\n > https://www.npmjs.com/package/wdio-wait-for', value: 'wdio-wait-for$--$wait-for' },
|
||||
{ name: 'angular-component-harnesses: support for Angular component test harnesses\n > https://www.npmjs.com/package/@badisi/wdio-harness', value: '@badisi/wdio-harness$--$harness' },
|
||||
{ name: 'Testing Library: utilities that encourage good testing practices laid down by dom-testing-library.\n > https://testing-library.com/docs/webdriverio-testing-library/intro', value: '@testing-library/webdriverio$--$testing-library' }
|
||||
],
|
||||
service: [
|
||||
// internal or community driver services
|
||||
{ name: 'visual', value: '@wdio/visual-service$--$visual' },
|
||||
{ name: 'vite', value: 'wdio-vite-service$--$vite' },
|
||||
{ name: 'nuxt', value: 'wdio-nuxt-service$--$nuxt' },
|
||||
{ name: 'firefox-profile', value: '@wdio/firefox-profile-service$--$firefox-profile' },
|
||||
{ name: 'gmail', value: 'wdio-gmail-service$--$gmail' },
|
||||
{ name: 'sauce', value: '@wdio/sauce-service$--$sauce' },
|
||||
{ name: 'testingbot', value: '@wdio/testingbot-service$--$testingbot' },
|
||||
{ name: 'browserstack', value: '@wdio/browserstack-service$--$browserstack' },
|
||||
{ name: 'lighthouse', value: '@wdio/lighthouse-service$--$lighthouse' },
|
||||
{ name: 'vscode', value: 'wdio-vscode-service$--$vscode' },
|
||||
{ name: 'electron', value: 'wdio-electron-service$--$electron' },
|
||||
{ name: 'appium', value: '@wdio/appium-service$--$appium' },
|
||||
// external
|
||||
{ name: 'eslinter-service', value: 'wdio-eslinter-service$--$eslinter' },
|
||||
{ name: 'lambdatest', value: 'wdio-lambdatest-service$--$lambdatest' },
|
||||
{ name: 'zafira-listener', value: 'wdio-zafira-listener-service$--$zafira-listener' },
|
||||
{ name: 'reportportal', value: 'wdio-reportportal-service$--$reportportal' },
|
||||
{ name: 'docker', value: 'wdio-docker-service$--$docker' },
|
||||
{ name: 'ui5', value: 'wdio-ui5-service$--$ui5' },
|
||||
{ name: 'wiremock', value: 'wdio-wiremock-service$--$wiremock' },
|
||||
{ name: 'ng-apimock', value: 'wdio-ng-apimock-service$--$ng-apimock' },
|
||||
{ name: 'slack', value: 'wdio-slack-service$--$slack' },
|
||||
{ name: 'cucumber-viewport-logger', value: 'wdio-cucumber-viewport-logger-service$--$cucumber-viewport-logger' },
|
||||
{ name: 'intercept', value: 'wdio-intercept-service$--$intercept' },
|
||||
{ name: 'docker', value: 'wdio-docker-service$--$docker' },
|
||||
{ name: 'novus-visual-regression', value: 'wdio-novus-visual-regression-service$--$novus-visual-regression' },
|
||||
{ name: 'rerun', value: 'wdio-rerun-service$--$rerun' },
|
||||
{ name: 'winappdriver', value: 'wdio-winappdriver-service$--$winappdriver' },
|
||||
{ name: 'ywinappdriver', value: 'wdio-ywinappdriver-service$--$ywinappdriver' },
|
||||
{ name: 'performancetotal', value: 'wdio-performancetotal-service$--$performancetotal' },
|
||||
{ name: 'cleanuptotal', value: 'wdio-cleanuptotal-service$--$cleanuptotal' },
|
||||
{ name: 'aws-device-farm', value: 'wdio-aws-device-farm-service$--$aws-device-farm' },
|
||||
{ name: 'ms-teams', value: 'wdio-ms-teams-service$--$ms-teams' },
|
||||
{ name: 'tesults', value: 'wdio-tesults-service$--$tesults' },
|
||||
{ name: 'azure-devops', value: '@gmangiapelo/wdio-azure-devops-service$--$azure-devops' },
|
||||
{ name: 'google-Chat', value: 'wdio-google-chat-service$--$google-chat' },
|
||||
{ name: 'qmate-service', value: '@sap_oss/wdio-qmate-service$--$qmate-service' },
|
||||
{ name: 'robonut', value: 'wdio-robonut-service$--$robonut' },
|
||||
{ name: 'qunit', value: 'wdio-qunit-service$--$qunit' },
|
||||
{ name: 'roku', value: 'wdio-roku-service$--$roku' },
|
||||
{ name: 'obsidian', value: 'wdio-obsidian-service$--$obsidian' }
|
||||
]
|
||||
}
|
||||
|
||||
export const SUPPORTED_BROWSER_RUNNER_PRESETS = [
|
||||
{ name: 'Lit (https://lit.dev/)', value: '$--$' },
|
||||
{ name: 'Vue.js (https://vuejs.org/)', value: '@vitejs/plugin-vue$--$vue' },
|
||||
{ name: 'Svelte (https://svelte.dev/)', value: '@sveltejs/vite-plugin-svelte$--$svelte' },
|
||||
{ name: 'SolidJS (https://www.solidjs.com/)', value: 'vite-plugin-solid$--$solid' },
|
||||
{ name: 'StencilJS (https://stenciljs.com/)', value: '$--$stencil' },
|
||||
{ name: 'React (https://reactjs.org/)', value: '@vitejs/plugin-react$--$react' },
|
||||
{ name: 'Preact (https://preactjs.com/)', value: '@preact/preset-vite$--$preact' },
|
||||
{ name: 'Other', value: null }
|
||||
]
|
||||
|
||||
export const TESTING_LIBRARY_PACKAGES: Record<string, string> = {
|
||||
react: '@testing-library/react',
|
||||
preact: '@testing-library/preact',
|
||||
vue: '@testing-library/vue',
|
||||
svelte: '@testing-library/svelte',
|
||||
solid: 'solid-testing-library'
|
||||
}
|
||||
|
||||
export enum BackendChoice {
|
||||
Local = 'On my local machine',
|
||||
Experitest = 'In the cloud using Experitest',
|
||||
Saucelabs = 'In the cloud using Sauce Labs',
|
||||
Browserstack = 'In the cloud using BrowserStack',
|
||||
OtherVendors = 'In the cloud using Testingbot or LambdaTest or a different service',
|
||||
Grid = 'I have my own Selenium cloud'
|
||||
}
|
||||
|
||||
export enum ElectronBuildToolChoice {
|
||||
ElectronForge = 'Electron Forge (https://www.electronforge.io/)',
|
||||
ElectronBuilder = 'electron-builder (https://www.electron.build/)',
|
||||
SomethingElse = 'Something else'
|
||||
}
|
||||
|
||||
enum ProtocolOptions {
|
||||
HTTPS = 'https',
|
||||
HTTP = 'http'
|
||||
}
|
||||
|
||||
export enum RegionOptions {
|
||||
US = 'us',
|
||||
EU = 'eu'
|
||||
}
|
||||
|
||||
export const E2E_ENVIRONMENTS = [
|
||||
{ name: 'Web - web applications in the browser', value: 'web' },
|
||||
{ name: 'Mobile - native, hybrid and mobile web apps, on Android or iOS', value: 'mobile' }
|
||||
]
|
||||
|
||||
export const MOBILE_ENVIRONMENTS = [
|
||||
{ name: 'Android - native, hybrid and mobile web apps, tested on emulators and real devices\n > using UiAutomator2 (https://www.npmjs.com/package/appium-uiautomator2-driver)', value: 'android' },
|
||||
{ name: 'iOS - applications on iOS, iPadOS, and tvOS\n > using XCTest (https://appium.github.io/appium-xcuitest-driver)', value: 'ios' }
|
||||
]
|
||||
|
||||
export const BROWSER_ENVIRONMENTS = [
|
||||
{ name: 'Chrome', value: 'chrome', checked: true },
|
||||
{ name: 'Firefox', value: 'firefox' },
|
||||
{ name: 'Safari', value: 'safari' },
|
||||
{ name: 'Microsoft Edge', value: 'MicrosoftEdge' }
|
||||
]
|
||||
|
||||
function isBrowserRunner (answers: Questionnair) {
|
||||
return answers.runner === SUPPORTED_PACKAGES.runner[1].value
|
||||
}
|
||||
|
||||
export function usesSerenity (answers: Questionnair) {
|
||||
return answers.framework.includes('serenity-js')
|
||||
}
|
||||
|
||||
function getTestingPurpose (answers: Questionnair) {
|
||||
return convertPackageHashToObject(answers.runner).purpose as 'e2e' | 'electron' | 'component' | 'vscode' | 'macos' | 'roku'
|
||||
}
|
||||
|
||||
export const isNuxtProject = [
|
||||
path.join(process.cwd(), 'nuxt.config.js'),
|
||||
path.join(process.cwd(), 'nuxt.config.ts'),
|
||||
path.join(process.cwd(), 'nuxt.config.mjs'),
|
||||
path.join(process.cwd(), 'nuxt.config.mts')
|
||||
].map((p) => {
|
||||
try {
|
||||
fs.accessSync(p)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}).some(Boolean)
|
||||
|
||||
function selectDefaultService (serviceNames: string | string[]) {
|
||||
serviceNames = Array.isArray(serviceNames) ? serviceNames : [serviceNames]
|
||||
return SUPPORTED_PACKAGES.service
|
||||
/* istanbul ignore next */
|
||||
.filter(({ name }) => serviceNames.includes(name))
|
||||
.map(({ value }) => value)
|
||||
}
|
||||
|
||||
function prioServiceOrderFor (serviceNamesParam: string | string[]) {
|
||||
const serviceNames = Array.isArray(serviceNamesParam) ? serviceNamesParam : [serviceNamesParam]
|
||||
let services = SUPPORTED_PACKAGES.service
|
||||
for (const serviceName of serviceNames) {
|
||||
const index = services.findIndex(({ name }) => name === serviceName)
|
||||
services = [services[index], ...services.slice(0, index), ...services.slice(index + 1)]
|
||||
}
|
||||
|
||||
return services
|
||||
}
|
||||
|
||||
export const QUESTIONNAIRE = [{
|
||||
type: 'list',
|
||||
name: 'runner',
|
||||
message: 'What type of testing would you like to do?',
|
||||
choices: SUPPORTED_PACKAGES.runner
|
||||
}, {
|
||||
type: 'list',
|
||||
name: 'preset',
|
||||
message: 'Which framework do you use for building components?',
|
||||
choices: SUPPORTED_BROWSER_RUNNER_PRESETS,
|
||||
// only ask if there are more than 1 runner to pick from
|
||||
when: /* istanbul ignore next */ isBrowserRunner
|
||||
}, {
|
||||
type: 'confirm',
|
||||
name: 'installTestingLibrary',
|
||||
message: 'Do you like to use Testing Library (https://testing-library.com/) as test utility?',
|
||||
default: true,
|
||||
// only ask if there are more than 1 runner to pick from
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => (
|
||||
isBrowserRunner(answers) &&
|
||||
/**
|
||||
* Only show if Testing Library has an add-on for framework
|
||||
*/
|
||||
answers.preset && TESTING_LIBRARY_PACKAGES[convertPackageHashToObject(answers.preset!).short]
|
||||
)
|
||||
}, {
|
||||
type: 'list',
|
||||
name: 'electronBuildTool',
|
||||
message: 'Which tool are you using to build your Electron app?',
|
||||
choices: Object.values(ElectronBuildToolChoice),
|
||||
when: /* instanbul ignore next */ (answers: Questionnair) => getTestingPurpose(answers) === 'electron'
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'electronAppBinaryPath',
|
||||
message: 'What is the path to the binary of your built Electron app?',
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => getTestingPurpose(answers) === 'electron' && (answers.electronBuildTool === ElectronBuildToolChoice.SomethingElse)
|
||||
}, {
|
||||
type: 'list',
|
||||
name: 'backend',
|
||||
message: 'Where is your automation backend located?',
|
||||
choices: Object.values(BackendChoice),
|
||||
when: /* instanbul ignore next */ (answers: Questionnair) => getTestingPurpose(answers) === 'e2e'
|
||||
}, {
|
||||
type: 'list',
|
||||
name: 'e2eEnvironment',
|
||||
message: 'Which environment you would like to automate?',
|
||||
choices: E2E_ENVIRONMENTS,
|
||||
default: 'web',
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => getTestingPurpose(answers) === 'e2e'
|
||||
}, {
|
||||
type: 'list',
|
||||
name: 'mobileEnvironment',
|
||||
message: 'Which mobile environment you\'d like to automate?',
|
||||
choices: MOBILE_ENVIRONMENTS,
|
||||
when: /* instanbul ignore next */ (answers: Questionnair) => (
|
||||
getTestingPurpose(answers) === 'e2e' &&
|
||||
answers.e2eEnvironment === 'mobile'
|
||||
)
|
||||
}, {
|
||||
type: 'checkbox',
|
||||
name: 'browserEnvironment',
|
||||
message: 'With which browser should we start?',
|
||||
choices: BROWSER_ENVIRONMENTS,
|
||||
when: /* instanbul ignore next */ (answers: Questionnair) => (
|
||||
getTestingPurpose(answers) === 'e2e' &&
|
||||
answers.e2eEnvironment === 'web'
|
||||
)
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'hostname',
|
||||
message: 'What is the host address of that cloud service?',
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => answers.backend && answers.backend.indexOf('different service') > -1
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'port',
|
||||
message: 'What is the port on which that service is running?',
|
||||
default: '80',
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => answers.backend && answers.backend.indexOf('different service') > -1
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'expEnvAccessKey',
|
||||
message: 'Access key from Experitest Cloud',
|
||||
default: 'EXPERITEST_ACCESS_KEY',
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => answers.backend === BackendChoice.Experitest
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'expEnvHostname',
|
||||
message: 'Environment variable for cloud url',
|
||||
default: 'example.experitest.com',
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => answers.backend === BackendChoice.Experitest
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'expEnvPort',
|
||||
message: 'Environment variable for port',
|
||||
default: '443',
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => answers.backend === BackendChoice.Experitest
|
||||
}, {
|
||||
type: 'list',
|
||||
name: 'expEnvProtocol',
|
||||
message: 'Choose a protocol for environment variable',
|
||||
default: ProtocolOptions.HTTPS,
|
||||
choices: Object.values(ProtocolOptions),
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => (
|
||||
answers.backend === BackendChoice.Experitest &&
|
||||
answers.expEnvPort !== '80' &&
|
||||
answers.expEnvPort !== '443'
|
||||
)
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'env_user',
|
||||
message: 'Environment variable for username',
|
||||
default: 'LT_USERNAME',
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => (
|
||||
answers.backend && answers.backend.indexOf('LambdaTest') > -1 &&
|
||||
answers.hostname!.indexOf('lambdatest.com') > -1
|
||||
)
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'env_key',
|
||||
message: 'Environment variable for access key',
|
||||
default: 'LT_ACCESS_KEY',
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => (
|
||||
answers.backend && answers.backend.indexOf('LambdaTest') > -1 &&
|
||||
answers.hostname!.indexOf('lambdatest.com') > -1
|
||||
)
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'env_user',
|
||||
message: 'Environment variable for username',
|
||||
default: 'BROWSERSTACK_USERNAME',
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => answers.backend === BackendChoice.Browserstack
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'env_key',
|
||||
message: 'Environment variable for access key',
|
||||
default: 'BROWSERSTACK_ACCESS_KEY',
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => answers.backend === BackendChoice.Browserstack
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'env_user',
|
||||
message: 'Environment variable for username',
|
||||
default: 'SAUCE_USERNAME',
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => answers.backend === BackendChoice.Saucelabs
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'env_key',
|
||||
message: 'Environment variable for access key',
|
||||
default: 'SAUCE_ACCESS_KEY',
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => answers.backend === BackendChoice.Saucelabs
|
||||
}, {
|
||||
type: 'list',
|
||||
name: 'region',
|
||||
message: 'In which region do you want to run your Sauce Labs tests in?',
|
||||
choices: Object.values(RegionOptions),
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => answers.backend === BackendChoice.Saucelabs
|
||||
}, {
|
||||
type: 'confirm',
|
||||
name: 'useSauceConnect',
|
||||
message: (
|
||||
'Are you testing a local application and need Sauce Connect to be set-up?\n' +
|
||||
'Read more on Sauce Connect at: https://docs.saucelabs.com/secure-connections/#sauce-connect-proxy'
|
||||
),
|
||||
default: isNuxtProject,
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => (
|
||||
answers.backend === BackendChoice.Saucelabs &&
|
||||
!isNuxtProject
|
||||
)
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'hostname',
|
||||
message: 'What is the IP or URI to your Selenium standalone or grid server?',
|
||||
default: 'localhost',
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => answers.backend && answers.backend.toString().indexOf('own Selenium cloud') > -1
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'port',
|
||||
message: 'What is the port which your Selenium standalone or grid server is running on?',
|
||||
default: '4444',
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => answers.backend && answers.backend.toString().indexOf('own Selenium cloud') > -1
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'path',
|
||||
message: 'What is the path to your browser driver or grid server?',
|
||||
default: '/',
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => answers.backend && answers.backend.toString().indexOf('own Selenium cloud') > -1
|
||||
}, {
|
||||
type: 'list',
|
||||
name: 'framework',
|
||||
message: 'Which framework do you want to use?',
|
||||
choices: /* instanbul ignore next */ (answers: Questionnair) => {
|
||||
/**
|
||||
* browser runner currently supports only Mocha framework
|
||||
*/
|
||||
if (isBrowserRunner(answers)) {
|
||||
return SUPPORTED_PACKAGES.framework.slice(0, 1)
|
||||
}
|
||||
/**
|
||||
* Serenity tests don't come with proper ElectronJS example files
|
||||
*/
|
||||
if (getTestingPurpose(answers) === 'electron') {
|
||||
return SUPPORTED_PACKAGES.framework.filter(
|
||||
({ value }) => !value.startsWith('@serenity-js')
|
||||
)
|
||||
}
|
||||
|
||||
return SUPPORTED_PACKAGES.framework
|
||||
}
|
||||
}, {
|
||||
type: 'confirm',
|
||||
name: 'isUsingTypeScript',
|
||||
message: 'Do you want to use Typescript to write tests?',
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => {
|
||||
/**
|
||||
* StencilJS only supports TypeScript - use the default
|
||||
*/
|
||||
if (answers.preset?.includes('stencil')) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
default: /* istanbul ignore next */ (answers: Questionnair) => answers.preset?.includes('stencil') || detectCompiler(answers)
|
||||
}, {
|
||||
type: 'confirm',
|
||||
name: 'generateTestFiles',
|
||||
message: 'Do you want WebdriverIO to autogenerate some test files?',
|
||||
default: true,
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => {
|
||||
/**
|
||||
* we only have examples for Mocha and Jasmine
|
||||
*/
|
||||
if (['vscode', 'electron', 'macos'].includes(getTestingPurpose(answers)) && answers.framework.includes('cucumber')) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'specs',
|
||||
message: 'What should be the location of your spec files?',
|
||||
default: /* istanbul ignore next */ (answers: Questionnair) => {
|
||||
const pattern = isBrowserRunner(answers) ? 'src/**/*.test' : 'test/specs/**/*'
|
||||
return getDefaultFiles(answers, pattern)
|
||||
},
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => answers.generateTestFiles && /(mocha|jasmine)/.test(answers.framework)
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'specs',
|
||||
message: 'What should be the location of your feature files?',
|
||||
default: (answers: Questionnair) => getDefaultFiles(answers, 'features/**/*.feature'),
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => answers.generateTestFiles && answers.framework.includes('cucumber')
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'stepDefinitions',
|
||||
message: 'What should be the location of your step definitions?',
|
||||
default: (answers: Questionnair) => getDefaultFiles(answers, 'features/step-definitions/steps'),
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => answers.generateTestFiles && answers.framework.includes('cucumber')
|
||||
}, {
|
||||
type: 'confirm',
|
||||
name: 'usePageObjects',
|
||||
message: 'Do you want to use page objects (https://martinfowler.com/bliki/PageObject.html)?',
|
||||
default: true,
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => (
|
||||
answers.generateTestFiles &&
|
||||
/**
|
||||
* page objects aren't common for component testing
|
||||
*/
|
||||
!isBrowserRunner(answers) &&
|
||||
/**
|
||||
* and also not needed when running VS Code tests since the service comes with
|
||||
* its own page object implementation, nor when running Electron or MacOS tests
|
||||
*/
|
||||
!['vscode', 'electron', 'macos'].includes(getTestingPurpose(answers)) &&
|
||||
/**
|
||||
* Serenity/JS generates Lean Page Objects by default, so there's no need to ask about it
|
||||
* See https://serenity-js.org/handbook/web-testing/page-objects-pattern/
|
||||
*/
|
||||
!usesSerenity(answers)
|
||||
)
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'pages',
|
||||
message: 'Where are your page objects located?',
|
||||
default: /* istanbul ignore next */ (answers: Questionnair) => (
|
||||
answers.framework.match(/(mocha|jasmine)/)
|
||||
? getDefaultFiles(answers, 'test/pageobjects/**/*')
|
||||
: getDefaultFiles(answers, 'features/pageobjects/**/*')
|
||||
),
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => answers.generateTestFiles && answers.usePageObjects
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'serenityLibPath',
|
||||
message: 'What should be the location of your Serenity/JS Screenplay Pattern library?',
|
||||
default: /* istanbul ignore next */ async (answers: Questionnair) => {
|
||||
const projectRootDir = await getProjectRoot(answers)
|
||||
const specsDir = path.resolve(projectRootDir, path.dirname(answers.specs || '').replace(/\*\*$/, ''))
|
||||
return path.resolve(specsDir, '..', 'serenity')
|
||||
},
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => answers.generateTestFiles && usesSerenity(answers)
|
||||
}, {
|
||||
type: 'checkbox',
|
||||
name: 'reporters',
|
||||
message: 'Which reporter do you want to use?',
|
||||
choices: SUPPORTED_PACKAGES.reporter
|
||||
}, {
|
||||
type: 'checkbox',
|
||||
name: 'plugins',
|
||||
message: 'Do you want to add a plugin to your test setup?',
|
||||
choices: SUPPORTED_PACKAGES.plugin,
|
||||
default: []
|
||||
}, {
|
||||
type: 'confirm',
|
||||
name: 'includeVisualTesting',
|
||||
message: 'Would you like to include Visual Testing to your setup? For more information see https://webdriver.io/docs/visual-testing!',
|
||||
default: false,
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => {
|
||||
/**
|
||||
* visual testing mostly makes sense for e2e and component tests
|
||||
*/
|
||||
return ['e2e', 'component'].includes(getTestingPurpose(answers))
|
||||
}
|
||||
}, {
|
||||
type: 'checkbox',
|
||||
name: 'services',
|
||||
message: 'Do you want to add a service to your test setup?',
|
||||
choices: (answers: Questionnair) => {
|
||||
const services: string[] = []
|
||||
if (answers.backend === BackendChoice.Browserstack) {
|
||||
services.push('browserstack')
|
||||
} else if (answers.backend === BackendChoice.Saucelabs) {
|
||||
services.push('sauce')
|
||||
}
|
||||
if (answers.e2eEnvironment === 'mobile') {
|
||||
services.push('appium')
|
||||
}
|
||||
if (getTestingPurpose(answers) === 'e2e' && isNuxtProject) {
|
||||
services.push('nuxt')
|
||||
}
|
||||
|
||||
if (getTestingPurpose(answers) === 'vscode') {
|
||||
return [SUPPORTED_PACKAGES.service.find(({ name }) => name === 'vscode')]
|
||||
} else if (getTestingPurpose(answers) === 'electron') {
|
||||
return [SUPPORTED_PACKAGES.service.find(({ name }) => name === 'electron')]
|
||||
} else if (getTestingPurpose(answers) === 'macos') {
|
||||
return [SUPPORTED_PACKAGES.service.find(({ name }) => name === 'appium')]
|
||||
} else if (getTestingPurpose(answers) === 'roku') {
|
||||
return [SUPPORTED_PACKAGES.service.find(({ name }) => name === 'roku')]
|
||||
}
|
||||
return prioServiceOrderFor(services)
|
||||
},
|
||||
default: (answers: Questionnair) => {
|
||||
const defaultServices: string[] = []
|
||||
if (answers.backend === BackendChoice.Browserstack) {
|
||||
defaultServices.push('browserstack')
|
||||
} else if (answers.backend === BackendChoice.Saucelabs) {
|
||||
defaultServices.push('sauce')
|
||||
}
|
||||
if (answers.e2eEnvironment === 'mobile' || getTestingPurpose(answers) === 'macos') {
|
||||
defaultServices.push('appium')
|
||||
}
|
||||
if (getTestingPurpose(answers) === 'vscode') {
|
||||
defaultServices.push('vscode')
|
||||
} else if (getTestingPurpose(answers) === 'electron') {
|
||||
defaultServices.push('electron')
|
||||
} else if (getTestingPurpose(answers) === 'roku') {
|
||||
defaultServices.push('roku')
|
||||
}
|
||||
if (isNuxtProject) {
|
||||
defaultServices.push('nuxt')
|
||||
}
|
||||
if (answers.includeVisualTesting) {
|
||||
defaultServices.push('visual')
|
||||
}
|
||||
return selectDefaultService(defaultServices)
|
||||
}
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'outputDir',
|
||||
message: 'In which directory should the xunit reports get stored?',
|
||||
default: './',
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => answers.reporters.includes('junit')
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'outputDir',
|
||||
message: 'In which directory should the json reports get stored?',
|
||||
default: './',
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => answers.reporters.includes('json')
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'outputDir',
|
||||
message: 'In which directory should the mochawesome json reports get stored?',
|
||||
default: './',
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => answers.reporters.includes('mochawesome')
|
||||
}, {
|
||||
type: 'confirm',
|
||||
name: 'npmInstall',
|
||||
message: () => `Do you want me to run \`${detectPackageManager()} install\``,
|
||||
default: true
|
||||
}]
|
||||
|
||||
const SUPPORTED_SNAPSHOTSTATE_OPTIONS = ['all', 'new', 'none'] as const
|
||||
export const COMMUNITY_PACKAGES_WITH_TS_SUPPORT = [
|
||||
'wdio-electron-service',
|
||||
'wdio-vscode-service',
|
||||
'wdio-nuxt-service',
|
||||
'wdio-vite-service',
|
||||
'wdio-gmail-service',
|
||||
'wdio-roku-service',
|
||||
'wdio-obsidian-service'
|
||||
]
|
||||
|
||||
export const TESTRUNNER_DEFAULTS: Options.Definition<Options.Testrunner & { capabilities: unknown }> = {
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,79 +1,8 @@
|
|||
import type { Options, Reporters } from '@wdio/types'
|
||||
import type { NormalizedPackageJson } from 'read-pkg-up'
|
||||
import type { BackendChoice, RegionOptions, ElectronBuildToolChoice, PMs } from './constants.js'
|
||||
import type { SUPPORTED_PACKAGE_MANAGERS } from 'create-wdio/utils'
|
||||
|
||||
export type PM = typeof PMs[number]
|
||||
|
||||
export interface Questionnair {
|
||||
runner: string
|
||||
preset?: string
|
||||
installTestingLibrary?: boolean
|
||||
electronAppBinaryPath?: string
|
||||
electronBuildTool?: ElectronBuildToolChoice
|
||||
electronBuilderConfigPath?: string
|
||||
backend?: BackendChoice
|
||||
hostname?: string
|
||||
port?: string
|
||||
path?: string
|
||||
expEnvAccessKey?: string
|
||||
expEnvHostname?: string
|
||||
expEnvPort?: string
|
||||
expEnvProtocol?: 'http' | 'https'
|
||||
|
||||
env_user?: string
|
||||
|
||||
env_key?: string
|
||||
region?: RegionOptions
|
||||
useSauceConnect?: boolean
|
||||
framework: string
|
||||
specs?: string
|
||||
stepDefinitions?: string
|
||||
generateTestFiles: boolean
|
||||
usePageObjects?: boolean
|
||||
pages?: string
|
||||
isUsingTypeScript: boolean
|
||||
reporters: string[]
|
||||
services: string[]
|
||||
serenityLibPath?: string
|
||||
plugins: string[]
|
||||
outputDir?: string
|
||||
includeVisualTesting: boolean
|
||||
npmInstall: boolean
|
||||
createPackageJSON?: boolean
|
||||
projectRootCorrect?: boolean
|
||||
projectRoot?: string
|
||||
e2eEnvironment?: 'web' | 'mobile'
|
||||
mobileEnvironment?: 'android' | 'ios'
|
||||
browserEnvironment?: ('chrome' | 'firefox' | 'safari' | 'microsoftedge')[]
|
||||
}
|
||||
|
||||
export interface ParsedAnswers extends Omit<Questionnair, 'runner' | 'framework' | 'reporters' | 'services' | 'plugins'> {
|
||||
rawAnswers: Questionnair
|
||||
runner: 'local' | 'browser'
|
||||
projectName: string
|
||||
framework: string
|
||||
purpose: string
|
||||
reporters: string[]
|
||||
plugins: string[]
|
||||
services: string[]
|
||||
packagesToInstall: string[]
|
||||
isUsingTypeScript: boolean
|
||||
serenityAdapter: string | false
|
||||
esmSupport: boolean
|
||||
isSync: boolean
|
||||
_async: string
|
||||
_await: string
|
||||
projectRootDir: string
|
||||
destSpecRootPath: string
|
||||
destPageObjectRootPath: string
|
||||
destStepRootPath: string;
|
||||
destSerenityLibRootPath: string
|
||||
relativePath: string
|
||||
hasRootTSConfig: boolean
|
||||
tsConfigFilePath: string
|
||||
tsProject: string
|
||||
wdioConfigPath: string
|
||||
}
|
||||
export type PM = typeof SUPPORTED_PACKAGE_MANAGERS[number]
|
||||
|
||||
export interface RunCommandArguments {
|
||||
coverage?: boolean
|
||||
|
|
@ -120,12 +49,6 @@ export interface InstallCommandArguments {
|
|||
name: string
|
||||
}
|
||||
|
||||
export interface ConfigCommandArguments {
|
||||
yarn: boolean
|
||||
yes: boolean
|
||||
npmTag: string
|
||||
}
|
||||
|
||||
export interface SupportedPackage {
|
||||
package: string
|
||||
short: string
|
||||
|
|
|
|||
|
|
@ -1,52 +1,25 @@
|
|||
import fs from 'node:fs/promises'
|
||||
import util, { promisify } from 'node:util'
|
||||
import path, { dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import type { SpawnOptions } from 'node:child_process'
|
||||
import { execSync, spawn } from 'node:child_process'
|
||||
|
||||
import { promisify } from 'node:util'
|
||||
|
||||
import ejs from 'ejs'
|
||||
import chalk from 'chalk'
|
||||
import inquirer from 'inquirer'
|
||||
import pickBy from 'lodash.pickby'
|
||||
import logger from '@wdio/logger'
|
||||
import readDir from 'recursive-readdir'
|
||||
import { $ } from 'execa'
|
||||
import { readPackageUp } from 'read-pkg-up'
|
||||
import { resolve } from 'import-meta-resolve'
|
||||
import { SevereServiceError } from 'webdriverio'
|
||||
import { ConfigParser } from '@wdio/config/node'
|
||||
import { CAPABILITY_KEYS } from '@wdio/protocols'
|
||||
import type { Capabilities, Services } from '@wdio/types'
|
||||
|
||||
import { installPackages, getInstallCommand } from './install.js'
|
||||
import {
|
||||
ANDROID_CONFIG,
|
||||
DEPENDENCIES_INSTALLATION_MESSAGE,
|
||||
IOS_CONFIG,
|
||||
pkg,
|
||||
QUESTIONNAIRE,
|
||||
TESTING_LIBRARY_PACKAGES,
|
||||
COMMUNITY_PACKAGES_WITH_TS_SUPPORT,
|
||||
usesSerenity,
|
||||
PMs,
|
||||
} from './constants.js'
|
||||
import type {
|
||||
OnCompleteResult,
|
||||
ParsedAnswers,
|
||||
ProjectProps,
|
||||
Questionnair,
|
||||
ReplCommandArguments,
|
||||
SupportedPackage,
|
||||
} from './types.js'
|
||||
import { EjsHelpers } from './templates/EjsHelpers.js'
|
||||
|
||||
const log = logger('@wdio/cli:utils')
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
const NPM_COMMAND = /^win/.test(process.platform) ? 'npm.cmd' : 'npm'
|
||||
const VERSION_REGEXP = /(\d+)\.(\d+)\.(\d+)-(alpha|beta|)\.(\d+)\+(.+)/g
|
||||
const TEMPLATE_ROOT_DIR = path.join(__dirname, 'templates', 'exampleFiles')
|
||||
export const renderFile = promisify(ejs.renderFile) as (path: string, data: Record<string, unknown>) => Promise<string>
|
||||
|
||||
export class HookError extends SevereServiceError {
|
||||
|
|
@ -172,24 +145,6 @@ export function getRunnerName(caps: WebdriverIO.Capabilities = {}) {
|
|||
return runner
|
||||
}
|
||||
|
||||
function buildNewConfigArray(str: string, type: string, change: string) {
|
||||
const newStr = str
|
||||
.split(`${type}s: `)[1]
|
||||
.replace(/'/g, '')
|
||||
|
||||
const newArray = newStr.match(/(\w*)/gmi)?.filter(e => !!e).concat([change]) || []
|
||||
|
||||
return str
|
||||
.replace('// ', '')
|
||||
.replace(
|
||||
new RegExp(`(${type}s: )((.*\\s*)*)`), `$1[${newArray.map(e => `'${e}'`)}]`
|
||||
)
|
||||
}
|
||||
|
||||
function buildNewConfigString(str: string, type: string, change: string) {
|
||||
return str.replace(new RegExp(`(${type}: )('\\w*')`), `$1'${change}'`)
|
||||
}
|
||||
|
||||
export function findInConfig(config: string, type: string) {
|
||||
let regexStr = `[\\/\\/]*[\\s]*${type}s: [\\s]*\\[([\\s]*['|"]\\w*['|"],*)*[\\s]*\\]`
|
||||
|
||||
|
|
@ -201,93 +156,6 @@ export function findInConfig(config: string, type: string) {
|
|||
return config.match(regex)
|
||||
}
|
||||
|
||||
export function replaceConfig(config: string, type: string, name: string) {
|
||||
if (type === 'framework') {
|
||||
return buildNewConfigString(config, type, name)
|
||||
}
|
||||
|
||||
const match = findInConfig(config, type)
|
||||
if (!match || match.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const text = match.pop() || ''
|
||||
return config.replace(text, buildNewConfigArray(text, type, name))
|
||||
}
|
||||
|
||||
export function addServiceDeps(names: SupportedPackage[], packages: string[], update = false) {
|
||||
/**
|
||||
* install Appium if it is not installed globally if `@wdio/appium-service`
|
||||
* was selected for install
|
||||
*/
|
||||
if (names.some(({ short }) => short === 'appium')) {
|
||||
const result = execSync('appium --version || echo APPIUM_MISSING', { stdio: 'pipe' }).toString().trim()
|
||||
if (result === 'APPIUM_MISSING') {
|
||||
packages.push('appium')
|
||||
} else if (update) {
|
||||
|
||||
console.log(
|
||||
'\n=======',
|
||||
'\nUsing globally installed appium', result,
|
||||
'\nPlease add the following to your wdio.conf.js:',
|
||||
"\nappium: { command: 'appium' }",
|
||||
'\n=======\n')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo add JSComments
|
||||
*/
|
||||
export function convertPackageHashToObject(pkg: string, hash = '$--$'): SupportedPackage {
|
||||
const [p, short, purpose] = pkg.split(hash)
|
||||
return { package: p, short, purpose }
|
||||
}
|
||||
|
||||
export function getSerenityPackages(answers: Questionnair): string[] {
|
||||
const framework = convertPackageHashToObject(answers.framework)
|
||||
|
||||
if (framework.package !== '@serenity-js/webdriverio') {
|
||||
return []
|
||||
}
|
||||
|
||||
const packages: Record<string, Array<string | false>> = {
|
||||
cucumber: [
|
||||
'@cucumber/cucumber',
|
||||
'@serenity-js/cucumber',
|
||||
],
|
||||
mocha: [
|
||||
'@serenity-js/mocha',
|
||||
'mocha',
|
||||
],
|
||||
jasmine: [
|
||||
'@serenity-js/jasmine',
|
||||
'jasmine',
|
||||
],
|
||||
common: [
|
||||
'@serenity-js/assertions',
|
||||
'@serenity-js/console-reporter',
|
||||
'@serenity-js/core',
|
||||
'@serenity-js/rest',
|
||||
'@serenity-js/serenity-bdd',
|
||||
'@serenity-js/web',
|
||||
'npm-failsafe',
|
||||
'rimraf',
|
||||
]
|
||||
}
|
||||
|
||||
if (answers.isUsingTypeScript) {
|
||||
packages.mocha.push('@types/mocha')
|
||||
packages.jasmine.push('@types/jasmine')
|
||||
packages.common.push('@types/node')
|
||||
}
|
||||
|
||||
return [
|
||||
...packages[framework.purpose],
|
||||
...packages.common,
|
||||
].filter(Boolean).sort() as string[]
|
||||
}
|
||||
|
||||
export async function getCapabilities(arg: ReplCommandArguments) {
|
||||
const optionalCapabilites = {
|
||||
platformVersion: arg.platformVersion,
|
||||
|
|
@ -337,723 +205,6 @@ export async function getCapabilities(arg: ReplCommandArguments) {
|
|||
return { capabilities: { browserName: arg.option } }
|
||||
}
|
||||
|
||||
/**
|
||||
* detect if project has a compiler file
|
||||
*/
|
||||
export async function detectCompiler(answers: Questionnair) {
|
||||
// obviously there is no compiler, if there is no package.json
|
||||
if (answers.createPackageJSON) {
|
||||
return false
|
||||
}
|
||||
|
||||
const root = await getProjectRoot(answers)
|
||||
const hasRootTSConfig = await fs.access(path.resolve(root, 'tsconfig.json')).then(() => true, () => false)
|
||||
return hasRootTSConfig
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if package is installed
|
||||
* @param {string} package to check existance for
|
||||
*/
|
||||
export async function hasPackage(pkg: string) {
|
||||
try {
|
||||
await resolve(pkg, import.meta.url)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* generate test files based on CLI answers
|
||||
*/
|
||||
export async function generateTestFiles(answers: ParsedAnswers) {
|
||||
if (answers.serenityAdapter) {
|
||||
return generateSerenityExamples(answers)
|
||||
}
|
||||
|
||||
if (answers.runner === 'local') {
|
||||
return generateLocalRunnerTestFiles(answers)
|
||||
}
|
||||
|
||||
return generateBrowserRunnerTestFiles(answers)
|
||||
}
|
||||
|
||||
const TSX_BASED_FRAMEWORKS = ['react', 'preact', 'solid', 'stencil']
|
||||
export async function generateBrowserRunnerTestFiles(answers: ParsedAnswers) {
|
||||
const isUsingFramework = typeof answers.preset === 'string'
|
||||
const preset = getPreset(answers)
|
||||
const tplRootDir = path.join(TEMPLATE_ROOT_DIR, 'browser')
|
||||
await fs.mkdir(answers.destSpecRootPath, { recursive: true })
|
||||
|
||||
/**
|
||||
* render css file
|
||||
*/
|
||||
if (isUsingFramework) {
|
||||
const renderedCss = await renderFile(path.join(tplRootDir, 'Component.css.ejs'), { answers })
|
||||
await fs.writeFile(path.join(answers.destSpecRootPath, 'Component.css'), renderedCss)
|
||||
}
|
||||
|
||||
/**
|
||||
* render component file
|
||||
*/
|
||||
const testExt = `${(answers.isUsingTypeScript ? 'ts' : 'js')}${TSX_BASED_FRAMEWORKS.includes(preset) ? 'x' : ''}`
|
||||
const fileExt = ['svelte', 'vue'].includes(preset as 'svelte' | 'vue')
|
||||
? preset!
|
||||
: testExt
|
||||
if (preset) {
|
||||
const componentOutFileName = `Component.${fileExt}`
|
||||
const renderedComponent = await renderFile(path.join(tplRootDir, `Component.${preset}.ejs`), { answers })
|
||||
await fs.writeFile(path.join(answers.destSpecRootPath, componentOutFileName), renderedComponent)
|
||||
}
|
||||
|
||||
/**
|
||||
* render test file
|
||||
*/
|
||||
const componentFileName = preset ? `Component.${preset}.test.ejs` : 'standalone.test.ejs'
|
||||
const renderedTest = await renderFile(path.join(tplRootDir, componentFileName), { answers })
|
||||
await fs.writeFile(path.join(answers.destSpecRootPath, `Component.test.${testExt}`), renderedTest)
|
||||
}
|
||||
|
||||
async function generateLocalRunnerTestFiles(answers: ParsedAnswers) {
|
||||
const testFiles = answers.framework === 'cucumber'
|
||||
? [path.join(TEMPLATE_ROOT_DIR, 'cucumber')]
|
||||
: [path.join(TEMPLATE_ROOT_DIR, 'mochaJasmine')]
|
||||
|
||||
if (answers.usePageObjects) {
|
||||
testFiles.push(path.join(TEMPLATE_ROOT_DIR, 'pageobjects'))
|
||||
}
|
||||
|
||||
const files = (await Promise.all(testFiles.map((dirPath) => readDir(
|
||||
dirPath,
|
||||
[(file, stats) => !stats.isDirectory() && !(file.endsWith('.ejs') || file.endsWith('.feature'))]
|
||||
)))).reduce((cur, acc) => [...acc, ...(cur)], [])
|
||||
|
||||
await Promise.all(files.map(async (file) => {
|
||||
const renderedTpl = await renderFile(file, { answers })
|
||||
const isJSX = answers.preset && TSX_BASED_FRAMEWORKS.includes(answers.preset)
|
||||
const fileEnding = (answers.isUsingTypeScript ? '.ts' : '.js') + (isJSX ? 'x' : '')
|
||||
const destPath = (
|
||||
file.endsWith('page.js.ejs')
|
||||
? path.join(answers.destPageObjectRootPath, path.basename(file))
|
||||
: file.includes('step_definition')
|
||||
? path.join(answers.destStepRootPath, path.basename(file))
|
||||
: path.join(answers.destSpecRootPath, path.basename(file))
|
||||
).replace(/\.ejs$/, '').replace(/\.js$/, fileEnding)
|
||||
|
||||
await fs.mkdir(path.dirname(destPath), { recursive: true })
|
||||
await fs.writeFile(destPath, renderedTpl)
|
||||
}))
|
||||
}
|
||||
|
||||
async function generateSerenityExamples(answers: ParsedAnswers): Promise<void> {
|
||||
const templateDirectories = Object.entries({
|
||||
[answers.projectRootDir]: path.join(TEMPLATE_ROOT_DIR, 'serenity-js', 'common', 'config'),
|
||||
[answers.destSpecRootPath]: path.join(TEMPLATE_ROOT_DIR, 'serenity-js', answers.serenityAdapter as string),
|
||||
[answers.destSerenityLibRootPath]: path.join(TEMPLATE_ROOT_DIR, 'serenity-js', 'common', 'serenity'),
|
||||
})
|
||||
|
||||
await Promise.all(templateDirectories.map(async ([destinationRootDir, templateRootDir]) => {
|
||||
const pathsToTemplates = await readDir(templateRootDir)
|
||||
|
||||
await Promise.all(pathsToTemplates.map(async (pathToTemplate) => {
|
||||
const extension = answers.isUsingTypeScript ? '.ts' : '.js'
|
||||
const destination = path.join(destinationRootDir, path.relative(templateRootDir, pathToTemplate))
|
||||
.replace(/\.ejs$/, '')
|
||||
.replace(/\.ts$/, extension)
|
||||
|
||||
const contents = await renderFile(
|
||||
pathToTemplate,
|
||||
{ answers, _: new EjsHelpers({ useEsm: answers.esmSupport, useTypeScript: answers.isUsingTypeScript }) },
|
||||
)
|
||||
|
||||
await fs.mkdir(path.dirname(destination), { recursive: true })
|
||||
await fs.writeFile(destination, contents)
|
||||
}))
|
||||
}))
|
||||
}
|
||||
|
||||
export async function getAnswers(yes: boolean): Promise<Questionnair> {
|
||||
if (yes) {
|
||||
const ignoredQuestions = ['e2eEnvironment']
|
||||
const filterdQuestionaire = QUESTIONNAIRE.filter((question) => !ignoredQuestions.includes(question.name))
|
||||
const answers = {} as Questionnair
|
||||
for (const question of filterdQuestionaire) {
|
||||
/**
|
||||
* set nothing if question doesn't apply
|
||||
*/
|
||||
if (question.when && !question.when(answers)) {
|
||||
continue
|
||||
}
|
||||
|
||||
Object.assign(answers, {
|
||||
[question.name]: typeof question.default !== 'undefined'
|
||||
/**
|
||||
* set default value if existing
|
||||
*/
|
||||
? typeof question.default === 'function'
|
||||
? await question.default(answers)
|
||||
: question.default
|
||||
: question.choices && question.choices.length
|
||||
/**
|
||||
* pick first choice, select value if it exists
|
||||
*/
|
||||
? typeof question.choices === 'function'
|
||||
? (question.choices(answers)[0] as unknown as { value: unknown }).value
|
||||
? (question.choices(answers)[0] as unknown as { value: unknown }).value
|
||||
: question.choices(answers)[0]
|
||||
: (question.choices[0] as { value: unknown }).value
|
||||
? question.type === 'checkbox'
|
||||
? [(question.choices[0] as { value: unknown }).value]
|
||||
: (question.choices[0] as { value: unknown }).value
|
||||
: question.choices[0]
|
||||
: {}
|
||||
})
|
||||
}
|
||||
/**
|
||||
* some questions have async defaults
|
||||
*/
|
||||
answers.isUsingTypeScript = await answers.isUsingTypeScript
|
||||
answers.specs = await answers.specs
|
||||
answers.pages = await answers.pages
|
||||
return answers
|
||||
}
|
||||
|
||||
const projectProps = await getProjectProps(process.cwd())
|
||||
const isProjectExisting = Boolean(projectProps)
|
||||
const nameInPackageJsonIsNotCreateWdioDefault = projectProps?.packageJson?.name !== 'my-new-project'
|
||||
const projectName = projectProps?.packageJson?.name ? ` named "${projectProps.packageJson.name}"` : ''
|
||||
|
||||
const projectRootQuestions = [
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'createPackageJSON',
|
||||
default: true,
|
||||
message: `Couldn't find a package.json in "${process.cwd()}" or any of the parent directories, do you want to create one?`,
|
||||
when: !isProjectExisting
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'projectRootCorrect',
|
||||
default: true,
|
||||
message: `A project${projectName} was detected at "${projectProps?.path}", correct?`,
|
||||
when: isProjectExisting && nameInPackageJsonIsNotCreateWdioDefault
|
||||
}, {
|
||||
type: 'input',
|
||||
name: 'projectRoot',
|
||||
message: 'What is the project root for your test project?',
|
||||
default: projectProps?.path,
|
||||
// only ask if there are more than 1 runner to pick from
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => isProjectExisting && nameInPackageJsonIsNotCreateWdioDefault && !answers.projectRootCorrect
|
||||
}
|
||||
]
|
||||
|
||||
// @ts-expect-error
|
||||
const answers = await inquirer.prompt(projectRootQuestions)
|
||||
if (answers.createPackageJSON) {
|
||||
answers.projectRoot = process.cwd()
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
return inquirer.prompt(QUESTIONNAIRE, answers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a valid file path from answers provided.
|
||||
* @param answers The answer from which a file path is to be generated.
|
||||
* @param projectRootDir The root directory of the project.
|
||||
* @returns filePath
|
||||
*/
|
||||
function generatePathfromAnswer(answers:string, projectRootDir:string):string {
|
||||
return path.resolve(
|
||||
projectRootDir, path.dirname(answers) === '.' ? path.resolve(answers) : path.dirname(answers))
|
||||
}
|
||||
|
||||
export function getPathForFileGeneration(answers: Questionnair, projectRootDir: string) {
|
||||
const specAnswer = answers.specs || ''
|
||||
const stepDefinitionAnswer = answers.stepDefinitions || ''
|
||||
const pageObjectAnswer = answers.pages || ''
|
||||
|
||||
const destSpecRootPath = generatePathfromAnswer(specAnswer, projectRootDir).replace(/\*\*$/, '')
|
||||
const destStepRootPath = generatePathfromAnswer(stepDefinitionAnswer, projectRootDir)
|
||||
const destPageObjectRootPath = answers.usePageObjects
|
||||
? generatePathfromAnswer(pageObjectAnswer, projectRootDir).replace(/\*\*$/, '')
|
||||
: ''
|
||||
const destSerenityLibRootPath = usesSerenity(answers)
|
||||
? path.resolve(projectRootDir, answers.serenityLibPath || 'serenity')
|
||||
: ''
|
||||
const relativePath = (answers.generateTestFiles && answers.usePageObjects)
|
||||
? !(convertPackageHashToObject(answers.framework).short === 'cucumber')
|
||||
? path.relative(destSpecRootPath, destPageObjectRootPath)
|
||||
: path.relative(destStepRootPath, destPageObjectRootPath)
|
||||
: ''
|
||||
|
||||
return {
|
||||
destSpecRootPath: destSpecRootPath,
|
||||
destStepRootPath: destStepRootPath,
|
||||
destPageObjectRootPath: destPageObjectRootPath,
|
||||
destSerenityLibRootPath: destSerenityLibRootPath,
|
||||
relativePath: relativePath.replaceAll(path.sep, '/')
|
||||
}
|
||||
}
|
||||
|
||||
export async function getDefaultFiles(answers: Questionnair, pattern: string) {
|
||||
const rootdir = await getProjectRoot(answers)
|
||||
const presetPackage = convertPackageHashToObject(answers.preset || '')
|
||||
const isJSX = TSX_BASED_FRAMEWORKS.includes(presetPackage.short || '')
|
||||
const val = pattern.endsWith('.feature')
|
||||
? path.join(rootdir, pattern)
|
||||
: answers?.isUsingTypeScript
|
||||
? `${path.join(rootdir, pattern)}.ts${isJSX ? 'x' : ''}`
|
||||
: `${path.join(rootdir, pattern)}.js${isJSX ? 'x' : ''}`
|
||||
return val
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure core WebdriverIO packages have the same version as cli so that if someone
|
||||
* installs `@wdio/cli@next` and runs the wizard, all related packages have the same version.
|
||||
* running `matchAll` to a version like "8.0.0-alpha.249+4bc237701", results in:
|
||||
* ['8.0.0-alpha.249+4bc237701', '8', '0', '0', 'alpha', '249', '4bc237701']
|
||||
*/
|
||||
export function specifyVersionIfNeeded(packagesToInstall: string[], version: string, npmTag: string) {
|
||||
const { value } = version.matchAll(VERSION_REGEXP).next()
|
||||
const [major, minor, patch, tagName, build] = (value || []).slice(1, -1) // drop commit bit
|
||||
return packagesToInstall.map((p) => {
|
||||
if (
|
||||
(p.startsWith('@wdio') && p !== '@wdio/visual-service') ||
|
||||
['webdriver', 'webdriverio'].includes(p)
|
||||
) {
|
||||
const tag = major && npmTag === 'latest'
|
||||
? `^${major}.${minor}.${patch}-${tagName}.${build}`
|
||||
: npmTag
|
||||
return `${p}@${tag}`
|
||||
}
|
||||
return p
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive project properties
|
||||
* @returns {@type ProjectProps} if a package.json can be found in cwd or parent directories, otherwise undefined
|
||||
* which means that a new project can be created
|
||||
*/
|
||||
export async function getProjectProps(cwd = process.cwd()): Promise<ProjectProps | undefined> {
|
||||
try {
|
||||
const { packageJson, path: packageJsonPath } = await readPackageUp({ cwd }) || {}
|
||||
if (!packageJson || !packageJsonPath) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return {
|
||||
esmSupported: (
|
||||
packageJson.type === 'module' ||
|
||||
typeof packageJson.module === 'string'
|
||||
),
|
||||
packageJson,
|
||||
path: path.dirname(packageJsonPath)
|
||||
}
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
export function runProgram(command: string, args: string[], options: SpawnOptions) {
|
||||
const child = spawn(command, args, { stdio: 'inherit', ...options })
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
let error: Error
|
||||
child.on('error', (e) => (error = e))
|
||||
child.on('close', code => {
|
||||
if (code !== 0) {
|
||||
return reject(new Error(
|
||||
(error && error.message) ||
|
||||
`Error calling: ${command} ${args.join(' ')}`
|
||||
))
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* create package.json if not already existing
|
||||
*/
|
||||
export async function createPackageJSON(parsedAnswers: ParsedAnswers) {
|
||||
const packageJsonExists = await fs.access(path.resolve(process.cwd(), 'package.json')).then(() => true, () => false)
|
||||
|
||||
// Use the exisitng package.json if it already exists.
|
||||
if (packageJsonExists) {
|
||||
return
|
||||
}
|
||||
|
||||
// If a user said no to creating a package.json, but it doesn't exist, abort.
|
||||
if (parsedAnswers.createPackageJSON === false) {
|
||||
/* istanbul ignore if */
|
||||
if (!packageJsonExists) {
|
||||
console.log(`No WebdriverIO configuration found in "${parsedAnswers.wdioConfigPath}"`)
|
||||
return !process.env.WDIO_UNIT_TESTS && process.exit(0)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Only create if the user gave explicit permission to
|
||||
if (parsedAnswers.createPackageJSON) {
|
||||
console.log(`Creating a ${chalk.bold('package.json')} for the directory...`)
|
||||
await fs.writeFile(path.resolve(process.cwd(), 'package.json'), JSON.stringify({
|
||||
name: 'webdriverio-tests',
|
||||
version: '0.0.0',
|
||||
private: true,
|
||||
license: 'ISC',
|
||||
type: 'module',
|
||||
dependencies: {},
|
||||
devDependencies: {}
|
||||
}, null, 2))
|
||||
console.log(chalk.green(chalk.bold('✔ Success!\n')))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* run npm install only if required by the user
|
||||
*/
|
||||
const SEP = '\n- '
|
||||
export async function npmInstall(parsedAnswers: ParsedAnswers, npmTag: string) {
|
||||
const servicePackages = parsedAnswers.rawAnswers.services.map((service) => convertPackageHashToObject(service))
|
||||
const presetPackage = convertPackageHashToObject(parsedAnswers.rawAnswers.preset || '')
|
||||
|
||||
/**
|
||||
* install Testing Library dependency if desired
|
||||
*/
|
||||
if (parsedAnswers.installTestingLibrary && TESTING_LIBRARY_PACKAGES[presetPackage.short]) {
|
||||
parsedAnswers.packagesToInstall.push(
|
||||
TESTING_LIBRARY_PACKAGES[presetPackage.short],
|
||||
'@testing-library/jest-dom'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* add helper package for Solidjs testing
|
||||
*/
|
||||
if (presetPackage.short === 'solid') {
|
||||
parsedAnswers.packagesToInstall.push('solid-js')
|
||||
}
|
||||
|
||||
/**
|
||||
* add visual service if user selected support for it
|
||||
*/
|
||||
if (parsedAnswers.includeVisualTesting) {
|
||||
parsedAnswers.packagesToInstall.push('@wdio/visual-service')
|
||||
}
|
||||
|
||||
/**
|
||||
* add dependency for Lit testing
|
||||
*/
|
||||
const preset = getPreset(parsedAnswers)
|
||||
if (preset === 'lit') {
|
||||
parsedAnswers.packagesToInstall.push('lit')
|
||||
}
|
||||
|
||||
/**
|
||||
* add dependency for Stencil testing
|
||||
*/
|
||||
if (preset === 'stencil') {
|
||||
parsedAnswers.packagesToInstall.push('@stencil/core')
|
||||
}
|
||||
|
||||
/**
|
||||
* add helper for React rendering when not using Testing Library
|
||||
*/
|
||||
if (presetPackage.short === 'react') {
|
||||
parsedAnswers.packagesToInstall.push('react')
|
||||
if (!parsedAnswers.installTestingLibrary) {
|
||||
parsedAnswers.packagesToInstall.push('react-dom')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* add Jasmine types if necessary
|
||||
*/
|
||||
if (parsedAnswers.framework === 'jasmine' && parsedAnswers.isUsingTypeScript) {
|
||||
parsedAnswers.packagesToInstall.push('@types/jasmine')
|
||||
}
|
||||
|
||||
/**
|
||||
* add Appium mobile drivers if desired
|
||||
*/
|
||||
if (parsedAnswers.purpose === 'macos') {
|
||||
parsedAnswers.packagesToInstall.push('appium-mac2-driver')
|
||||
}
|
||||
if (parsedAnswers.mobileEnvironment === 'android') {
|
||||
parsedAnswers.packagesToInstall.push('appium-uiautomator2-driver')
|
||||
}
|
||||
if (parsedAnswers.mobileEnvironment === 'ios') {
|
||||
parsedAnswers.packagesToInstall.push('appium-xcuitest-driver')
|
||||
}
|
||||
|
||||
/**
|
||||
* add packages that are required by services
|
||||
*/
|
||||
addServiceDeps(servicePackages, parsedAnswers.packagesToInstall)
|
||||
|
||||
/**
|
||||
* update package version if CLI is a pre release
|
||||
*/
|
||||
parsedAnswers.packagesToInstall = specifyVersionIfNeeded(parsedAnswers.packagesToInstall, pkg.version, npmTag)
|
||||
|
||||
const cwd = await getProjectRoot(parsedAnswers)
|
||||
const pm = detectPackageManager()
|
||||
if (parsedAnswers.npmInstall) {
|
||||
console.log(`Installing packages using ${pm}:${SEP}${parsedAnswers.packagesToInstall.join(SEP)}`)
|
||||
const success = await installPackages(cwd, parsedAnswers.packagesToInstall, true)
|
||||
if (success) {
|
||||
console.log(chalk.green(chalk.bold('✔ Success!\n')))
|
||||
}
|
||||
} else {
|
||||
const installationCommand = getInstallCommand(pm, parsedAnswers.packagesToInstall, true)
|
||||
console.log(util.format(DEPENDENCIES_INSTALLATION_MESSAGE, installationCommand))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* detect the package manager that was used
|
||||
* uses the environment variable `npm_config_user_agent` to detect the package manager
|
||||
* falls back to `npm` if no package manager could be detected
|
||||
*/
|
||||
export function detectPackageManager() {
|
||||
if (!process.env.npm_config_user_agent) {
|
||||
return 'npm'
|
||||
}
|
||||
const detectedPM = process.env.npm_config_user_agent.split('/')[0].toLowerCase()
|
||||
|
||||
const matchedPM = PMs.find(pm => pm.toLowerCase() === detectedPM)
|
||||
|
||||
return matchedPM || 'npm'
|
||||
}
|
||||
|
||||
/**
|
||||
* set up TypeScript if it is desired but not installed
|
||||
*/
|
||||
export async function setupTypeScript(parsedAnswers: ParsedAnswers) {
|
||||
/**
|
||||
* don't create a `tsconfig.json` if user doesn't want to use TypeScript
|
||||
*/
|
||||
if (!parsedAnswers.isUsingTypeScript) {
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
* don't set up TypeScript if a `tsconfig.json` already exists but ensure we install `tsx`
|
||||
* as it is a requirement for running tests with TypeScript
|
||||
*/
|
||||
if (parsedAnswers.hasRootTSConfig) {
|
||||
return
|
||||
}
|
||||
|
||||
console.log('Setting up TypeScript...')
|
||||
const frameworkPackage = convertPackageHashToObject(parsedAnswers.rawAnswers.framework)
|
||||
const servicePackages = parsedAnswers.rawAnswers.services.map((service) => convertPackageHashToObject(service))
|
||||
const serenityTypes = parsedAnswers.serenityAdapter === 'jasmine' ? ['jasmine'] : []
|
||||
|
||||
const types = [
|
||||
'node',
|
||||
...(parsedAnswers.framework === 'jasmine' ? ['jasmine'] : []),
|
||||
'@wdio/globals/types',
|
||||
...(parsedAnswers.framework === 'jasmine' ? ['expect-webdriverio/jasmine'] : ['expect-webdriverio']),
|
||||
...(parsedAnswers.serenityAdapter ? serenityTypes : [frameworkPackage.package]),
|
||||
...(parsedAnswers.runner === 'browser' ? ['@wdio/browser-runner'] : []),
|
||||
...servicePackages
|
||||
.filter(service => (
|
||||
/**
|
||||
* given that we know that all "official" services have
|
||||
* typescript support we only include them
|
||||
*/
|
||||
service.package.startsWith('@wdio') ||
|
||||
/**
|
||||
* also include community maintained packages with known
|
||||
* support for TypeScript
|
||||
*/
|
||||
COMMUNITY_PACKAGES_WITH_TS_SUPPORT.includes(service.package)
|
||||
)).map(service => service.package)
|
||||
]
|
||||
|
||||
const preset = getPreset(parsedAnswers)
|
||||
const config = {
|
||||
compilerOptions: {
|
||||
// compiler
|
||||
moduleResolution: 'node',
|
||||
module: !parsedAnswers.esmSupport ? 'commonjs' : 'ESNext',
|
||||
target: 'es2022',
|
||||
lib: ['es2022', 'dom'],
|
||||
types,
|
||||
skipLibCheck: true,
|
||||
// bundler
|
||||
noEmit: true,
|
||||
allowImportingTsExtensions: true,
|
||||
resolveJsonModule: true,
|
||||
isolatedModules: true,
|
||||
// linting
|
||||
strict: true,
|
||||
noUnusedLocals: true,
|
||||
noUnusedParameters: true,
|
||||
noFallthroughCasesInSwitch: true,
|
||||
...Object.assign(
|
||||
preset === 'lit'
|
||||
? {
|
||||
experimentalDecorators: true,
|
||||
useDefineForClassFields: false
|
||||
}
|
||||
: {},
|
||||
preset === 'react'
|
||||
? {
|
||||
jsx: 'react-jsx'
|
||||
}
|
||||
: {},
|
||||
preset === 'preact'
|
||||
? {
|
||||
jsx: 'react-jsx',
|
||||
jsxImportSource: 'preact'
|
||||
}
|
||||
: {},
|
||||
preset === 'solid'
|
||||
? {
|
||||
jsx: 'preserve',
|
||||
jsxImportSource: 'solid-js'
|
||||
}
|
||||
: {},
|
||||
preset === 'stencil'
|
||||
? {
|
||||
experimentalDecorators: true,
|
||||
jsx: 'react',
|
||||
jsxFactory: 'h',
|
||||
jsxFragmentFactory: 'Fragment'
|
||||
}
|
||||
: {}
|
||||
)
|
||||
},
|
||||
include: preset === 'svelte'
|
||||
? ['src/**/*.d.ts', 'src/**/*.ts', 'src/**/*.js', 'src/**/*.svelte']
|
||||
: preset === 'vue'
|
||||
? ['src/**/*.ts', 'src/**/*.d.ts', 'src/**/*.tsx', 'src/**/*.vue']
|
||||
: ['test', 'wdio.conf.ts']
|
||||
}
|
||||
|
||||
if (parsedAnswers.framework === 'cucumber') {
|
||||
config.include.push('features')
|
||||
}
|
||||
|
||||
await fs.mkdir(path.dirname(parsedAnswers.tsConfigFilePath), { recursive: true })
|
||||
await fs.writeFile(
|
||||
parsedAnswers.tsConfigFilePath,
|
||||
JSON.stringify(config, null, 4)
|
||||
)
|
||||
|
||||
console.log(chalk.green(chalk.bold('✔ Success!\n')))
|
||||
}
|
||||
|
||||
function getPreset (parsedAnswers: ParsedAnswers) {
|
||||
const isUsingFramework = typeof parsedAnswers.preset === 'string'
|
||||
return isUsingFramework ? (parsedAnswers.preset || 'lit') : ''
|
||||
}
|
||||
|
||||
export async function createWDIOConfig(parsedAnswers: ParsedAnswers) {
|
||||
try {
|
||||
console.log('Creating a WebdriverIO config file...')
|
||||
const tplPath = path.resolve(__dirname, 'templates', 'wdio.conf.tpl.ejs')
|
||||
const renderedTpl = await renderFile(tplPath, {
|
||||
answers: parsedAnswers,
|
||||
_: new EjsHelpers({ useEsm: parsedAnswers.esmSupport, useTypeScript: parsedAnswers.isUsingTypeScript })
|
||||
})
|
||||
await fs.writeFile(parsedAnswers.wdioConfigPath, renderedTpl)
|
||||
console.log(chalk.green(chalk.bold('✔ Success!\n')))
|
||||
|
||||
if (parsedAnswers.generateTestFiles) {
|
||||
console.log('Autogenerating test files...')
|
||||
await generateTestFiles(parsedAnswers)
|
||||
console.log(chalk.green(chalk.bold('✔ Success!\n')))
|
||||
}
|
||||
} catch (err) {
|
||||
throw new Error(`⚠️ Couldn't write config file: ${(err as Error).stack}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get project root directory based on questionair answers
|
||||
* @param answers questionair answers
|
||||
* @param projectProps project properties received via `getProjectProps`
|
||||
* @returns project root path
|
||||
*/
|
||||
export async function getProjectRoot (parsedAnswers?: Questionnair) {
|
||||
if (parsedAnswers?.createPackageJSON && parsedAnswers.projectRoot) {
|
||||
return parsedAnswers.projectRoot
|
||||
}
|
||||
|
||||
const root = (await getProjectProps())?.path
|
||||
if (!root) {
|
||||
throw new Error('Could not find project root directory with a package.json')
|
||||
}
|
||||
|
||||
return !parsedAnswers || parsedAnswers.projectRootCorrect
|
||||
? root
|
||||
: parsedAnswers.projectRoot || process.cwd()
|
||||
}
|
||||
|
||||
export async function createWDIOScript(parsedAnswers: ParsedAnswers) {
|
||||
const rootDir = await getProjectRoot(parsedAnswers)
|
||||
const pathToWdioConfig = `./${path.join('.', parsedAnswers.wdioConfigPath.replace(rootDir, ''))}`
|
||||
|
||||
const wdioScripts = {
|
||||
'wdio': `wdio run ${ pathToWdioConfig }`,
|
||||
}
|
||||
|
||||
const serenityScripts = {
|
||||
'serenity': 'failsafe serenity:clean wdio serenity:report',
|
||||
'serenity:clean': 'rimraf target',
|
||||
'wdio': `wdio run ${ pathToWdioConfig }`,
|
||||
'serenity:report': 'serenity-bdd run',
|
||||
}
|
||||
|
||||
const scripts = parsedAnswers.serenityAdapter ? serenityScripts : wdioScripts
|
||||
|
||||
for (const [script, command] of Object.entries(scripts)) {
|
||||
|
||||
const args = ['pkg', 'set', `scripts.${ script }=${ command }`]
|
||||
|
||||
try {
|
||||
console.log(`Adding ${chalk.bold(`"${ script }"`)} script to package.json`)
|
||||
await runProgram(NPM_COMMAND, args, { cwd: parsedAnswers.projectRootDir })
|
||||
} catch (err) {
|
||||
const [preArgs, scriptPath] = args.join(' ').split('=')
|
||||
console.error(
|
||||
`⚠️ Couldn't add script to package.json: "${(err as Error).message}", you can add it manually ` +
|
||||
`by running:\n\n\t${NPM_COMMAND} ${preArgs}="${scriptPath}"`
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
console.log(chalk.green(chalk.bold('✔ Success!')))
|
||||
return true
|
||||
}
|
||||
|
||||
export async function runAppiumInstaller(parsedAnswers: ParsedAnswers) {
|
||||
if (parsedAnswers.e2eEnvironment !== 'mobile') {
|
||||
return
|
||||
}
|
||||
|
||||
const answer = await inquirer.prompt({
|
||||
name: 'continueWithAppiumSetup',
|
||||
message: 'Continue with Appium setup using appium-installer (https://github.com/AppiumTestDistribution/appium-installer)?',
|
||||
type: 'confirm',
|
||||
default: true
|
||||
})
|
||||
|
||||
if (!answer.continueWithAppiumSetup) {
|
||||
return console.log(
|
||||
'Ok! You can learn more about setting up mobile environments in the ' +
|
||||
'Appium docs at https://appium.io/docs/en/latest/quickstart/'
|
||||
)
|
||||
}
|
||||
|
||||
return $({ stdio: 'inherit' })`npx appium-installer`
|
||||
}
|
||||
|
||||
const cucumberTypes: Record<string, string> = {
|
||||
paths: 'array',
|
||||
backtrace: 'boolean',
|
||||
|
|
|
|||
|
|
@ -1,23 +1,5 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`getAnswers 1`] = `
|
||||
{
|
||||
"backend": "On my local machine",
|
||||
"framework": "@wdio/mocha-framework$--$mocha",
|
||||
"generateTestFiles": true,
|
||||
"includeVisualTesting": false,
|
||||
"isUsingTypeScript": false,
|
||||
"npmInstall": true,
|
||||
"plugins": [],
|
||||
"reporters": [
|
||||
"@wdio/spec-reporter$--$spec",
|
||||
],
|
||||
"runner": "@wdio/local-runner$--$local$--$e2e",
|
||||
"services": [],
|
||||
"usePageObjects": true,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`getCapabilities > should get capability from wdio.conf.js 1`] = `
|
||||
{
|
||||
"capabilities": {
|
||||
|
|
@ -98,54 +80,3 @@ exports[`getCapabilities > should throw capability not provided 1`] = `[Error: P
|
|||
exports[`getCapabilities > should throw config not found error 1`] = `[Error: Config File not found: ./test.js]`;
|
||||
|
||||
exports[`getCapabilities > should throw config not found error 2`] = `[Error: Could not parse ./test.js, failed with error: ups]`;
|
||||
|
||||
exports[`npmInstall 1`] = `
|
||||
[
|
||||
[
|
||||
"/foo/bar",
|
||||
[
|
||||
"foo$--$bar",
|
||||
"bar$--$foo",
|
||||
"@testing-library/vue",
|
||||
"@testing-library/jest-dom",
|
||||
"@wdio/visual-service",
|
||||
"@types/jasmine",
|
||||
],
|
||||
true,
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`setupTypeScript 1`] = `
|
||||
"{
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "node",
|
||||
"module": "ESNext",
|
||||
"target": "es2022",
|
||||
"lib": [
|
||||
"es2022",
|
||||
"dom"
|
||||
],
|
||||
"types": [
|
||||
"node",
|
||||
"@wdio/globals/types",
|
||||
"expect-webdriverio",
|
||||
"foo",
|
||||
"wdio-electron-service"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": [
|
||||
"test",
|
||||
"wdio.conf.ts"
|
||||
]
|
||||
}"
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { vi, describe, it, expect, afterEach, beforeEach } from 'vitest'
|
|||
// @ts-expect-error mock
|
||||
import { yargs } from 'yargs'
|
||||
import * as runCmd from '../../src/commands/run.js'
|
||||
import * as configCmd from '../../src/commands/config.js'
|
||||
import { config as configCmd } from 'create-wdio/config/cli'
|
||||
|
||||
vi.mock('yargs')
|
||||
vi.mock('node:child_process', () => ({
|
||||
|
|
@ -16,12 +16,21 @@ vi.mock('node:child_process', () => ({
|
|||
exec: vi.fn()
|
||||
}))
|
||||
vi.mock('node:fs/promises', async (orig) => ({
|
||||
...(await orig()) as any,
|
||||
// ...(await orig()) as any,
|
||||
default: {
|
||||
access: vi.fn().mockResolvedValue(''),
|
||||
readFile: vi.fn()
|
||||
}
|
||||
}))
|
||||
vi.mock('create-wdio/config/cli', async (importActual)=>{
|
||||
const actual = await importActual() as any
|
||||
return {
|
||||
config: {
|
||||
...actual.config,
|
||||
missingConfigurationPrompt:vi.fn(),
|
||||
}
|
||||
}
|
||||
})
|
||||
vi.mock('./../../src/launcher', () => ({
|
||||
default: class {
|
||||
constructor (public wdioConfPath: string) {}
|
||||
|
|
@ -48,7 +57,7 @@ describe('Command: run', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
vi.mocked(fs.access).mockResolvedValue()
|
||||
vi.spyOn(configCmd, 'missingConfigurationPrompt').mockImplementation((): Promise<never> => {
|
||||
vi.mocked(configCmd.missingConfigurationPrompt).mockImplementation((): Promise<never> => {
|
||||
return undefined as never
|
||||
})
|
||||
vi.spyOn(console, 'error')
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
import { test, vi, expect } from 'vitest'
|
||||
import { getAnswers } from '../src/utils.js'
|
||||
|
||||
vi.mock('node:fs/promises', async (orig) => ({
|
||||
...(await orig()) as any,
|
||||
default: {
|
||||
access: vi.fn().mockRejectedValue(new Error('ENOENT'))
|
||||
}
|
||||
}))
|
||||
|
||||
test('runConfig with yes param', async () => {
|
||||
const answers = await getAnswers(true)
|
||||
expect(answers.backend).toEqual('On my local machine')
|
||||
expect(answers.includeVisualTesting).toEqual(false)
|
||||
expect(answers.generateTestFiles).toBeTruthy()
|
||||
expect(answers.usePageObjects).toBeTruthy()
|
||||
expect(answers.framework).toEqual('@wdio/mocha-framework$--$mocha')
|
||||
expect(answers.specs).toEqual(expect.any(String))
|
||||
})
|
||||
|
|
@ -1,26 +1,9 @@
|
|||
import { describe, it, expect } from 'vitest'
|
||||
import { TESTRUNNER_DEFAULTS } from '../src/constants.js'
|
||||
|
||||
import { convertPackageHashToObject } from '../src/utils.js'
|
||||
import { SUPPORTED_PACKAGES } from '../src/constants.js'
|
||||
|
||||
const supportedInstallations = [
|
||||
...SUPPORTED_PACKAGES.runner.map(({ value }) => convertPackageHashToObject(value)),
|
||||
...SUPPORTED_PACKAGES.plugin.map(({ value }) => convertPackageHashToObject(value)),
|
||||
...SUPPORTED_PACKAGES.service.map(({ value }) => convertPackageHashToObject(value)),
|
||||
...SUPPORTED_PACKAGES.reporter.map(({ value }) => convertPackageHashToObject(value)),
|
||||
...SUPPORTED_PACKAGES.framework.map(({ value }) => convertPackageHashToObject(value))
|
||||
]
|
||||
|
||||
class CustomReporter {}
|
||||
|
||||
describe('constants', () => {
|
||||
describe('plugin list', () => {
|
||||
it('should provide all a short name', () => {
|
||||
const pluginsWithoutShorts = supportedInstallations.filter((plugin) => !plugin.short)
|
||||
expect(pluginsWithoutShorts).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('TESTRUNNER_DEFAULTS', () => {
|
||||
it('should expect specs to be an array', () => {
|
||||
|
|
@ -53,11 +36,8 @@ describe('constants', () => {
|
|||
TESTRUNNER_DEFAULTS.capabilities!.validate!([{}])
|
||||
TESTRUNNER_DEFAULTS.capabilities!.validate!({})
|
||||
|
||||
// @ts-expect-error wrong parameter
|
||||
expect(() => TESTRUNNER_DEFAULTS.capabilities!.validate!('foobar')).toThrow()
|
||||
// @ts-expect-error wrong parameter
|
||||
expect(() => TESTRUNNER_DEFAULTS.capabilities!.validate!(false)).toThrow()
|
||||
// @ts-expect-error wrong parameter
|
||||
expect(() => TESTRUNNER_DEFAULTS.capabilities!.validate!([{}, 'foobar'])).toThrow()
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,31 +0,0 @@
|
|||
import { describe, it, vi, expect } from 'vitest'
|
||||
|
||||
import { execa } from 'execa'
|
||||
|
||||
import { installPackages, getInstallCommand } from '../src/install.js'
|
||||
|
||||
vi.mock('execa', () => ({
|
||||
execa: vi.fn().mockResolvedValue({ stdout: 'foo', stderr: 'bar', exitCode: 0 })
|
||||
}))
|
||||
|
||||
vi.mock('detect', () => ({
|
||||
detect: vi.fn().mockResolvedValue('pnpm')
|
||||
}))
|
||||
|
||||
describe('install', () => {
|
||||
it('installPackages passing', async () => {
|
||||
expect(await installPackages('/foo/bar', ['foo', 'bar'], true))
|
||||
.toBe(true)
|
||||
})
|
||||
|
||||
it('installPackages failing', async () => {
|
||||
vi.mocked(execa).mockResolvedValue({ stdout: 'foo', stderr: 'bar', exitCode: 1 } as any)
|
||||
expect(await installPackages('/foo/bar', ['foo', 'bar'], false))
|
||||
.toBe(false)
|
||||
})
|
||||
|
||||
it('getInstallCommand', () => {
|
||||
expect(getInstallCommand('pnpm', ['foo', 'bar'], true))
|
||||
.toBe('pnpm add foo bar --save-dev')
|
||||
})
|
||||
})
|
||||
|
|
@ -3,7 +3,6 @@ import * as cp from 'node:child_process'
|
|||
import fs from 'node:fs/promises'
|
||||
|
||||
import { vi, describe, it, expect, afterEach, beforeEach, test } from 'vitest'
|
||||
import { $ } from 'execa'
|
||||
import ejs from 'ejs'
|
||||
import inquirer from 'inquirer'
|
||||
import readDir from 'recursive-readdir'
|
||||
|
|
@ -17,31 +16,8 @@ import {
|
|||
runServiceHook,
|
||||
getRunnerName,
|
||||
findInConfig,
|
||||
replaceConfig,
|
||||
addServiceDeps,
|
||||
convertPackageHashToObject,
|
||||
getCapabilities,
|
||||
generateTestFiles,
|
||||
getPathForFileGeneration,
|
||||
getDefaultFiles,
|
||||
hasPackage,
|
||||
specifyVersionIfNeeded,
|
||||
getProjectRoot,
|
||||
getAnswers,
|
||||
getProjectProps,
|
||||
runProgram,
|
||||
createPackageJSON,
|
||||
npmInstall,
|
||||
setupTypeScript,
|
||||
createWDIOConfig,
|
||||
createWDIOScript,
|
||||
runAppiumInstaller,
|
||||
detectPackageManager,
|
||||
detectCompiler
|
||||
} from '../src/utils.js'
|
||||
import { parseAnswers } from '../src/commands/config.js'
|
||||
import { installPackages } from '../src/install.js'
|
||||
import type { Questionnair } from '../src/types.js'
|
||||
|
||||
vi.mock('ejs')
|
||||
vi.mock('inquirer')
|
||||
|
|
@ -83,11 +59,6 @@ vi.mock('execa', () => ({
|
|||
$: vi.fn().mockReturnValue(async (sh: string) => sh)
|
||||
}))
|
||||
|
||||
vi.mock('../src/install', () => ({
|
||||
installPackages: vi.fn(),
|
||||
getInstallCommand: vi.fn().mockReturnValue('npm install foo bar --save-dev')
|
||||
}))
|
||||
|
||||
beforeEach(() => {
|
||||
global.console.log = vi.fn()
|
||||
|
||||
|
|
@ -287,86 +258,6 @@ describe('findInConfig', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('replaceConfig', () => {
|
||||
it('correctly changes framework', () => {
|
||||
const fakeConfig = `exports.config = {
|
||||
runner: 'local',
|
||||
specs: [
|
||||
'./test/specs/**/*.js'
|
||||
],
|
||||
framework: 'mocha',
|
||||
}`
|
||||
|
||||
expect(replaceConfig(fakeConfig, 'framework', 'jasmine')).toBe(
|
||||
`exports.config = {
|
||||
runner: 'local',
|
||||
specs: [
|
||||
'./test/specs/**/*.js'
|
||||
],
|
||||
framework: 'jasmine',
|
||||
}`
|
||||
)
|
||||
})
|
||||
|
||||
it('correctly changes service', () => {
|
||||
const fakeConfig = `exports.config = {
|
||||
runner: 'local',
|
||||
specs: [
|
||||
'./test/specs/**/*.js'
|
||||
],
|
||||
services: [],
|
||||
framework: 'mocha',
|
||||
}`
|
||||
expect(replaceConfig(fakeConfig, 'service', 'sauce')).toBe(
|
||||
`exports.config = {
|
||||
runner: 'local',
|
||||
specs: [
|
||||
'./test/specs/**/*.js'
|
||||
],
|
||||
services: ['sauce'],
|
||||
framework: 'mocha',
|
||||
}`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('addServiceDeps', () => {
|
||||
it('should add appium', () => {
|
||||
const packages: any = []
|
||||
addServiceDeps([{ package: '@wdio/appium-service', short: 'appium', purpose: 'e2e' }], packages)
|
||||
expect(packages).toEqual(['appium'])
|
||||
})
|
||||
|
||||
it('should not add appium if globally installed', () => {
|
||||
// @ts-ignore
|
||||
|
||||
cp.execSyncRes = '1.13.0'
|
||||
const packages: any = []
|
||||
addServiceDeps([{ package: '@wdio/appium-service', short: 'appium', purpose: 'e2e' }], packages)
|
||||
expect(packages).toEqual([])
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.mocked(global.console.log).mockClear()
|
||||
})
|
||||
})
|
||||
|
||||
describe('convertPackageHashToObject', () => {
|
||||
it('works with default `$--$` hash', () => {
|
||||
expect(convertPackageHashToObject('test/package-name$--$package-name')).toMatchObject({
|
||||
package: 'test/package-name',
|
||||
short: 'package-name'
|
||||
})
|
||||
})
|
||||
|
||||
it('works with custom hash', () => {
|
||||
expect(convertPackageHashToObject('test/package-name##-##package-name', '##-##')).toMatchObject({
|
||||
package: 'test/package-name',
|
||||
short: 'package-name'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('getCapabilities', () => {
|
||||
it('should return driver with capabilities for android', async () => {
|
||||
expect(await getCapabilities({ option: 'foo.apk' } as any)).toMatchSnapshot()
|
||||
|
|
@ -431,574 +322,6 @@ describe('getCapabilities', () => {
|
|||
})
|
||||
})
|
||||
|
||||
test('hasPackage', async () => {
|
||||
expect(await hasPackage('yargs')).toBe(true)
|
||||
expect(await hasPackage('foobar')).toBe(false)
|
||||
})
|
||||
|
||||
describe('generateTestFiles', () => {
|
||||
it('Mocha with page objects', async () => {
|
||||
vi.mocked(readDir).mockResolvedValue([
|
||||
'/foo/bar/loo/page.js.ejs',
|
||||
'/foo/bar/example.e2e.js'
|
||||
] as any)
|
||||
const answers = {
|
||||
runner: 'local',
|
||||
framework: 'mocha',
|
||||
usePageObjects: true,
|
||||
generateTestFiles: true,
|
||||
destPageObjectRootPath: '/tests/page/objects/model',
|
||||
destSpecRootPath: '/tests/specs'
|
||||
}
|
||||
|
||||
await generateTestFiles(answers as any)
|
||||
|
||||
expect(readDir).toBeCalledTimes(2)
|
||||
expect(vi.mocked(readDir).mock.calls[0][0]).toContain('mocha')
|
||||
expect(vi.mocked(readDir).mock.calls[1][0]).toContain('pageobjects')
|
||||
|
||||
/**
|
||||
* test readDir callback
|
||||
*/
|
||||
const readDirCb = vi.mocked(readDir).mock.calls[0][1][0] as Function
|
||||
const stats = { isDirectory: vi.fn().mockReturnValue(false) }
|
||||
expect(readDirCb('/foo/bar.lala', stats)).toBe(true)
|
||||
expect(readDirCb('/foo/bar.js.ejs', stats)).toBe(false)
|
||||
expect(readDirCb('/foo/bar.feature', stats)).toBe(false)
|
||||
stats.isDirectory.mockReturnValue(true)
|
||||
expect(readDirCb('/foo/bar.lala', stats)).toBe(false)
|
||||
expect(readDirCb('/foo/bar.js.ejs', stats)).toBe(false)
|
||||
expect(readDirCb('/foo/bar.feature', stats)).toBe(false)
|
||||
|
||||
expect(ejs.renderFile).toBeCalledTimes(4)
|
||||
expect(ejs.renderFile).toBeCalledWith(
|
||||
'/foo/bar/loo/page.js.ejs',
|
||||
{ answers },
|
||||
expect.any(Function)
|
||||
)
|
||||
expect(ejs.renderFile).toBeCalledWith(
|
||||
'/foo/bar/example.e2e.js',
|
||||
{ answers },
|
||||
expect.any(Function)
|
||||
)
|
||||
expect(fs.mkdir).toBeCalledTimes(4)
|
||||
expect((vi.mocked(fs.writeFile).mock.calls[0][0] as string)
|
||||
.endsWith(`${path.sep}page${path.sep}objects${path.sep}model${path.sep}page.js`))
|
||||
.toBe(true)
|
||||
expect((vi.mocked(fs.writeFile).mock.calls[1][0] as string)
|
||||
.endsWith(`${path.sep}example.e2e.js`))
|
||||
.toBe(true)
|
||||
})
|
||||
|
||||
it('jasmine with page objects', async () => {
|
||||
const answers = {
|
||||
runner: 'local',
|
||||
framework: 'jasmine',
|
||||
usePageObjects: true,
|
||||
generateTestFiles: true,
|
||||
destPageObjectRootPath: '/tests/page/objects/model',
|
||||
destSpecRootPath: '/tests/specs'
|
||||
}
|
||||
|
||||
await generateTestFiles(answers as any)
|
||||
|
||||
expect(readDir).toBeCalledTimes(2)
|
||||
expect(vi.mocked(readDir).mock.calls[0][0]).toContain('mochaJasmine')
|
||||
expect(vi.mocked(readDir).mock.calls[1][0]).toContain('pageobjects')
|
||||
|
||||
/**
|
||||
* test readDir callback
|
||||
*/
|
||||
const readDirCb = vi.mocked(readDir).mock.calls[0][1][0] as Function
|
||||
const stats = { isDirectory: vi.fn().mockReturnValue(false) }
|
||||
expect(readDirCb('/foo/bar.lala', stats)).toBe(true)
|
||||
expect(readDirCb('/foo/bar.js.ejs', stats)).toBe(false)
|
||||
expect(readDirCb('/foo/bar.feature', stats)).toBe(false)
|
||||
stats.isDirectory.mockReturnValue(true)
|
||||
expect(readDirCb('/foo/bar.lala', stats)).toBe(false)
|
||||
expect(readDirCb('/foo/bar.js.ejs', stats)).toBe(false)
|
||||
expect(readDirCb('/foo/bar.feature', stats)).toBe(false)
|
||||
|
||||
expect(ejs.renderFile).toBeCalledTimes(4)
|
||||
expect(ejs.renderFile).toBeCalledWith(
|
||||
'/foo/bar/loo/page.js.ejs',
|
||||
{ answers },
|
||||
expect.any(Function)
|
||||
)
|
||||
expect(ejs.renderFile).toBeCalledWith(
|
||||
'/foo/bar/example.e2e.js',
|
||||
{ answers },
|
||||
expect.any(Function)
|
||||
)
|
||||
expect(fs.mkdir).toBeCalledTimes(4)
|
||||
expect((vi.mocked(fs.writeFile).mock.calls[0][0] as string)
|
||||
.endsWith(`${path.sep}page${path.sep}objects${path.sep}model${path.sep}page.js`))
|
||||
.toBe(true)
|
||||
expect((vi.mocked(fs.writeFile).mock.calls[1][0] as string)
|
||||
.endsWith(`${path.sep}example.e2e.js`))
|
||||
.toBe(true)
|
||||
})
|
||||
|
||||
it('Jasmine with page generation and no pageObjects', async () => {
|
||||
vi.mocked(readDir).mockResolvedValue([] as any)
|
||||
const answers = {
|
||||
runner: 'local',
|
||||
specs: './tests/e2e/**/*.js',
|
||||
framework: 'jasmine',
|
||||
generateTestFiles: false,
|
||||
usePageObjects: false
|
||||
}
|
||||
|
||||
await generateTestFiles(answers as any)
|
||||
|
||||
expect(readDir).toBeCalledTimes(1)
|
||||
expect(ejs.renderFile).toBeCalledTimes(0)
|
||||
})
|
||||
|
||||
it('Cucumber with page generation and no pageObjects', async () => {
|
||||
vi.mocked(readDir).mockResolvedValue([] as any)
|
||||
const answers = {
|
||||
runner: 'local',
|
||||
specs: './tests/e2e/**/*.js',
|
||||
framework: 'cucumber',
|
||||
generateTestFiles: false,
|
||||
usePageObjects: false,
|
||||
}
|
||||
|
||||
await generateTestFiles(answers as any)
|
||||
|
||||
expect(readDir).toBeCalledTimes(1)
|
||||
expect(ejs.renderFile).toBeCalledTimes(0)
|
||||
})
|
||||
|
||||
it('Cucumber without page objects', async () => {
|
||||
vi.mocked(readDir).mockResolvedValue([
|
||||
'/foo/bar/loo/step_definition/example.step.js',
|
||||
'/foo/bar/example.feature'
|
||||
] as any)
|
||||
const answers = {
|
||||
runner: 'local',
|
||||
specs: './tests/e2e/*.js',
|
||||
framework: 'cucumber',
|
||||
stepDefinitions: '/some/step/defs',
|
||||
usePageObjects: false,
|
||||
generateTestFiles: true,
|
||||
destSpecRootPath: '/tests/specs',
|
||||
destStepRootPath: '/tests/stepDefinitions'
|
||||
}
|
||||
await generateTestFiles(answers as any)
|
||||
|
||||
expect(readDir).toBeCalledTimes(1)
|
||||
expect(vi.mocked(readDir).mock.calls[0][0]).toContain('cucumber')
|
||||
expect(ejs.renderFile).toBeCalledTimes(2)
|
||||
expect(ejs.renderFile).toBeCalledWith(
|
||||
'/foo/bar/loo/step_definition/example.step.js',
|
||||
{ answers },
|
||||
expect.any(Function)
|
||||
)
|
||||
expect(ejs.renderFile).toBeCalledWith(
|
||||
'/foo/bar/example.feature',
|
||||
{ answers },
|
||||
expect.any(Function)
|
||||
)
|
||||
expect(fs.mkdir).toBeCalledTimes(2)
|
||||
})
|
||||
|
||||
it('Cucumber with page objects and TypeScript', async () => {
|
||||
vi.mocked(readDir).mockResolvedValue([
|
||||
'/foo/bar/loo/page.js.ejs',
|
||||
'/foo/bar/loo/step_definition/example.step.js',
|
||||
'/foo/bar/example.feature'
|
||||
] as any)
|
||||
const answers = {
|
||||
runner: 'local',
|
||||
framework: 'cucumber',
|
||||
usePageObjects: true,
|
||||
isUsingTypeScript: true,
|
||||
destStepRootPath: '/tests/stepDefinitions',
|
||||
destSpecRootPath: '/tests/specs',
|
||||
destPageObjectRootPath: '/some/page/objects',
|
||||
relativePath: '../page/object'
|
||||
}
|
||||
await generateTestFiles(answers as any)
|
||||
|
||||
expect(readDir).toBeCalledTimes(2)
|
||||
expect(vi.mocked(readDir).mock.calls[0][0]).toContain('cucumber')
|
||||
expect(ejs.renderFile).toBeCalledTimes(6)
|
||||
expect(ejs.renderFile).toBeCalledWith(
|
||||
'/foo/bar/loo/step_definition/example.step.js',
|
||||
{ answers },
|
||||
expect.any(Function)
|
||||
)
|
||||
expect(ejs.renderFile).toBeCalledWith(
|
||||
'/foo/bar/example.feature',
|
||||
{ answers },
|
||||
expect.any(Function)
|
||||
)
|
||||
expect(fs.mkdir).toBeCalledTimes(6)
|
||||
expect(
|
||||
(vi.mocked(fs.writeFile).mock.calls[0][0] as string)
|
||||
.endsWith(`${path.sep}some${path.sep}page${path.sep}objects${path.sep}page.ts`)
|
||||
).toBe(true)
|
||||
expect(
|
||||
(vi.mocked(fs.writeFile).mock.calls[2][0] as string)
|
||||
.endsWith(`${path.sep}example.feature`)
|
||||
).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getPathForFileGeneration', () => {
|
||||
it('Cucumber with pageobjects default values', () => {
|
||||
const generatedPaths = getPathForFileGeneration({
|
||||
runner: 'local',
|
||||
stepDefinitions: './features/step-definitions/steps.js',
|
||||
pages: './features/pageobjects/**/*.js',
|
||||
generateTestFiles: true,
|
||||
usePageObjects: true,
|
||||
framework: '@wdio/cucumber-service$--$cucumber'
|
||||
} as any, '/foo/bar')
|
||||
expect(generatedPaths.relativePath).toEqual('../pageobjects')
|
||||
})
|
||||
|
||||
it('Cucumber with pageobjects default different path', () => {
|
||||
const generatedPaths = getPathForFileGeneration({
|
||||
runner: 'local',
|
||||
stepDefinitions: './features/step-definitions/steps.js',
|
||||
pages: './features/page/objects/**/*.js',
|
||||
generateTestFiles: true,
|
||||
usePageObjects: true,
|
||||
framework: '@wdio/cucumber-service$--$cucumber'
|
||||
} as any, '/foo/bar')
|
||||
expect(generatedPaths.relativePath).toEqual('../page/objects')
|
||||
})
|
||||
it('Cucumber with pageobjects and steps different path', () => {
|
||||
const generatedPaths = getPathForFileGeneration({
|
||||
runner: 'local',
|
||||
stepDefinitions: 'cucumber/features/steps',
|
||||
pages: 'cucumber/features/pages',
|
||||
generateTestFiles: true,
|
||||
usePageObjects: true,
|
||||
framework: '@wdio/cucumber-service$--$cucumber'
|
||||
} as any, '/foo/bar')
|
||||
expect(generatedPaths.relativePath).toEqual('')
|
||||
})
|
||||
|
||||
it('Cucumber with answer that is not a path', () => {
|
||||
const generatedPaths = getPathForFileGeneration({
|
||||
runner: 'local',
|
||||
stepDefinitions: 'y',
|
||||
pages: 'h',
|
||||
generateTestFiles: true,
|
||||
usePageObjects: true,
|
||||
framework: '@wdio/cucumber-service$--$cucumber'
|
||||
} as any, '/foo/bar')
|
||||
expect(generatedPaths.relativePath).toEqual('../h')
|
||||
})
|
||||
|
||||
it('Mocha with pageobjects default values', () => {
|
||||
const generatedPaths = getPathForFileGeneration({
|
||||
runner: 'local',
|
||||
specs: './test/specs/**/*.js',
|
||||
pages: './test/pageobjects/**/*.js',
|
||||
generateTestFiles: true,
|
||||
usePageObjects: true,
|
||||
framework: '@wdio/cucumber-service$--$mocha'
|
||||
} as any, '/foo/bar')
|
||||
expect(generatedPaths.relativePath).toEqual('../pageobjects')
|
||||
})
|
||||
|
||||
it('Mocha with pageobjects different path', () => {
|
||||
const generatedPaths = getPathForFileGeneration({
|
||||
runner: 'local',
|
||||
specs: './test/specs/files/**/*.js',
|
||||
pages: './test/pageobjects/**/*.js',
|
||||
generateTestFiles: true,
|
||||
usePageObjects: true,
|
||||
framework: '@wdio/cucumber-service$--$mocha'
|
||||
} as any, '/foo/bar')
|
||||
expect(generatedPaths.relativePath).toEqual('../../pageobjects')
|
||||
})
|
||||
|
||||
it('Do not auto generate file', () => {
|
||||
const generatedPaths = getPathForFileGeneration({
|
||||
runner: 'local',
|
||||
specs: './test/specs/files/**/*.js',
|
||||
pages: './test/pageobjects/**/*.js',
|
||||
generateTestFiles: false,
|
||||
usePageObjects: true,
|
||||
framework: '@wdio/cucumber-service$--$mocha'
|
||||
} as any, '/foo/bar')
|
||||
expect(generatedPaths.relativePath).toEqual('')
|
||||
})
|
||||
|
||||
it('Do not use PageObjects', () => {
|
||||
const generatedPaths = getPathForFileGeneration({
|
||||
runner: 'local',
|
||||
specs: './test/specs/files/**/*.js',
|
||||
pages: './test/pageobjects/**/*.js',
|
||||
generateTestFiles: true,
|
||||
usePageObjects: false,
|
||||
framework: '@wdio/cucumber-service$--$mocha'
|
||||
} as any, '/foo/bar')
|
||||
expect(generatedPaths.relativePath).toEqual('')
|
||||
})
|
||||
})
|
||||
|
||||
test('getDefaultFiles', async () => {
|
||||
const files = '/foo/bar'
|
||||
expect(await getDefaultFiles({ projectRootCorrect: false, projectRoot: '/bar', isUsingTypeScript: true } as any, files))
|
||||
.toBe(path.join('/bar', 'foo', 'bar.ts'))
|
||||
expect(await getDefaultFiles({ projectRootCorrect: false, projectRoot: '/bar', isUsingTypeScript: true, preset: 'vite-plugin-solid$--$solid' } as any, files))
|
||||
.toBe(path.join('/bar', 'foo', 'bar.tsx'))
|
||||
expect(await getDefaultFiles({ projectRootCorrect: false, projectRoot: '/bar', isUsingTypeScript: false } as any, files))
|
||||
.toBe(path.join('/bar', 'foo', 'bar.js'))
|
||||
})
|
||||
|
||||
test('original implementation of getDefaultFiles handles projectRoot with no package.json', async () => {
|
||||
const files = '/foo/bar'
|
||||
vi.mocked(readPackageUp).mockRestore()
|
||||
expect(await getDefaultFiles({ createPackageJSON: true, projectRoot: '/project-root-with-no-package.json', isUsingTypeScript: false } as any, files))
|
||||
.toBe(path.join('/project-root-with-no-package.json', 'foo', 'bar.js'))
|
||||
})
|
||||
|
||||
test('specifyVersionIfNeeded', () => {
|
||||
expect(specifyVersionIfNeeded(
|
||||
['webdriverio', '@wdio/spec-reporter'],
|
||||
'8.0.0-alpha.249+4bc237701',
|
||||
'latest'
|
||||
)).toEqual([
|
||||
'webdriverio@^8.0.0-alpha.249',
|
||||
'@wdio/spec-reporter@^8.0.0-alpha.249'
|
||||
])
|
||||
})
|
||||
|
||||
test('getProjectRoot', async () => {
|
||||
expect(await getProjectRoot()).toBe('/foo')
|
||||
expect(await getProjectRoot({
|
||||
projectRootCorrect: true
|
||||
} as any)).toBe('/foo')
|
||||
expect(await getProjectRoot({
|
||||
projectRootCorrect: false,
|
||||
projectRoot: '/bar/foo'
|
||||
} as any)).toBe('/bar/foo')
|
||||
})
|
||||
|
||||
test('detectCompiler', async () => {
|
||||
vi.mocked(readPackageUp).mockRestore()
|
||||
const answers = { createPackageJSON: true } as Questionnair
|
||||
expect(await detectCompiler(answers)).toBe(false)
|
||||
})
|
||||
|
||||
test('getAnswers', async () => {
|
||||
let answers = await getAnswers(true)
|
||||
delete answers.pages // delete so it doesn't fail in Windows
|
||||
delete answers.specs // delete so it doesn't fail in Windows
|
||||
expect(answers).toMatchSnapshot()
|
||||
vi.mocked(inquirer.prompt).mockReturnValue('some value' as any)
|
||||
answers = await getAnswers(false)
|
||||
delete answers.pages // delete so it doesn't fail in Windows
|
||||
delete answers.specs // delete so it doesn't fail in Windows
|
||||
expect(answers).toBe('some value')
|
||||
expect(inquirer.prompt).toBeCalledTimes(2)
|
||||
// @ts-ignore
|
||||
expect(vi.mocked(inquirer.prompt).mock.calls[0][0][0].when).toBe(false)
|
||||
vi.mocked(readPackageUp).mockResolvedValue(undefined)
|
||||
vi.mocked(inquirer.prompt).mockClear()
|
||||
expect(await getAnswers(false)).toBe('some value')
|
||||
expect(inquirer.prompt).toBeCalledTimes(2)
|
||||
// @ts-ignore
|
||||
expect(vi.mocked(inquirer.prompt).mock.calls[0][0][0].when).toBe(true)
|
||||
})
|
||||
|
||||
test('getProjectProps', async () => {
|
||||
vi.mocked(readPackageUp).mockResolvedValue(undefined)
|
||||
expect(await getProjectProps('/foo/bar')).toBe(undefined)
|
||||
expect(readPackageUp).toBeCalledWith({ cwd: '/foo/bar' })
|
||||
vi.mocked(readPackageUp).mockResolvedValue({
|
||||
path: '/foo/bar',
|
||||
packageJson: {
|
||||
name: 'cool-test-module2',
|
||||
}
|
||||
})
|
||||
expect(await getProjectProps('/foo/bar')).toEqual({
|
||||
esmSupported: false,
|
||||
packageJson: { name: 'cool-test-module2' },
|
||||
path: '/foo'
|
||||
})
|
||||
})
|
||||
|
||||
test('runProgram', () => {
|
||||
runProgram('foobar', [1, 2] as any, { foo: 'bar' } as any)
|
||||
expect(cp.spawn).toBeCalledWith('foobar', [1, 2], { foo: 'bar', stdio: 'inherit' })
|
||||
})
|
||||
|
||||
test('createPackageJSON', async () => {
|
||||
await createPackageJSON({} as any)
|
||||
expect(fs.writeFile).toBeCalledTimes(0)
|
||||
await createPackageJSON({
|
||||
createPackageJSON: true
|
||||
} as any)
|
||||
expect(console.log).toBeCalledTimes(2)
|
||||
})
|
||||
|
||||
test('npmInstall', async () => {
|
||||
const parsedAnswers = {
|
||||
rawAnswers: {
|
||||
services: ['foo$--$bar'],
|
||||
preset: 'barfoo$--$vue'
|
||||
},
|
||||
projectRootCorrect: false,
|
||||
projectRoot: '/foo/bar',
|
||||
isUsingTypeScript: true,
|
||||
framework: 'jasmine',
|
||||
installTestingLibrary: true,
|
||||
packagesToInstall: ['foo$--$bar', 'bar$--$foo'],
|
||||
npmInstall: true,
|
||||
includeVisualTesting: true
|
||||
} as any
|
||||
await npmInstall(parsedAnswers, 'next')
|
||||
expect(installPackages).toBeCalledTimes(1)
|
||||
expect(vi.mocked(installPackages).mock.calls).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('not npmInstall', async () => {
|
||||
const parsedAnswers = {
|
||||
rawAnswers: {
|
||||
services: ['foo$--$bar'],
|
||||
preset: 'barfoo$--$vue'
|
||||
},
|
||||
installTestingLibrary: true,
|
||||
packagesToInstall: ['foo$--$bar', 'bar$--$foo'],
|
||||
npmInstall: false
|
||||
} as any
|
||||
await npmInstall(parsedAnswers, 'next')
|
||||
expect(installPackages).toBeCalledTimes(0)
|
||||
expect(vi.mocked(console.log).mock.calls[0][0]).toContain('To install dependencies, execute')
|
||||
})
|
||||
|
||||
test('setupTypeScript', async () => {
|
||||
await setupTypeScript({} as any)
|
||||
expect(fs.writeFile).toBeCalledTimes(0)
|
||||
const parsedAnswers = {
|
||||
isUsingTypeScript: true,
|
||||
esmSupport: true,
|
||||
rawAnswers: {
|
||||
framework: 'foo',
|
||||
services: [
|
||||
'wdio-foobar-service$--$foobar',
|
||||
'wdio-electron-service$--$electron'
|
||||
]
|
||||
},
|
||||
packagesToInstall: [],
|
||||
tsConfigFilePath: '/foobar/tsconfig.json'
|
||||
} as any
|
||||
await setupTypeScript(parsedAnswers)
|
||||
expect(vi.mocked(fs.writeFile).mock.calls[0][1]).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('setupTypeScript does not create tsconfig.json if TypeScript was not selected', async () => {
|
||||
const parsedAnswers = {
|
||||
isUsingTypeScript: false,
|
||||
esmSupport: true,
|
||||
rawAnswers: {
|
||||
framework: 'foo',
|
||||
services: [
|
||||
'wdio-foobar-service$--$foobar',
|
||||
'wdio-electron-service$--$electron'
|
||||
]
|
||||
},
|
||||
packagesToInstall: [],
|
||||
tsConfigFilePath: '/foobar/tsconfig.json'
|
||||
} as any
|
||||
await setupTypeScript(parsedAnswers)
|
||||
expect(fs.writeFile).not.toBeCalled()
|
||||
expect(parsedAnswers.packagesToInstall).toEqual([])
|
||||
})
|
||||
|
||||
test('setupTypeScript does not create tsconfig.json if there is already one', async () => {
|
||||
const parsedAnswers = {
|
||||
isUsingTypeScript: true,
|
||||
esmSupport: true,
|
||||
rawAnswers: {
|
||||
framework: 'foo',
|
||||
services: [
|
||||
'wdio-foobar-service$--$foobar',
|
||||
'wdio-electron-service$--$electron'
|
||||
]
|
||||
},
|
||||
packagesToInstall: [],
|
||||
tsConfigFilePath: '/foobar/tsconfig.json',
|
||||
hasRootTSConfig: true
|
||||
} as any
|
||||
await setupTypeScript(parsedAnswers)
|
||||
expect(fs.writeFile).not.toBeCalled()
|
||||
})
|
||||
|
||||
test('createWDIOConfig', async () => {
|
||||
const answers = await parseAnswers(true)
|
||||
answers.destSpecRootPath = '/tests/specs'
|
||||
answers.destPageObjectRootPath = '/tests/specs'
|
||||
answers.stepDefinitions = './foo/bar'
|
||||
answers.specs = '/foo/bar/**'
|
||||
await createWDIOConfig(answers as any)
|
||||
expect(fs.writeFile).toBeCalledTimes(7)
|
||||
expect(
|
||||
vi.mocked(fs.writeFile).mock.calls[0][0]
|
||||
.toString()
|
||||
.endsWith(path.resolve('wdio.conf.js'))
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
describe('createWDIOScript', () => {
|
||||
it('can run with success', async () => {
|
||||
expect(await createWDIOScript({ wdioConfigPath: '/foo/bar/wdio.conf.js' } as any))
|
||||
.toBe(true)
|
||||
expect(cp.spawn).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
it('does not fail the process if spawn errors out', async () => {
|
||||
vi.mocked(cp.spawn).mockReturnValue({ on: vi.fn().mockImplementation((ev, fn) => fn(1)) } as any)
|
||||
expect(await createWDIOScript({ wdioConfigPath: '/foo/bar/wdio.conf.js' } as any))
|
||||
.toBe(false)
|
||||
expect(cp.spawn).toBeCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
test('runAppiumInstaller', async () => {
|
||||
expect(await runAppiumInstaller({ e2eEnvironment: 'web' } as any))
|
||||
.toBe(undefined)
|
||||
expect(console.log).toBeCalledTimes(0)
|
||||
expect($).toBeCalledTimes(0)
|
||||
|
||||
vi.mocked(inquirer.prompt).mockResolvedValue({
|
||||
continueWithAppiumSetup: false
|
||||
})
|
||||
|
||||
expect(await runAppiumInstaller({ e2eEnvironment: 'mobile' } as any))
|
||||
.toBe(undefined)
|
||||
expect(console.log).toBeCalledTimes(1)
|
||||
expect($).toBeCalledTimes(0)
|
||||
|
||||
vi.mocked(inquirer.prompt).mockResolvedValue({
|
||||
continueWithAppiumSetup: true
|
||||
})
|
||||
expect(await runAppiumInstaller({ e2eEnvironment: 'mobile' } as any))
|
||||
.toEqual(['npx appium-installer'])
|
||||
expect($).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
test.each([
|
||||
['with empty variable should fallback to npm', '', 'npm'],
|
||||
['with pnpm should return pnpm', 'pnpm/9.10.0 npm/? node/v20.11.0 darwin arm64', 'pnpm'],
|
||||
['with yarn should return yarn', 'yarn/4.5.0 npm/? node/v20.11.0 darwin arm64', 'yarn'],
|
||||
['with npm should return npm', 'npm/10.2.4 node/v20.11.0 darwin arm64 workspaces/false', 'npm'],
|
||||
['with bun should return bun', 'bun/1.1.27 npm/? node/v22.6.0 darwin arm64', 'bun'],
|
||||
['with unsupported package manager should return npm', 'not supported package manager/x.x.x npm/? node/v22.6.0 darwin arm64', 'npm'],
|
||||
])('detectPackageManager %s', async (_, stub, pm) => {
|
||||
vi.stubEnv('npm_config_user_agent', stub)
|
||||
expect(detectPackageManager()).toEqual(pm)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.mocked(inquirer.prompt).mockClear()
|
||||
vi.mocked(console.log).mockRestore()
|
||||
|
|
@ -1007,5 +330,4 @@ afterEach(() => {
|
|||
vi.mocked(cp.spawn).mockClear()
|
||||
vi.mocked(fs.mkdir).mockClear()
|
||||
vi.mocked(ejs.renderFile).mockClear()
|
||||
vi.mocked(installPackages).mockClear()
|
||||
})
|
||||
|
|
|
|||
457
pnpm-lock.yaml
457
pnpm-lock.yaml
|
|
@ -480,6 +480,67 @@ importers:
|
|||
|
||||
infra/lernaPatch: {}
|
||||
|
||||
packages/create-wdio:
|
||||
dependencies:
|
||||
chalk:
|
||||
specifier: ^5.3.0
|
||||
version: 5.4.1
|
||||
commander:
|
||||
specifier: ^14.0.0
|
||||
version: 14.0.0
|
||||
cross-spawn:
|
||||
specifier: ^7.0.3
|
||||
version: 7.0.6
|
||||
ejs:
|
||||
specifier: ^3.1.10
|
||||
version: 3.1.10
|
||||
execa:
|
||||
specifier: ^9.6.0
|
||||
version: 9.6.0
|
||||
import-meta-resolve:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0
|
||||
inquirer:
|
||||
specifier: ^12.7.0
|
||||
version: 12.7.0(@types/node@22.15.34)
|
||||
normalize-package-data:
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.0
|
||||
read-pkg-up:
|
||||
specifier: ^10.1.0
|
||||
version: 10.1.0
|
||||
recursive-readdir:
|
||||
specifier: ^2.2.3
|
||||
version: 2.2.3
|
||||
semver:
|
||||
specifier: ^7.6.3
|
||||
version: 7.7.2
|
||||
type-fest:
|
||||
specifier: ^4.41.0
|
||||
version: 4.41.0
|
||||
yargs:
|
||||
specifier: ^17.7.2
|
||||
version: 17.7.2
|
||||
devDependencies:
|
||||
'@types/cross-spawn':
|
||||
specifier: ^6.0.6
|
||||
version: 6.0.6
|
||||
'@types/ejs':
|
||||
specifier: ^3.1.5
|
||||
version: 3.1.5
|
||||
'@types/fs-extra':
|
||||
specifier: ^11.0.4
|
||||
version: 11.0.4
|
||||
'@types/normalize-package-data':
|
||||
specifier: ^2.4.4
|
||||
version: 2.4.4
|
||||
'@types/semver':
|
||||
specifier: ^7.5.8
|
||||
version: 7.7.0
|
||||
'@types/yargs':
|
||||
specifier: ^17.0.33
|
||||
version: 17.0.33
|
||||
|
||||
packages/eslint-plugin-wdio:
|
||||
devDependencies:
|
||||
'@types/estree':
|
||||
|
|
@ -774,6 +835,9 @@ importers:
|
|||
chokidar:
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.3
|
||||
create-wdio:
|
||||
specifier: workspace:*
|
||||
version: link:../create-wdio
|
||||
dotenv:
|
||||
specifier: ^16.3.1
|
||||
version: 16.6.1
|
||||
|
|
@ -801,9 +865,6 @@ importers:
|
|||
read-pkg-up:
|
||||
specifier: ^10.0.0
|
||||
version: 10.1.0
|
||||
recursive-readdir:
|
||||
specifier: ^2.2.3
|
||||
version: 2.2.3
|
||||
tsx:
|
||||
specifier: ^4.7.2
|
||||
version: 4.20.3
|
||||
|
|
@ -3939,6 +4000,15 @@ packages:
|
|||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/checkbox@4.1.9':
|
||||
resolution: {integrity: sha512-DBJBkzI5Wx4jFaYm221LHvAhpKYkhVS0k9plqHwaHhofGNxvYB7J3Bz8w+bFJ05zaMb0sZNHo4KdmENQFlNTuQ==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/confirm@4.0.1':
|
||||
resolution: {integrity: sha512-46yL28o2NJ9doViqOy0VDcoTzng7rAb6yPQKU7VDLqkmbCaH4JqK4yk4XqlzNWy9PVC5pG1ZUXPBQv+VqnYs2w==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -3952,6 +4022,15 @@ packages:
|
|||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/confirm@5.1.13':
|
||||
resolution: {integrity: sha512-EkCtvp67ICIVVzjsquUiVSd+V5HRGOGQfsqA4E4vMWhYnB7InUL0pa0TIWt1i+OfP16Gkds8CdIu6yGZwOM1Yw==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/core@10.1.13':
|
||||
resolution: {integrity: sha512-1viSxebkYN2nJULlzCxES6G9/stgHSepZ9LqqfdIGPHj5OHhiBUXVS0a6R0bEC2A+VL4D9w6QB66ebCr6HGllA==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -3961,6 +4040,15 @@ packages:
|
|||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/core@10.1.14':
|
||||
resolution: {integrity: sha512-Ma+ZpOJPewtIYl6HZHZckeX1STvDnHTCB2GVINNUlSEn2Am6LddWwfPkIGY0IUFVjUUrr/93XlBwTK6mfLjf0A==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/core@9.2.1':
|
||||
resolution: {integrity: sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -3978,6 +4066,15 @@ packages:
|
|||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/editor@4.2.14':
|
||||
resolution: {integrity: sha512-yd2qtLl4QIIax9DTMZ1ZN2pFrrj+yL3kgIWxm34SS6uwCr0sIhsNyudUjAo5q3TqI03xx4SEBkUJqZuAInp9uA==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/expand@3.0.1':
|
||||
resolution: {integrity: sha512-ToG8d6RIbnVpbdPdiN7BCxZGiHOTomOX94C2FaT5KOHupV40tKEDozp12res6cMIfRKrXLJyexAZhWVHgbALSQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -3991,6 +4088,15 @@ packages:
|
|||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/expand@4.0.16':
|
||||
resolution: {integrity: sha512-oiDqafWzMtofeJyyGkb1CTPaxUkjIcSxePHHQCfif8t3HV9pHcw1Kgdw3/uGpDvaFfeTluwQtWiqzPVjAqS3zA==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/figures@1.0.12':
|
||||
resolution: {integrity: sha512-MJttijd8rMFcKJC8NYmprWr6hD3r9Gd9qUC0XwPNwoEPWSMVJwA2MlXxF+nhZZNMY+HXsWa+o7KY2emWYIn0jQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -4008,6 +4114,15 @@ packages:
|
|||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/input@4.2.0':
|
||||
resolution: {integrity: sha512-opqpHPB1NjAmDISi3uvZOTrjEEU5CWVu/HBkDby8t93+6UxYX0Z7Ps0Ltjm5sZiEbWenjubwUkivAEYQmy9xHw==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/number@2.0.1':
|
||||
resolution: {integrity: sha512-QpR8jPhRjSmlr/mD2cw3IR8HRO7lSVOnqUvQa8scv1Lsr3xoAMMworcYW3J13z3ppjBFBD2ef1Ci6AE5Qn8goQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -4021,6 +4136,15 @@ packages:
|
|||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/number@3.0.16':
|
||||
resolution: {integrity: sha512-kMrXAaKGavBEoBYUCgualbwA9jWUx2TjMA46ek+pEKy38+LFpL9QHlTd8PO2kWPUgI/KB+qi02o4y2rwXbzr3Q==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/password@3.0.1':
|
||||
resolution: {integrity: sha512-haoeEPUisD1NeE2IanLOiFr4wcTXGWrBOyAyPZi1FfLJuXOzNmxCJPgUrGYKVh+Y8hfGJenIfz5Wb/DkE9KkMQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -4034,6 +4158,15 @@ packages:
|
|||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/password@4.0.16':
|
||||
resolution: {integrity: sha512-g8BVNBj5Zeb5/Y3cSN+hDUL7CsIFDIuVxb9EPty3lkxBaYpjL5BNRKSYOF9yOLe+JOcKFd+TSVeADQ4iSY7rbg==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/prompts@6.0.1':
|
||||
resolution: {integrity: sha512-yl43JD/86CIj3Mz5mvvLJqAOfIup7ncxfJ0Btnl0/v5TouVUyeEdcpknfgc+yMevS/48oH9WAkkw93m7otLb/A==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -4047,6 +4180,15 @@ packages:
|
|||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/prompts@7.6.0':
|
||||
resolution: {integrity: sha512-jAhL7tyMxB3Gfwn4HIJ0yuJ5pvcB5maYUcouGcgd/ub79f9MqZ+aVnBtuFf+VC2GTkCBF+R+eo7Vi63w5VZlzw==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/rawlist@3.0.1':
|
||||
resolution: {integrity: sha512-VgRtFIwZInUzTiPLSfDXK5jLrnpkuSOh1ctfaoygKAdPqjcjKYmGh6sCY1pb0aGnCGsmhUxoqLDUAU0ud+lGXQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -4060,6 +4202,15 @@ packages:
|
|||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/rawlist@4.1.4':
|
||||
resolution: {integrity: sha512-5GGvxVpXXMmfZNtvWw4IsHpR7RzqAR624xtkPd1NxxlV5M+pShMqzL4oRddRkg8rVEOK9fKdJp1jjVML2Lr7TQ==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/search@2.0.1':
|
||||
resolution: {integrity: sha512-r5hBKZk3g5MkIzLVoSgE4evypGqtOannnB3PKTG9NRZxyFRKcfzrdxXXPcoJQsxJPzvdSU2Rn7pB7lw0GCmGAg==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -4073,6 +4224,15 @@ packages:
|
|||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/search@3.0.16':
|
||||
resolution: {integrity: sha512-POCmXo+j97kTGU6aeRjsPyuCpQQfKcMXdeTMw708ZMtWrj5aykZvlUxH4Qgz3+Y1L/cAVZsSpA+UgZCu2GMOMg==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/select@3.0.1':
|
||||
resolution: {integrity: sha512-lUDGUxPhdWMkN/fHy1Lk7pF3nK1fh/gqeyWXmctefhxLYxlDsc7vsPBEpxrfVGDsVdyYJsiJoD4bJ1b623cV1Q==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -4086,6 +4246,15 @@ packages:
|
|||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/select@4.2.4':
|
||||
resolution: {integrity: sha512-unTppUcTjmnbl/q+h8XeQDhAqIOmwWYWNyiiP2e3orXrg6tOaa5DHXja9PChCSbChOsktyKgOieRZFnajzxoBg==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/type@2.0.0':
|
||||
resolution: {integrity: sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -4504,6 +4673,46 @@ packages:
|
|||
'@originjs/vite-plugin-commonjs@1.0.3':
|
||||
resolution: {integrity: sha512-KuEXeGPptM2lyxdIEJ4R11+5ztipHoE7hy8ClZt3PYaOVQ/pyngd2alaSrPnwyFeOW1UagRBaQ752aA1dTMdOQ==}
|
||||
|
||||
'@oxlint/darwin-arm64@1.6.0':
|
||||
resolution: {integrity: sha512-m3wyqBh1TOHjpr/dXeIZY7OoX+MQazb+bMHQdDtwUvefrafUx+5YHRvulYh1sZSQ449nQ3nk3qj5qj535vZRjg==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@oxlint/darwin-x64@1.6.0':
|
||||
resolution: {integrity: sha512-75fJfF/9xNypr7cnOYoZBhfmG1yP7ex3pUOeYGakmtZRffO9z1i1quLYhjZsmaDXsAIZ3drMhenYHMmFKS3SRg==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@oxlint/linux-arm64-gnu@1.6.0':
|
||||
resolution: {integrity: sha512-YhXGf0FXa72bEt4F7eTVKx5X3zWpbAOPnaA/dZ6/g8tGhw1m9IFjrabVHFjzcx3dQny4MgA59EhyElkDvpUe8A==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@oxlint/linux-arm64-musl@1.6.0':
|
||||
resolution: {integrity: sha512-T3JDhx8mjGjvh5INsPZJrlKHmZsecgDYvtvussKRdkc1Nnn7WC+jH9sh5qlmYvwzvmetlPVNezAoNvmGO9vtMg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@oxlint/linux-x64-gnu@1.6.0':
|
||||
resolution: {integrity: sha512-Dx7ghtAl8aXBdqofJpi338At6lkeCtTfoinTYQXd9/TEJx+f+zCGNlQO6nJz3ydJBX48FDuOFKkNC+lUlWrd8w==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@oxlint/linux-x64-musl@1.6.0':
|
||||
resolution: {integrity: sha512-7KvMGdWmAZtAtg6IjoEJHKxTXdAcrHnUnqfgs0JpXst7trquV2mxBeRZusQXwxpu4HCSomKMvJfsp1qKaqSFDg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@oxlint/win32-arm64@1.6.0':
|
||||
resolution: {integrity: sha512-iSGC9RwX+dl7o5KFr5aH7Gq3nFbkq/3Gda6mxNPMvNkWrgXdIyiINxpyD8hJu566M+QSv1wEAu934BZotFDyoQ==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@oxlint/win32-x64@1.6.0':
|
||||
resolution: {integrity: sha512-jOj3L/gfLc0IwgOTkZMiZ5c673i/hbAmidlaylT0gE6H18hln9HxPgp5GCf4E4y6mwEJlW8QC5hQi221+9otdA==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@paralleldrive/cuid2@2.2.2':
|
||||
resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==}
|
||||
|
||||
|
|
@ -5402,6 +5611,9 @@ packages:
|
|||
'@types/connect@3.4.38':
|
||||
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
|
||||
|
||||
'@types/cross-spawn@6.0.6':
|
||||
resolution: {integrity: sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==}
|
||||
|
||||
'@types/dateformat@5.0.3':
|
||||
resolution: {integrity: sha512-hvskLa9ObgUwJe0rtfkyBbU+VIK8Ia3BACsxRsp4CPnyPBAOyIHbqImfBGFfaVVxn82wGx5SD8Twll6jMc/BaQ==}
|
||||
|
||||
|
|
@ -5447,6 +5659,9 @@ packages:
|
|||
'@types/express@5.0.3':
|
||||
resolution: {integrity: sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==}
|
||||
|
||||
'@types/fs-extra@11.0.4':
|
||||
resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==}
|
||||
|
||||
'@types/gitconfiglocal@2.0.3':
|
||||
resolution: {integrity: sha512-W6hyZux6TrtKfF2I9XNLVcsFr4xRr0T+S6hrJ9nDkhA2vzsFPIEAbnY4vgb6v2yKXQ9MJVcbLsARNlMfg4EVtQ==}
|
||||
|
||||
|
|
@ -5507,6 +5722,9 @@ packages:
|
|||
'@types/json-stringify-safe@5.0.3':
|
||||
resolution: {integrity: sha512-oNOjRxLfPeYbBSQ60maucaFNqbslVOPU4WWs5t/sHvAh6tyo/CThXSG+E24tEzkgh/fzvxyDrYdOJufgeNy1sQ==}
|
||||
|
||||
'@types/jsonfile@6.1.4':
|
||||
resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==}
|
||||
|
||||
'@types/keyv@3.1.4':
|
||||
resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==}
|
||||
|
||||
|
|
@ -5626,6 +5844,9 @@ packages:
|
|||
'@types/sax@1.2.7':
|
||||
resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==}
|
||||
|
||||
'@types/semver@7.7.0':
|
||||
resolution: {integrity: sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==}
|
||||
|
||||
'@types/send@0.17.5':
|
||||
resolution: {integrity: sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==}
|
||||
|
||||
|
|
@ -6841,6 +7062,10 @@ packages:
|
|||
resolution: {integrity: sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
commander@14.0.0:
|
||||
resolution: {integrity: sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
commander@2.20.3:
|
||||
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
||||
|
||||
|
|
@ -8720,6 +8945,10 @@ packages:
|
|||
resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==}
|
||||
engines: {node: ^16.14.0 || >=18.0.0}
|
||||
|
||||
hosted-git-info@8.1.0:
|
||||
resolution: {integrity: sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw==}
|
||||
engines: {node: ^18.17.0 || >=20.5.0}
|
||||
|
||||
hpack.js@2.1.6:
|
||||
resolution: {integrity: sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==}
|
||||
|
||||
|
|
@ -8994,6 +9223,15 @@ packages:
|
|||
'@types/node':
|
||||
optional: true
|
||||
|
||||
inquirer@12.7.0:
|
||||
resolution: {integrity: sha512-KKFRc++IONSyE2UYw9CJ1V0IWx5yQKomwB+pp3cWomWs+v2+ZsG11G2OVfAjFS6WWCppKw+RfKmpqGfSzD5QBQ==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
inquirer@8.2.6:
|
||||
resolution: {integrity: sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
|
@ -10497,6 +10735,10 @@ packages:
|
|||
resolution: {integrity: sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==}
|
||||
engines: {node: ^16.14.0 || >=18.0.0}
|
||||
|
||||
normalize-package-data@7.0.0:
|
||||
resolution: {integrity: sha512-k6U0gKRIuNCTkwHGZqblCfLfBRh+w1vI6tBo+IeJwq2M8FUiOqhX7GH+GArQGScA7azd1WfyRCvxoXDO3hQDIA==}
|
||||
engines: {node: ^18.17.0 || >=20.5.0}
|
||||
|
||||
normalize-path@3.0.0:
|
||||
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
|
@ -10687,6 +10929,11 @@ packages:
|
|||
resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
oxlint@1.6.0:
|
||||
resolution: {integrity: sha512-jtaD65PqzIa1udvSxxscTKBxYKuZoFXyKGLiU1Qjo1ulq3uv/fQDtoV1yey1FrQZrQjACGPi1Widsy1TucC7Jg==}
|
||||
engines: {node: '>=8.*'}
|
||||
hasBin: true
|
||||
|
||||
p-cancelable@1.1.0:
|
||||
resolution: {integrity: sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==}
|
||||
engines: {node: '>=6'}
|
||||
|
|
@ -11480,6 +11727,11 @@ packages:
|
|||
resolution: {integrity: sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
prettier@3.6.2:
|
||||
resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
|
||||
pretty-bytes@5.6.0:
|
||||
resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==}
|
||||
engines: {node: '>=6'}
|
||||
|
|
@ -12083,6 +12335,10 @@ packages:
|
|||
resolution: {integrity: sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==}
|
||||
engines: {node: '>=0.12.0'}
|
||||
|
||||
run-async@4.0.4:
|
||||
resolution: {integrity: sha512-2cgeRHnV11lSXBEhq7sN7a5UVjTKm9JTb9x8ApIT//16D7QL96AgnNeWSGoB4gIHc0iYw/Ha0Z+waBaCYZVNhg==}
|
||||
engines: {node: '>=0.12.0'}
|
||||
|
||||
run-parallel@1.2.0:
|
||||
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
||||
|
||||
|
|
@ -16899,6 +17155,16 @@ snapshots:
|
|||
optionalDependencies:
|
||||
'@types/node': 20.19.2
|
||||
|
||||
'@inquirer/checkbox@4.1.9(@types/node@22.15.34)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.1.14(@types/node@22.15.34)
|
||||
'@inquirer/figures': 1.0.12
|
||||
'@inquirer/type': 3.0.7(@types/node@22.15.34)
|
||||
ansi-escapes: 4.3.2
|
||||
yoctocolors-cjs: 2.1.2
|
||||
optionalDependencies:
|
||||
'@types/node': 22.15.34
|
||||
|
||||
'@inquirer/confirm@4.0.1':
|
||||
dependencies:
|
||||
'@inquirer/core': 9.2.1
|
||||
|
|
@ -16911,6 +17177,13 @@ snapshots:
|
|||
optionalDependencies:
|
||||
'@types/node': 20.19.2
|
||||
|
||||
'@inquirer/confirm@5.1.13(@types/node@22.15.34)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.1.14(@types/node@22.15.34)
|
||||
'@inquirer/type': 3.0.7(@types/node@22.15.34)
|
||||
optionalDependencies:
|
||||
'@types/node': 22.15.34
|
||||
|
||||
'@inquirer/core@10.1.13(@types/node@20.19.2)':
|
||||
dependencies:
|
||||
'@inquirer/figures': 1.0.12
|
||||
|
|
@ -16924,6 +17197,19 @@ snapshots:
|
|||
optionalDependencies:
|
||||
'@types/node': 20.19.2
|
||||
|
||||
'@inquirer/core@10.1.14(@types/node@22.15.34)':
|
||||
dependencies:
|
||||
'@inquirer/figures': 1.0.12
|
||||
'@inquirer/type': 3.0.7(@types/node@22.15.34)
|
||||
ansi-escapes: 4.3.2
|
||||
cli-width: 4.1.0
|
||||
mute-stream: 2.0.0
|
||||
signal-exit: 4.1.0
|
||||
wrap-ansi: 6.2.0
|
||||
yoctocolors-cjs: 2.1.2
|
||||
optionalDependencies:
|
||||
'@types/node': 22.15.34
|
||||
|
||||
'@inquirer/core@9.2.1':
|
||||
dependencies:
|
||||
'@inquirer/figures': 1.0.12
|
||||
|
|
@ -16953,6 +17239,14 @@ snapshots:
|
|||
optionalDependencies:
|
||||
'@types/node': 20.19.2
|
||||
|
||||
'@inquirer/editor@4.2.14(@types/node@22.15.34)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.1.14(@types/node@22.15.34)
|
||||
'@inquirer/type': 3.0.7(@types/node@22.15.34)
|
||||
external-editor: 3.1.0
|
||||
optionalDependencies:
|
||||
'@types/node': 22.15.34
|
||||
|
||||
'@inquirer/expand@3.0.1':
|
||||
dependencies:
|
||||
'@inquirer/core': 9.2.1
|
||||
|
|
@ -16967,6 +17261,14 @@ snapshots:
|
|||
optionalDependencies:
|
||||
'@types/node': 20.19.2
|
||||
|
||||
'@inquirer/expand@4.0.16(@types/node@22.15.34)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.1.14(@types/node@22.15.34)
|
||||
'@inquirer/type': 3.0.7(@types/node@22.15.34)
|
||||
yoctocolors-cjs: 2.1.2
|
||||
optionalDependencies:
|
||||
'@types/node': 22.15.34
|
||||
|
||||
'@inquirer/figures@1.0.12': {}
|
||||
|
||||
'@inquirer/input@3.0.1':
|
||||
|
|
@ -16981,6 +17283,13 @@ snapshots:
|
|||
optionalDependencies:
|
||||
'@types/node': 20.19.2
|
||||
|
||||
'@inquirer/input@4.2.0(@types/node@22.15.34)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.1.14(@types/node@22.15.34)
|
||||
'@inquirer/type': 3.0.7(@types/node@22.15.34)
|
||||
optionalDependencies:
|
||||
'@types/node': 22.15.34
|
||||
|
||||
'@inquirer/number@2.0.1':
|
||||
dependencies:
|
||||
'@inquirer/core': 9.2.1
|
||||
|
|
@ -16993,6 +17302,13 @@ snapshots:
|
|||
optionalDependencies:
|
||||
'@types/node': 20.19.2
|
||||
|
||||
'@inquirer/number@3.0.16(@types/node@22.15.34)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.1.14(@types/node@22.15.34)
|
||||
'@inquirer/type': 3.0.7(@types/node@22.15.34)
|
||||
optionalDependencies:
|
||||
'@types/node': 22.15.34
|
||||
|
||||
'@inquirer/password@3.0.1':
|
||||
dependencies:
|
||||
'@inquirer/core': 9.2.1
|
||||
|
|
@ -17007,6 +17323,14 @@ snapshots:
|
|||
optionalDependencies:
|
||||
'@types/node': 20.19.2
|
||||
|
||||
'@inquirer/password@4.0.16(@types/node@22.15.34)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.1.14(@types/node@22.15.34)
|
||||
'@inquirer/type': 3.0.7(@types/node@22.15.34)
|
||||
ansi-escapes: 4.3.2
|
||||
optionalDependencies:
|
||||
'@types/node': 22.15.34
|
||||
|
||||
'@inquirer/prompts@6.0.1':
|
||||
dependencies:
|
||||
'@inquirer/checkbox': 3.0.1
|
||||
|
|
@ -17035,6 +17359,21 @@ snapshots:
|
|||
optionalDependencies:
|
||||
'@types/node': 20.19.2
|
||||
|
||||
'@inquirer/prompts@7.6.0(@types/node@22.15.34)':
|
||||
dependencies:
|
||||
'@inquirer/checkbox': 4.1.9(@types/node@22.15.34)
|
||||
'@inquirer/confirm': 5.1.13(@types/node@22.15.34)
|
||||
'@inquirer/editor': 4.2.14(@types/node@22.15.34)
|
||||
'@inquirer/expand': 4.0.16(@types/node@22.15.34)
|
||||
'@inquirer/input': 4.2.0(@types/node@22.15.34)
|
||||
'@inquirer/number': 3.0.16(@types/node@22.15.34)
|
||||
'@inquirer/password': 4.0.16(@types/node@22.15.34)
|
||||
'@inquirer/rawlist': 4.1.4(@types/node@22.15.34)
|
||||
'@inquirer/search': 3.0.16(@types/node@22.15.34)
|
||||
'@inquirer/select': 4.2.4(@types/node@22.15.34)
|
||||
optionalDependencies:
|
||||
'@types/node': 22.15.34
|
||||
|
||||
'@inquirer/rawlist@3.0.1':
|
||||
dependencies:
|
||||
'@inquirer/core': 9.2.1
|
||||
|
|
@ -17049,6 +17388,14 @@ snapshots:
|
|||
optionalDependencies:
|
||||
'@types/node': 20.19.2
|
||||
|
||||
'@inquirer/rawlist@4.1.4(@types/node@22.15.34)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.1.14(@types/node@22.15.34)
|
||||
'@inquirer/type': 3.0.7(@types/node@22.15.34)
|
||||
yoctocolors-cjs: 2.1.2
|
||||
optionalDependencies:
|
||||
'@types/node': 22.15.34
|
||||
|
||||
'@inquirer/search@2.0.1':
|
||||
dependencies:
|
||||
'@inquirer/core': 9.2.1
|
||||
|
|
@ -17065,6 +17412,15 @@ snapshots:
|
|||
optionalDependencies:
|
||||
'@types/node': 20.19.2
|
||||
|
||||
'@inquirer/search@3.0.16(@types/node@22.15.34)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.1.14(@types/node@22.15.34)
|
||||
'@inquirer/figures': 1.0.12
|
||||
'@inquirer/type': 3.0.7(@types/node@22.15.34)
|
||||
yoctocolors-cjs: 2.1.2
|
||||
optionalDependencies:
|
||||
'@types/node': 22.15.34
|
||||
|
||||
'@inquirer/select@3.0.1':
|
||||
dependencies:
|
||||
'@inquirer/core': 9.2.1
|
||||
|
|
@ -17083,6 +17439,16 @@ snapshots:
|
|||
optionalDependencies:
|
||||
'@types/node': 20.19.2
|
||||
|
||||
'@inquirer/select@4.2.4(@types/node@22.15.34)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.1.14(@types/node@22.15.34)
|
||||
'@inquirer/figures': 1.0.12
|
||||
'@inquirer/type': 3.0.7(@types/node@22.15.34)
|
||||
ansi-escapes: 4.3.2
|
||||
yoctocolors-cjs: 2.1.2
|
||||
optionalDependencies:
|
||||
'@types/node': 22.15.34
|
||||
|
||||
'@inquirer/type@2.0.0':
|
||||
dependencies:
|
||||
mute-stream: 1.0.0
|
||||
|
|
@ -17091,6 +17457,10 @@ snapshots:
|
|||
optionalDependencies:
|
||||
'@types/node': 20.19.2
|
||||
|
||||
'@inquirer/type@3.0.7(@types/node@22.15.34)':
|
||||
optionalDependencies:
|
||||
'@types/node': 22.15.34
|
||||
|
||||
'@isaacs/balanced-match@4.0.1': {}
|
||||
|
||||
'@isaacs/brace-expansion@5.0.0':
|
||||
|
|
@ -17725,6 +18095,30 @@ snapshots:
|
|||
dependencies:
|
||||
esbuild: 0.14.54
|
||||
|
||||
'@oxlint/darwin-arm64@1.6.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/darwin-x64@1.6.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/linux-arm64-gnu@1.6.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/linux-arm64-musl@1.6.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/linux-x64-gnu@1.6.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/linux-x64-musl@1.6.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/win32-arm64@1.6.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/win32-x64@1.6.0':
|
||||
optional: true
|
||||
|
||||
'@paralleldrive/cuid2@2.2.2':
|
||||
dependencies:
|
||||
'@noble/hashes': 1.8.0
|
||||
|
|
@ -18806,6 +19200,10 @@ snapshots:
|
|||
dependencies:
|
||||
'@types/node': 20.19.2
|
||||
|
||||
'@types/cross-spawn@6.0.6':
|
||||
dependencies:
|
||||
'@types/node': 20.19.2
|
||||
|
||||
'@types/dateformat@5.0.3': {}
|
||||
|
||||
'@types/debug@4.1.12':
|
||||
|
|
@ -18868,6 +19266,11 @@ snapshots:
|
|||
'@types/express-serve-static-core': 5.0.6
|
||||
'@types/serve-static': 1.15.8
|
||||
|
||||
'@types/fs-extra@11.0.4':
|
||||
dependencies:
|
||||
'@types/jsonfile': 6.1.4
|
||||
'@types/node': 20.19.2
|
||||
|
||||
'@types/gitconfiglocal@2.0.3': {}
|
||||
|
||||
'@types/glob@7.2.0':
|
||||
|
|
@ -18930,6 +19333,10 @@ snapshots:
|
|||
|
||||
'@types/json-stringify-safe@5.0.3': {}
|
||||
|
||||
'@types/jsonfile@6.1.4':
|
||||
dependencies:
|
||||
'@types/node': 20.19.2
|
||||
|
||||
'@types/keyv@3.1.4':
|
||||
dependencies:
|
||||
'@types/node': 20.19.2
|
||||
|
|
@ -19055,6 +19462,8 @@ snapshots:
|
|||
dependencies:
|
||||
'@types/node': 20.19.2
|
||||
|
||||
'@types/semver@7.7.0': {}
|
||||
|
||||
'@types/send@0.17.5':
|
||||
dependencies:
|
||||
'@types/mime': 1.3.5
|
||||
|
|
@ -20620,6 +21029,8 @@ snapshots:
|
|||
|
||||
commander@12.0.0: {}
|
||||
|
||||
commander@14.0.0: {}
|
||||
|
||||
commander@2.20.3: {}
|
||||
|
||||
commander@4.1.1: {}
|
||||
|
|
@ -22922,6 +23333,10 @@ snapshots:
|
|||
dependencies:
|
||||
lru-cache: 10.4.3
|
||||
|
||||
hosted-git-info@8.1.0:
|
||||
dependencies:
|
||||
lru-cache: 10.4.3
|
||||
|
||||
hpack.js@2.1.6:
|
||||
dependencies:
|
||||
inherits: 2.0.4
|
||||
|
|
@ -23224,6 +23639,18 @@ snapshots:
|
|||
optionalDependencies:
|
||||
'@types/node': 20.19.2
|
||||
|
||||
inquirer@12.7.0(@types/node@22.15.34):
|
||||
dependencies:
|
||||
'@inquirer/core': 10.1.14(@types/node@22.15.34)
|
||||
'@inquirer/prompts': 7.6.0(@types/node@22.15.34)
|
||||
'@inquirer/type': 3.0.7(@types/node@22.15.34)
|
||||
ansi-escapes: 4.3.2
|
||||
mute-stream: 2.0.0
|
||||
run-async: 4.0.4
|
||||
rxjs: 7.8.2
|
||||
optionalDependencies:
|
||||
'@types/node': 22.15.34
|
||||
|
||||
inquirer@8.2.6:
|
||||
dependencies:
|
||||
ansi-escapes: 4.3.2
|
||||
|
|
@ -25233,6 +25660,12 @@ snapshots:
|
|||
semver: 7.7.2
|
||||
validate-npm-package-license: 3.0.4
|
||||
|
||||
normalize-package-data@7.0.0:
|
||||
dependencies:
|
||||
hosted-git-info: 8.1.0
|
||||
semver: 7.7.2
|
||||
validate-npm-package-license: 3.0.4
|
||||
|
||||
normalize-path@3.0.0: {}
|
||||
|
||||
normalize-range@0.1.2: {}
|
||||
|
|
@ -25492,6 +25925,17 @@ snapshots:
|
|||
object-keys: 1.1.1
|
||||
safe-push-apply: 1.0.0
|
||||
|
||||
oxlint@1.6.0:
|
||||
optionalDependencies:
|
||||
'@oxlint/darwin-arm64': 1.6.0
|
||||
'@oxlint/darwin-x64': 1.6.0
|
||||
'@oxlint/linux-arm64-gnu': 1.6.0
|
||||
'@oxlint/linux-arm64-musl': 1.6.0
|
||||
'@oxlint/linux-x64-gnu': 1.6.0
|
||||
'@oxlint/linux-x64-musl': 1.6.0
|
||||
'@oxlint/win32-arm64': 1.6.0
|
||||
'@oxlint/win32-x64': 1.6.0
|
||||
|
||||
p-cancelable@1.1.0: {}
|
||||
|
||||
p-cancelable@2.1.1: {}
|
||||
|
|
@ -26330,6 +26774,8 @@ snapshots:
|
|||
|
||||
prepend-http@2.0.0: {}
|
||||
|
||||
prettier@3.6.2: {}
|
||||
|
||||
pretty-bytes@5.6.0: {}
|
||||
|
||||
pretty-error@4.0.0:
|
||||
|
|
@ -27071,6 +27517,11 @@ snapshots:
|
|||
|
||||
run-async@3.0.0: {}
|
||||
|
||||
run-async@4.0.4:
|
||||
dependencies:
|
||||
oxlint: 1.6.0
|
||||
prettier: 3.6.2
|
||||
|
||||
run-parallel@1.2.0:
|
||||
dependencies:
|
||||
queue-microtask: 1.2.3
|
||||
|
|
|
|||
Loading…
Reference in a new issue