├── .editorconfig ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── e2-tests.yml │ └── publish-node.js.yml ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── babel.config.js ├── bin └── codecept-ui.js ├── build └── icons │ ├── 1024x1024.png │ ├── 128x128.png │ ├── 16x16.png │ ├── 24x24.png │ ├── 256x256.png │ ├── 32x32.png │ ├── 48x48.png │ ├── 512x512.png │ ├── 64x64.png │ ├── icon.icns │ └── icon.ico ├── codecept-ui2.gif ├── codeceptui.gif ├── lib ├── api │ ├── get-config.js │ ├── get-file.js │ ├── get-page-objects.js │ ├── get-scenario-status.js │ ├── get-scenario.js │ ├── get-settings.js │ ├── get-snapshot-html.js │ ├── get-snapshot-image.js │ ├── get-steps.js │ ├── get-testrun.js │ ├── index.js │ ├── list-actions.js │ ├── list-profiles.js │ ├── list-scenarios.js │ ├── list-steps.js │ ├── new-test.js │ ├── open-test-in-editor.js │ ├── run-scenario-parallel.js │ ├── run-scenario.js │ ├── save-testrun.js │ ├── script.js │ ├── stop.js │ └── store-settings.js ├── app.js ├── codeceptjs │ ├── brk.js │ ├── configure │ │ └── setBrowser.js │ ├── console-recorder.helper.js │ ├── network-recorder.helper.js │ ├── realtime-reporter.helper.js │ ├── reporter-utils.js │ └── single-session.helper.js ├── commands │ ├── electron.js │ └── init.js ├── config │ ├── env.js │ └── url.js ├── model │ ├── codeceptjs-factory.js │ ├── codeceptjs-run-workers │ │ └── index.js │ ├── open-in-editor.js │ ├── profile-repository.js │ ├── scenario-repository.js │ ├── scenario-status-repository.js │ ├── settings-repository.js │ ├── snapshot-store │ │ ├── fix-html-snapshot.js │ │ ├── fix-htmls-snapshot.spec.js │ │ └── index.js │ ├── testrun-repository.js │ ├── throttling.js │ └── ws-events.js └── utils │ ├── absolutize-paths.js │ ├── mkdir.js │ ├── port-type-validator.js │ └── port-validator.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── favicon.ico ├── favicon.png ├── icon.png └── index.html ├── src ├── App.vue ├── assets │ ├── logo.png │ └── tailwind.css ├── background.js ├── components │ ├── CapabilityFolder.vue │ ├── Console.vue │ ├── EditorNotFound.vue │ ├── Feature.vue │ ├── Header.vue │ ├── Logo.vue │ ├── Pause.vue │ ├── ProfileSelection.vue │ ├── RunButton.vue │ ├── Scenario.vue │ ├── ScenarioSource.vue │ ├── SettingsMenu.vue │ ├── Snapshot.vue │ ├── SnapshotREST.vue │ ├── SnapshotSource.vue │ ├── Step.vue │ ├── Test.vue │ ├── TestResult.vue │ ├── TestStatistics.vue │ ├── pages │ │ ├── NewTestPage.vue │ │ ├── PageObjectsPage.vue │ │ ├── ScenariosPage.vue │ │ ├── SettingsPage.vue │ │ └── TestRunPage.vue │ └── steps │ │ ├── ActionStep.vue │ │ ├── Argument.vue │ │ ├── AssertionStep.vue │ │ ├── ConsoleLogStep.vue │ │ ├── GrabberStep.vue │ │ ├── MetaStep.vue │ │ └── WaiterStep.vue ├── main.js ├── routes.js ├── services │ ├── selector-finder.js │ └── selector.js └── store │ ├── index.js │ └── modules │ ├── cli.js │ ├── profiles.js │ ├── scenarios.js │ ├── settings.js │ ├── testrun-page.js │ └── testruns.js ├── tailwind.js ├── test └── e2e │ ├── codecept.conf.ts │ ├── homepage_test.ts │ ├── package.json │ ├── steps.d.ts │ ├── steps_file.ts │ └── tsconfig.json └── vue.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Installing 2 | 3 | 1. clone repository 4 | 2. Run `npm install` 5 | 3. Run `npm run build` (this is required for a fresh start) 6 | 7 | We need frontend and backend server to be started for this application. 8 | 9 | Frontend server compiles assets, while backend server communicates with CodeceptJS and processes HTTP and WebSocket requests. HTTP is used to send commands from client to CodeceptJS, and websockets are used to send notifications from CodeceptJS to application. 10 | 11 | Note: if you error when building the app, just set this `NODE_OPTIONS=--openssl-legacy-provider` in your terminal 12 | 13 | ``` 14 | > @codeceptjs/ui@0.7.3 build 15 | > vue-cli-service build 16 | 17 | 18 | ⠹ Building for production...Error: error:0308010C:digital envelope routines::unsupported 19 | at new Hash (node:internal/crypto/hash:69:19) 20 | at Object.createHash (node:crypto:133:10) 21 | ``` 22 | 23 | 24 | Both servers must be executed for development: 25 | 26 | ### Launch application in Electron mode: 27 | 28 | ``` 29 | npm run electron:serve 30 | npm run app 31 | ``` 32 | 33 | ### Launch application in WebServer mode: 34 | 35 | ``` 36 | npm run frontend 37 | npm run backend 38 | ``` 39 | 40 | Open application at **http://127.0.0.1:8080**. 41 | 42 | > Pay attention that the port is **8080** and not 3333 in this case. 43 | 44 | ## Making Pull Requests 45 | 46 | 1. Create your own branch (fork project) 47 | 2. Create a branch for the feature you work on 48 | 3. Create a pull request 49 | 4. Wait for it to be approved... 50 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | #### What are you trying to achieve? 2 | 3 | - Provide a descriptive text that what you are trying to achieve with CodeceptUI. 4 | - Attach a screenshot should be more than welcome. 5 | - Provide a console output for frontend (open DevTools and copy or make a screenshot of last messages) 6 | - Provide output for backend (copy last mesage from terminal) 7 | 8 | #### What do you get instead? 9 | 10 | > Provide console output if related. 11 | 12 | ```bash 13 | # paste output here 14 | ``` 15 | 16 | > Provide test source code if related 17 | 18 | ```js 19 | // paste test source code here 20 | ``` 21 | 22 | ### Environment info 23 | 24 | - Copy and paste your environment info by using `npx codeceptjs info` 25 | 26 | ```js 27 | // paste env info here 28 | ``` 29 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # github-actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | target-branch: "master" 9 | # npm 10 | - package-ecosystem: "npm" 11 | directory: "/" 12 | schedule: 13 | interval: "weekly" 14 | target-branch: "master" 15 | -------------------------------------------------------------------------------- /.github/workflows/e2-tests.yml: -------------------------------------------------------------------------------- 1 | # This to verify lib version bump doesn't break anything 2 | name: E2E Tests 3 | 4 | on: 5 | push: 6 | branches: 7 | - master 8 | - main 9 | pull_request: 10 | branches: 11 | - '**' 12 | 13 | jobs: 14 | publish-npm: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-node@v4 19 | with: 20 | node-version: 20 21 | registry-url: https://registry.npmjs.org/ 22 | - run: git config --global user.name "GitHub CD bot" 23 | - run: git config --global user.email "github-cd-bot@example.com" 24 | - name: Install deps 25 | run: export NODE_OPTIONS=--openssl-legacy-provider && npm i -g wait-for-localhost-cli && npm i -f 26 | - name: Start app and run tests 27 | run: export NODE_OPTIONS=--openssl-legacy-provider && npm run serve & wait-for-localhost 8080; cd test/e2e; npm i && npx playwright install chromium && npm run test 28 | env: 29 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | -------------------------------------------------------------------------------- /.github/workflows/publish-node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | name: Publish npm Package 4 | 5 | on: 6 | push: 7 | branches: 8 | - master 9 | 10 | jobs: 11 | publish-npm: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version: 20 18 | registry-url: https://registry.npmjs.org/ 19 | - run: git config --global user.name "GitHub CD bot" 20 | - run: git config --global user.email "github-cd-bot@example.com" 21 | - name: Install deps 22 | run: npm i 23 | - name: Build the app 24 | run: export NODE_OPTIONS=--openssl-legacy-provider && npm run build 25 | - run: npx semantic-release 26 | env: 27 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 28 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | # push the version changes to GitHub 30 | - run: git add package.json package-lock.json && git commit -m'update version' && git push 31 | env: 32 | # The secret is passed automatically. Nothing to configure. 33 | github-token: ${{ secrets.GITHUB_TOKEN }} 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | 23 | #Electron-builder output 24 | /dist_electron 25 | 26 | package-lock.json 27 | 28 | 29 | test/e2e/node_modules 30 | test/e2e/yarn.lock 31 | test/e2e/output 32 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CodeceptUI 2 | 3 | An interactive, graphical test runner for [CodeceptJS](https://codecept.io). 4 | 5 | 6 | ![codeceptui](codecept-ui2.gif) 7 | 8 | * Runs as Electron app or as a web server 9 | * Headless & window mode supported 10 | * Test write mode 11 | * Interactive pause built-in 12 | * Snapshots & Time travel 13 | * Runs tests in CodeceptJS supported engines: 14 | * Playwright 15 | * Puppeteer 16 | * webdriverio 17 | * TestCafe 18 | 19 | ## Quickstart 20 | 21 | **Requires [CodeceptJS 3](https://codecept.io) to be installed** 22 | 23 | Install CodeceptUI in a project where CodeceptJS is already used 24 | 25 | ``` 26 | npm i @codeceptjs/ui --save 27 | ``` 28 | 29 | ### Application Mode 30 | 31 | Run CodeceptUI in application mode (recommended for development, local debug): 32 | 33 | ``` 34 | npx codecept-ui --app 35 | ``` 36 | 37 | Uses `codecept.conf.js` config from the current directory. 38 | 39 | If needed, provide a path to config file with `--config` option: 40 | 41 | ``` 42 | npx codecept run --config tests/codecept.conf.js 43 | ``` 44 | 45 | ### WebServer Mode 46 | 47 | ![webserver mode](codeceptui.gif) 48 | 49 | Run CodeceptUI as a web server (recommended for headless mode, remote debug): 50 | 51 | ``` 52 | npx codecept-ui 53 | ``` 54 | 55 | Open `http://localhost:3333` to see all tests and run them. 56 | 57 | 58 | Uses `codecept.conf.js` config from the current directory. 59 | 60 | If needed, provide a path to config file with `--config` option: 61 | 62 | ``` 63 | npx codecept run --config tests/codecept.conf.js 64 | ``` 65 | 66 | #### Ports 67 | 68 | CodeceptUI requires two ports HTTP and WebSocket. 69 | 70 | * HTTP Port = 3333 71 | * WebSocket Port = 2999 72 | 73 | Default HTTP port is 3333. You can change the port by specifying it to **--port** option: 74 | 75 | ``` 76 | npx codecept-ui --app --port=3000 77 | ``` 78 | 79 | 80 | Default WebSocket port is 2999. You can change the port by specifying it to **--wsPort** option: 81 | ``` 82 | npx codecept-ui --app --wsPort=4444 83 | ``` 84 | 85 | 86 | ## Development 87 | 88 | See [CONTRIBUTING.md](.github/CONTRIBUTING.md) 89 | 90 | 91 | ## Start CodeceptUI with debug output 92 | 93 | CodeceptUI uses the [debug](https://github.com/debug-js/debug) package to output debug information. This is useful to troubleshoot problems or just to see what CodeceptUI is doing. To turn on debug information do 94 | 95 | ``` 96 | # verbose: get all debug information 97 | DEBUG=codeceptjs:* npx codecept-ui 98 | 99 | # just get debug output of one module 100 | DEBUG=codeceptjs:codeceptjs-factory npx codecept-ui 101 | ``` 102 | 103 | # Credits 104 | 105 | - Originally created by Stefan Huber @hubidu27 106 | - Maintained my @davertmik 107 | - Icons/Logos Code Icon by Elegant Themes on Iconscout 108 | 109 | # Contributors 110 | 111 | Thanks all for the contributions! 112 | 113 | [//]: contributor-faces 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | [//]: contributor-faces 126 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | }; 6 | -------------------------------------------------------------------------------- /bin/codecept-ui.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const debug = require('debug')('codeceptjs:ui'); 3 | 4 | // initialize CodeceptJS and return startup options 5 | const path = require('path'); 6 | const { existsSync } = require('fs'); 7 | const express = require('express'); 8 | const options = require('../lib/commands/init')(); 9 | const codeceptjsFactory = require('../lib/model/codeceptjs-factory'); 10 | const io = require('socket.io')(); 11 | const { events } = require('../lib/model/ws-events'); 12 | 13 | // Serve frontend from dist 14 | const AppDir = path.join(__dirname, '..', 'dist'); 15 | if (!existsSync(AppDir)) { 16 | // eslint-disable-next-line no-console 17 | console.error('\n ⚠️You have to build Vue application by `npm run build`\n'); 18 | process.exit(1); 19 | } 20 | 21 | 22 | codeceptjsFactory.create({}, options).then(() => { 23 | debug('CodeceptJS initialized, starting application'); 24 | 25 | const api = require('../lib/api'); 26 | const app = express(); 27 | 28 | 29 | 30 | /** 31 | * HTTP Routes 32 | */ 33 | app.use(express.static(AppDir)); 34 | app.use('/api', api); 35 | 36 | /** 37 | * Websocket Events 38 | */ 39 | io.on('connection', socket => { 40 | const emit = (evtName, data) => { 41 | debug(evtName); 42 | socket.broadcast.emit(evtName, data); 43 | }; 44 | debug('socket connects'); 45 | 46 | for (const eventName of events) { 47 | socket.on(eventName, (data) => { 48 | emit(eventName, data); 49 | }); 50 | } 51 | }); 52 | 53 | const applicationPort = options.port; 54 | const webSocketsPort = options.wsPort; 55 | 56 | io.listen(webSocketsPort); 57 | app.listen(applicationPort); 58 | 59 | // eslint-disable-next-line no-console 60 | console.log('🌟 CodeceptUI started!'); 61 | 62 | // eslint-disable-next-line no-console 63 | console.log(`👉 Open http://localhost:${applicationPort} to see CodeceptUI in a browser\n\n`); 64 | 65 | // eslint-disable-next-line no-console 66 | debug(`Listening for websocket connections on port ${webSocketsPort}`); 67 | 68 | if (options.app) { 69 | // open electron app 70 | global.isElectron = true; 71 | require('../lib/commands/electron'); 72 | } 73 | 74 | }).catch((e) => { 75 | console.error(e.message); 76 | process.exit(1); 77 | }); 78 | -------------------------------------------------------------------------------- /build/icons/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeceptjs/ui/7e875809819f2c4134d340122ec99e4f1acae971/build/icons/1024x1024.png -------------------------------------------------------------------------------- /build/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeceptjs/ui/7e875809819f2c4134d340122ec99e4f1acae971/build/icons/128x128.png -------------------------------------------------------------------------------- /build/icons/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeceptjs/ui/7e875809819f2c4134d340122ec99e4f1acae971/build/icons/16x16.png -------------------------------------------------------------------------------- /build/icons/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeceptjs/ui/7e875809819f2c4134d340122ec99e4f1acae971/build/icons/24x24.png -------------------------------------------------------------------------------- /build/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeceptjs/ui/7e875809819f2c4134d340122ec99e4f1acae971/build/icons/256x256.png -------------------------------------------------------------------------------- /build/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeceptjs/ui/7e875809819f2c4134d340122ec99e4f1acae971/build/icons/32x32.png -------------------------------------------------------------------------------- /build/icons/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeceptjs/ui/7e875809819f2c4134d340122ec99e4f1acae971/build/icons/48x48.png -------------------------------------------------------------------------------- /build/icons/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeceptjs/ui/7e875809819f2c4134d340122ec99e4f1acae971/build/icons/512x512.png -------------------------------------------------------------------------------- /build/icons/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeceptjs/ui/7e875809819f2c4134d340122ec99e4f1acae971/build/icons/64x64.png -------------------------------------------------------------------------------- /build/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeceptjs/ui/7e875809819f2c4134d340122ec99e4f1acae971/build/icons/icon.icns -------------------------------------------------------------------------------- /build/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeceptjs/ui/7e875809819f2c4134d340122ec99e4f1acae971/build/icons/icon.ico -------------------------------------------------------------------------------- /codecept-ui2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeceptjs/ui/7e875809819f2c4134d340122ec99e4f1acae971/codecept-ui2.gif -------------------------------------------------------------------------------- /codeceptui.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeceptjs/ui/7e875809819f2c4134d340122ec99e4f1acae971/codeceptui.gif -------------------------------------------------------------------------------- /lib/api/get-config.js: -------------------------------------------------------------------------------- 1 | const codeceptjsFactory = require('../model/codeceptjs-factory'); 2 | 3 | module.exports = (req, res) => { 4 | const internalHelpers = Object.keys(codeceptjsFactory.codeceptjsHelpersConfig.helpers); 5 | const { config, container } = codeceptjsFactory.getInstance(); 6 | const helpers = Object.keys(container.helpers()).filter(helper => internalHelpers.indexOf(helper) < 0); 7 | 8 | const currentConfig = { 9 | helpers, 10 | plugins: Object.keys(container.plugins()), 11 | file: codeceptjsFactory.getConfigFile(), 12 | config: config.get(), 13 | }; 14 | 15 | res.json(currentConfig); 16 | }; 17 | -------------------------------------------------------------------------------- /lib/api/get-file.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs').promises; 2 | 3 | module.exports = (req, res) => { 4 | if (!req.file) return; 5 | if (!req.file.startsWith(global.codecept_dir)) return; // not a codecept file 6 | res.send(fs.readFileSync(req.file)); 7 | }; 8 | -------------------------------------------------------------------------------- /lib/api/get-page-objects.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs').promises; 2 | const absolutizePaths = require('../utils/absolutize-paths'); 3 | const codeceptjsFactory = require('../model/codeceptjs-factory'); 4 | const { container, config } = codeceptjsFactory.getInstance(); 5 | 6 | const files = absolutizePaths(config.get('include', {})); 7 | 8 | const supportObjects = container.support(); 9 | const pageObjects = {}; 10 | 11 | Object.getOwnPropertyNames(supportObjects).map(async pageObject => { 12 | pageObjects[pageObject] = { 13 | name: pageObjects[pageObject], 14 | path: files[pageObject], 15 | source: await fs.readFile(files[pageObject], 'utf-8'), 16 | methods: Object.keys(supportObjects[pageObject]), 17 | }; 18 | }); 19 | 20 | module.exports = (req, res) => { 21 | res.json(pageObjects); 22 | }; 23 | -------------------------------------------------------------------------------- /lib/api/get-scenario-status.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('codeceptjs:get-scenario-status'); 2 | const scenarioStatusRepository = require('../model/scenario-status-repository'); 3 | 4 | module.exports = (req, res) => { 5 | const status = scenarioStatusRepository.getStatus(); 6 | debug(`Initial status is ${JSON.stringify(status)}`); 7 | 8 | res.json(status); 9 | }; 10 | -------------------------------------------------------------------------------- /lib/api/get-scenario.js: -------------------------------------------------------------------------------- 1 | const scenarioRepository = require('../model/scenario-repository'); 2 | 3 | module.exports = (req, res) => { 4 | const { id } = req.params; 5 | 6 | const scenario = scenarioRepository.getScenario(id); 7 | if (!scenario) { 8 | return res.status(404).json({ 9 | message: `Could not find scenario ${id}`, 10 | }); 11 | } 12 | res.json(scenario); 13 | }; 14 | -------------------------------------------------------------------------------- /lib/api/get-settings.js: -------------------------------------------------------------------------------- 1 | const settingsRepository = require('../model/settings-repository'); 2 | 3 | module.exports = (req, res) => { 4 | const settings = settingsRepository.getSettings(); 5 | 6 | res.json(settings); 7 | }; 8 | -------------------------------------------------------------------------------- /lib/api/get-snapshot-html.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('codeceptjs:get-snapshot-html'); 2 | const snapshotStore = require('../model/snapshot-store'); 3 | 4 | module.exports = (req, res) => { 5 | const { id } = req.params; 6 | 7 | if (!snapshotStore.exists(id)) { 8 | debug(`step ${id} does not exist`); 9 | res.status(404).send(`A snapshot for this step (${id}) does not exist`); 10 | return; 11 | } 12 | if (!snapshotStore.hasSnapshot(id)) { 13 | debug(`step ${id} does not have a snapshot`); 14 | res.status(404).send(`No snapshot for step id ${id}`); 15 | return; 16 | } 17 | 18 | const source = snapshotStore.get(id).source; 19 | 20 | res.send(source); 21 | }; -------------------------------------------------------------------------------- /lib/api/get-snapshot-image.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('codeceptjs:get-snapshot-image'); 2 | const snapshotStore = require('../model/snapshot-store'); 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | module.exports = (req, res) => { 7 | const { id } = req.params; 8 | 9 | if (!snapshotStore.exists(id)) { 10 | res.status(404).send(`A snapshot for this step (${id}) does not exist`); 11 | return; 12 | } 13 | if (!snapshotStore.hasSnapshot(id)) { 14 | res.status(404).send(`No snapshot for step id ${id}`); 15 | return; 16 | } 17 | 18 | if (!snapshotStore.get(id).screenshot) { 19 | res.status(404).send(`Screenshot file does not exists for ${id}`); 20 | } 21 | 22 | const screenshotFile = path.join(global.output_dir, snapshotStore.get(id).screenshot); 23 | 24 | if (!fs.existsSync(screenshotFile)) { 25 | res.status(404).send(`Screenshot file does not exists for ${id}`); 26 | return; 27 | } 28 | 29 | try { 30 | res.sendFile(screenshotFile); 31 | } catch (err) { 32 | debug(`Screenshot was not loaded by id ${id}`, err); 33 | } 34 | }; -------------------------------------------------------------------------------- /lib/api/get-steps.js: -------------------------------------------------------------------------------- 1 | const bddHelper = require('codeceptjs/lib/interfaces/bdd'); 2 | const stepsData = bddHelper.getSteps(); 3 | const steps = {}; 4 | 5 | Object.keys(stepsData).map(name => { 6 | const line = stepsData[name].line; 7 | const matches = line.match(/^\((?.+):(?\d+):(?\d+)\)$/); 8 | if (matches && matches.groups) { 9 | steps[name] = { 10 | ...matches.groups, 11 | }; 12 | } 13 | }); 14 | 15 | module.exports = (req, res) => { 16 | res.json(steps); 17 | }; 18 | -------------------------------------------------------------------------------- /lib/api/get-testrun.js: -------------------------------------------------------------------------------- 1 | const testRunRepository = require('../model/testrun-repository'); 2 | const snapshotStore = require('../model/snapshot-store'); 3 | 4 | module.exports = (req, res) => { 5 | const { id } = req.params; 6 | 7 | const saveId = encodeURIComponent(id); 8 | 9 | const testRun = testRunRepository.getTestRun(saveId); 10 | if (!testRun) return res.status(404).json({ 11 | message: `No testrun with id ${saveId}`, 12 | }); 13 | 14 | snapshotStore.restoreFromTestRun(saveId); 15 | 16 | res.json(testRun); 17 | }; -------------------------------------------------------------------------------- /lib/api/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const bodyParser = require('body-parser'); 3 | const router = express.Router(); 4 | 5 | const { getPort } = require('./../config/env'); 6 | 7 | const getSnapshotHtml = require('./get-snapshot-html'); 8 | const getSnapshotImage = require('./get-snapshot-image'); 9 | const listActions = require('./list-actions'); 10 | const listScenarios = require('./list-scenarios'); 11 | const getScenario = require('./get-scenario'); 12 | const getFile = require('./get-file'); 13 | const runScenario = require('./run-scenario'); 14 | const runNew = require('./new-test'); 15 | const stopScenario = require('./stop'); 16 | const runScenarioParallel = require('./run-scenario-parallel'); 17 | const openTestInEditor = require('./open-test-in-editor'); 18 | const saveTestRun = require('./save-testrun'); 19 | const getTestRun = require('./get-testrun'); 20 | // const listProfiles = require('./list-profiles'); 21 | const getSettings = require('./get-settings'); 22 | const getConfig = require('./get-config'); 23 | const storeSettings = require('./store-settings'); 24 | const getScenarioStatus = require('./get-scenario-status'); 25 | const getSteps = require('./get-steps'); 26 | const getPageObjects = require('./get-page-objects'); 27 | 28 | const jsonParser = bodyParser.json({ limit: '50mb' }); 29 | 30 | router.get('/steps', getSteps); 31 | router.get('/page-objects', getPageObjects); 32 | router.get('/snapshots/html/:id', getSnapshotHtml); 33 | router.get('/snapshots/screenshot/:id', getSnapshotImage); 34 | router.post('/file', getFile); 35 | 36 | router.get('/scenario-status', getScenarioStatus); 37 | 38 | router.get('/scenarios', listScenarios); 39 | router.get('/actions', listActions); 40 | router.get('/scenarios/:id', getScenario); 41 | 42 | router.post('/scenarios/run', jsonParser, runScenario); 43 | router.post('/scenarios/grep/:grep/run', jsonParser, runScenario); 44 | router.post('/scenarios/:id/run', jsonParser, runScenario); 45 | router.post('/scenarios/stop', jsonParser, stopScenario); 46 | router.post('/run-new', jsonParser, runNew); 47 | router.post('/scenarios/:grep/run-parallel', jsonParser, runScenarioParallel); 48 | router.get('/tests/:file/open', openTestInEditor); 49 | 50 | router.get('/testruns/:id', getTestRun); 51 | router.put('/testruns/:id', jsonParser, saveTestRun); 52 | 53 | router.get('/config', getConfig); 54 | router.get('/settings', getSettings); 55 | router.put('/settings', jsonParser, storeSettings); 56 | // router.get('/profiles', listProfiles); 57 | 58 | router.get('/ports', (req, res) => { 59 | res.json({ 60 | port: getPort('application'), 61 | wsPort: getPort('ws'), 62 | }); 63 | }); 64 | 65 | module.exports = router; 66 | -------------------------------------------------------------------------------- /lib/api/list-actions.js: -------------------------------------------------------------------------------- 1 | function _interopDefault(ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex.default : ex; } 2 | const acorn = require('acorn'); 3 | const parser = _interopDefault(require('parse-function'))({ parse: acorn.parse, ecmaVersion: 11 }); 4 | const debug = require('debug')('codeceptjs:codeceptjs-factory'); 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | const codeceptjsFactory = require('../model/codeceptjs-factory'); 8 | const { methodsOfObject } = require('codeceptjs/lib/utils'); 9 | 10 | module.exports = (req, res) => { 11 | const { container } = codeceptjsFactory.getInstance(); 12 | const docsWebApiFolderPath = path.join(path.dirname(require.resolve('codeceptjs')), '/../docs/webapi'); 13 | const docFileList = []; 14 | try { 15 | fs.readdirSync(docsWebApiFolderPath).map(fileName => { 16 | docFileList.push(path.basename(fileName,'.mustache')); 17 | }); 18 | } catch (e) { 19 | debug(`No documentation found due to ${e.message}`); 20 | } 21 | const helpers = container.helpers(); 22 | const actions = {}; 23 | for (const name in helpers) { 24 | const helper = helpers[name]; 25 | methodsOfObject(helper).forEach((action) => { 26 | 27 | if (docFileList.includes(action)) { 28 | let filePath = path.join(docsWebApiFolderPath, action + '.mustache'); 29 | let fn = helper[action].toString().replace(/\n/g, ' ').replace(/\{.*\}/gm, '{}'); 30 | try { 31 | let docData = fs.readFileSync(filePath, 'utf-8'); 32 | let params = parser.parse(fn); 33 | actions[action] = { params: params, actionDoc: docData }; 34 | } catch (err) { 35 | debug('Error in fetching doc for file content', fn, err); 36 | } 37 | } 38 | }); 39 | } 40 | res.send({ actions }); 41 | }; 42 | -------------------------------------------------------------------------------- /lib/api/list-profiles.js: -------------------------------------------------------------------------------- 1 | const profileRepository = require('../model/profile-repository'); 2 | 3 | module.exports = (req, res) => { 4 | const profiles = profileRepository.getProfiles(); 5 | if (!profiles) { 6 | res.status(404).json({ message: 'No profiles configured' }); 7 | } 8 | res.json(profiles); 9 | }; 10 | -------------------------------------------------------------------------------- /lib/api/list-scenarios.js: -------------------------------------------------------------------------------- 1 | const scenarioRepository = require('../model/scenario-repository'); 2 | const codeceptjsFactory = require('../model/codeceptjs-factory'); 3 | 4 | module.exports = (req, res) => { 5 | const { config } = codeceptjsFactory.getInstance(); 6 | const searchQuery = req.query.q; 7 | const matchType = req.query.m || 'all'; 8 | 9 | scenarioRepository.reloadSuites(); 10 | const features = scenarioRepository.getFeatures(searchQuery, { matchType }); 11 | 12 | res.send({ 13 | name: config.get('name'), 14 | features: scenarioRepository.groupFeaturesByCapability(features), 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /lib/api/list-steps.js: -------------------------------------------------------------------------------- 1 | const { getUrl } = require('../config/url'); 2 | const { event } = require('codeceptjs'); 3 | const codeceptjsFactory = require('../model/codeceptjs-factory'); 4 | 5 | const WS_URL = getUrl('ws'); 6 | const TestProjectDir = process.cwd(); 7 | 8 | 9 | module.exports = async (req, res) => { 10 | const socket = require('socket.io-client')(WS_URL); 11 | 12 | const { scenario } = req.params; 13 | const { codecept, container } = codeceptjsFactory.getInstance(); 14 | 15 | const mocha = container.mocha(); 16 | mocha.grep(scenario); 17 | 18 | process.chdir(TestProjectDir); 19 | 20 | event.dispatcher.once(event.all.result, () => { 21 | mocha.unloadFiles(); 22 | mocha.suite.cleanReferences(); 23 | mocha.suite.suites = []; 24 | socket.emit('codeceptjs.exit', process.exitCode); 25 | }); 26 | 27 | codecept.run(); 28 | 29 | res.status(200).send('OK'); 30 | }; 31 | 32 | -------------------------------------------------------------------------------- /lib/api/new-test.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('codeceptjs:run-scenario'); 2 | const wsEvents = require('../model/ws-events'); 3 | const pause = require('../codeceptjs/brk'); 4 | const { event } = require('codeceptjs'); 5 | const Test = require('mocha/lib/test'); 6 | const Suite = require('mocha/lib/suite'); 7 | const scenario = require('codeceptjs/lib/scenario'); 8 | const codeceptjsFactory = require('../model/codeceptjs-factory'); 9 | 10 | module.exports = async (req, res) => { 11 | const { codecept, container } = codeceptjsFactory.getInstance(); 12 | 13 | const mocha = container.mocha(); 14 | mocha.grep(); 15 | mocha.files = []; 16 | mocha.suite.suites = []; 17 | 18 | const code = eval(req.body.code); 19 | const test = scenario.test(new Test('new test', code)); 20 | test.uid = 'new-test'; 21 | 22 | const suite = Suite.create(mocha.suite, 'new test'); 23 | 24 | let pauseEnabled = true; 25 | 26 | suite.beforeEach('codeceptjs.before', () => scenario.setup(suite)); 27 | suite.afterEach('finalize codeceptjs', () => scenario.teardown(suite)); 28 | 29 | suite.beforeAll('codeceptjs.beforeSuite', () => scenario.suiteSetup(suite)); 30 | suite.afterAll('codeceptjs.afterSuite', () => scenario.suiteTeardown(suite)); 31 | 32 | 33 | suite.addTest(test); 34 | suite.appendOnlyTest(test); 35 | 36 | event.dispatcher.once(event.test.after, () => { 37 | if (pauseEnabled) pause(); 38 | }); 39 | 40 | event.dispatcher.once(event.all.result, () => { 41 | pauseEnabled = false; 42 | debug('testrun finished'); 43 | try { 44 | codeceptjsFactory.reloadSuites(); 45 | } catch (err) { 46 | debug('ERROR resetting suites', err); 47 | } 48 | wsEvents.codeceptjs.exit(0); 49 | mocha.suite.suites = []; 50 | }); 51 | 52 | debug('codecept.run()'); 53 | const done = () => { 54 | event.emit(event.all.result, codecept); 55 | event.emit(event.all.after, codecept); 56 | }; 57 | 58 | try { 59 | event.emit(event.all.before, codecept); 60 | global.runner = mocha.run(done); 61 | } catch (e) { 62 | throw new Error(e); 63 | } 64 | 65 | return res.status(200).send('OK'); 66 | }; 67 | -------------------------------------------------------------------------------- /lib/api/open-test-in-editor.js: -------------------------------------------------------------------------------- 1 | const openInEditor = require('../model/open-in-editor'); 2 | 3 | 4 | module.exports = async (req, res) => { 5 | const { file } = req.params; 6 | try { 7 | await openInEditor(file); 8 | res.json('OK'); 9 | } catch (e) { 10 | res.status(422).send({ message: 'Editor can\'t be started', description: e.stderr.toString().trim() }); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /lib/api/run-scenario-parallel.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const debug = require('debug')('codeceptjs:run-scenario-multiple'); 3 | const wsEvents = require('../model/ws-events'); 4 | const { event } = require('codeceptjs'); 5 | const codeceptjsFactory = require('../model/codeceptjs-factory'); 6 | const runWithWorkers = require('../model/codeceptjs-run-workers'); 7 | 8 | module.exports = async (req, res) => { 9 | const { grep } = req.params; 10 | const numberOfWorkers = os.cpus().length; 11 | 12 | const { config, codecept, container } = codeceptjsFactory.getInstance(); 13 | 14 | event.dispatcher.once(event.all.after, () => { 15 | debug('testrun finished'); 16 | try { 17 | codeceptjsFactory.resetSuites(); 18 | } catch (err) { 19 | debug('ERROR resetting suites', err); 20 | } 21 | wsEvents.codeceptjs.exit(0); 22 | }); 23 | 24 | debug('Running with workers...'); 25 | runWithWorkers(numberOfWorkers, config.get(), { grep }, codecept, container); 26 | 27 | return res.status(200).send('OK'); 28 | }; 29 | -------------------------------------------------------------------------------- /lib/api/run-scenario.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('codeceptjs:run-scenario'); 2 | const wsEvents = require('../model/ws-events'); 3 | const { event } = require('codeceptjs'); 4 | const codeceptjsFactory = require('../model/codeceptjs-factory'); 5 | 6 | module.exports = async (req, res) => { 7 | let { id, grep } = req.params; 8 | const { codecept, container } = codeceptjsFactory.getInstance(); 9 | 10 | const mocha = container.mocha(); 11 | mocha.grep(); // disable current grep 12 | 13 | if (id) { 14 | mocha.suite.eachTest(test => { 15 | if (test.uid === decodeURIComponent(id)) { 16 | test.parent.appendOnlyTest(test); 17 | } 18 | }); 19 | 20 | event.dispatcher.once(event.suite.before, suite => { 21 | suite._onlyTests = []; 22 | }); 23 | } 24 | 25 | if (grep) { 26 | mocha.fgrep(grep); 27 | } 28 | 29 | event.dispatcher.once(event.all.result, () => { 30 | mocha.grep(); // disable grep 31 | debug('testrun finished'); 32 | try { 33 | codeceptjsFactory.reloadSuites(); 34 | } catch (err) { 35 | debug('ERROR resetting suites', err); 36 | } 37 | wsEvents.codeceptjs.exit(0); 38 | }); 39 | 40 | debug('codecept.run()'); 41 | 42 | const done = () => { 43 | event.emit(event.all.result, codecept); 44 | event.emit(event.all.after, codecept); 45 | }; 46 | 47 | try { 48 | event.emit(event.all.before, codecept); 49 | global.runner = mocha.run(done); 50 | } catch (e) { 51 | throw new Error(e); 52 | } 53 | 54 | return res.status(200).send('OK'); 55 | }; 56 | -------------------------------------------------------------------------------- /lib/api/save-testrun.js: -------------------------------------------------------------------------------- 1 | const testRunRepository = require('../model/testrun-repository'); 2 | const snapshotStore = require('../model/snapshot-store'); 3 | 4 | module.exports = (req, res) => { 5 | const { id } = req.params; 6 | 7 | const saveId = encodeURIComponent(id); 8 | 9 | testRunRepository.saveTestRun(saveId, req.body); 10 | snapshotStore.saveWithTestRun(saveId); 11 | 12 | res.json({ 13 | message: 'OK' 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /lib/api/script.js: -------------------------------------------------------------------------------- 1 | const { event } = require('codeceptjs'); 2 | const codeceptjsFactory = require('../model/codeceptjs-factory'); 3 | 4 | const { codecept, container } = codeceptjsFactory.getInstance({}, { 5 | grep: 'Create a new todo item' 6 | }); 7 | 8 | const mocha = container.mocha(); 9 | mocha.grep('Create a new todo item'); 10 | 11 | // run bootstrap function from config 12 | codecept.runBootstrap(); 13 | 14 | // process.chdir(TestProjectDir); 15 | 16 | codecept.run(); 17 | 18 | event.dispatcher.once(event.all.result, () => { 19 | mocha.unloadFiles(); 20 | mocha.suite.cleanReferences(); 21 | mocha.suite.suites = []; 22 | codecept.run(); 23 | }); 24 | 25 | -------------------------------------------------------------------------------- /lib/api/stop.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('codeceptjs:run-scenario'); 2 | const { event } = require('codeceptjs'); 3 | 4 | module.exports = async (req, res) => { 5 | 6 | if (global.runner) { 7 | global.runner.abort(); 8 | event.dispatcher.once(event.all.result, () => { 9 | global.runner._abort = false; 10 | }); 11 | } 12 | 13 | debug('codecept.run()'); 14 | // codecept.run(); 15 | 16 | return res.status(200).send('OK'); 17 | }; 18 | -------------------------------------------------------------------------------- /lib/api/store-settings.js: -------------------------------------------------------------------------------- 1 | const codeceptjsFactory = require('../model/codeceptjs-factory'); 2 | const { storeSettings } = require('../model/settings-repository'); 3 | const setBrowser = require('../codeceptjs/configure/setBrowser'); 4 | const { setHeadlessWhen, setWindowSize, setHeadedWhen } = require('@codeceptjs/configure'); 5 | 6 | module.exports = (req, res) => { 7 | const settings = req.body || {}; 8 | const { isHeadless, windowSize, browser, editor, isSingleSession } = settings; 9 | 10 | codeceptjsFactory.reloadConfig(() => { 11 | setHeadlessWhen(isHeadless === true); 12 | setHeadedWhen(isHeadless === false); 13 | if (browser) setBrowser(browser); 14 | if (windowSize && windowSize.width && windowSize.height) { 15 | setWindowSize(windowSize.width, windowSize.height); 16 | } 17 | }); 18 | 19 | storeSettings({ editor, isSingleSession }); 20 | 21 | res.json({ 22 | message: 'Settings stored' 23 | }); 24 | }; 25 | 26 | -------------------------------------------------------------------------------- /lib/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const { BrowserWindow, app, shell } = require('electron'); 5 | const { getPort } = require('./config/env'); 6 | 7 | const NAME = 'CodeceptUI'; 8 | 9 | // Keep a global reference of the window object, if you don't, the window will 10 | // be closed automatically when the JavaScript object is garbage collected. 11 | let win; 12 | 13 | // Scheme must be registered before the app is ready 14 | // protocol.registerSchemesAsPrivileged([{scheme: 'app', privileges: { secure: true, standard: true } }]); 15 | 16 | app.name = NAME; 17 | app.setName(NAME); 18 | 19 | function createWindow() { 20 | const { screen } = require('electron'); 21 | const { width: screenWidth, height: screenHeight } = screen.getPrimaryDisplay().size; 22 | 23 | const width = Math.floor(screenWidth / 3.5) || 500; 24 | const x = screenWidth - Math.max(width, 500); 25 | // Create the browser window. 26 | win = new BrowserWindow({ 27 | width: screenWidth / 1.2, 28 | height: screenHeight, 29 | minWidth: 500, 30 | x, 31 | y: 0, 32 | title: NAME, 33 | autoHideMenuBar: true, 34 | icon: path.join(__dirname, '/build/icons/64x64.png'), 35 | webPreferences: { 36 | nodeIntegration: true, 37 | nodeIntegrationInWorker: true, 38 | }, 39 | }); 40 | 41 | win.loadURL(`http://localhost:${getPort('application')}`); 42 | 43 | // eslint-disable-next-line no-console 44 | console.log('Application window opened, switch to it to run tests...'); 45 | 46 | win.on('closed', () => { 47 | win = null; 48 | }); 49 | 50 | win.webContents.setWindowOpenHandler(({ url }) => { 51 | // open url in a browser and prevent default 52 | shell.openExternal(url); 53 | }); 54 | } 55 | 56 | // Quit when all windows are closed. 57 | app.on('window-all-closed', () => { 58 | // On macOS it is common for applications and their menu bar 59 | // to stay active until the user quits explicitly with Cmd + Q 60 | if (process.platform !== 'darwin') { 61 | app.quit(); 62 | } 63 | }); 64 | 65 | app.on('activate', () => { 66 | // On macOS it's common to re-create a window in the app when the 67 | // dock icon is clicked and there are no other windows open. 68 | if (win === null) { 69 | createWindow(); 70 | } 71 | }); 72 | 73 | // This method will be called when Electron has finished 74 | // initialization and is ready to create browser windows. 75 | // Some APIs can only be used after this event occurs. 76 | app.on('ready', () => { 77 | createWindow(); 78 | }); 79 | -------------------------------------------------------------------------------- /lib/codeceptjs/brk.js: -------------------------------------------------------------------------------- 1 | const { getUrl } = require('../config/url'); 2 | const container = require('codeceptjs').container; 3 | const store = require('codeceptjs').store; 4 | const recorder = require('codeceptjs').recorder; 5 | const event = require('codeceptjs').event; 6 | const output = require('codeceptjs').output; 7 | const readline = require('readline'); 8 | const methodsOfObject = require('codeceptjs/lib/utils').methodsOfObject; 9 | 10 | // const readline = require('readline'); 11 | const colors = require('chalk'); 12 | let nextStep; 13 | let finish; 14 | let next; 15 | 16 | let socket; 17 | let rl; 18 | 19 | let isPauseOn = false; 20 | 21 | /** 22 | * Pauses test execution and starts interactive shell 23 | */ 24 | const pause = function () { 25 | next = false; 26 | if (isPauseOn) return; 27 | isPauseOn = true; 28 | // add listener to all next steps to provide next() functionality 29 | event.dispatcher.on(event.step.after, () => { 30 | recorder.add('Start next pause session', () => { 31 | if (!next) return; 32 | return pauseSession(); 33 | }); 34 | }); 35 | recorder.add('Start new session', pauseSession); 36 | }; 37 | 38 | function pauseSession() { 39 | socket = require('socket.io-client')(getUrl('ws')); 40 | rl = readline.createInterface(process.stdin, process.stdout); 41 | 42 | recorder.session.start('pause'); 43 | if (!next) { 44 | const I = container.support('I'); 45 | socket.emit('cli.start', { 46 | prompt: 'Interactive shell started', 47 | commands: methodsOfObject(I) 48 | }); 49 | output.print(colors.yellow('Remote Interactive shell started')); 50 | } 51 | 52 | rl.on('line', parseInput); 53 | socket.on('cli.line', parseInput); 54 | socket.on('cli.close', () => { 55 | rl.close(); 56 | if (!next) console.log('Exiting interactive shell....'); // eslint-disable-line no-console 57 | }); 58 | 59 | return new Promise(((resolve) => { 60 | finish = resolve; 61 | return askForStep(); 62 | })); 63 | } 64 | 65 | function parseInput(cmd) { 66 | console.log('CMD', cmd); // eslint-disable-line no-console 67 | next = false; 68 | store.debugMode = false; 69 | if (cmd === '') next = true; 70 | if (!cmd || cmd === 'resume' || cmd === 'exit') { 71 | isPauseOn = false; 72 | finish(); 73 | recorder.session.restore(); 74 | socket.emit('cli.stop'); 75 | socket.close(); 76 | return nextStep(); 77 | } 78 | store.debugMode = true; 79 | 80 | try { 81 | const locate = global.locate; // eslint-disable-line no-unused-vars 82 | const I = container.support('I'); // eslint-disable-line no-unused-vars 83 | 84 | const fullCommand = `I.${cmd}`; 85 | event.dispatcher.once(event.step.before, step => step.command = fullCommand); 86 | 87 | eval(fullCommand); // eslint-disable-line no-eval 88 | 89 | } catch (err) { 90 | socket.emit('cli.error', { message: err.cliMessage ? err.cliMessage() : err.message }); 91 | output.print(output.styles.error(' ERROR '), err.message); 92 | } 93 | recorder.session.catch((err) => { 94 | const msg = err.cliMessage ? err.cliMessage() : err.message; 95 | 96 | socket.emit('cli.error', { message: err.cliMessage ? err.cliMessage() : err.message }); 97 | return output.print(output.styles.error(' FAIL '), msg); 98 | }); 99 | recorder.add('ask for next step', askForStep); 100 | nextStep(); 101 | } 102 | 103 | function askForStep() { 104 | return new Promise(((resolve) => { 105 | rl.setPrompt(' I.', 3); 106 | rl.resume(); 107 | rl.prompt(); 108 | nextStep = resolve; 109 | socket.emit('cli.start', { 110 | prompt: 'Continue', 111 | }); 112 | 113 | })); 114 | } 115 | 116 | module.exports = pause; 117 | -------------------------------------------------------------------------------- /lib/codeceptjs/configure/setBrowser.js: -------------------------------------------------------------------------------- 1 | const { config } = require('codeceptjs'); 2 | 3 | module.exports = function(browser) { 4 | 5 | config.addHook((cfg) => { 6 | if (cfg.helpers.Puppeteer) { 7 | cfg.helpers.Puppeteer.browser = browser; 8 | } 9 | if (cfg.helpers.Protractor) { 10 | cfg.helpers.Protractor.browser = browser; 11 | } 12 | if (cfg.helpers.WebDriver) { 13 | cfg.helpers.WebDriver.browser = browser; 14 | } 15 | if (cfg.helpers.TestCafe) { 16 | cfg.helpers.TestCafe.browser = browser; 17 | } 18 | if (cfg.helpers.Appium) { 19 | cfg.helpers.Appium.browser = browser; 20 | } 21 | }); 22 | }; -------------------------------------------------------------------------------- /lib/codeceptjs/console-recorder.helper.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('codeceptjs:console-recorder-helper'); 2 | const wsEvents = require('../model/ws-events'); 3 | const { v4: uuid } = require('uuid'); 4 | // eslint-disable-next-line no-undef 5 | let Helper = codecept_helper; 6 | 7 | class ConsoleRecorderHelper extends Helper { 8 | constructor(options) { 9 | super(options); 10 | } 11 | 12 | async _before() { 13 | const helper = this.helpers['Puppeteer']; 14 | if (!helper) { 15 | debug('Puppeteer helper not found -> console error reporting is disabled'); 16 | return; 17 | } 18 | const page = helper.page; 19 | 20 | if (!page) return; 21 | 22 | page.on('pageerror', async err => { 23 | debug('Got page error', err); 24 | wsEvents.console.jsError(err); 25 | 26 | this._addToLog({ 27 | type: 'error', 28 | message: err.toString() 29 | }); 30 | }); 31 | page.on('error', async err => { 32 | debug('Got error', err); 33 | wsEvents.console.error(err); 34 | 35 | this._addToLog({ 36 | type: 'error', 37 | message: err.toString() 38 | }); 39 | }); 40 | 41 | page.on('console', async msg => { 42 | // Parse all console.log args 43 | for (let i = 0; i < msg.args().length; ++i) { 44 | const arg = msg.args()[i]; 45 | let argVal = arg; 46 | if (arg.jsonValue) { 47 | try { 48 | argVal = JSON.stringify(await arg.jsonValue(), null, 2); 49 | } catch (err) { 50 | debug('ERROR getting json value', err); 51 | } 52 | } 53 | 54 | this._addToLog({ 55 | type: msg.type(), 56 | url: msg.location().url, 57 | lineNumber: msg.location().lineNumber, 58 | message: argVal 59 | }); 60 | debug('Got console message', msg.type()); 61 | } 62 | }); 63 | 64 | debug('Recording console logs is enabled'); 65 | } 66 | 67 | _addToLog(log) { 68 | 69 | if (!this.helpers.RealtimeReporterHelper) return; 70 | const step = this.helpers.RealtimeReporterHelper.step; 71 | 72 | if (!step.logs) step.logs = []; 73 | 74 | step.logs.push({ ...log, 75 | id: uuid, 76 | time: Date.now(), 77 | }); 78 | } 79 | } 80 | 81 | module.exports = ConsoleRecorderHelper; 82 | -------------------------------------------------------------------------------- /lib/codeceptjs/network-recorder.helper.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('codeceptjs:network-helper'); 2 | const wsEvents = require('../model/ws-events'); 3 | 4 | // eslint-disable-next-line no-undef 5 | let Helper = codecept_helper; 6 | 7 | class NetworkRecorderHelper extends Helper { 8 | constructor(options) { 9 | super(options); 10 | this.isInitialized = false; 11 | } 12 | 13 | async _before() { 14 | if (this.isInitialized) { 15 | return; 16 | } 17 | const helper = this.helpers['Puppeteer']; 18 | if (!helper) { 19 | debug('Puppeteer helper not found -> network error reporting is disabled'); 20 | } 21 | 22 | const page = helper.page; 23 | 24 | debug('Setting request interception to true'); 25 | await page.setRequestInterception(true); 26 | 27 | page.on('request', interceptedRequest => { 28 | interceptedRequest.continue(); 29 | }); 30 | 31 | page.on('requestfailed', request => { 32 | const failedRequest = Object.assign({}, { 33 | status: 999, 34 | method: request.method(), 35 | url: request.url(), 36 | resourceType: request.resourceType(), 37 | errorMessage: request.failure().errorText, 38 | duration: undefined, 39 | ok: false, 40 | }); 41 | 42 | debug('Got a failed request', request); 43 | wsEvents.network.failedRequest(failedRequest); 44 | }); 45 | 46 | this.isInitialized = true; 47 | } 48 | } 49 | 50 | module.exports = NetworkRecorderHelper; 51 | -------------------------------------------------------------------------------- /lib/codeceptjs/reporter-utils.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('codeceptjs:reporter-utils'); 2 | const assert = require('assert'); 3 | const crypto = require('crypto'); 4 | 5 | const toString = sth => { 6 | if (typeof sth === 'string') return sth; 7 | if (typeof sth === 'object') return JSON.stringify(sth); 8 | return '' + sth; 9 | }; 10 | const toError = err => { 11 | assert(err, 'err is required'); 12 | 13 | let message = err.message; 14 | if (err.inspect) { // AssertionFailedError 15 | message = err.message = err.inspect(); 16 | } 17 | message = toString(message); 18 | 19 | return { 20 | name: err.name, 21 | message, 22 | hash: hashString(message, err.stack), 23 | stack: err.stack, 24 | actual: err.actual, 25 | expected: err.expected, 26 | operator: err.operator 27 | }; 28 | }; 29 | 30 | const hashString = (...args) => { 31 | const str = args.join('-'); 32 | const hash = crypto.createHash('sha1'); 33 | hash.setEncoding('hex'); 34 | hash.write(str); 35 | hash.end(); 36 | return hash.read(); 37 | }; 38 | 39 | const isSnapshotStepBefore = (step) => { 40 | if (step.name.startsWith('click') || step.name.startsWith('double')) return true; 41 | if (step.name.startsWith('switch')) return true; // trigger a force snapshot on the next step 42 | if (step.name.startsWith('scroll')) return true; // make sure we have a screenshot after the scroll 43 | return false; 44 | }; 45 | 46 | const isSnapshotStep = () => { // step 47 | // which steps to ignore? 48 | return true; 49 | }; 50 | 51 | const isRetvalStep = step => { 52 | // TODO Need to map retrun values (e. g. axios responses) 53 | if (step.name.startsWith('send')) return true; 54 | if (step.name.startsWith('grab')) return true; 55 | return false; 56 | }; 57 | 58 | const getRetval = async (step, retvalPromise) => { 59 | let retval; 60 | if (retvalPromise) { 61 | 62 | try { 63 | retval = await retvalPromise; 64 | if (retval) { 65 | if (step.name.startsWith('send')) { 66 | retval = retval.data; 67 | } 68 | } 69 | } catch (err) { 70 | debug('ERROR getting retval in step', step.name); 71 | } 72 | } 73 | return retval; 74 | }; 75 | 76 | const isScreenshotStep = (step) => { 77 | if (step.name.match(/^wait/)) return true; 78 | if (step.name.match(/click/)) return true; 79 | if (step.name.match(/screenshot/)) return true; 80 | if (step.name.match(/^see/)) return true; 81 | if (step.name.match(/^dontSee/)) return true; 82 | return ['amOnPage', 'fillField', 'appendField', 'pressKey'].includes(step.name); 83 | }; 84 | 85 | /** 86 | * Grab html source from current iframe 87 | */ 88 | const grabSource = async helper => { 89 | if (!helper.isRunning) return; 90 | try { 91 | return helper.grabSource(); 92 | } catch (err) { 93 | debug('Failed to grab source', err); 94 | } 95 | }; 96 | 97 | const getViewportSize = function() { 98 | return { 99 | width: Math.max(document.documentElement.clientWidth, window.innerWidth || 0), 100 | height: Math.max(document.documentElement.clientHeight, window.innerHeight || 0) 101 | }; 102 | }; 103 | 104 | /** 105 | * Save screenshot and catch errors 106 | * @param {*} helper 107 | * @param {*} filename 108 | */ 109 | const saveScreenshot = async (helper, filename) => { 110 | try{ 111 | return helper.saveScreenshot(filename); 112 | } catch (err) { 113 | debug('Failed to save screenshot', err); 114 | } 115 | }; 116 | 117 | /** 118 | * Take an HTML snapshot 119 | * @param {*} helper 120 | * @param {*} snapshotId 121 | * @param {*} takeScreenshot 122 | */ 123 | const takeSnapshot = async (helper, snapshotId, takeScreenshot = false, retry = 3) => { 124 | assert(helper, 'helper is required'); 125 | assert(snapshotId, 'snapshotId is required'); 126 | 127 | const HelperName = helper.constructor.name; 128 | const StepFileName = snapshotId + '_step_screenshot.png'; 129 | 130 | let _, pageUrl, pageTitle, scrollPosition, viewportSize; // eslint-disable-line no-unused-vars 131 | 132 | try { 133 | [_, pageUrl, pageTitle, scrollPosition, viewportSize] = await Promise.all([ 134 | takeScreenshot ? saveScreenshot(helper, StepFileName) : Promise.resolve(undefined), 135 | helper.grabCurrentActivity ? helper.grabCurrentActivity() : helper.grabCurrentUrl(), 136 | helper.grabTitle ? helper.grabTitle() : '', 137 | helper.grabPageScrollPosition(), 138 | helper.executeScript(getViewportSize), 139 | ]); 140 | 141 | const snapshot = { 142 | id: snapshotId, 143 | screenshot: takeScreenshot ? StepFileName : undefined, 144 | scrollPosition, 145 | sourceContentType: HelperName === 'Appium' ? 'xml' : 'html', 146 | pageUrl, 147 | pageTitle, 148 | viewportSize, 149 | }; 150 | 151 | return snapshot; 152 | } catch (err) { 153 | if (retry > 0) { 154 | await new Promise(done => setTimeout(done, 100)); 155 | debug('Retrying snapshot taking'); 156 | return takeSnapshot(helper, snapshotId, takeScreenshot, retry--); 157 | } 158 | debug('ERROR Exception in takeSnapshot', err); 159 | 160 | return {}; 161 | } 162 | }; 163 | 164 | /** 165 | * Filter step stacktrace 166 | */ 167 | const filterStack = step => { 168 | const stackFrames = step.stack.split('\n'); 169 | const stackFramesOfProject = stackFrames 170 | .filter(sf => sf && sf.includes(process.cwd())) // keep only stackframes pointing to source within the test project 171 | ; 172 | 173 | const cwd = process.cwd(); 174 | 175 | return { 176 | stackFrameOfStep: stackFramesOfProject.find(sf => sf.includes(cwd)), 177 | stackFrameInTest: stackFramesOfProject.find(sf => sf.includes('Test.Scenario') || sf.includes('Test.')) 178 | }; 179 | }; 180 | 181 | /** 182 | * Safe version of stringify to serialize circular objects 183 | */ 184 | const stringifySafe = (o) => { 185 | let cache = []; 186 | const ret = JSON.stringify(o, (key, value) => { 187 | if (typeof value === 'object' && value !== null) { 188 | if (cache.indexOf(value) !== -1) { 189 | // Duplicate reference found, discard key 190 | return; 191 | } 192 | // Store value in our collection 193 | cache.push(value); 194 | } 195 | return value; 196 | }); 197 | cache = null; // Enable garbage collection 198 | return ret; 199 | }; 200 | 201 | module.exports = { 202 | toString, 203 | toError, 204 | grabSource, 205 | hashString, 206 | isRetvalStep, 207 | getRetval, 208 | isSnapshotStep, 209 | isSnapshotStepBefore, 210 | isScreenshotStep, 211 | takeSnapshot, 212 | filterStack, 213 | stringifySafe 214 | }; 215 | -------------------------------------------------------------------------------- /lib/codeceptjs/single-session.helper.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-undef 2 | let Helper = codecept_helper; 3 | 4 | const { getSettings } = require('../model/settings-repository'); 5 | const { container } = require('codeceptjs'); 6 | 7 | const supportedHelpers = [ 8 | 'TestCafe', 9 | 'Protractor', 10 | 'Puppeteer', 11 | 'Nightmare', 12 | 'WebDriver', 13 | 'Playwright' 14 | ]; 15 | 16 | class SingleSessionHelper extends Helper { 17 | constructor(options) { 18 | super(options); 19 | this.helper = null; 20 | this.enabled = global.isElectron || false; 21 | } 22 | 23 | _init() { 24 | const helpers = container.helpers(); 25 | for (const supportedHelper of supportedHelpers) { 26 | const helper = helpers[supportedHelper]; 27 | if (!helper) continue; 28 | this.helper = helper; 29 | break; 30 | } 31 | } 32 | 33 | async _beforeSuite() { 34 | const { isSingleSession } = getSettings(); 35 | if (isSingleSession) this.enabled = true; 36 | if (!this.enabled || !this.helper) return; 37 | this.helper.options.manualStart = true; 38 | this.helper.options.restart = false; 39 | 40 | await this._startBrowserIfNotRunning(); 41 | } 42 | 43 | _afterSuite() { 44 | if (!this.enabled || !this.helper) return; 45 | // dont close browser in the end 46 | this.helper.isRunning = false; 47 | } 48 | 49 | async _startBrowserIfNotRunning() { 50 | if (!this.helper) return; 51 | 52 | try { 53 | await this.helper.grabCurrentUrl(); 54 | } catch (err) { 55 | await this.helper._startBrowser(); 56 | } 57 | this.helper.isRunning = true; 58 | } 59 | 60 | } 61 | 62 | module.exports = SingleSessionHelper; 63 | -------------------------------------------------------------------------------- /lib/commands/electron.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | const { spawn } = require('child_process'); 3 | const { setHeadedWhen } = require('@codeceptjs/configure'); 4 | 5 | setHeadedWhen(true); 6 | 7 | const electron = 8 | process.env.ELECTRON_PATH || 9 | resolve('electron') || 10 | resolve('electron-prebuilt') || 11 | resolve('electron', require('which').sync); 12 | 13 | if (!electron) { 14 | /* eslint-disable no-console */ 15 | console.error(''); 16 | console.error(' Can not find `electron` in $PATH and $ELECTRON_PATH is not set.'); 17 | console.error(' Please either set $ELECTRON_PATH or `npm install electron`.'); 18 | console.error(''); 19 | process.exit(1); 20 | } 21 | 22 | run(electron); 23 | 24 | function resolve (module, resolver) { 25 | try { 26 | return (resolver || require)(module); 27 | } catch (_) { 28 | // ignore 29 | } 30 | } 31 | 32 | function run (electron) { 33 | let args = [ 34 | join(__dirname, '../app.js'), 35 | ...process.argv.slice(2) 36 | ]; 37 | 38 | let child = spawn(electron, args); 39 | 40 | // stdio 'inherit' not work reliably in Renderer! 41 | child.stdout.pipe(process.stdout); 42 | child.stderr.pipe(process.stderr); 43 | process.stdin.pipe(child.stdin); 44 | 45 | child.on('exit', (code, signal) => { 46 | if (signal) { 47 | process.kill(process.pid, signal); 48 | } else { 49 | process.exit(code); 50 | } 51 | }); 52 | 53 | process.on('SIGINT', () => { 54 | child.kill('SIGINT'); 55 | child.kill('SIGTERM'); 56 | }); 57 | } -------------------------------------------------------------------------------- /lib/commands/init.js: -------------------------------------------------------------------------------- 1 | const commander = require('commander'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | 5 | const codeceptjsFactory = require('../model/codeceptjs-factory'); 6 | const { setPort } = require('../config/env'); 7 | const portValidator = require('../utils/port-validator'); 8 | 9 | module.exports = () => { 10 | const program = new commander.Command(); 11 | program.allowUnknownOption(); 12 | program.version(JSON.parse(fs.readFileSync(`${__dirname}/../../package.json`, 'utf8')).version); 13 | program 14 | .option('--app', 'launch Electron application') 15 | .option('--port ', 'Port for http:// connection', portValidator) 16 | .option('--wsPort ', 'Port for ws:// connection', portValidator) 17 | // codecept-only options 18 | .option('--steps', 'show step-by-step execution') 19 | .option('--debug', 'output additional information') 20 | .option('--verbose', 'output internal logging information') 21 | .option('-o, --override [value]', 'override current config options') 22 | .option('-c, --config [file]', 'configuration file to be used') 23 | .option('--features', 'run only *.feature files and skip tests') 24 | .option('--tests', 'run only JS test files and skip features') 25 | .option('-p, --plugins ', 'enable plugins, comma-separated'); 26 | 27 | program.parse(process.argv); 28 | 29 | const options = program.opts(); 30 | 31 | if (options.config) { 32 | const configFile = options.config; 33 | let configPath = configFile; 34 | if (!path.isAbsolute(configPath)) { 35 | configPath = path.join(process.cwd(), configFile); 36 | } 37 | if (!fs.lstatSync(configPath).isDirectory()) { 38 | codeceptjsFactory.setConfigFile(path.basename(configPath)); 39 | configPath = path.dirname(configPath); 40 | } 41 | process.chdir(configPath); 42 | codeceptjsFactory.setRootDir(configPath); 43 | } 44 | 45 | options.port = setPort('application', options.port); 46 | options.wsPort = setPort('ws', options.wsPort); 47 | 48 | return options; 49 | }; 50 | 51 | -------------------------------------------------------------------------------- /lib/config/env.js: -------------------------------------------------------------------------------- 1 | const portTypeValidator = require('../utils/port-type-validator'); 2 | 3 | const DEFAULTS = { 4 | application: process.env.PORT || 3333, 5 | ws: process.env.WS_PORT || 2999, 6 | }; 7 | 8 | module.exports = { 9 | getPort(type) { 10 | portTypeValidator(type); 11 | return process.env[`${type}Port`] || DEFAULTS[type]; 12 | }, 13 | setPort(type, port) { 14 | portTypeValidator(type); 15 | return process.env[`${type}Port`] = port && Number(port) || DEFAULTS[type]; 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /lib/config/url.js: -------------------------------------------------------------------------------- 1 | const { getPort } = require('./env'); 2 | const portTypeValidator = require('../utils/port-type-validator'); 3 | 4 | module.exports = { 5 | getUrl(type) { 6 | portTypeValidator(type); 7 | return `http://localhost:${getPort(type)}`; 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /lib/model/codeceptjs-factory.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('codeceptjs:codeceptjs-factory'); 2 | const path = require('path'); 3 | const { codecept: Codecept, container, config } = require('codeceptjs'); 4 | 5 | const defaultOpts = { 6 | // verbose: true, 7 | }; 8 | 9 | let TestProject = process.cwd(); 10 | // current instance 11 | let instance; 12 | 13 | module.exports = new class CodeceptjsFactory { 14 | constructor(configFile = 'codecept.conf.js') { 15 | this.configFile = configFile; 16 | } 17 | 18 | loadCodeceptJSHelpers() { 19 | debug('Loading helpers...'); 20 | const RealtimeReporterHelper = require('../codeceptjs/realtime-reporter.helper'); 21 | const NetworkRecorderHelper = require('../codeceptjs/network-recorder.helper'); 22 | const ConsoleRecorderHelper = require('../codeceptjs/console-recorder.helper'); 23 | const SingleSessionHelper = require('../codeceptjs/single-session.helper'); 24 | const pause = require('./../codeceptjs/brk'); 25 | 26 | const reporterHelper = new RealtimeReporterHelper(); 27 | const networkHelper = new NetworkRecorderHelper(); 28 | const consoleHelper = new ConsoleRecorderHelper(); 29 | const singleSessionHelper = new SingleSessionHelper(); 30 | 31 | reporterHelper._init(); 32 | networkHelper._init(); 33 | consoleHelper._init(); 34 | singleSessionHelper._init(); 35 | 36 | global.pause = pause; 37 | 38 | return { 39 | helpers: { 40 | RealtimeReporterHelper: reporterHelper, 41 | // NetworkRecorderHelper: networkHelper, 42 | ConsoleRecorderHelper: consoleHelper, 43 | SingleSessionHelper: singleSessionHelper, 44 | } 45 | }; 46 | } 47 | 48 | getInstance() { 49 | if (!instance) throw new Error('CodeceptJS is not initialized, initialize it with create()'); 50 | return instance; 51 | } 52 | 53 | getConfigFile() { 54 | return this.configFile; 55 | } 56 | 57 | setConfigFile(configFile) { 58 | this.configFile = configFile; 59 | } 60 | 61 | getRootDir() { 62 | return TestProject; 63 | } 64 | 65 | setRootDir(rootDir) { 66 | TestProject = rootDir; 67 | } 68 | 69 | async create(cfg = {}, opts = {}) { 70 | debug('Creating codeceptjs instance...', cfg); 71 | 72 | config.reset(); 73 | config.load(this.getConfigFile()); 74 | config.append(cfg); 75 | cfg = config.get(); 76 | 77 | debug('Using CodeceptJS config', cfg); 78 | 79 | container.clear(); 80 | // create runner 81 | const codecept = new Codecept(cfg, opts = Object.assign(opts, defaultOpts)); 82 | 83 | // initialize codeceptjs in current TestProject 84 | codecept.initGlobals(TestProject); 85 | 86 | // create helpers, support files, mocha 87 | container.create(cfg, opts); 88 | 89 | this.codeceptjsHelpersConfig = this.loadCodeceptJSHelpers(container); 90 | container.append(this.codeceptjsHelpersConfig); 91 | 92 | debug('Running hooks...'); 93 | codecept.runHooks(); 94 | 95 | await codecept.bootstrap(); 96 | 97 | // load tests 98 | debug('Loading tests...'); 99 | codecept.loadTests(); 100 | 101 | 102 | instance = { 103 | config, 104 | codecept, 105 | container, 106 | }; 107 | return instance; 108 | } 109 | 110 | unrequireFile(filePath) { 111 | filePath = path.join(this.getRootDir(), filePath); 112 | let modulePath; 113 | try { 114 | modulePath = require.resolve(filePath); 115 | } catch (err) { 116 | return; 117 | } 118 | if (require.cache[modulePath]) { 119 | delete require.cache[modulePath]; 120 | } 121 | } 122 | 123 | resetSuites() { 124 | const { container } = this.getInstance(); 125 | const mocha = container.mocha(); 126 | 127 | mocha.unloadFiles(); 128 | mocha.suite.cleanReferences(); 129 | mocha.suite.suites = []; 130 | } 131 | 132 | reloadSuites() { 133 | const { container, codecept } = this.getInstance(); 134 | 135 | const mocha = container.mocha(); 136 | 137 | this.resetSuites(); 138 | 139 | // Reload 140 | mocha.files = codecept.testFiles; 141 | mocha.loadFiles(); 142 | 143 | // mocha.suites.forEach(s => s._onlyTests = []); 144 | return mocha.suite.suites; 145 | } 146 | 147 | cleanupSupportObject(supportName) { 148 | const { container, config } = this.getInstance(); 149 | const includesConfig = config.get('include'); 150 | if (!includesConfig[supportName]) return; 151 | const support = container.support(); 152 | delete support[supportName]; 153 | } 154 | 155 | reloadConfig(configHooks = null) { 156 | const { config, container } = this.getInstance(); 157 | config.reset(); 158 | if (configHooks) configHooks(); 159 | config.load(this.getConfigFile()); 160 | 161 | const helpersConfig = config.get('helpers'); 162 | 163 | for (const helperName in container.helpers()) { 164 | try { 165 | container.helpers(helperName)._setConfig(helpersConfig[helperName]); 166 | } catch (e) { 167 | debug(`Cannot run _setConfig due to: ${e.message}`); 168 | } 169 | } 170 | 171 | Object.keys(config.get('include')).forEach(s => this.cleanupSupportObject(s)); 172 | debug('Updated config file. Refreshing...', ); 173 | } 174 | 175 | reloadConfigIfNecessary(filePath) { 176 | if (filePath === this.getConfigFile()) { 177 | this.reloadConfig(); 178 | } 179 | } 180 | 181 | reloadSupportObjectIfNecessary(filePath) { 182 | const { config } = this.getInstance(); 183 | // if it is a support object => reinclude it 184 | Object.entries(config.get('include')) 185 | .filter(e => e[1] === path.join(this.getRootDir(), filePath)) 186 | .forEach(e => this.cleanupSupportObject(e[0])); 187 | } 188 | }; 189 | -------------------------------------------------------------------------------- /lib/model/codeceptjs-run-workers/index.js: -------------------------------------------------------------------------------- 1 | const { satisfyNodeVersion, getTestRoot } = require('codeceptjs/lib/command/utils'); 2 | // For Node version >=10.5.0, have to use experimental flag 3 | 4 | const path = require('path'); 5 | const Codecept = require('codeceptjs/lib/codecept'); 6 | // const Container = require('codeceptjs/lib/container'); 7 | // const { tryOrDefault } = require('codeceptjs/lib/utils'); 8 | const output = require('codeceptjs/lib/output'); 9 | const event = require('codeceptjs/lib/event'); 10 | const runHook = require('codeceptjs/lib/hooks'); 11 | const { Suite, Test, reporters: { Base } } = require('mocha'); 12 | 13 | const stats = { 14 | suites: 0, 15 | passes: 0, 16 | failures: 0, 17 | tests: 0, 18 | }; 19 | 20 | 21 | let numberOfWorkersClosed = 0; 22 | const hasFailures = () => stats.failures || errors.length; 23 | const pathToWorker = path.join(process.cwd(), 'node_modules', 'codeceptjs', 'lib', 'command', 'workers', 'runTests.js'); 24 | const finishedTests = []; 25 | const errors = []; 26 | 27 | module.exports = function (workers, config, options, codecept, container) { 28 | satisfyNodeVersion( 29 | '>=11.7.0', 30 | 'Required minimum Node version of 11.7.0 to work with "run-workers"', 31 | ); 32 | const { Worker } = require('worker_threads'); 33 | 34 | const numberOfWorkers = parseInt(workers, 10); 35 | 36 | const testRoot = getTestRoot(); 37 | config.tests = path.resolve(testRoot, config.tests); 38 | 39 | const groups = createGroupsOfTests(codecept, container, numberOfWorkers); 40 | 41 | stats.start = new Date(); 42 | 43 | output.print(`CodeceptJS v${Codecept.version()}`); 44 | output.print(`Running tests in ${output.styles.bold(workers)} workers...`); 45 | output.print(); 46 | 47 | // run bootstrap all 48 | runHook(config.bootstrapAll, () => { 49 | const workerList = createWorkers(groups, config, options, testRoot); 50 | workerList.forEach(worker => assignWorkerMethods(worker, groups.length)); 51 | }, 'bootstrapAll'); 52 | 53 | function createWorkers(groups, config, options, testRoot) { 54 | delete config.mocha; 55 | const workers = groups.map((tests, workerIndex) => { 56 | workerIndex++; 57 | return new Worker(pathToWorker, { 58 | workerData: { 59 | config: simplifyObject(config, { objects: false, underscores: false }), 60 | options: simplifyObject(options), 61 | tests, 62 | testRoot, 63 | workerIndex, 64 | }, 65 | }); 66 | }); 67 | 68 | return workers; 69 | } 70 | 71 | function assignWorkerMethods(worker, totalWorkers) { 72 | worker.on('message', (message) => { 73 | output.process(message.workerIndex); 74 | 75 | switch (message.event) { 76 | case event.test.failed: 77 | finishedTests.push(repackTest(message.data)); 78 | output.test.failed(repackTest(message.data)); 79 | break; 80 | case event.test.passed: output.test.passed(repackTest(message.data)); break; 81 | case event.suite.before: output.suite.started(message.data); break; 82 | case event.all.after: appendStats(message.data); break; 83 | } 84 | output.process(null); 85 | }); 86 | 87 | worker.on('error', (err) => { 88 | errors.push(err); 89 | // eslint-disable-next-line no-console 90 | console.error(err); 91 | }); 92 | 93 | worker.on('exit', () => { 94 | numberOfWorkersClosed++; 95 | if (numberOfWorkersClosed >= totalWorkers) { 96 | printResults(); 97 | runHook(config.teardownAll, () => { 98 | if (hasFailures()) { 99 | // eslint-disable-next-line no-console 100 | console.log('ERROR Test run has failures'); 101 | return; 102 | } 103 | // eslint-disable-next-line no-console 104 | console.log('FInished Successfully'); 105 | // process.exit(0); 106 | }); 107 | } 108 | }); 109 | } 110 | }; 111 | 112 | function printResults() { 113 | stats.end = new Date(); 114 | stats.duration = stats.end - stats.start; 115 | output.print(); 116 | if (stats.passes && !errors.length) { 117 | output.result(stats.passes, stats.failures, 0, stats.duration); 118 | } 119 | if (stats.failures) { 120 | output.print(); 121 | output.print('-- FAILURES:'); 122 | Base.list(finishedTests.filter(t => t.err)); 123 | } 124 | } 125 | 126 | function createGroupsOfTests(codecept, container, numberOfGroups) { 127 | const files = codecept.testFiles; 128 | const mocha = container.mocha(); 129 | mocha.files = files; 130 | 131 | mocha.loadFiles(); 132 | 133 | const groups = []; 134 | let groupCounter = 0; 135 | 136 | mocha.suite.eachTest((test) => { 137 | const i = groupCounter % numberOfGroups; 138 | if (groups[i] === undefined) groups[i] = []; 139 | if (test) { 140 | const { id } = test; 141 | groups[i].push(id); 142 | groupCounter++; 143 | } 144 | }); 145 | 146 | return groups; 147 | } 148 | 149 | function appendStats(newStats) { 150 | stats.passes += newStats.passes; 151 | stats.failures += newStats.failures; 152 | stats.tests += newStats.tests; 153 | } 154 | 155 | function repackTest(test) { 156 | test = Object.assign(new Test(test.title || '', () => {}), test); 157 | test.parent = Object.assign(new Suite(test.parent.title), test.parent); 158 | return test; 159 | } 160 | 161 | // function repackSuite(suite) { 162 | // return Object.assign(new Suite(suite.title), suite); 163 | // } 164 | 165 | function simplifyObject(object, remove = {}) { 166 | const defaultRemove = { 167 | objects: true, 168 | functions: true, 169 | underscores: true, 170 | }; 171 | 172 | remove = Object.assign(defaultRemove, remove); 173 | 174 | let tempObj = Object.keys(object); 175 | if (remove.underscores) tempObj = tempObj.filter(k => k.indexOf('_') !== 0); 176 | if (remove.functions) tempObj = tempObj.filter(k => typeof object[k] !== 'function'); 177 | if (remove.objects) tempObj = tempObj.filter(k => typeof object[k] !== 'object'); 178 | 179 | return tempObj.reduce((obj, key) => { 180 | obj[key] = object[key]; 181 | return obj; 182 | }, {}); 183 | } 184 | -------------------------------------------------------------------------------- /lib/model/open-in-editor.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('codeceptjs:open-in-editor'); 2 | const { execSync } = require('child_process'); 3 | const { getSettings } = require('./settings-repository'); 4 | 5 | const openVSCode = fileName => { 6 | const { editor } = getSettings(); 7 | return execSync(`${editor} "${fileName}"`, (err) => { 8 | if (!err) return 'Ok'; 9 | console.error('Failed to open editor: ', err.toString()); // eslint-disable-line 10 | console.error(`Please, update your settings. Current editor command: ${editor}`); // eslint-disable-line 11 | debug(`Failed to open Editor - ${err}`); 12 | return `${err}`; 13 | }); 14 | }; 15 | 16 | module.exports = fileName => { 17 | // TODO Make this configurable 18 | openVSCode(fileName); 19 | }; 20 | -------------------------------------------------------------------------------- /lib/model/profile-repository.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('codeceptjs:profile-repository'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | 5 | const CodeceptJSDir = path.join(process.cwd(), '.codeceptjs'); 6 | const ProfileConfigFile = path.join(CodeceptJSDir, 'profile.conf.js'); 7 | 8 | const getProfiles = () => { 9 | if (!fs.existsSync(ProfileConfigFile)) return; 10 | 11 | const profileConfig = require(ProfileConfigFile); 12 | 13 | // TODO Do a schema check 14 | debug('Read profile config', profileConfig); 15 | 16 | return profileConfig; 17 | }; 18 | 19 | const getProfile = profileName => { 20 | const profiles = getProfiles(); 21 | return profiles[profileName] || profiles[profiles.default]; 22 | }; 23 | 24 | module.exports = { 25 | getProfiles, 26 | getProfile 27 | }; -------------------------------------------------------------------------------- /lib/model/scenario-status-repository.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const debug = require('debug')('codeceptjs:scenario-status-repository'); 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const mkdir = require('../utils/mkdir'); 6 | 7 | mkdir(path.join(global.output_dir, '.ui')); 8 | const ScenarioStatusFile = path.join(global.output_dir, '.ui', 'scenario-status.json'); 9 | 10 | const ScenarioStatus = ['passed', 'failed']; 11 | 12 | let scenarioStatus = {}; 13 | 14 | const saveStatus = () => { 15 | fs.writeFileSync(ScenarioStatusFile, JSON.stringify(scenarioStatus), 'utf8'); 16 | }; 17 | 18 | const restoreStatus = () => { 19 | if (!fs.existsSync(ScenarioStatusFile)) return; 20 | 21 | const scenarioStatusAsString = fs.readFileSync(ScenarioStatusFile); 22 | scenarioStatus = JSON.parse(scenarioStatusAsString); 23 | return scenarioStatus; 24 | }; 25 | 26 | restoreStatus(); 27 | 28 | module.exports = { 29 | setStatus(scenarioId, status) { 30 | assert(scenarioId, 'scenarioId is required'); 31 | 32 | if (!ScenarioStatus.includes(status.status)) { 33 | throw new Error(`status must be one of ${ScenarioStatus}`); 34 | } 35 | debug(`Setting scenario status of ${scenarioId} to ${status}`); 36 | scenarioStatus[scenarioId] = status; 37 | return scenarioStatus; 38 | }, 39 | getStatus() { 40 | return scenarioStatus; 41 | }, 42 | saveStatus, 43 | restoreStatus, 44 | }; 45 | 46 | process.on('SIGINT', () => { 47 | // eslint-disable-next-line no-console 48 | console.log('Saving status...'); 49 | saveStatus(); 50 | process.exit(); 51 | }); 52 | -------------------------------------------------------------------------------- /lib/model/settings-repository.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('codeceptjs:settings-repository'); 2 | 3 | const settings = {}; 4 | 5 | const getSettings = () => { 6 | debug('get settings', settings); 7 | return settings; 8 | }; 9 | 10 | const storeSettings = newSettings => { 11 | debug('set settings', settings); 12 | Object.assign(settings, newSettings); 13 | }; 14 | 15 | module.exports = { 16 | getSettings, 17 | storeSettings 18 | }; 19 | -------------------------------------------------------------------------------- /lib/model/snapshot-store/fix-html-snapshot.js: -------------------------------------------------------------------------------- 1 | const { URL } = require('url'); 2 | const cheerio = require('cheerio'); 3 | 4 | const extractBaseUrl = str => { 5 | const parsed = new URL(str); 6 | return `${parsed.protocol}//${parsed.host}`; 7 | }; 8 | 9 | const extractUrlWithPath = str => { 10 | const parsed = new URL(str); 11 | const pathname = parsed.pathname && !parsed.pathname.startsWith('/') ? `/${parsed.pathname}` : parsed.pathname; 12 | return `${parsed.protocol}//${parsed.hostname}${parsed.port ? ':' : ''}${parsed.port ? parsed.port : ''}${pathname}`; 13 | }; 14 | 15 | const isDataUrl = linkValue => { 16 | return linkValue.startsWith('data:'); 17 | }; 18 | 19 | const isSameResource = linkValue => { 20 | return (linkValue[0] !== '/' && !linkValue.startsWith('http')); 21 | }; 22 | 23 | const isSameDomain = linkValue => { 24 | return linkValue[0] === '/' && linkValue[1] !== '/'; 25 | }; 26 | 27 | const isSameProtocol = linkValue => { 28 | return linkValue[0] === '/' && linkValue[1] === '/'; 29 | }; 30 | 31 | const mapAttr = ($, snapshot, attrName) => { 32 | const parsed = new URL(snapshot.pageUrl); 33 | const protocolHostPort = extractBaseUrl(snapshot.pageUrl); 34 | const protocolHostPortPath = extractUrlWithPath(snapshot.pageUrl); 35 | 36 | return function () { 37 | const linkValue = $(this).attr(attrName); 38 | if (!linkValue) return; 39 | 40 | if (isDataUrl(linkValue)) { 41 | return; 42 | } 43 | if (isSameResource(linkValue)) { 44 | $(this).attr(attrName, `${protocolHostPortPath}/${linkValue}`); 45 | } 46 | if (isSameDomain(linkValue)) { 47 | $(this).attr(attrName, `${protocolHostPort}${linkValue}`); 48 | } 49 | if (isSameProtocol(linkValue)) { 50 | $(this).attr(attrName, `${parsed.protocol}${linkValue}`); 51 | } 52 | }; 53 | }; 54 | 55 | module.exports = snapshot => { 56 | // disable script tags 57 | snapshot.source = snapshot.source || ''; 58 | let $ = cheerio.load(snapshot.source.replace(/)<[^<]*)*<\/script>/gi, '')); 59 | 60 | // Convert links 61 | $('link').map(mapAttr($, snapshot, 'href')); 62 | $('img').map(mapAttr($, snapshot, 'src')); 63 | $('script').map(mapAttr($, snapshot, 'src')); 64 | 65 | snapshot.source = $.html(); 66 | 67 | return snapshot; 68 | }; 69 | -------------------------------------------------------------------------------- /lib/model/snapshot-store/fix-htmls-snapshot.spec.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const fixHtmlSnapshot = require('./fix-html-snapshot'); 3 | 4 | const makeHtml = snippet => { 5 | return `${snippet}`; 6 | }; 7 | 8 | test('Relative Url: same resource', (t) => { 9 | const pageUrl = 'http://foo/some/path'; 10 | const fixedSnapshot = fixHtmlSnapshot({ 11 | pageUrl, 12 | source: makeHtml('') 13 | }); 14 | t.is(fixedSnapshot.source, makeHtml('')); 15 | }); 16 | 17 | test('Relative Url: same protocol', (t) => { 18 | const pageUrl = 'http://www.check24-test.de'; 19 | const fixedSnapshot = fixHtmlSnapshot({ 20 | pageUrl, 21 | source: makeHtml('') 22 | }); 23 | t.is(fixedSnapshot.source, makeHtml('')); 24 | }); 25 | 26 | test('Relative Url: same domain', (t) => { 27 | const pageUrl = 'http://foo:1234'; 28 | const fixedSnapshot = fixHtmlSnapshot({ 29 | pageUrl, 30 | source: makeHtml('') 31 | }); 32 | t.is(fixedSnapshot.source, makeHtml('')); 33 | }); 34 | 35 | test('script tags are removed', (t) => { 36 | const pageUrl = 'http://foo:1234'; 37 | const fixedSnapshot = fixHtmlSnapshot({ 38 | pageUrl, 39 | source: makeHtml('') 40 | }); 41 | t.is(fixedSnapshot.source, makeHtml('')); 42 | }); -------------------------------------------------------------------------------- /lib/model/snapshot-store/index.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('codeceptjs:snapshot-store'); 2 | const assert = require('assert'); 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | const fixHtmlSnapshot = require('./fix-html-snapshot'); 7 | 8 | const SnapshotBaseDir = path.join(global.output_dir || process.cwd(), '.ui', 'testruns'); 9 | 10 | const fixSnapshot = snapshot => { 11 | if (snapshot && snapshot.sourceContentType === 'html') { 12 | snapshot = fixHtmlSnapshot(snapshot); 13 | } 14 | return snapshot; 15 | }; 16 | 17 | const fileNameFromId = id => `${id}.snapshots.json`; 18 | 19 | module.exports = new class SnapshotStore { 20 | constructor() { 21 | this.steps = {}; 22 | } 23 | 24 | clear() { 25 | this.steps = {}; 26 | } 27 | 28 | add(id, snapshot) { 29 | assert(id, 'step id is required'); 30 | assert(snapshot, 'snapshot is required'); 31 | 32 | debug(`Adding step ${id}`); 33 | 34 | this.steps[id] = fixSnapshot(snapshot); 35 | } 36 | 37 | get(id) { 38 | return this.steps[id]; 39 | } 40 | 41 | exists(id) { 42 | return this.steps[id] !== undefined; 43 | } 44 | 45 | hasSnapshot(id) { 46 | return this.exists(id) && this.get(id); 47 | } 48 | 49 | saveWithTestRun(testRunId) { 50 | const snapshotFile = path.join(SnapshotBaseDir, fileNameFromId(testRunId)); 51 | 52 | debug(`Saving snapshots to ${snapshotFile}`); 53 | 54 | fs.writeFileSync(snapshotFile, JSON.stringify(this.steps), 'utf8'); 55 | } 56 | 57 | restoreFromTestRun(testRunId) { 58 | const snapshotFile = path.join(SnapshotBaseDir, fileNameFromId(testRunId)); 59 | if (!fs.existsSync(snapshotFile)) return; 60 | 61 | debug(`Retrieving snapshot for testrun ${testRunId}`); 62 | const snapshotData = fs.readFileSync(snapshotFile); 63 | this.steps = JSON.parse(snapshotData); 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /lib/model/testrun-repository.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('codeceptjs:testRunRepository'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const mkdir = require('../utils/mkdir'); 5 | 6 | mkdir(path.join(global.output_dir, '.ui')); 7 | const TestRunBaseDir = path.join(global.output_dir, '.ui', 'testruns'); 8 | mkdir(TestRunBaseDir); 9 | 10 | const fileNameFromId = id => `${encodeURIComponent(id)}.json`; 11 | 12 | module.exports = { 13 | saveTestRun(id, testRun) { 14 | debug(`Saving testrun ${id}`); 15 | fs.writeFileSync(path.join(TestRunBaseDir, fileNameFromId(id)), JSON.stringify(testRun), 'utf8'); 16 | }, 17 | 18 | getTestRun(id) { 19 | const testRunFile = path.join(TestRunBaseDir, fileNameFromId(id)); 20 | if (!fs.existsSync(testRunFile)) return; 21 | 22 | debug(`Retrieving testrun ${id}`); 23 | const testRunAsString = fs.readFileSync(testRunFile); 24 | return JSON.parse(testRunAsString); 25 | } 26 | }; -------------------------------------------------------------------------------- /lib/model/throttling.js: -------------------------------------------------------------------------------- 1 | const throttled = (delay, fn) => { 2 | let lastCall = 0; 3 | return function (...args) { 4 | const now = (new Date).getTime(); 5 | if (now - lastCall < delay) { 6 | return; 7 | } 8 | lastCall = now; 9 | return fn(...args); 10 | }; 11 | }; 12 | 13 | module.exports = throttled; 14 | -------------------------------------------------------------------------------- /lib/model/ws-events.js: -------------------------------------------------------------------------------- 1 | const { getUrl } = require('../config/url'); 2 | const WS_URL = getUrl('ws'); 3 | 4 | const socket = require('socket.io-client')(WS_URL); 5 | 6 | module.exports = { 7 | events: [ 8 | 'console.error', 9 | 'console.log', 10 | 'network.failed_request', 11 | 'codeceptjs:scenarios.updated', 12 | 'codeceptjs:scenarios.parseerror', 13 | 'codeceptjs.started', 14 | 'codeceptjs.exit', 15 | 'metastep.changed', 16 | 'cli.start', 17 | 'cli.stop', 18 | 'cli.error', 19 | 'cli.output', 20 | 'cli.line', 21 | 'cli.close', 22 | 'suite.before', 23 | 'test.before', 24 | 'test.after', 25 | 'test.failed', 26 | 'test.passed', 27 | 'step.before', 28 | 'step.after', 29 | 'step.comment', 30 | 'step.passed', 31 | 'finish', 32 | ], 33 | console: { 34 | jsError(err) { 35 | socket.emit('console.error', { 36 | type: 'js', 37 | error: err, 38 | }); 39 | }, 40 | error(err) { 41 | socket.emit('console.error', { 42 | type: 'error', 43 | error: err, 44 | }); 45 | }, 46 | log(type, url, lineno, args) { 47 | socket.emit('console.log', { 48 | type, 49 | url, 50 | lineno, 51 | args 52 | }); 53 | } 54 | }, 55 | network: { 56 | failedRequest(data) { 57 | socket.emit('network.failed_request', data); 58 | } 59 | }, 60 | rtr: { 61 | suiteBefore(data) { 62 | socket.emit('suite.before', data); 63 | }, 64 | testBefore(data) { 65 | socket.emit('test.before', data); 66 | }, 67 | testAfter(data) { 68 | socket.emit('test.after', data); 69 | }, 70 | stepBefore(data) { 71 | socket.emit('step.before', data); 72 | }, 73 | stepAfter(data) { 74 | socket.emit('step.after', data); 75 | }, 76 | stepComment(comment) { 77 | socket.emit('step.comment', comment); 78 | }, 79 | stepPassed(data) { 80 | socket.emit('step.passed', data); 81 | }, 82 | metaStepChanged(data) { 83 | socket.emit('metastep.changed', data); 84 | }, 85 | testPassed(data) { 86 | socket.emit('test.passed', data); 87 | }, 88 | testFailed(data) { 89 | socket.emit('test.failed', data); 90 | }, 91 | testRunFinished() { 92 | socket.emit('testrun.finish'); 93 | } 94 | }, 95 | codeceptjs: { 96 | scenariosUpdated() { 97 | // just notify the frontend that scenarios have been changed 98 | // it's the frontends responsibilty to actually get 99 | // the updated list of scenarios 100 | socket.emit('codeceptjs:scenarios.updated'); 101 | }, 102 | scenariosParseError(err) { 103 | socket.emit('codeceptjs:scenarios.parseerror', { 104 | message: err.message, 105 | stack: err.stack, 106 | }); 107 | }, 108 | started(data) { 109 | socket.emit('codeceptjs.started', data); 110 | }, 111 | exit(data) { 112 | socket.emit('codeceptjs.exit', data); 113 | }, 114 | error(err) { 115 | socket.emit('codeceptjs.error', err); 116 | } 117 | } 118 | }; 119 | -------------------------------------------------------------------------------- /lib/utils/absolutize-paths.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = (obj) => { 4 | Object.keys(obj).forEach(key => { 5 | if (typeof obj[key] === 'string' && obj[key] && !path.isAbsolute(obj[key])) { 6 | obj[key] = path.resolve(global.codecept_dir || '', obj[key]); 7 | } 8 | }); 9 | 10 | return obj; 11 | }; 12 | -------------------------------------------------------------------------------- /lib/utils/mkdir.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const mkdir = function(dir) { 3 | // making directory without exception if exists 4 | try { 5 | if (!fs.existsSync(dir)) { 6 | fs.mkdirSync(dir, { recursive: true }); 7 | } 8 | } catch(e) { 9 | if(e.code !== 'EEXIST') { 10 | throw e; 11 | } 12 | } 13 | }; 14 | 15 | module.exports = mkdir; 16 | -------------------------------------------------------------------------------- /lib/utils/port-type-validator.js: -------------------------------------------------------------------------------- 1 | const TYPES = ['application', 'ws']; 2 | 3 | module.exports = (type) => { 4 | if (!TYPES.includes(type)) { 5 | throw Error('Type must be "application" or "ws"'); 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /lib/utils/port-validator.js: -------------------------------------------------------------------------------- 1 | module.exports = (port) => port && parseInt(port); 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@codeceptjs/ui", 3 | "version": "1.2.4", 4 | "license": "MIT", 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint --fix && vue-cli-service lint lib/** --fix", 9 | "app": "node bin/codecept-ui.js --app -c node_modules/@codeceptjs/examples", 10 | "backend": "node bin/codecept-ui.js -c node_modules/@codeceptjs/examples/codecept.conf.js", 11 | "electron:serve": "vue-cli-service electron:serve", 12 | "electron:generate-icons": "electron-icon-builder --input=./public/icon.png --output=build --flatten", 13 | "frontend": "vue-cli-service serve", 14 | "deploy": "npm run build && npm publish", 15 | "test": "ava", 16 | "test:watch": "ava -w" 17 | }, 18 | "main": "background.js", 19 | "files": [ 20 | "dist", 21 | "bin", 22 | "lib", 23 | "build" 24 | ], 25 | "dependencies": { 26 | "@codeceptjs/configure": "^1.0.3", 27 | "axios": "^1.8.4", 28 | "body-parser": "^2.2.0", 29 | "cheerio": "^1.0.0-rc.12", 30 | "chokidar": "^4.0.3", 31 | "commander": "^13.1.0", 32 | "core-js": "2.6.12", 33 | "dayjs": "^1.11.13", 34 | "debug": "^4.3.4", 35 | "electron": "^35.1.4", 36 | "express": "^4.21.2", 37 | "lodash.clonedeep": "^4.5.0", 38 | "nanoid": "^5.1.5", 39 | "socket.io": "^4.8.1", 40 | "socket.io-client": "^4.8.1" 41 | }, 42 | "devDependencies": { 43 | "@codeceptjs/examples": "1.2.4", 44 | "@medv/finder": "^3.2.0", 45 | "@vue/cli-plugin-babel": "^3.12.1", 46 | "@vue/cli-plugin-eslint": "^3.12.1", 47 | "@vue/cli-service": "^3.12.1", 48 | "@vue/eslint-config-prettier": "^6.0.0", 49 | "ansi-to-html": "^0.7.2", 50 | "ava": "^6.2.0", 51 | "babel-eslint": "^10.1.0", 52 | "buefy": "^0.9.13", 53 | "codeceptjs": "^3.7.3", 54 | "contributor-faces": "^1.1.0", 55 | "copy-text-to-clipboard": "^3.2.0", 56 | "electron-icon-builder": "^2.0.1", 57 | "eslint": "^7.32.0", 58 | "eslint-plugin-vue": "^7.20.0", 59 | "husky": "^8.0.3", 60 | "lint-staged": "^15.5.0", 61 | "playwright": "^1.51.1", 62 | "prismjs": "^1.30.0", 63 | "puppeteer": "^24.4.0", 64 | "qrcode-terminal": "^0.12.0", 65 | "sass": "^1.86.3", 66 | "sass-loader": "^10.2.0", 67 | "tailwindcss": "^1.9.6", 68 | "testcafe": "^3.7.2", 69 | "uuid": "^11.1.0", 70 | "vue": "^2.6.14", 71 | "vue-cli-plugin-electron-builder": "~2.1.1", 72 | "vue-highlightjs": "^1.3.3", 73 | "vue-monaco": "^1.2.2", 74 | "vue-prism-editor": "^1.3.0", 75 | "vue-router": "^3.5.3", 76 | "vue-socket.io": "^3.0.10", 77 | "vue-template-compiler": "2.6.14", 78 | "vuex": "^3.6.2", 79 | "webdriverio": "^9.12.0" 80 | }, 81 | "resolutions": { 82 | "sumchecker": "3.0.1" 83 | }, 84 | "peerDependencies": { 85 | "codeceptjs": "^3.7.3" 86 | }, 87 | "eslintConfig": { 88 | "root": true, 89 | "env": { 90 | "node": true 91 | }, 92 | "extends": [ 93 | "plugin:vue/essential", 94 | "plugin:vue/strongly-recommended", 95 | "eslint:recommended" 96 | ], 97 | "rules": { 98 | "indent": [ 99 | "error", 100 | 2 101 | ], 102 | "object-curly-spacing": [ 103 | "error", 104 | "always" 105 | ], 106 | "vue/script-indent": [ 107 | "error", 108 | 2, 109 | { 110 | "baseIndent": 0, 111 | "switchCase": 0 112 | } 113 | ], 114 | "quotes": [ 115 | "error", 116 | "single" 117 | ], 118 | "semi": "error", 119 | "sort-imports": [ 120 | "error", 121 | { 122 | "ignoreCase": false, 123 | "ignoreDeclarationSort": true, 124 | "ignoreMemberSort": false, 125 | "memberSyntaxSortOrder": [ 126 | "none", 127 | "all", 128 | "multiple", 129 | "single" 130 | ] 131 | } 132 | ] 133 | }, 134 | "overrides": [ 135 | { 136 | "files": [ 137 | "*.vue" 138 | ], 139 | "rules": { 140 | "indent": "off", 141 | "vue/script-indent": [ 142 | "error", 143 | 2 144 | ] 145 | } 146 | } 147 | ], 148 | "parserOptions": { 149 | "parser": "babel-eslint" 150 | } 151 | }, 152 | "browserslist": [ 153 | "> 1%", 154 | "last 2 versions" 155 | ], 156 | "bin": { 157 | "codecept-ui": "./bin/codecept-ui.js" 158 | }, 159 | "husky": { 160 | "hooks": { 161 | "pre-commit": "lint-staged" 162 | } 163 | }, 164 | "lint-staged": { 165 | "*.{js,vue}": [ 166 | "npm run lint", 167 | "git add" 168 | ] 169 | }, 170 | "repository": "codeceptjs/ui" 171 | } 172 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'plugins': [ 3 | require('tailwindcss')('tailwind.js'), 4 | require('autoprefixer')(), 5 | ] 6 | }; -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeceptjs/ui/7e875809819f2c4134d340122ec99e4f1acae971/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeceptjs/ui/7e875809819f2c4134d340122ec99e4f1acae971/public/favicon.png -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeceptjs/ui/7e875809819f2c4134d340122ec99e4f1acae971/public/icon.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | CodeceptUI - Interactive Runner for CodeceptJS 15 | 16 | 17 | 20 |
21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 14 | 123 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeceptjs/ui/7e875809819f2c4134d340122ec99e4f1acae971/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /src/background.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import path from 'path'; 4 | import { BrowserWindow, app, protocol, screen } from 'electron'; 5 | import { 6 | createProtocol, 7 | /* installVueDevtools */ 8 | } from 'vue-cli-plugin-electron-builder/lib'; 9 | const isDevelopment = process.env.NODE_ENV !== 'production'; 10 | 11 | // Keep a global reference of the window object, if you don't, the window will 12 | // be closed automatically when the JavaScript object is garbage collected. 13 | let win; 14 | 15 | // Scheme must be registered before the app is ready 16 | protocol.registerSchemesAsPrivileged([{ scheme: 'app', privileges: { secure: true, standard: true } }]); 17 | 18 | 19 | function createWindow () { 20 | const { width: screenWidth, height: screenHeight } = screen.getPrimaryDisplay().size; 21 | 22 | const width = Math.floor(screenWidth / 3.5) || 500; 23 | // Create the browser window. 24 | win = new BrowserWindow({ 25 | width, 26 | height: screenHeight-50, 27 | minWidth: 500, 28 | x: screenWidth, 29 | y: 0, 30 | autoHideMenuBar: true, 31 | icon: path.join(__dirname, 'public/icon.png'), 32 | webPreferences: { 33 | nodeIntegration: true, 34 | nodeIntegrationInWorker: true, 35 | }, 36 | }); 37 | 38 | if (process.env.WEBPACK_DEV_SERVER_URL) { 39 | // Load the url of the dev server if in development mode 40 | win.loadURL(process.env.WEBPACK_DEV_SERVER_URL); 41 | // To open Dev Tools by default uncomment next line 42 | // if (!process.env.IS_TEST) win.webContents.openDevTools(); 43 | } else { 44 | createProtocol('app'); 45 | // Load the index.html when not in development 46 | win.loadURL('app://./index.html'); 47 | } 48 | 49 | win.on('closed', () => { 50 | win = null; 51 | }); 52 | } 53 | 54 | // Quit when all windows are closed. 55 | app.on('window-all-closed', () => { 56 | // On macOS it is common for applications and their menu bar 57 | // to stay active until the user quits explicitly with Cmd + Q 58 | if (process.platform !== 'darwin') { 59 | app.quit(); 60 | } 61 | }); 62 | 63 | app.on('activate', () => { 64 | // On macOS it's common to re-create a window in the app when the 65 | // dock icon is clicked and there are no other windows open. 66 | if (win === null) { 67 | createWindow(); 68 | } 69 | }); 70 | 71 | // This method will be called when Electron has finished 72 | // initialization and is ready to create browser windows. 73 | // Some APIs can only be used after this event occurs. 74 | app.on('ready', async () => { 75 | if (isDevelopment && !process.env.IS_TEST) { 76 | // Install Vue Devtools 77 | // Devtools extensions are broken in Electron 6.0.0 and greater 78 | // See https://github.com/nklayman/vue-cli-plugin-electron-builder/issues/378 for more info 79 | // Electron will not launch with Devtools extensions installed on Windows 10 with dark mode 80 | // If you are not using Windows 10 dark mode, you may uncomment these lines 81 | // In addition, if the linked issue is closed, you can upgrade electron and uncomment these lines 82 | // try { 83 | // await installVueDevtools() 84 | // } catch (e) { 85 | // console.error('Vue Devtools failed to install:', e.toString()) 86 | // } 87 | 88 | } 89 | createWindow(); 90 | }); 91 | 92 | // Exit cleanly on request from parent process in development mode. 93 | if (isDevelopment) { 94 | if (process.platform === 'win32') { 95 | process.on('message', data => { 96 | if (data === 'graceful-exit') { 97 | app.quit(); 98 | } 99 | }); 100 | } else { 101 | process.on('SIGTERM', () => { 102 | app.quit(); 103 | }); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/components/CapabilityFolder.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 30 | 38 | -------------------------------------------------------------------------------- /src/components/Console.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 97 | 98 | 158 | -------------------------------------------------------------------------------- /src/components/EditorNotFound.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 78 | 79 | -------------------------------------------------------------------------------- /src/components/Feature.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 97 | 98 | 143 | -------------------------------------------------------------------------------- /src/components/Header.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 74 | 75 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /src/components/Logo.vue: -------------------------------------------------------------------------------- 1 | 62 | 63 | 83 | 84 | 90 | -------------------------------------------------------------------------------- /src/components/ProfileSelection.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 76 | -------------------------------------------------------------------------------- /src/components/RunButton.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 78 | 83 | -------------------------------------------------------------------------------- /src/components/Scenario.vue: -------------------------------------------------------------------------------- 1 | 64 | 65 | 132 | 150 | -------------------------------------------------------------------------------- /src/components/ScenarioSource.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 57 | 58 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /src/components/SettingsMenu.vue: -------------------------------------------------------------------------------- 1 | 163 | 164 | 256 | 261 | -------------------------------------------------------------------------------- /src/components/Snapshot.vue: -------------------------------------------------------------------------------- 1 | 120 | 121 | 187 | 188 | 220 | -------------------------------------------------------------------------------- /src/components/SnapshotREST.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 53 | 58 | -------------------------------------------------------------------------------- /src/components/SnapshotSource.vue: -------------------------------------------------------------------------------- 1 |