diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 1f8dc9769..1004d072f 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -15,7 +15,6 @@ env: jobs: build: - runs-on: ubuntu-latest strategy: @@ -23,32 +22,34 @@ jobs: node-version: [20.x] steps: - - uses: actions/checkout@v4 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - - uses: shivammathur/setup-php@v2 - with: - php-version: 7.4 - - name: npm install - run: | - npm i --force - env: - PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true - - name: Install browsers and deps - run: npx playwright install && npx playwright install-deps - - name: start a server - run: "php -S 127.0.0.1:8000 -t test/data/app &" - - name: run chromium tests - run: "./bin/codecept.js run -c test/acceptance/codecept.Playwright.js --grep @Playwright --debug" - - name: run chromium with restart==browser tests - run: "BROWSER_RESTART=browser ./bin/codecept.js run -c test/acceptance/codecept.Playwright.js --grep @Playwright --debug" - - name: run chromium with restart==session tests - run: "BROWSER_RESTART=session ./bin/codecept.js run -c test/acceptance/codecept.Playwright.js --grep @Playwright --debug" - - name: run firefox tests - run: "BROWSER=firefox node ./bin/codecept.js run -c test/acceptance/codecept.Playwright.js --grep @Playwright --debug" - - name: run webkit tests - run: "BROWSER=webkit node ./bin/codecept.js run -c test/acceptance/codecept.Playwright.js --grep @Playwright --debug" - - name: run chromium unit tests - run: ./node_modules/.bin/mocha test/helper/Playwright_test.js --timeout 5000 + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + - uses: shivammathur/setup-php@v2 + with: + php-version: 7.4 + - name: npm install + run: | + npm i --force + env: + PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true + - name: Install browsers and deps + run: npx playwright install && npx playwright install-deps + - name: check + run: './bin/codecept.js check -c test/acceptance/codecept.Playwright.js' + - name: start a server + run: 'php -S 127.0.0.1:8000 -t test/data/app &' + - name: run chromium tests + run: './bin/codecept.js run -c test/acceptance/codecept.Playwright.js --grep @Playwright --debug' + - name: run chromium with restart==browser tests + run: 'BROWSER_RESTART=browser ./bin/codecept.js run -c test/acceptance/codecept.Playwright.js --grep @Playwright --debug' + - name: run chromium with restart==session tests + run: 'BROWSER_RESTART=session ./bin/codecept.js run -c test/acceptance/codecept.Playwright.js --grep @Playwright --debug' + - name: run firefox tests + run: 'BROWSER=firefox node ./bin/codecept.js run -c test/acceptance/codecept.Playwright.js --grep @Playwright --debug' + - name: run webkit tests + run: 'BROWSER=webkit node ./bin/codecept.js run -c test/acceptance/codecept.Playwright.js --grep @Playwright --debug' + - name: run chromium unit tests + run: ./node_modules/.bin/mocha test/helper/Playwright_test.js --timeout 5000 diff --git a/.github/workflows/webdriver.yml b/.github/workflows/webdriver.yml index 08753cdf5..98cef5bbd 100644 --- a/.github/workflows/webdriver.yml +++ b/.github/workflows/webdriver.yml @@ -38,6 +38,8 @@ jobs: PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true - name: start a server run: 'php -S 127.0.0.1:8000 -t test/data/app &' + - name: Check CodeceptJS can be started + run: './bin/codecept.js check -c test/acceptance/codecept.WebDriver.js' - name: run unit tests run: ./node_modules/.bin/mocha test/helper/WebDriver_test.js --exit - name: run tests diff --git a/bin/codecept.js b/bin/codecept.js index 06b116e65..985da3890 100755 --- a/bin/codecept.js +++ b/bin/codecept.js @@ -58,6 +58,13 @@ program .description('Creates dummy config in current dir or [path]') .action(errorHandler(require('../lib/command/init'))) +program + .command('check') + .option(commandFlags.config.flag, commandFlags.config.description) + .description('Checks configuration and environment before running tests') + .option('-t, --timeout [ms]', 'timeout for checks in ms, 20000 by default') + .action(errorHandler(require('../lib/command/check'))) + program .command('migrate [path]') .description('Migrate json config to js config in current dir or [path]') diff --git a/lib/command/check.js b/lib/command/check.js new file mode 100644 index 000000000..bfa45cc67 --- /dev/null +++ b/lib/command/check.js @@ -0,0 +1,173 @@ +const { getConfig, getTestRoot } = require('./utils') +const Codecept = require('../codecept') +const output = require('../output') +const standardActingHelpers = require('../plugin/standardActingHelpers') +const store = require('../store') +const container = require('../container') +const figures = require('figures') +const chalk = require('chalk') +const { createTest } = require('../mocha/test') +const { getMachineInfo } = require('./info') +const definitions = require('./definitions') + +module.exports = async function (options) { + const configFile = options.config + + setTimeout(() => { + output.error("Something went wrong. Checks didn't pass and timed out. Please check your config and helpers.") + process.exit(1) + }, options.timeout || 50000) + + const checks = { + config: false, + container: false, + pageObjects: false, + helpers: false, + setup: false, + tests: false, + def: false, + } + + const testRoot = getTestRoot(configFile) + let config = getConfig(configFile) + + try { + config = getConfig(configFile) + checks['config'] = true + } catch (err) { + checks['config'] = err + } + + printCheck('config', checks['config'], config.name) + + let codecept + try { + codecept = new Codecept(config, options) + codecept.init(testRoot) + await container.started() + checks.container = true + } catch (err) { + checks.container = err + } + + printCheck('container', checks['container']) + + if (codecept) { + try { + if (options.bootstrap) await codecept.bootstrap() + checks.bootstrap = true + } catch (err) { + checks.bootstrap = err + } + printCheck('bootstrap', checks['bootstrap'], options.bootstrap ? 'Bootstrap was executed' : 'No bootstrap command') + } + + let numTests = 0 + if (codecept) { + try { + codecept.loadTests() + const mocha = container.mocha() + mocha.files = codecept.testFiles + mocha.loadFiles() + mocha.suite.suites.forEach(suite => { + numTests += suite.tests.length + }) + if (numTests > 0) { + checks.tests = true + } else { + throw new Error('No tests found') + } + } catch (err) { + checks.tests = err + } + } + + printCheck('tests', checks['tests'], `Total: ${numTests} tests`) + + store.dryRun = true + + const helpers = container.helpers() + + try { + if (!Object.keys(helpers).length) throw new Error('No helpers found') + // load helpers + for (const helper of Object.values(helpers)) { + if (helper._init) helper._init() + } + checks.helpers = true + } catch (err) { + checks.helpers = err + } + + printCheck('helpers', checks['helpers'], `${Object.keys(helpers).join(', ')}`) + + const pageObjects = container.support() + + try { + if (Object.keys(pageObjects).length) { + for (const pageObject of Object.values(pageObjects)) { + pageObject.name + } + } + checks.pageObjects = true + } catch (err) { + checks.pageObjects = err + } + printCheck('page objects', checks['pageObjects'], `Total: ${Object.keys(pageObjects).length} support objects`) + + if (Object.keys(helpers).length) { + const suite = container.mocha().suite + const test = createTest('test', () => {}) + try { + for (const helper of Object.values(helpers)) { + if (helper._beforeSuite) await helper._beforeSuite(suite) + if (helper._before) await helper._before(test) + if (helper._passed) await helper._passed(test) + if (helper._after) await helper._after(test) + if (helper._finishTest) await helper._finishTest(suite) + if (helper._afterSuite) await helper._afterSuite(suite) + } + checks.setup = true + } catch (err) { + checks.setup = err + } + } + + printCheck('Helpers Before/After', checks['setup'], standardActingHelpers.some(h => Object.keys(helpers).includes(h)) ? 'Initializing and closing browser' : '') + + try { + definitions(configFile, { dryRun: true }) + checks.def = true + } catch (err) { + checks.def = err + } + + printCheck('TypeScript Definitions', checks['def']) + + output.print('') + + if (!Object.values(checks).every(check => check === true)) { + output.error("Something went wrong. Checks didn't pass.") + output.print() + await getMachineInfo() + process.exit(1) + } + + output.print(output.styles.success('All checks passed'.toUpperCase()), 'Ready to run your tests 🚀') + process.exit(0) +} + +function printCheck(name, value, comment = '') { + let status = '' + if (value == true) { + status += chalk.bold.green(figures.tick) + } else { + status += chalk.bold.red(figures.cross) + } + + if (value instanceof Error) { + comment = `${comment} ${chalk.red.italic(value.message)}`.trim() + } + + output.print(status, name.toUpperCase(), chalk.dim(comment)) +} diff --git a/lib/command/definitions.js b/lib/command/definitions.js index 5e32ac1f8..580698ddc 100644 --- a/lib/command/definitions.js +++ b/lib/command/definitions.js @@ -185,6 +185,8 @@ module.exports = function (genPath, options) { definitionsFileContent += `\n${translationAliases.join('\n')}` } + if (options.dryRun) return + fs.writeFileSync(path.join(targetFolderPath, 'steps.d.ts'), definitionsFileContent) output.print('TypeScript Definitions provide autocompletion in Visual Studio Code and other IDEs') output.print('Definitions were generated in steps.d.ts') diff --git a/lib/container.js b/lib/container.js index 6fe565369..d8d25ebfd 100644 --- a/lib/container.js +++ b/lib/container.js @@ -1,5 +1,6 @@ const glob = require('glob') const path = require('path') +const debug = require('debug')('codeceptjs:container') const { MetaStep } = require('./step') const { methodsOfObject, fileExists, isFunction, isAsyncFunction, installedLocally } = require('./utils') const Translation = require('./translation') @@ -38,6 +39,7 @@ class Container { * @param {*} opts */ static create(config, opts) { + debug('creating container') asyncHelperPromise = Promise.resolve() // dynamically create mocha instance @@ -134,6 +136,7 @@ class Container { static append(newContainer) { const deepMerge = require('./utils').deepMerge container = deepMerge(container, newContainer) + debug('appended', JSON.stringify(newContainer).slice(0, 300)) } /** @@ -150,6 +153,7 @@ class Container { container.plugins = newPlugins || {} asyncHelperPromise = Promise.resolve() store.actor = null + debug('container cleared') } /** @@ -216,6 +220,8 @@ function createHelpers(config) { throw new Error(`Helper class from module '${helperName}' is not a class. Use CJS async module syntax.`) } + debug(`helper ${helperName} async initialized`) + helpers[helperName] = new ResolvedHelperClass(config[helperName]) }) @@ -225,6 +231,7 @@ function createHelpers(config) { checkHelperRequirements(HelperClass) helpers[helperName] = new HelperClass(config[helperName]) + debug(`helper ${helperName} initialized`) } catch (err) { throw new Error(`Could not load helper ${helperName} (${err.message})`) } @@ -322,6 +329,7 @@ function createSupportObjects(config) { if (container.support[name]._init) { container.support[name]._init() } + debug(`support object ${name} initialized`) } catch (err) { throw new Error(`Initialization failed for ${name}: ${container.support[name]}\n${err.message}\n${err.stack}`) } @@ -334,6 +342,7 @@ function createSupportObjects(config) { const ms = new MetaStep(name, prop) ms.setContext(currentObject) if (isAsyncFunction(currentValue)) currentValue = asyncWrapper(currentValue) + debug(`metastep is created for ${name}.${prop.toString()}()`) return ms.run.bind(ms, currentValue) }