From 337dc328f0c4c6fb39b0750a05bc984a1b850f89 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Wed, 14 Oct 2026 03:53:15 +0300 Subject: [PATCH 1/2] chore: migrate task runner from runok to Bunosh Bunoshfile.js is now the only task file; runok.cjs and the runok dependency are removed. npm scripts docs, def and update-contributor-faces call bunosh. Fixes Lightpanda docs missing inherited CDPBrowser methods: CI ran runok.cjs, whose inheritedHelperDocs list lacked Lightpanda. Bunoshfile fixes: - restore MockRequest section in docs:external-helpers - drop extra blank line after generated front matter - sort helper/plugin file lists (Bun readdir is unordered) Removes legacy release tasks (release, publishSite, wiki, versioning, get:commit-log, publish:next-beta-version) and their npm scripts: releases go through GitHub Releases + publish workflow, and the website refreshes itself via its own `bunosh docs`. CI workflows that run def/docs now set up bun. bunosh pinned to ^0.6.0. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/doc-generation.yml | 2 + .github/workflows/dtslint.yml | 1 + .github/workflows/publish-beta.yml | 1 + Bunoshfile.js | 161 +-- docs/helpers/Lightpanda.md | 1844 +++++++++++++++++++++++++- package.json | 20 +- runok.cjs | 763 ----------- 7 files changed, 1862 insertions(+), 930 deletions(-) delete mode 100755 runok.cjs diff --git a/.github/workflows/doc-generation.yml b/.github/workflows/doc-generation.yml index 7c2acbd11..0c8b799ed 100644 --- a/.github/workflows/doc-generation.yml +++ b/.github/workflows/doc-generation.yml @@ -26,6 +26,8 @@ jobs: with: node-version: ${{ matrix.node-version }} + - uses: oven-sh/setup-bun@v2 + - name: Install Dependencies run: npm i --force diff --git a/.github/workflows/dtslint.yml b/.github/workflows/dtslint.yml index 753de549d..e91612c29 100644 --- a/.github/workflows/dtslint.yml +++ b/.github/workflows/dtslint.yml @@ -24,6 +24,7 @@ jobs: uses: actions/setup-node@v6 with: node-version: ${{ matrix.node-version }} + - uses: oven-sh/setup-bun@v2 - run: npm i --force env: PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: true diff --git a/.github/workflows/publish-beta.yml b/.github/workflows/publish-beta.yml index 743f6d849..b4cdc69c6 100644 --- a/.github/workflows/publish-beta.yml +++ b/.github/workflows/publish-beta.yml @@ -22,6 +22,7 @@ jobs: node-version: 24 registry-url: 'https://registry.npmjs.org' + - uses: oven-sh/setup-bun@v2 - run: npm install - run: npm install -g npm@latest diff --git a/Bunoshfile.js b/Bunoshfile.js index d29f1df0d..bf1741ddc 100644 --- a/Bunoshfile.js +++ b/Bunoshfile.js @@ -1,8 +1,6 @@ import fs from 'node:fs' import path from 'node:path' -import { execSync } from 'node:child_process' import axios from 'axios' -import semver from 'semver' const { shell, writeToFile, copyFile, task } = global.bunosh const { say, yell } = global.bunosh @@ -47,7 +45,7 @@ export async function docsPlugins() { task.stopOnFailures() ensureDir('docs/plugins') - const files = fs.readdirSync('lib/plugin').filter(f => path.extname(f) === '.js') + const files = fs.readdirSync('lib/plugin').filter(f => path.extname(f) === '.js').sort() const sharedPartials = fs.readdirSync('docs/shared').filter(f => path.extname(f) === '.mustache') const sharedPlaceholders = sharedPartials.map(file => `{{ ${path.basename(file, '.mustache')} }}`) @@ -90,7 +88,6 @@ editLink: false sidebar: auto title: ${name} --- - ` line.fromFile(pluginMarkDownFile(name)) }) @@ -140,6 +137,14 @@ export async function docsExternalHelpers() { cfg.replace(/string \| object/g, 'LocatorOrString') }) + say('Building @codeceptjs/mock-request') + helper = 'MockRequest' + replaceInFile('node_modules/@codeceptjs/mock-request/index.js', cfg => { + cfg.replace(/CodeceptJS.LocatorOrString/g, 'string | object') + cfg.replace(/LocatorOrString/g, 'string | object') + }) + await shell`npx documentation build node_modules/@codeceptjs/mock-request/index.js -o ${helperMarkDownFile(helper)} ${documentjsCliArgs}` + await writeToFile(helperMarkDownFile(helper), line => { line`---\npermalink: /helpers/${helper}\nsidebar: auto\ntitle: ${helper}\n---\n\n# ${helper}\n\n` line.fromFile(helperMarkDownFile(helper)) @@ -170,7 +175,7 @@ export async function docsExternalPlugins() { */ export async function buildLibWithDocs(forTypings = false) { task.stopOnFailures() - const files = fs.readdirSync('lib/helper').filter(f => path.extname(f) === '.js') + const files = fs.readdirSync('lib/helper').filter(f => path.extname(f) === '.js').sort() ensureDir('docs/build') const partials = fs.readdirSync('docs/webapi').filter(f => path.extname(f) === '.mustache') @@ -229,7 +234,7 @@ export async function buildLibWithDocs(forTypings = false) { */ export async function docsHelpers() { task.stopOnFailures() - const files = fs.readdirSync('lib/helper').filter(f => path.extname(f) === '.js') + const files = fs.readdirSync('lib/helper').filter(f => path.extname(f) === '.js').sort() ensureDir('docs/build') const ignoreList = ['Polly', 'MockRequest'] @@ -321,78 +326,12 @@ editLink: false sidebar: auto title: ${name} --- - ` line.fromFile(helperMarkDownFile(name)) }) } } -/** - * Sync wiki pages from the CodeceptJS.wiki repo into website docs. - */ -export async function wiki() { - task.stopOnFailures() - if (!fs.existsSync('docs/wiki/Home.md')) { - await shell`git clone git@github.com:codeceptjs/CodeceptJS.wiki.git docs/wiki` - } - await shell`git pull origin master`.cwd('docs/wiki') - - await writeToFile('docs/community-helpers.md', line => { - line`---` - line`permalink: /community-helpers` - line`title: Community Helpers` - line`editLink: false` - line`---` - line`` - line`# Community Helpers` - line`> Share your helpers at our [Wiki Page](https://github.com/codeceptjs/CodeceptJS/wiki/Community-Helpers)` - line`` - line.fromFile('docs/wiki/Community-Helpers-&-Plugins.md') - }) - - await writeToFile('docs/examples.md', line => { - line`---` - line`permalink: /examples` - line`layout: Section` - line`sidebar: false` - line`title: Examples` - line`editLink: false` - line`---` - line`` - line`# Examples` - line`> Add your own examples to our [Wiki Page](https://github.com/codeceptjs/CodeceptJS/wiki/Examples)` - line.fromFile('docs/wiki/Examples.md') - }) - - await writeToFile('docs/books.md', line => { - line`---` - line`permalink: /books` - line`layout: Section` - line`sidebar: false` - line`title: Books & Posts` - line`editLink: false` - line`---` - line`` - line`# Books & Posts` - line`> Add your own books or posts to our [Wiki Page](https://github.com/codeceptjs/CodeceptJS/wiki/Books-&-Posts)` - line.fromFile('docs/wiki/Books-&-Posts.md') - }) - - await writeToFile('docs/videos.md', line => { - line`---` - line`permalink: /videos` - line`layout: Section` - line`sidebar: false` - line`title: Videos` - line`editLink: false` - line`---` - line`` - line`> Add your own videos to our [Wiki Page](https://github.com/codeceptjs/CodeceptJS/wiki/Videos)` - line.fromFile('docs/wiki/Videos.md') - }) -} - const inheritedHelperDocs = { Appium: { parent: 'WebDriver', @@ -456,28 +395,6 @@ async function docsHelperMarkdown(name, { parent, exclude = [], excludeConfig = fs.writeFileSync(helperMarkDownFile(name), output) } -/** - * Update the codecept.io website: regenerate changelog, sync wiki, push docs and publish. - */ -export async function publishSite() { - await processChangelog() - await wiki() - - const dir = 'website' - if (fs.existsSync(dir)) { - await shell`rm -rf ${dir}` - } - - await shell`git clone git@github.com:codeceptjs/website.git ${dir}` - copyFile('docs', 'website/docs') - - await shell`git add -A`.cwd(dir) - await shell`git commit -m "synchronized with docs"`.cwd(dir) - await shell`git pull`.cwd(dir) - await shell`git push`.cwd(dir) - await shell`./runok.js publish`.cwd(dir) -} - /** * Run the PHP test app and the REST test server together. Warning! PHP required! */ @@ -485,32 +402,6 @@ export async function server() { await Promise.all([shell`php -S 127.0.0.1:8000 -t test/data/app`, testServer()]) } -/** - * Release CodeceptJS to npm. Optionally bump package.json version first. - * @param {string} [releaseType] - "patch", "minor" or "major" to bump before releasing. - */ -export async function release(releaseType = null) { - task.stopOnFailures() - const packageInfo = JSON.parse(fs.readFileSync('package.json')) - if (releaseType) { - packageInfo.version = semver.inc(packageInfo.version, releaseType) - fs.writeFileSync('package.json', JSON.stringify(packageInfo)) - await shell`git add package.json` - await shell`git commit -m "version bump"` - } - const version = packageInfo.version - await docs() - await def() - await publishSite() - await shell`git pull` - await shell`git tag ${version}` - await shell`git push origin 3.x --tags` - await shell`rm -rf docs/wiki/.git` - await shell`npm publish` - say('-- RELEASED --') -} - - /** * Update the contributors table in README.md from the GitHub API. */ @@ -569,7 +460,6 @@ export async function contributorFaces() { } } - /** * Scaffold a config, feature test and runner test for a new feature. * @param {string} featureName - Name of the feature to scaffold tests for. @@ -633,28 +523,6 @@ describe('CodeceptJS ${featureName}', function () { say(`npx mocha test/runner --grep ${featureName}`) } -async function processChangelog() { - const file = 'CHANGELOG.md' - let changelog = fs.readFileSync(file).toString() - - changelog = changelog.replace(/\s@([\w-]+)/gm, ' **[$1](https://github.com/$1)**') - changelog = changelog.replace(/#(\d+)/gm, '[#$1](https://github.com/codeceptjs/CodeceptJS/issues/$1)') - changelog = changelog.replace(/\s\[(\w+)\]\s/gm, ' **[$1]** ') - - await writeToFile('docs/changelog.md', line => { - line`---` - line`permalink: /changelog` - line`title: Releases` - line`sidebar: false` - line`layout: Section` - line`---` - line`` - line`# Releases` - line`` - line`${changelog}` - }) -} - /** * Run the read-only REST test server on port 8010. */ @@ -694,21 +562,21 @@ export async function graphql() { * Lint the codebase with ESLint. */ export async function lint() { - await shell`eslint bin/ examples/ lib/ test/ translations/ runok.cjs` + await shell`eslint bin/ examples/ lib/ test/ translations/ Bunoshfile.js` } /** * Lint and auto-fix the codebase with ESLint. */ export async function lintFix() { - await shell`eslint bin/ examples/ lib/ test/ translations/ runok.cjs --fix` + await shell`eslint bin/ examples/ lib/ test/ translations/ Bunoshfile.js --fix` } /** * Format the codebase with Prettier. */ export async function prettier() { - await shell`prettier --config prettier.config.js --write bin/**/*.js lib/**/*.js test/**/*.js translations/**/*.js runok.cjs` + await shell`prettier --config prettier.config.js --write bin/**/*.js lib/**/*.js test/**/*.js translations/**/*.js Bunoshfile.js` } /** @@ -858,7 +726,6 @@ export async function prepare() { await shell`husky install` } - function replaceInFile(file, fn) { let text = fs.readFileSync(file).toString() fn({ diff --git a/docs/helpers/Lightpanda.md b/docs/helpers/Lightpanda.md index 6c44bedb2..bde92b064 100644 --- a/docs/helpers/Lightpanda.md +++ b/docs/helpers/Lightpanda.md @@ -78,20 +78,30 @@ These are set automatically and only need overriding for unusual setups: This helper should be configured in codecept.conf.js -Type: [object][3] +Type: [object][4] ### Properties -* `endpoint` **[string][4]?** explicit CDP endpoint. Setting this switches the helper to ATTACH +* `endpoint` **[string][3]?** explicit CDP endpoint. Setting this switches the helper to ATTACH mode: it only connects, and never spawns or kills a process, no matter what else is configured. Leave it unset for SELF-MANAGED mode (see below). -* `binaryPath` **[string][4]?** path to the `lightpanda` executable, used in SELF-MANAGED mode +* `binaryPath` **[string][3]?** path to the `lightpanda` executable, used in SELF-MANAGED mode (`endpoint` unset). Checked before `LIGHTPANDA_PATH` and `PATH`. -* `port` **[number][5]?** port `lightpanda serve` listens on, in SELF-MANAGED mode. When unset, a +* `port` **[number][8]?** port `lightpanda serve` listens on, in SELF-MANAGED mode. When unset, a free port is picked automatically, which is what makes `run-workers` collision-free — every worker gets its own instance on its own port with zero config. -* `serverStartTimeout` **[number][5]?** milliseconds to wait for a spawned `lightpanda serve` +* `serverStartTimeout` **[number][8]?** milliseconds to wait for a spawned `lightpanda serve` to answer `/json/version` before `_connect` gives up. +* `url` **[string][3]?** base url of website to be tested. +* `headers` **[object][4]?** headers sent when connecting to the endpoint, e.g. for authenticated remote browsers. +* `input` **[string][3]?** how actions are dispatched: `cdp` (real mouse and keyboard events) or `synthetic` (DOM events). `auto` picks `cdp` when the browser renders layout. +* `xpathPolyfill` **([string][3] | [boolean][18])?** inject an XPath polyfill for browsers with incomplete XPath support. `auto` detects when it is needed. +* `capabilities` **[object][4]?** browser capabilities (`layout`, `xpath`, `screenshot`, `video`, `innerText`, `aria`) to use instead of detecting them at runtime. +* `waitForTimeout` **[number][8]?** default timeout for wait* actions, in seconds. +* `waitForAction` **[number][8]?** fixed delay after each action, in milliseconds. When unset, actions wait only for the navigation they trigger. +* `pollInterval` **[number][8]?** interval between checks while waiting, in milliseconds. +* `getPageTimeout` **[number][8]?** maximum time to wait for a page to load, in seconds. +* `waitForNavigation` **[string][3]?** when a navigation is considered finished: `load`, `domcontentloaded`, or `networkidle`. @@ -128,12 +138,1830 @@ Type: [object][3] * `config` **LightpandaConfig** +### amOnPage + +Opens a web page in a browser. Requires relative or absolute url. +If url starts with `/`, opens a web page of a site defined in `url` config parameter. + +```js +I.amOnPage('/'); // opens main page of website +I.amOnPage('https://github.com'); // opens github +I.amOnPage('/login'); // opens a login page +``` + +#### Parameters + +* `url` **[string][3]** url path or global url. + +Returns **void** automatically synchronized promise through #recorder + +### appendField + +Appends text to a input field or textarea. +Field is located by name, label, CSS or XPath + +The third parameter is an optional context (CSS or XPath locator) to narrow the search. + +```js +I.appendField('#myTextField', 'appended'); +// typing secret +I.appendField('password', secret('123456')); +// within a context +I.appendField('name', 'John', '.form-container'); +``` + +#### Parameters + +* `field` **([string][3] | [object][4])** located by label|name|CSS|XPath|strict locator +* `value` **[string][3]** text value to append. +* `context` **([string][3]? | [object][4])** (optional, `null` by default) element located by CSS | XPath | strict locator. + +Returns **void** automatically synchronized promise through #recorder + +### attachFile + +Attaches a file to element located by label, name, CSS or XPath +Path to file is relative current codecept directory (where codecept.conf.ts or codecept.conf.js is located). +File will be uploaded to remote system (if tests are running remotely). + +The third parameter is an optional context (CSS or XPath locator) to narrow the search. + +```js +I.attachFile('Avatar', 'data/avatar.jpg'); +I.attachFile('form input[name=avatar]', 'data/avatar.jpg'); +// within a context +I.attachFile('Avatar', 'data/avatar.jpg', '.form-container'); +``` + +If the locator points to a non-file-input element (e.g., a dropzone area), +the file will be dropped onto that element using drag-and-drop events. + +```js +I.attachFile('#dropzone', 'data/avatar.jpg'); +``` + +#### Parameters + +* `field` +* `pathToFile` **[string][3]** local file path relative to codecept.conf.ts or codecept.conf.js config file. +* `context` **([string][3]? | [object][4])** (optional, `null` by default) element located by CSS | XPath | strict locator. +* `locator` **([string][3] | [object][4])** field located by label|name|CSS|XPath|strict locator. + +Returns **void** automatically synchronized promise through #recorder + +### blur + +Remove focus from a text input, button, etc. +Calls [blur][5] on the element. + +Examples: + +```js +I.blur('.text-area') +``` + +```js +//element `#product-tile` is focused +I.see('#add-to-cart-btn'); +I.blur('#product-tile') +I.dontSee('#add-to-cart-btn'); +``` + +#### Parameters + +* `locator` **([string][3] | [object][4])** field located by label|name|CSS|XPath|strict locator. +* `options` **any?** Playwright only: [Additional options][6] for available options object as 2nd argument. + +Returns **void** automatically synchronized promise through #recorder + +### capabilities + +Type: CodeceptJS.HelperCapabilities + +### checkOption + +Selects a checkbox or radio button. +Element is located by label or name or CSS or XPath. + +The second parameter is an optional context (CSS or XPath locator) to narrow the search. + +```js +I.checkOption('#agree'); +I.checkOption('I Agree to Terms and Conditions'); +I.checkOption('agree', '//form'); +``` + +#### Parameters + +* `field` **([string][3] | [object][4])** checkbox located by label | name | CSS | XPath | strict locator. +* `context` **([string][3]? | [object][4])** (optional, `null` by default) element located by CSS | XPath | strict locator. + +Returns **void** automatically synchronized promise through #recorder + +### clearClipboard + +Clears the system clipboard. + +```js +I.clearClipboard(); +I.seeClipboardEquals(''); +``` + +Returns **void** automatically synchronized promise through #recorder + +### clearCookie + +Clears a cookie by name, +if none provided clears all cookies. + +```js +I.clearCookie(); +I.clearCookie('test'); +``` + +#### Parameters + +* `name` +* `cookie` **[string][3]?** (optional, `null` by default) cookie name + +### clearField + +Clears a `