├── .editorconfig
├── .github
├── FUNDING.yml
├── assets
│ └── demo.gif
├── dependabot.yml
└── workflows
│ ├── expense.yml
│ ├── npm-publish.yml
│ ├── test.yml
│ └── update.yaml
├── .gitignore
├── .npmignore
├── .nvmrc
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── __mocks__
└── commander.ts
├── bin
└── wdio.js
├── eslint.config.js
├── package-lock.json
├── package.json
├── src
├── constants.ts
├── index.ts
├── types.ts
└── utils.ts
├── tests
├── index.test.ts
└── utils.test.ts
├── tsconfig.json
└── vitest.config.ts
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 | [*]
8 |
9 | indent_style = space
10 | indent_size = 4
11 |
12 | end_of_line = lf
13 | charset = utf-8
14 | trim_trailing_whitespace = true
15 | insert_final_newline = true
16 |
17 | [{*.yaml,*.json}]
18 | indent_size = 2
19 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | tidelift: "npm/geckodriver"
2 | open_collective: webdriverio
3 | github: [christian-bromann,webdriverio]
4 |
--------------------------------------------------------------------------------
/.github/assets/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webdriverio/create-wdio/9ca7b7c71d872cf328ae6d45c3d0437eb41ce410/.github/assets/demo.gif
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: npm
4 | directory: "/"
5 | schedule:
6 | interval: weekly
7 | time: "11:00"
8 | open-pull-requests-limit: 10
9 | versioning-strategy: increase-if-necessary
10 | groups:
11 | patch-deps-updates-main:
12 | update-types:
13 | - "patch"
14 | minor-deps-updates-main:
15 | update-types:
16 | - "minor"
17 | major-deps-updates-main:
18 | update-types:
19 | - "major"
20 |
21 | - package-ecosystem: github-actions
22 | directory: /
23 | schedule:
24 | interval: weekly
25 |
--------------------------------------------------------------------------------
/.github/workflows/expense.yml:
--------------------------------------------------------------------------------
1 | name: Expense Contribution
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | prNumber:
7 | description: "Number of the PR (without #)"
8 | required: true
9 | amount:
10 | description: "The expense amount you like to grant for the contribution in $"
11 | required: true
12 | type: choice
13 | options:
14 | - 15
15 | - 25
16 | - 35
17 | - 50
18 | - 100
19 | - 150
20 | - 200
21 | - 250
22 | - 300
23 | - 350
24 | - 400
25 | - 450
26 | - 500
27 | - 550
28 | - 600
29 | - 650
30 | - 700
31 | - 750
32 | - 800
33 | - 850
34 | - 900
35 | - 950
36 | - 1000
37 |
38 | jobs:
39 | authorize:
40 | runs-on: ubuntu-latest
41 | steps:
42 | - uses: octokit/request-action@v2.4.0
43 | with:
44 | route: GET /orgs/:organisation/teams/:team/memberships/${{ github.actor }}
45 | team: technical-steering-committee
46 | organisation: webdriverio
47 | env:
48 | GITHUB_TOKEN: ${{ secrets.WDIO_BOT_GITHUB_TOKEN }}
49 | expense:
50 | permissions:
51 | contents: write
52 | id-token: write
53 | needs: [authorize]
54 | runs-on: ubuntu-latest
55 | steps:
56 | - name: Run Expense Flow
57 | uses: webdriverio/expense-action@v1
58 | with:
59 | prNumber: ${{ github.event.inputs.prNumber }}
60 | amount: ${{ github.event.inputs.amount }}
61 | env:
62 | RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
63 | GH_TOKEN: ${{ secrets.WDIO_BOT_GITHUB_TOKEN }}
64 |
--------------------------------------------------------------------------------
/.github/workflows/npm-publish.yml:
--------------------------------------------------------------------------------
1 | name: Manual NPM Publish
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | releaseType:
7 | description: "Release Type"
8 | required: true
9 | type: choice
10 | default: "patch"
11 | options:
12 | - patch
13 | - minor
14 | - major
15 |
16 | env:
17 | NPM_TOKEN: ${{secrets.NPM_TOKEN}}
18 | NPM_CONFIG_PROVENANCE: true
19 |
20 | jobs:
21 | test:
22 | runs-on: ubuntu-latest
23 | steps:
24 | - name: Clone Repository
25 | uses: actions/checkout@v4
26 | - name: Setup Node version
27 | uses: actions/setup-node@v4
28 | with:
29 | node-version: 20.x
30 | - name: Install dependencies
31 | run: npm ci
32 | - run: npm run build
33 | - name: Run tests
34 | run: npm test
35 | - name: Upload built package
36 | uses: actions/upload-artifact@v4
37 | with:
38 | name: compiled-package
39 | path: build/
40 |
41 | release:
42 | needs: test
43 | runs-on: ubuntu-latest
44 | permissions:
45 | contents: write
46 | id-token: write
47 | steps:
48 | - name: Clone Repository
49 | uses: actions/checkout@v4
50 | - name: Setup Node version
51 | uses: actions/setup-node@v4
52 | with:
53 | node-version: 20.x
54 | registry-url: https://registry.npmjs.org/
55 | - name: Setup Git
56 | run: |
57 | git config --global user.email "bot@webdriver.io"
58 | git config --global user.name "WebdriverIO Release Bot"
59 | - name: Install dependencies
60 | run: npm ci
61 | - name: Download built package
62 | uses: actions/download-artifact@v4
63 | with:
64 | name: compiled-package
65 | path: build/
66 | - name: Release
67 | run: |
68 | npm pack --dry-run
69 | npm run release:ci -- ${{github.event.inputs.releaseType}}
70 | env:
71 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
72 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
73 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test Changes
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | test:
7 | runs-on: ubuntu-latest
8 | strategy:
9 | matrix:
10 | node-version: [20.x]
11 | steps:
12 | - name: Clone Repository
13 | uses: actions/checkout@v4
14 | - name: Setup Node version
15 | uses: actions/setup-node@v4
16 | with:
17 | node-version: ${{ matrix.node-version }}
18 | - name: Install dependencies
19 | run: npm ci
20 | - run: npm run build
21 | - name: Run tests
22 | run: npm test
23 |
--------------------------------------------------------------------------------
/.github/workflows/update.yaml:
--------------------------------------------------------------------------------
1 | # this workflow merges requests from Dependabot if tests are passing
2 | # ref https://docs.github.com/en/code-security/dependabot/working-with-dependabot/automating-dependabot-with-github-actions
3 | # and https://github.com/dependabot/fetch-metadata
4 | name: Auto-merge
5 |
6 | # `pull_request_target` means this uses code in the base branch, not the PR.
7 | on: pull_request_target
8 |
9 | # Dependabot PRs' tokens have read permissions by default and thus we must enable write permissions.
10 | permissions:
11 | contents: write
12 | pull-requests: write
13 |
14 | jobs:
15 | dependencies:
16 | runs-on: ubuntu-latest
17 | if: github.actor == 'dependabot[bot]'
18 |
19 | steps:
20 | - name: Fetch PR metadata
21 | id: metadata
22 | uses: dependabot/fetch-metadata@v2.4.0
23 | with:
24 | github-token: ${{ secrets.GITHUB_TOKEN }}
25 |
26 | - name: Wait for PR CI
27 | # Don't merge updates to GitHub Actions versions automatically.
28 | # (Some repos may wish to limit by version range (major/minor/patch), or scope (dep vs dev-dep), too.)
29 | if: contains(steps.metadata.outputs.package-ecosystem, 'npm')
30 | uses: lewagon/wait-on-check-action@v1.3.4
31 | with:
32 | ref: ${{ github.event.pull_request.head.sha }}
33 | repo-token: ${{ secrets.GITHUB_TOKEN }}
34 | wait-interval: 30 # seconds
35 | running-workflow-name: dependencies # wait for all checks except this one
36 | allowed-conclusions: success # all other checks must pass, being skipped or cancelled is not sufficient
37 |
38 | - name: Auto-merge dependabot PRs
39 | # Don't merge updates to GitHub Actions versions automatically.
40 | # (Some repos may wish to limit by version range (major/minor/patch), or scope (dep vs dev-dep), too.)
41 | if: contains(steps.metadata.outputs.package-ecosystem, 'npm')
42 | env:
43 | PR_URL: ${{ github.event.pull_request.html_url }}
44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
45 | # The "auto" flag will only merge once all of the target branch's required checks
46 | # are met. Configure those in the "branch protection" settings for each repo.
47 | run: gh pr merge --auto --squash "$PR_URL"
48 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # TypeScript v1 declaration files
40 | tests/typings/dist
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # next.js build output
61 | .next
62 |
63 | build
64 | /*.js
65 | !eslint.config.js
66 | .idea/
67 | .DS_Store
68 | *.tsbuildinfo
69 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | assets
2 | coverage
3 | src
4 | tests
5 | !build
6 | /*.js
7 | /*.yml
8 | /*.tgz
9 | .gitignore
10 | .editorconfig
11 | .nvmrc
12 | .github
13 | protocol
14 | tsconfig.*
15 | CONTRIBUTING.md
16 | !scripts
17 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | v20.11.1
2 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to `create-wdio`
2 |
3 | **Thank you for your interest in `create-wdio`. Your contributions are highly welcome.**
4 |
5 | There are multiple ways of getting involved:
6 |
7 | - [Report a bug](#report-a-bug)
8 | - [Suggest a feature](#suggest-a-feature)
9 | - [Contribute code](#contribute-code)
10 |
11 | Below are a few guidelines we would like you to follow.
12 | If you need help, please reach out to us by opening an issue.
13 |
14 | ## Report a bug
15 | Reporting bugs is one of the best ways to contribute. Before creating a bug report, please check that an [issue](/issues) reporting the same problem does not already exist. If there is such an issue, you may add your information as a comment.
16 |
17 | To report a new bug you should open an issue that summarizes the bug and set the label to "bug".
18 |
19 | If you want to provide a fix along with your bug report: That is great! In this case please send us a pull request as described in section [Contribute Code](#contribute-code).
20 |
21 | ## Suggest a feature
22 | To request a new feature you should open an [issue](../../issues/new) and summarize the desired functionality and its use case. Set the issue label to "feature".
23 |
24 | ## Contribute code
25 | This is an outline of what the workflow for code contributions looks like
26 |
27 | - Check the list of open [issues](../../issues). Either assign an existing issue to yourself, or
28 | create a new one that you would like work on and discuss your ideas and use cases.
29 |
30 | It is always best to discuss your plans beforehand, to ensure that your contribution is in line with our goals.
31 |
32 | - Fork the repository on GitHub
33 | - Create a topic branch from where you want to base your work. This is usually master.
34 | - Open a new pull request, label it `work in progress` and outline what you will be contributing
35 | - Make commits of logical units.
36 | - Make sure you sign-off on your commits `git commit -s -m "adding X to change Y"`
37 | - Write good commit messages (see below).
38 | - Push your changes to a topic branch in your fork of the repository.
39 | - As you push your changes, update the pull request with new infomation and tasks as you complete them
40 | - Project maintainers might comment on your work as you progress
41 | - When you are done, remove the `work in progess` label and ping the maintainers for a review
42 | - Your pull request must receive a :thumbsup: from two [maintainers](MAINTAINERS)
43 |
44 | ### Prerequisites
45 |
46 | To build and work on this project you need to install:
47 |
48 | - [Node.js](https://nodejs.org/en/) (LTS)
49 |
50 | ### Check out code
51 |
52 | To get the code base, have [git](https://git-scm.com/downloads) installed and run:
53 |
54 | ```sh
55 | $ git clone git@github.com:webdriverio/create-wdio.git
56 | ```
57 |
58 | then ensure to install all project dependencies:
59 |
60 | ```sh
61 | $ cd create-wdio
62 | $ npm install
63 | ```
64 |
65 | ### Build Project
66 |
67 | To compile all TypeScript files, run:
68 |
69 | ```sh
70 | $ npm run build
71 | ```
72 |
73 | In order to automatically re-compile the files when files change, you can use the watch command:
74 |
75 | ```sh
76 | $ npm run watch
77 | ```
78 |
79 | ### Test Project
80 |
81 | To test the project, run:
82 |
83 | ```sh
84 | $ npm run test
85 | ```
86 |
87 | ### Commit messages
88 | Your commit messages ideally can answer two questions: what changed and why. The subject line should feature the “what” and the body of the commit should describe the “why”.
89 |
90 | When creating a pull request, its description should reference the corresponding issue id.
91 |
92 | ### Sign your work / Developer certificate of origin
93 | All contributions (including pull requests) must agree to the Developer Certificate of Origin (DCO) version 1.1. This is exactly the same one created and used by the Linux kernel developers and posted on http://developercertificate.org/. This is a developer's certification that he or she has the right to submit the patch for inclusion into the project. Simply submitting a contribution implies this agreement, however, please include a "Signed-off-by" tag in every patch (this tag is a conventional way to confirm that you agree to the DCO) - you can automate this with a [Git hook](https://stackoverflow.com/questions/15015894/git-add-signed-off-by-line-using-format-signoff-not-working)
94 |
95 | ```
96 | git commit -s -m "adding X to change Y"
97 | ```
98 |
99 | ## Release Project
100 |
101 | Contributor with push access to this repo can at any time make a release. To do so, just trigger the GitHub Action that releases the package. Ensure you pick the correct release type by following the [semantic versioning](https://semver.org/) principle.
102 |
103 | ---
104 |
105 | **Have fun, and happy hacking!**
106 |
107 | Thanks for your contributions!
108 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) OpenJS Foundation and other contributors
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | 'Software'), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | WebdriverIO Starter Toolkit [](https://github.com/webdriverio/create-wdio/actions/workflows/test.yml) [](https://github.com/webdriverio/create-wdio/blob/main/CONTRIBUTING.md)
2 | ===========================
3 |
4 |
5 |
6 | One command to create a fresh WebdriverIO project or add WebdriverIO to an existing project.
7 |
8 | - [Get Started Guide](https://webdriver.io/docs/gettingstarted) - How to get started with WebdriverIO
9 | - [Supported Options](#supported-options) - command line parameters
10 |
11 | `create-wdio` works on macOS, Windows, and Linux.
12 | If something doesn’t work, please [file an issue](https://github.com/webdriverio/create-wdio/issues/new).
13 | If you have questions or need help, please ask in our [Discord Support channel](https://discord.webdriver.io).
14 |
15 |
16 |
17 |
18 |
19 | ## Usage
20 |
21 | To install a WebdriverIO project, you may choose one of the following methods:
22 |
23 | #### npx
24 |
25 | ```sh
26 | npx create-wdio@latest ./e2e
27 | ```
28 |
29 | _[`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)_
30 |
31 | #### npm
32 |
33 | ```sh
34 | npm init wdio@latest ./e2e
35 | ```
36 |
37 | _[`npm init `](https://docs.npmjs.com/cli/v10/commands/npm-init) is available in npm 6+_
38 |
39 | #### yarn
40 |
41 | ```sh
42 | yarn create wdio@latest ./e2e
43 | ```
44 |
45 | _[`yarn create `](https://yarnpkg.com/lang/en/docs/cli/create/) is available in Yarn 0.25+_
46 |
47 | #### pnpm
48 |
49 | ```sh
50 | pnpm create wdio ./e2e
51 | ```
52 |
53 | _[`pnpm create `](https://pnpm.io/cli/create) is available in pnpm v7+_
54 |
55 | It will create a directory called `e2e` inside the current folder.
56 | Then it will run the configuration wizard that will help you set-up your framework.
57 |
58 |
59 | ## Supported Options
60 |
61 | You can pass the following command line flags to modify the bootstrap mechanism:
62 |
63 | * `--dev` - Install all packages as `devDependencies` (default: `true`)
64 | * `--yes` - Will fill in all config defaults without prompting (default: `false`)
65 | * `--npm-tag` - use a specific NPM tag for `@wdio/cli` package (default: `latest`)
66 |
67 | ----
68 |
69 | For more information on WebdriverIO see the [homepage](https://webdriver.io).
70 |
--------------------------------------------------------------------------------
/__mocks__/commander.ts:
--------------------------------------------------------------------------------
1 | import { vi } from 'vitest'
2 | import * as path from 'node:path'
3 |
4 | const command: Record = {}
5 | command.name = vi.fn().mockReturnValue(command)
6 | command.version = vi.fn().mockReturnValue(command)
7 | command.usage = vi.fn().mockReturnValue(command)
8 | command.arguments = vi.fn().mockReturnValue(command)
9 | command.action = vi.fn((cb) => {
10 | cb(`${path.sep}foo${path.sep}bar${path.sep}someProjectName`)
11 | return command
12 | })
13 | command.option = vi.fn().mockReturnValue(command)
14 | command.allowUnknownOption = vi.fn().mockReturnValue(command)
15 | command.on = vi.fn().mockReturnValue(command)
16 | command.parse = vi.fn().mockReturnValue(command)
17 | command.opts = vi.fn().mockReturnValue('foobar')
18 |
19 | export const Command = vi.fn().mockReturnValue(command)
20 |
--------------------------------------------------------------------------------
/bin/wdio.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * This source code is licensed under the MIT license found in the
5 | * LICENSE file in the root directory of this source tree.
6 | */
7 | import { run } from '../build/index.js'
8 |
9 | run()
10 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import wdioEslint from '@wdio/eslint'
2 |
3 | export default wdioEslint.config([
4 | {
5 | ignores: ['build']
6 | },
7 | /**
8 | * custom test configuration
9 | */
10 | {
11 | files: ['tests/**/*'],
12 | rules: {
13 | '@typescript-eslint/no-require-imports': 'off',
14 | '@typescript-eslint/no-explicit-any': 'off'
15 | }
16 | }
17 | ])
18 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "create-wdio",
3 | "version": "8.4.10",
4 | "description": "Install and setup a WebdriverIO project with all its dependencies in a single run",
5 | "author": "Christian Bromann ",
6 | "license": "MIT",
7 | "homepage": "https://github.com/webdriverio/create-wdio#readme",
8 | "repository": {
9 | "type": "git",
10 | "url": "git+https://github.com/webdriverio/create-wdio.git"
11 | },
12 | "bugs": {
13 | "url": "https://github.com/webdriverio/create-wdio/issues"
14 | },
15 | "keywords": [
16 | "webdriverio",
17 | "create-wdio",
18 | "wdio",
19 | "installer",
20 | "e2e"
21 | ],
22 | "bin": {
23 | "create-wdio": "./bin/wdio.js"
24 | },
25 | "type": "module",
26 | "scripts": {
27 | "build": "run-s clean compile",
28 | "clean": "rm -rf tsconfig.tsbuildinfo ./build ./coverage",
29 | "compile": "tsc -p ./tsconfig.json",
30 | "release": "release-it --github.release",
31 | "release:ci": "npm run release -- --ci --npm.skipChecks --no-git.requireCleanWorkingDir",
32 | "release:patch": "npm run release -- patch",
33 | "release:minor": "npm run release -- minor",
34 | "release:major": "npm run release -- major",
35 | "test": "run-s build test:*",
36 | "test:lint": "eslint .",
37 | "test:unit": "vitest run",
38 | "watch": "npm run compile -- --watch",
39 | "watch:test": "vitest watch"
40 | },
41 | "devDependencies": {
42 | "@types/cross-spawn": "^6.0.6",
43 | "@types/node": "^22.0.0",
44 | "@types/semver": "^7.5.8",
45 | "@vitest/coverage-v8": "^3.0.2",
46 | "@wdio/eslint": "^0.0.5",
47 | "eslint": "^9.13.0",
48 | "npm-run-all": "^4.1.5",
49 | "release-it": "^18.0.0",
50 | "typescript": "^5.6.3",
51 | "vitest": "^3.0.2"
52 | },
53 | "dependencies": {
54 | "chalk": "^5.3.0",
55 | "commander": "^13.0.0",
56 | "cross-spawn": "^7.0.3",
57 | "import-meta-resolve": "^4.1.0",
58 | "semver": "^7.6.3"
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | import { colorItBold, colorIt } from './utils.js'
2 |
3 | type PM = 'npm' | 'pnpm' | 'yarn' | 'bun';
4 |
5 | export const DEFAULT_NPM_TAG = 'latest'
6 | export const ASCII_ROBOT = `
7 | -:...........................-:.
8 | + +
9 | \`\` + \`...\` \`...\` + \`
10 | ./+/ + .:://:::\` \`::///::\` \` + ++/.
11 | .+oo+ + /:+ooo+-/ /-+ooo+-/ ./ + +oo+.
12 | -ooo+ + /-+ooo+-/ /-+ooo+-/ .: + +ooo.
13 | -+o+ + \`::///:-\` \`::///::\` + +o+-
14 | \`\`. /. \`\`\`\`\` \`\`\`\`\` .: .\`\`
15 | .----------------------------.
16 | \`-::::::::::::::::::::::::::::::::::::::::-\`
17 | .+oooo/:------------------------------:/oooo+.
18 | \`.--/oooo- :oooo/--.\`
19 | .::-\`\`:oooo\` .oooo-\`\`-::.
20 | ./-\` -oooo\`--.: :.-- .oooo- \`-/.
21 | -/\` \`-/oooo////////////////////////////////////oooo/.\` \`/-
22 | \`+\` \`/+oooooooooooooooooooooooooooooooooooooooooooooooo+:\` .+\`
23 | -/ +o/.:oooooooooooooooooooooooooooooooooooooooooooo:-/o/ +.
24 | -/ .o+ -oooosoooososssssooooo------------------:oooo- \`oo\` +.
25 | -/ .o+ -oooodooohyyssosshoooo\` .oooo- oo. +.
26 | -/ .o+ -oooodooysdooooooyyooo\` \`.--.\`\` .:::-oooo- oo. +.
27 | -/ .o+ -oooodoyyodsoooooyyooo.//-..-:/:.\`.//.\`./oooo- oo. +.
28 | -/ .o+ -oooohsyoooyysssysoooo+-\` \`-:::. .oooo- oo. +.
29 | -/ .o+ -ooooosooooooosooooooo+//////////////////oooo- oo. +.
30 | -/ .o+ -oooooooooooooooooooooooooooooooooooooooooooo- oo. +.
31 | -/ .o+ -oooooooooooooooooooooooooooooooooooooooooooo- oo. +.
32 | -+////o+\` -oooo---:///:----://::------------------:oooo- \`oo////+-
33 | +ooooooo/\`-oooo\`\`:-\`\`\`.:\`.:.\`.+/- .::::::::::\` .oooo-\`+ooooooo+
34 | oooooooo+\`-oooo\`-- \`/\` .:+ -/-\`/\` .:::::::::: .oooo-.+oooooooo
35 | +-/+://-/ -oooo-\`:\`.o-\`:.:-\`\`\`\`.: .///:\`\`\`\`\`\` -oooo-\`/-//:+:-+
36 | : :..--:-:.+ooo+/://o+/-.-:////:-....-::::-....--/+ooo+.:.:--.-- /
37 | - /./\`-:-\` .:///+/ooooo/+///////////////+++ooooo/+///:. .-:.\`+./ :
38 | :-:/. :\`ooooo\`/\` .:.ooooo : ./---
39 | :\`ooooo\`/\` .:.ooooo :
40 | :\`ooooo./\` .:-ooooo :
41 | :\`ooooo./\` .:-ooooo :
42 | \`...:-+++++:/. ./:+++++-:...\`
43 | :-.\`\`\`\`\`\`\`\`/../ /.-:\`\`\`\`\`\`\`\`.:-
44 | -/::::::::://:/+ \`+/:+::::::::::+.
45 | :oooooooooooo++/ +++oooooooooooo-
46 | `
47 |
48 | export const PROGRAM_TITLE = `
49 | ${colorItBold('Webdriver.IO')}
50 | ${colorIt('Next-gen browser and mobile automation')}
51 | ${colorIt('test framework for Node.js')}
52 | `
53 |
54 | export const UNSUPPORTED_NODE_VERSION = (
55 | '⚠️ Unsupported Node.js Version Error ⚠️\n' +
56 | `You are using Node.js ${process.version} which is too old to be used with WebdriverIO.\n` +
57 | 'Please update to Node.js v20 to continue.\n'
58 | )
59 |
60 | export const INSTALL_COMMAND: Record = {
61 | npm: 'install',
62 | pnpm: 'add',
63 | yarn: 'add',
64 | bun: 'install'
65 | } as const
66 |
67 | export const EXECUTER: Record = {
68 | npm: 'npx',
69 | pnpm: 'pnpm',
70 | yarn: 'yarn',
71 | bun: 'bunx'
72 | } as const
73 |
74 | export const EXECUTE_COMMAND: Record = {
75 | npm: '',
76 | pnpm: 'exec',
77 | yarn: 'exec',
78 | bun: ''
79 | } as const
80 |
81 | export const DEV_FLAG: Record = {
82 | npm: '--save-dev',
83 | pnpm: '--save-dev',
84 | yarn: '--dev',
85 | bun: '--dev'
86 | } as const
87 |
88 | export const PMs = Object.keys(INSTALL_COMMAND) as PM[]
89 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import fs from 'node:fs/promises'
2 | import path from 'node:path'
3 | import { pathToFileURL } from 'node:url'
4 | import { execSync } from 'node:child_process'
5 |
6 | import chalk from 'chalk'
7 | import semver from 'semver'
8 | import { Command } from 'commander'
9 | import { resolve } from 'import-meta-resolve'
10 |
11 | import { runProgram, getPackageVersion } from './utils.js'
12 | import {
13 | ASCII_ROBOT, PROGRAM_TITLE, UNSUPPORTED_NODE_VERSION, DEFAULT_NPM_TAG,
14 | INSTALL_COMMAND, DEV_FLAG, PMs, EXECUTER, EXECUTE_COMMAND
15 | } from './constants.js'
16 | import type { ProgramOpts } from './types'
17 |
18 | const WDIO_COMMAND = 'wdio'
19 | let projectDir: string | undefined
20 |
21 | export async function run(operation = createWebdriverIO) {
22 | const version = await getPackageVersion()
23 |
24 | /**
25 | * print program ASCII art
26 | */
27 | if (!(process.argv.includes('--version') || process.argv.includes('-v'))) {
28 | console.log(ASCII_ROBOT, PROGRAM_TITLE)
29 | }
30 |
31 | /**
32 | * ensure right Node.js version is used
33 | */
34 | const unsupportedNodeVersion = !semver.satisfies(process.version, '>=18.18.0')
35 | if (unsupportedNodeVersion) {
36 | console.log(chalk.yellow(UNSUPPORTED_NODE_VERSION))
37 | return
38 | }
39 |
40 | const program = new Command(WDIO_COMMAND)
41 | .version(version, '-v, --version')
42 | .arguments('[project-path]')
43 | .usage(`${chalk.green('[project-path]')} [options]`)
44 | .action(name => (projectDir = name))
45 |
46 | .option('-t, --npm-tag ', 'Which NPM version you like to install, e.g. @next', DEFAULT_NPM_TAG)
47 | .option('-y, --yes', 'will fill in all config defaults without prompting', false)
48 | .option('-d, --dev', 'Install all packages as into devDependencies', true)
49 |
50 | .allowUnknownOption()
51 | .on('--help', () => console.log())
52 | .parse(process.argv)
53 |
54 | return operation(program.opts())
55 | }
56 |
57 | /**
58 | * detects the package manager that was used
59 | * uses the environment variable `npm_config_user_agent` to detect the package manager
60 | * falls back to `npm` if no package manager could be detected
61 | */
62 | function detectPackageManager() {
63 | if (!process.env.npm_config_user_agent) {
64 | return 'npm'
65 | }
66 | const detectedPM = process.env.npm_config_user_agent.split('/')[0].toLowerCase()
67 |
68 | const matchedPM = PMs.find(pm => pm.toLowerCase() === detectedPM)
69 |
70 | return matchedPM || 'npm'
71 | }
72 |
73 | export async function createWebdriverIO(opts: ProgramOpts) {
74 | const npmTag = opts.npmTag.startsWith('@') ? opts.npmTag : `@${opts.npmTag}`
75 | const root = path.resolve(process.cwd(), projectDir || '')
76 |
77 | /**
78 | * find package manager that was used to create project
79 | */
80 | const pm = detectPackageManager()
81 |
82 | const hasPackageJson = await fs.access(path.resolve(root, 'package.json')).then(() => true).catch(() => false)
83 | if (!hasPackageJson) {
84 | await fs.mkdir(root, { recursive: true })
85 | await fs.writeFile(path.resolve(root, 'package.json'), JSON.stringify({
86 | name: root.substring(root.lastIndexOf(path.sep) + 1),
87 | type: 'module'
88 | }, null, 2))
89 | }
90 |
91 | const cliInstalled = await isCLIInstalled(root)
92 | if (!cliInstalled) {
93 | console.log(`\nInstalling ${chalk.bold('@wdio/cli')} to initialize project...`)
94 | const args = [INSTALL_COMMAND[pm]]
95 | if (opts.dev) {
96 | args.push(DEV_FLAG[pm])
97 | }
98 | args.push(`@wdio/cli${npmTag}`)
99 | await runProgram(pm, args, { cwd: root, stdio: 'ignore' })
100 | console.log(chalk.green.bold('✔ Success!'))
101 | }
102 |
103 | return runProgram(EXECUTER[pm], [
104 | EXECUTE_COMMAND[pm],
105 | WDIO_COMMAND,
106 | 'config',
107 | ...(opts.yes ? ['--yes'] : []),
108 | ...(opts.npmTag ? ['--npm-tag', opts.npmTag] : [])
109 | ].filter(i => !!i), { cwd: root })
110 | }
111 |
112 | async function isCLIInstalled(path: string) {
113 | try {
114 | // can be replaced with import.meta.resolve('@wdio/cli', new URL(`file:///${root}`).href) in the future
115 | // check if the cli is installed in the project
116 | resolve('@wdio/cli', pathToFileURL(path).href)
117 | return true
118 | } catch {
119 | // check of the cli is installed globally
120 | // wrap in try/catch as it can fail on Windows
121 | try {
122 | const output = execSync('npm ls -g', {
123 | encoding: 'utf-8',
124 | stdio: ['ignore', 'pipe', 'ignore']
125 | })
126 | if (output.includes('@wdio/cli')) {
127 | return true
128 | }
129 | } catch {
130 | return false
131 | }
132 |
133 | return false
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | export interface ProgramOpts {
2 | info: boolean
3 | dev: boolean
4 | yes: boolean
5 | npmTag: string
6 | }
7 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import url from 'node:url'
2 | import path from 'node:path'
3 | import fs from 'node:fs/promises'
4 | import type { SpawnOptions } from 'node:child_process'
5 |
6 | import spawn from 'cross-spawn'
7 | import chalk from 'chalk'
8 |
9 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url))
10 |
11 | export const colorItBold = chalk.bold.rgb(234, 89, 6)
12 | export const colorIt = chalk.rgb(234, 89, 6)
13 |
14 | process.on('SIGINT', () => printAndExit(undefined, 'SIGINT'))
15 |
16 | export function runProgram (command: string, args: string[], options: SpawnOptions) {
17 | const child = spawn(command, args, { stdio: 'inherit', ...options })
18 | return new Promise((resolve, rejects) => {
19 | let error: Error
20 | child.on('error', (e) => (error = e))
21 | child.on('close', (code, signal) => {
22 | if (code !== 0) {
23 | const errorMessage = (error && error.message) || `Error calling: ${command} ${args.join(' ')}`
24 | printAndExit(errorMessage, signal)
25 | return rejects(errorMessage)
26 | }
27 | resolve()
28 | })
29 | })
30 | }
31 |
32 | export async function getPackageVersion() {
33 | try {
34 | const pkgJsonPath = path.join(__dirname, '..', 'package.json')
35 | const pkg = JSON.parse((await fs.readFile(pkgJsonPath)).toString())
36 | return `v${pkg.version}`
37 | } catch {
38 | /* ignore */
39 | }
40 | return 'unknown'
41 | }
42 |
43 | function printAndExit (error?: string, signal?: NodeJS.Signals | null) {
44 | if (signal === 'SIGINT') {
45 | console.log('\n\nGoodbye 👋')
46 | } else {
47 | console.log(`\n\n⚠️ Ups, something went wrong${error ? `: ${error}` : ''}!`)
48 | }
49 |
50 | process.exit(1)
51 | }
52 |
--------------------------------------------------------------------------------
/tests/index.test.ts:
--------------------------------------------------------------------------------
1 | import fs from 'node:fs/promises'
2 | import semver from 'semver'
3 | import { resolve } from 'import-meta-resolve'
4 | import { vi, test, expect, beforeEach, afterEach } from 'vitest'
5 | import { Command } from 'commander'
6 | import { execSync } from 'node:child_process'
7 |
8 | import { run, createWebdriverIO } from '../src'
9 | import { runProgram } from '../src/utils'
10 | import type { ProgramOpts } from '../src/types'
11 |
12 | vi.mock('node:fs/promises', () => ({
13 | default: {
14 | access: vi.fn(),
15 | mkdir: vi.fn(),
16 | writeFile: vi.fn()
17 | }
18 | }))
19 | vi.mock('node:child_process', () => ({
20 | execSync: vi.fn()
21 | }))
22 | vi.mock('import-meta-resolve', () => ({
23 | resolve: vi.fn()
24 | }))
25 | vi.mock('commander')
26 | vi.mock('semver', () => ({
27 | default: {
28 | satisfies: vi.fn().mockReturnValue(true)
29 | }
30 | }))
31 | vi.mock('../src/utils.js', () => ({
32 | runProgram: vi.fn(),
33 | colorItBold: vi.fn((log) => log),
34 | colorIt: vi.fn((log) => log),
35 | getPackageVersion: vi.fn().mockReturnValue('0.1.1'),
36 | shouldUseYarn: vi.fn().mockReturnValue(true)
37 | }))
38 |
39 | const consoleLog = console.log.bind(console)
40 | beforeEach(() => {
41 | console.log = vi.fn()
42 | vi.mocked(runProgram).mockClear()
43 | vi.mocked(resolve).mockImplementation(() => {
44 | throw new Error('foo')
45 | })
46 | vi.mocked(execSync).mockReturnValue(`
47 | ├── corepack@0.20.0
48 | ├── npm@10.2.0
49 | └── yarn@1.22.19
50 | `)
51 | vi.mocked(fs.access).mockResolvedValue()
52 | })
53 |
54 | test('run', async () => {
55 | const op = vi.fn().mockResolvedValue({})
56 | await run(op)
57 |
58 | expect(op).toBeCalledTimes(1)
59 | expect(op).toBeCalledWith('foobar')
60 | expect(new Command().arguments).toBeCalledWith('[project-path]')
61 | expect(console.log).toBeCalledTimes(1)
62 | expect(vi.mocked(console.log).mock.calls[0][0]).toContain('oooooooooooo')
63 | expect(vi.mocked(console.log).mock.calls[0][1]).toContain('Next-gen browser and mobile automation')
64 | })
65 |
66 | test('does not run if Node.js version is too low', async () => {
67 | const consoleLog = vi.spyOn(console, 'log')
68 | vi.mocked(semver.satisfies).mockReturnValue(false)
69 | const op = vi.fn().mockResolvedValue({})
70 | await run(op)
71 | expect(op).toBeCalledTimes(0)
72 | expect(consoleLog).toBeCalledWith(expect.stringContaining('Please update to Node.js v20 to continue.'))
73 | })
74 |
75 | test('createWebdriverIO with Yarn', async () => {
76 | vi.stubEnv('npm_config_user_agent', 'yarn/4.5.0 npm/? node/v20.11.0 darwin arm64')
77 | await createWebdriverIO({ npmTag: 'latest' } as ProgramOpts)
78 | expect(runProgram).toBeCalledWith(
79 | 'yarn',
80 | ['add', '@wdio/cli@latest'],
81 | expect.any(Object)
82 | )
83 | expect(runProgram).toBeCalledWith(
84 | 'yarn',
85 | ['exec', 'wdio', 'config', '--npm-tag', 'latest'],
86 | expect.any(Object)
87 | )
88 | expect(runProgram).toBeCalledTimes(2)
89 | expect(fs.mkdir).toBeCalledTimes(0)
90 | expect(fs.writeFile).toBeCalledTimes(0)
91 | })
92 |
93 | test('createWebdriverIO with NPM', async () => {
94 | vi.stubEnv('npm_config_user_agent', 'npm/10.2.4 node/v20.11.0 darwin arm64 workspaces/false')
95 | await createWebdriverIO({ npmTag: 'latest' } as ProgramOpts)
96 | expect(runProgram).toBeCalledWith(
97 | 'npm',
98 | ['install', '@wdio/cli@latest'],
99 | expect.any(Object)
100 | )
101 | expect(runProgram).toBeCalledWith(
102 | 'npx',
103 | ['wdio', 'config', '--npm-tag', 'latest'],
104 | expect.any(Object)
105 | )
106 | expect(runProgram).toBeCalledTimes(2)
107 | expect(fs.mkdir).toBeCalledTimes(0)
108 | expect(fs.writeFile).toBeCalledTimes(0)
109 | })
110 |
111 | test('createWebdriverIO with invalid agent should run npm commands', async () => {
112 | vi.stubEnv('npm_config_user_agent', 'invalid/10.2.4 node/v20.11.0 darwin arm64 workspaces/false')
113 | await createWebdriverIO({ npmTag: 'latest' } as ProgramOpts)
114 | expect(runProgram).toBeCalledWith(
115 | 'npm',
116 | ['install', '@wdio/cli@latest'],
117 | expect.any(Object)
118 | )
119 | expect(runProgram).toBeCalledWith(
120 | 'npx',
121 | ['wdio', 'config', '--npm-tag', 'latest'],
122 | expect.any(Object)
123 | )
124 | expect(runProgram).toBeCalledTimes(2)
125 | expect(fs.mkdir).toBeCalledTimes(0)
126 | expect(fs.writeFile).toBeCalledTimes(0)
127 | })
128 |
129 | test('createWebdriverIO with no npm user agent should run npm commands', async () => {
130 | vi.stubEnv('npm_config_user_agent', '')
131 | await createWebdriverIO({ npmTag: 'latest' } as ProgramOpts)
132 | expect(runProgram).toBeCalledWith(
133 | 'npm',
134 | ['install', '@wdio/cli@latest'],
135 | expect.any(Object)
136 | )
137 | expect(runProgram).toBeCalledWith(
138 | 'npx',
139 | ['wdio', 'config', '--npm-tag', 'latest'],
140 | expect.any(Object)
141 | )
142 | expect(runProgram).toBeCalledTimes(2)
143 | expect(fs.mkdir).toBeCalledTimes(0)
144 | expect(fs.writeFile).toBeCalledTimes(0)
145 | })
146 |
147 | test('createWebdriverIO with pnpm', async () => {
148 | vi.stubEnv('npm_config_user_agent', 'pnpm/9.10.0 npm/? node/v20.11.0 darwin arm64')
149 | await createWebdriverIO({ npmTag: 'latest' } as ProgramOpts)
150 | expect(runProgram).toBeCalledWith(
151 | 'pnpm',
152 | ['add', '@wdio/cli@latest'],
153 | expect.any(Object)
154 | )
155 | expect(runProgram).toBeCalledWith(
156 | 'pnpm',
157 | ['exec', 'wdio', 'config', '--npm-tag', 'latest'],
158 | expect.any(Object)
159 | )
160 | expect(runProgram).toBeCalledTimes(2)
161 | expect(fs.mkdir).toBeCalledTimes(0)
162 | expect(fs.writeFile).toBeCalledTimes(0)
163 | })
164 |
165 | test('createWebdriverIO with bun', async () => {
166 | vi.stubEnv('npm_config_user_agent', 'bun/1.1.27 npm/? node/v22.6.0 darwin arm64')
167 | await createWebdriverIO({ npmTag: 'latest' } as ProgramOpts)
168 | expect(runProgram).toBeCalledWith(
169 | 'bun',
170 | ['install', '@wdio/cli@latest'],
171 | expect.any(Object)
172 | )
173 | expect(runProgram).toBeCalledWith(
174 | 'bunx',
175 | ['wdio', 'config', '--npm-tag', 'latest'],
176 | expect.any(Object)
177 | )
178 | expect(runProgram).toBeCalledTimes(2)
179 | expect(fs.mkdir).toBeCalledTimes(0)
180 | expect(fs.writeFile).toBeCalledTimes(0)
181 | })
182 |
183 | test('creates a directory if it does not exist', async () => {
184 | vi.stubEnv('npm_config_user_agent', 'npm/10.2.4 node/v20.11.0 darwin arm64 workspaces/false')
185 | await createWebdriverIO({ npmTag: 'latest', dev: true } as ProgramOpts)
186 | expect(runProgram).toBeCalledWith(
187 | 'npm',
188 | ['install', '--save-dev', '@wdio/cli@latest'],
189 | expect.any(Object)
190 | )
191 | expect(runProgram).toBeCalledWith(
192 | 'npx',
193 | ['wdio', 'config', '--npm-tag', 'latest'],
194 | expect.any(Object)
195 | )
196 | expect(runProgram).toBeCalledTimes(2)
197 | expect(fs.mkdir).toBeCalledTimes(0)
198 | expect(fs.writeFile).toBeCalledTimes(0)
199 | })
200 |
201 | test('does not install the @wdio/cli package when the @wdio/cli package is already installed in the current project', async () => {
202 | vi.stubEnv('npm_config_user_agent', 'npm/10.2.4 node/v20.11.0 darwin arm64 workspaces/false')
203 | vi.mocked(resolve).mockReturnValue('/Users/user/dev/my-monorepo/package.json')
204 | await createWebdriverIO({ npmTag: 'latest' } as ProgramOpts)
205 | expect(runProgram).toBeCalledWith(
206 | 'npx',
207 | ['wdio', 'config', '--npm-tag', 'latest'],
208 | expect.any(Object)
209 | )
210 | expect(runProgram).toBeCalledTimes(1)
211 | expect(fs.mkdir).toBeCalledTimes(0)
212 | expect(fs.writeFile).toBeCalledTimes(0)
213 | })
214 |
215 | test('does not install the @wdio/cli package when the @wdio/cli package is already installed globally', async () => {
216 | vi.stubEnv('npm_config_user_agent', 'npm/10.2.4 node/v20.11.0 darwin arm64 workspaces/false')
217 | vi.mocked(execSync).mockReturnValue(`
218 | ├── @wdio/cli@8.24.3
219 | ├── corepack@0.20.0
220 | ├── npm@10.2.0
221 | └── yarn@1.22.19
222 | `)
223 | await createWebdriverIO({ npmTag: 'latest' } as ProgramOpts)
224 | expect(runProgram).toBeCalledWith(
225 | 'npx',
226 | ['wdio', 'config', '--npm-tag', 'latest'],
227 | expect.any(Object)
228 | )
229 | expect(runProgram).toBeCalledTimes(1)
230 | expect(fs.mkdir).toBeCalledTimes(0)
231 | expect(fs.writeFile).toBeCalledTimes(0)
232 | })
233 |
234 | test('runs the wdio config command with --yes when the yes option is set to true', async () => {
235 | vi.stubEnv('npm_config_user_agent', 'npm/10.2.4 node/v20.11.0 darwin arm64 workspaces/false')
236 | await createWebdriverIO({ npmTag: 'latest', yes: true } as ProgramOpts)
237 | expect(runProgram).toBeCalledWith(
238 | 'npm',
239 | ['install', '@wdio/cli@latest'],
240 | expect.any(Object)
241 | )
242 | expect(runProgram).toBeCalledWith(
243 | 'npx',
244 | ['wdio', 'config', '--yes', '--npm-tag', 'latest'],
245 | expect.any(Object)
246 | )
247 | expect(runProgram).toBeCalledTimes(2)
248 | expect(fs.mkdir).toBeCalledTimes(0)
249 | expect(fs.writeFile).toBeCalledTimes(0)
250 | })
251 |
252 | test('does create a package.json to be used by the wdio config command when one does not exist', async () => {
253 | vi.stubEnv('npm_config_user_agent', 'npm/10.2.4 node/v20.11.0 darwin arm64 workspaces/false')
254 | vi.mocked(fs.access).mockRejectedValue(new Error('not existing'))
255 | await createWebdriverIO({ npmTag: 'next' } as ProgramOpts)
256 | expect(runProgram).toBeCalledWith(
257 | 'npm',
258 | ['install', '@wdio/cli@next'],
259 | expect.any(Object)
260 | )
261 | expect(runProgram).toBeCalledWith(
262 | 'npx',
263 | ['wdio', 'config', '--npm-tag', 'next'],
264 | expect.any(Object)
265 | )
266 | expect(runProgram).toBeCalledTimes(2)
267 | expect(fs.mkdir).toBeCalledTimes(1)
268 | expect(fs.writeFile).toBeCalledTimes(1)
269 | expect(fs.writeFile).toBeCalledWith(
270 | expect.any(String),
271 | JSON.stringify({ name: 'someProjectName', type: 'module' }, null, 2),
272 | )
273 | })
274 |
275 | test('installs the next version when the npmTag option is set to "next"', async () => {
276 | vi.stubEnv('npm_config_user_agent', 'npm/10.2.4 node/v20.11.0 darwin arm64 workspaces/false')
277 | await createWebdriverIO({ npmTag: 'next' } as ProgramOpts)
278 | expect(runProgram).toBeCalledWith(
279 | 'npm',
280 | ['install', '@wdio/cli@next'],
281 | expect.any(Object)
282 | )
283 | expect(runProgram).toBeCalledWith(
284 | 'npx',
285 | ['wdio', 'config', '--npm-tag', 'next'],
286 | expect.any(Object)
287 | )
288 | expect(runProgram).toBeCalledTimes(2)
289 | expect(fs.mkdir).toBeCalledTimes(0)
290 | expect(fs.writeFile).toBeCalledTimes(0)
291 | })
292 |
293 | afterEach(() => {
294 | vi.mocked(fs.access).mockClear()
295 | vi.mocked(fs.mkdir).mockClear()
296 | vi.mocked(fs.writeFile).mockClear()
297 | vi.mocked(runProgram).mockClear()
298 | vi.mocked(resolve).mockClear()
299 | vi.mocked(execSync).mockClear()
300 | console.log = consoleLog
301 | })
302 |
--------------------------------------------------------------------------------
/tests/utils.test.ts:
--------------------------------------------------------------------------------
1 | import { vi, test, expect, beforeEach, afterEach } from 'vitest'
2 | import { runProgram, getPackageVersion } from '../src/utils'
3 |
4 | const consoleLog = console.log.bind(console)
5 | const processExit = process.exit.bind(process)
6 | beforeEach(() => {
7 | process.exit = vi.fn()
8 | console.log = vi.fn()
9 | })
10 | afterEach(() => {
11 | process.exit = processExit
12 | console.log = consoleLog
13 | })
14 |
15 | test('runProgram', async () => {
16 | expect(await runProgram('echo', ['123'], {})).toBe(undefined)
17 |
18 | await runProgram('node', ['-e', 'throw new Error(\'ups\')'], {}).catch((e) => e)
19 | expect(vi.mocked(console.log).mock.calls[0][0]).toMatch(/Error calling: node -e throw new Error/)
20 | expect(process.exit).toBeCalledTimes(1)
21 |
22 | await runProgram('foobarloo', [], {}).catch((e) => e)
23 |
24 | expect(vi.mocked(console.log).mock.calls[1][0]).toMatch(/spawn foobarloo ENOENT/)
25 | expect(process.exit).toBeCalledTimes(2)
26 | })
27 |
28 | test('getPackageVersion', async () => {
29 | expect(await getPackageVersion()).toEqual(expect.any(String))
30 | })
31 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */
4 |
5 | /* Basic Options */
6 | "incremental": true, /* Enable incremental compilation */
7 | "target": "ES2020", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */
8 | "module": "ESNext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
9 | // "lib": [], /* Specify library files to be included in the compilation. */
10 | // "allowJs": true, /* Allow javascript files to be compiled. */
11 | // "checkJs": true, /* Report errors in .js files. */
12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */
13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */
14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
15 | // "sourceMap": true, /* Generates corresponding '.map' file. */
16 | // "outFile": "./", /* Concatenate and emit output to single file. */
17 | "outDir": "./build", /* Redirect output structure to the directory. */
18 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
19 | // "composite": true, /* Enable project compilation */
20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
21 | // "removeComments": true, /* Do not emit comments to output. */
22 | // "noEmit": true, /* Do not emit outputs. */
23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
26 |
27 | /* Strict Type-Checking Options */
28 | "strict": true, /* Enable all strict type-checking options. */
29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
30 | // "strictNullChecks": true, /* Enable strict null checks. */
31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
36 |
37 | /* Additional Checks */
38 | // "noUnusedLocals": true, /* Report errors on unused locals. */
39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
42 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
43 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */
44 | // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */
45 |
46 | /* Module Resolution Options */
47 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
48 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
49 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
50 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
51 | // "typeRoots": [], /* List of folders to include type definitions from. */
52 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
53 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
54 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
55 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
56 |
57 | /* Source Map Options */
58 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
60 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
61 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
62 |
63 | /* Experimental Options */
64 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
65 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
66 |
67 | /* Advanced Options */
68 | "skipLibCheck": true, /* Skip type checking of declaration files. */
69 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
70 | },
71 | "exclude": [
72 | "tests/**",
73 | "example",
74 | "scripts",
75 | "build",
76 | "__mocks__",
77 | "coverage",
78 | "node_modules",
79 | "vitest.config.ts"
80 | ]
81 | }
82 |
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { defineConfig } from 'vite'
3 |
4 | export default defineConfig({
5 | test: {
6 | include: ['tests/**/*.test.ts'],
7 | /**
8 | * not to ESM ported packages
9 | */
10 | exclude: ['build', '.idea', '.git', '.cache', '**/node_modules/**', '__mocks__'],
11 | coverage: {
12 | enabled: true,
13 | include: ['src/**/*.ts'],
14 | exclude: ['src/types.ts'],
15 | thresholds: {
16 | lines: 96,
17 | functions: 80,
18 | branches: 70,
19 | statements: 96,
20 | },
21 | },
22 | },
23 | })
24 |
--------------------------------------------------------------------------------