├── .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 [![Test Changes](https://github.com/webdriverio/create-wdio/actions/workflows/test.yml/badge.svg?branch=main&event=push)](https://github.com/webdriverio/create-wdio/actions/workflows/test.yml) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-green.svg)](https://github.com/webdriverio/create-wdio/blob/main/CONTRIBUTING.md) 2 | =========================== 3 | 4 | Logo 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 | Example 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 | --------------------------------------------------------------------------------