diff --git a/.github/workflows/acceptance-tests.yml b/.github/workflows/acceptance-tests.yml index 63107de63..9af54c7d9 100644 --- a/.github/workflows/acceptance-tests.yml +++ b/.github/workflows/acceptance-tests.yml @@ -24,12 +24,12 @@ jobs: steps: # Checkout the repository - name: Checkout Repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 # Install Docker Compose - name: Install Docker Compose run: | - sudo apt-get update + sudo apt-get update --allow-releaseinfo-change sudo apt-get install -y docker-compose # Run rest tests using docker-compose diff --git a/.github/workflows/appium_Android.yml b/.github/workflows/appium_Android.yml index c5e884b81..7c96714ec 100644 --- a/.github/workflows/appium_Android.yml +++ b/.github/workflows/appium_Android.yml @@ -22,7 +22,7 @@ jobs: test-suite: ['other', 'quick'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 diff --git a/.github/workflows/appium_iOS.yml b/.github/workflows/appium_iOS.yml index c44a71df7..fb70b43d4 100644 --- a/.github/workflows/appium_iOS.yml +++ b/.github/workflows/appium_iOS.yml @@ -23,7 +23,7 @@ jobs: test-suite: ['other', 'quick'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 341fb4b8e..3aebdd856 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-22.04 name: Check Tests steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 - uses: testomatio/check-tests@stable diff --git a/.github/workflows/doc-generation.yml b/.github/workflows/doc-generation.yml index c5b84a0fb..e1f56ff39 100644 --- a/.github/workflows/doc-generation.yml +++ b/.github/workflows/doc-generation.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Check out the repo - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index a33def88e..f6fa3b907 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -9,11 +9,21 @@ jobs: push_to_registry: name: Build and push Docker image to Docker Hub runs-on: ubuntu-22.04 - if: startsWith(github.event.ref_name, 'release-') + env: + DOCKER_REPO: ${{ secrets.DOCKERHUB_REPOSITORY }} steps: - - name: Check out the repo with the latest code - uses: actions/checkout@v4 + - name: Checkout code + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Get version from package.json + id: get_version + run: | + VERSION=$(jq -r .version package.json) + echo "VERSION=$VERSION" >> $GITHUB_ENV + echo "version=$VERSION" >> $GITHUB_OUTPUT - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -24,19 +34,19 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_PASSWORD }} - - name: Get the current tag - id: currentTag + - name: Check if Docker tag exists on Docker Hub + id: tag_check run: | - git fetch --prune --unshallow - TAG=$(git describe --tags --abbrev=0) - echo $TAG - echo "TAG=$TAG" >> $GITHUB_ENV + STATUS_CODE=$(curl -s -o /dev/null -w "%{http_code}" \ + https://hub.docker.com/v2/repositories/${{ env.DOCKER_REPO }}/tags/${{ steps.get_version.outputs.version }}/) + echo "status_code=$STATUS_CODE" >> $GITHUB_OUTPUT - name: Build and push Docker image + if: steps.tag_check.outputs.status_code != '200' uses: docker/build-push-action@v6 with: context: . push: true tags: | - ${{ secrets.DOCKERHUB_REPOSITORY }}:latest - ${{ secrets.DOCKERHUB_REPOSITORY }}:${{ env.TAG }} + ${{ env.DOCKER_REPO }}:latest + ${{ env.DOCKER_REPO }}:${{ env.VERSION }} \ No newline at end of file diff --git a/.github/workflows/dtslint.yml b/.github/workflows/dtslint.yml index e47fa15ff..97e2622b7 100644 --- a/.github/workflows/dtslint.yml +++ b/.github/workflows/dtslint.yml @@ -15,7 +15,7 @@ jobs: matrix: node-version: [20.x] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 1004d072f..de1a0fdf9 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -22,7 +22,7 @@ jobs: node-version: [20.x] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: @@ -35,6 +35,9 @@ jobs: npm i --force env: PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true + - name: Allow Release info Change + run: | + sudo apt-get update --allow-releaseinfo-change - name: Install browsers and deps run: npx playwright install && npx playwright install-deps - name: check diff --git a/.github/workflows/plugin.yml b/.github/workflows/plugin.yml index ec456fa7f..ea8d319e1 100644 --- a/.github/workflows/plugin.yml +++ b/.github/workflows/plugin.yml @@ -15,7 +15,6 @@ env: jobs: build: - runs-on: ubuntu-22.04 strategy: @@ -23,22 +22,25 @@ 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 chromium && npx playwright install-deps - - name: start a server - run: "php -S 127.0.0.1:8000 -t test/data/app &" - - name: run plugin tests - run: npm run test:plugin + - uses: actions/checkout@v5 + - 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: Allow Release info Change + run: | + sudo apt-get update --allow-releaseinfo-change + - name: Install browsers and deps + run: npx playwright install chromium && npx playwright install-deps + - name: start a server + run: 'php -S 127.0.0.1:8000 -t test/data/app &' + - name: run plugin tests + run: npm run test:plugin diff --git a/.github/workflows/puppeteer.yml b/.github/workflows/puppeteer.yml index 0d040fdee..2807c3998 100644 --- a/.github/workflows/puppeteer.yml +++ b/.github/workflows/puppeteer.yml @@ -23,7 +23,7 @@ jobs: node-version: [20.x] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: @@ -38,7 +38,7 @@ jobs: PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: true - name: start a server run: "php -S 127.0.0.1:8000 -t test/data/app &" - - uses: browser-actions/setup-chrome@v1 + - uses: browser-actions/setup-chrome@v2 - run: chrome --version - name: run tests run: "./bin/codecept.js run-workers 2 -c test/acceptance/codecept.Puppeteer.js --grep @Puppeteer --debug" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6ac6e3d7e..585b33b29 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,7 @@ jobs: node-version: [20.x, 22.x] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: @@ -38,7 +38,7 @@ jobs: node-version: [20.x, 22.x] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: diff --git a/.github/workflows/testcafe.yml b/.github/workflows/testcafe.yml index c6b8844cf..f2d962911 100644 --- a/.github/workflows/testcafe.yml +++ b/.github/workflows/testcafe.yml @@ -24,7 +24,7 @@ jobs: node-version: [20.x] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: diff --git a/.github/workflows/webdriver.yml b/.github/workflows/webdriver.yml index 74e1a9882..646fb6fa4 100644 --- a/.github/workflows/webdriver.yml +++ b/.github/workflows/webdriver.yml @@ -22,7 +22,7 @@ jobs: steps: - run: docker run -d --net=host --shm-size=2g selenium/standalone-chrome:4.27 - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: diff --git a/.gitignore b/.gitignore index 899a0b988..fc1f70320 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ examples/output examples/selenoid-example/output test/data/app/db test/data/sandbox/steps.d.ts +test/data/sandbox/configs/custom-reporter-plugin/output/result.json testpullfilecache* .DS_Store package-lock.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ce6dd1b6..ab167b23d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,125 @@ +## 3.7.4 + +❤️ Thanks all to those who contributed to make this release! ❤️ + +🛩️ _Features_ + +- **Test Suite Shuffling**: Randomize test execution order to discover test dependencies and improve test isolation (#5051) - by @NivYarmus + + ```bash + # Shuffle tests to find order-dependent failures using lodash.shuffle algorithm + npx codeceptjs run --shuffle + + # Combined with grep and other options + npx codeceptjs run --shuffle --grep "@smoke" --steps + ``` + +- **Enhanced Interactive Debugging**: Better logging for `I.grab*` methods in live interactive mode for clearer debugging output (#4986) - by @owenizedd + + ```js + // Interactive pause() now shows detailed grab results with JSON formatting + I.amOnPage('/checkout') + pause() // Interactive shell started + > I.grabTextFrom('.price') + Result $res= "Grabbed text: $29.99" // Pretty-printed JSON output + > I.grabValueFrom('input[name="email"]') + {"value":"user@example.com"} // Structured JSON response + ``` + + 🐛 _Bug Fixes_ + +- **Playwright Session Traces**: Fixed trace file naming convention and improved error handling for multi-session test scenarios (#5073) - by @julien-ft-64 @kobenguyent + + ```js + // Example outputs: + // - a1b2c3d4-e5f6_checkout_login_test.failed.zip + // - b2c3d4e5-f6g7_admin_dashboard_test.failed.zip + ``` + + _Trace files use UUID prefixes with `sessionName_testTitle.status.zip` format_ + +- **Worker Data Injection**: Resolved proxy object serialization preventing data sharing between parallel test workers (#5072) - by @kobenguyent + + ```js + // Fixed: Complex objects can now be properly shared and injected between workers + // Bootstrap data sharing in codecept.conf.js: + exports.config = { + bootstrap() { + share({ + userData: { id: 123, preferences: { theme: 'dark' } }, + apiConfig: { baseUrl: 'https://api.test.com', timeout: 5000 }, + }) + }, + } + + // In tests across different workers: + const testData = inject() + console.log(testData.userData.preferences.theme) // 'dark' - deep nesting works + console.log(Object.keys(testData)) // ['userData', 'apiConfig'] - key enumeration works + + // Dynamic sharing during test execution: + share({ newData: 'shared across workers' }) + ``` + +- **Hook Exit Codes**: Fixed improper exit codes when test hooks fail, ensuring CI/CD pipelines properly detect failures (#5058) - by @kobenguyent + + ```bash + # Before: Exit code 0 even when beforeEach/afterEach failed + # After: Exit code 1 when any hook fails, properly failing CI builds + ``` + +- **TypeScript Effects Support**: Added complete TypeScript definitions for effects functionality (#5027) - by @kobenguyent + + ```typescript + // Import effects with full TypeScript type definitions + import { tryTo, retryTo, within } from 'codeceptjs/effects' + + // tryTo returns Promise for conditional actions + const success: boolean = await tryTo(async () => { + await I.see('Cookie banner') + await I.click('Accept') + }) + + // retryTo with typed parameters for reliability + await retryTo(() => { + I.click('Submit') + I.see('Success') + }, 3) // retry up to 3 times + ``` + + _Note: Replaces deprecated global plugins - import from 'codeceptjs/effects' module_ + +- **Mochawesome Screenshot Uniqueness**: Fixed screenshot naming to prevent test failures from being overwritten when multiple tests run at the same time (#4959) - by @Lando1n + + ```js + // Problem: When tests run in parallel, screenshots had identical names + // This caused later test screenshots to overwrite earlier ones + + // Before: All failed tests saved as "screenshot.png" + // Result: Only the last failure screenshot was kept + + // After: Each screenshot gets a unique name with timestamp + // Examples: + // - "login_test_1645123456.failed.png" + // - "checkout_test_1645123789.failed.png" + // - "profile_test_1645124012.failed.png" + + // Configuration in codecept.conf.js: + helpers: { + Mochawesome: { + uniqueScreenshotNames: true // Enable unique naming + } + } + ``` + + _Ensures every failed test keeps its own screenshot for easier debugging_ + +📖 _Documentation_ + +- Fixed Docker build issues and improved container deployment process (#4980) - by @thomashohn +- Updated dependency versions to maintain security and compatibility (#4957, #4950, #4943) - by @thomashohn +- Fixed automatic documentation generation system for custom plugins (#4973) - by @Lando1n + ## 3.7.3 ❤️ Thanks all to those who contributed to make this release! ❤️ @@ -481,7 +603,6 @@ I.flushSoftAssertions() // Throws an error if any soft assertions have failed. T ``` - feat(cli): print failed hooks (#4476) - by @kobenguyent - - run command ![Screenshot 2024-09-02 at 15 25 20](https://github.com/user-attachments/assets/625c6b54-03f6-41c6-9d0c-cd699582404a) @@ -744,7 +865,6 @@ heal.addRecipe('reloadPageIfModalIsNotVisisble', { ``` - **Breaking Change** **AI** features refactored. Read updated [AI guide](./ai): - - **removed dependency on `openai`** - added support for **Azure OpenAI**, **Claude**, **Mistal**, or any AI via custom request function - `--ai` option added to explicitly enable AI features @@ -755,7 +875,6 @@ heal.addRecipe('reloadPageIfModalIsNotVisisble', { - `OpenAI` helper renamed to `AI` - feat(puppeteer): network traffic manipulation. See #4263 by @KobeNguyenT - - `startRecordingTraffic` - `grabRecordedNetworkTraffics` - `flushNetworkTraffics` @@ -2096,7 +2215,6 @@ await I.seeTraffic({ - **🪄 [AI Powered Test Automation](/ai)** - use OpenAI as a copilot for test automation. #3713 By @davertmik ![](https://user-images.githubusercontent.com/220264/250418764-c382709a-3ccb-4eb5-b6bc-538f3b3b3d35.png) - - [AI guide](/ai) added - added support for OpenAI in `pause()` - added [`heal` plugin](/plugins#heal) for self-healing tests @@ -2107,7 +2225,6 @@ await I.seeTraffic({ ![](https://user-images.githubusercontent.com/220264/250415226-a7620418-56a4-4837-b790-b15e91e5d1f0.png) - [Playwright] Support for APIs in Playwright (#3665) - by Egor Bodnar - - `clearField` replaced to use new Playwright API - `blur` added - `focus` added @@ -3519,9 +3636,7 @@ I.seeFile(fileName) ## 2.0.0 - [WebDriver] **Breaking Change.** Updated to webdriverio v5. New helper **WebDriver** helper introduced. - - **Upgrade plan**: - 1. Install latest webdriverio ``` @@ -3538,9 +3653,7 @@ I.seeFile(fileName) - [Appium] **Breaking Change.** Updated to use webdriverio v5 as well. See upgrade plan ↑ - [REST] **Breaking Change.** Replaced `unirest` library with `axios`. - - **Upgrade plan**: - 1. Refer to [axios API](https://github.com/axios/axios). 2. If you were using `unirest` requests/responses in your tests change them to axios format. diff --git a/Dockerfile b/Dockerfile index 7d4f6bc9d..d637da4b5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,6 +2,8 @@ FROM mcr.microsoft.com/playwright:v1.48.1-noble ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true +RUN apt-get update --allow-releaseinfo-change + # Installing the pre-required packages and libraries RUN apt-get update && \ apt-get install -y libgtk2.0-0 \ diff --git a/bin/codecept.js b/bin/codecept.js index 5a4752129..8a5d65b20 100755 --- a/bin/codecept.js +++ b/bin/codecept.js @@ -164,6 +164,7 @@ program .option('--tests', 'run only JS test files and skip features') .option('--no-timeouts', 'disable all timeouts') .option('-p, --plugins ', 'enable plugins, comma-separated') + .option('--shuffle', 'Shuffle the order in which test files run') // mocha options .option('--colors', 'force enabling of colors') diff --git a/docs/changelog.md b/docs/changelog.md index 5cd90701a..1b27678ad 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -7,6 +7,184 @@ layout: Section # Releases +## 3.7.4 + +❤️ Thanks all to those who contributed to make this release! ❤️ + +🛩️ _Features_ + +- **Test Suite Shuffling**: Randomize test execution order to discover test dependencies and improve test isolation ([#5051](https://github.com/codeceptjs/CodeceptJS/issues/5051)) - by **[NivYarmus](https://github.com/NivYarmus)** + + ```bash + # Shuffle tests to find order-dependent failures using lodash.shuffle algorithm + npx codeceptjs run --shuffle + + # Combined with grep and other options + npx codeceptjs run --shuffle --grep "@smoke" --steps + ``` + +- **Enhanced Interactive Debugging**: Better logging for `I.grab*` methods in live interactive mode for clearer debugging output ([#4986](https://github.com/codeceptjs/CodeceptJS/issues/4986)) - by **[owenizedd](https://github.com/owenizedd)** + + ```js + // Interactive pause() now shows detailed grab results with JSON formatting + I.amOnPage('/checkout') + pause() // Interactive shell started + > I.grabTextFrom('.price') + Result $res= "Grabbed text: $29.99" // Pretty-printed JSON output + > I.grabValueFrom('input[name="email"]') + {"value":"user@example.com"} // Structured JSON response + ``` + + 🐛 _Bug Fixes_ + +- **Playwright Session Traces**: Fixed trace file naming convention and improved error handling for multi-session test scenarios ([#5073](https://github.com/codeceptjs/CodeceptJS/issues/5073)) - by **[julien-ft-64](https://github.com/julien-ft-64)** **[kobenguyent](https://github.com/kobenguyent)** + + ```js + // Example outputs: + // - a1b2c3d4-e5f6_checkout_login_test.failed.zip + // - b2c3d4e5-f6g7_admin_dashboard_test.failed.zip + ``` + + _Trace files use UUID prefixes with `sessionName_testTitle.status.zip` format_ + +- **Worker Data Injection**: Resolved proxy object serialization preventing data sharing between parallel test workers ([#5072](https://github.com/codeceptjs/CodeceptJS/issues/5072)) - by **[kobenguyent](https://github.com/kobenguyent)** + + ```js + // Fixed: Complex objects can now be properly shared and injected between workers + // Bootstrap data sharing in codecept.conf.js: + exports.config = { + bootstrap() { + share({ + userData: { id: 123, preferences: { theme: 'dark' } }, + apiConfig: { baseUrl: 'https://api.test.com', timeout: 5000 }, + }) + }, + } + + // In tests across different workers: + const testData = inject() + console.log(testData.userData.preferences.theme) // 'dark' - deep nesting works + console.log(Object.keys(testData)) // ['userData', 'apiConfig'] - key enumeration works + + // Dynamic sharing during test execution: + share({ newData: 'shared across workers' }) + ``` + +- **Hook Exit Codes**: Fixed improper exit codes when test hooks fail, ensuring CI/CD pipelines properly detect failures ([#5058](https://github.com/codeceptjs/CodeceptJS/issues/5058)) - by **[kobenguyent](https://github.com/kobenguyent)** + + ```bash + # Before: Exit code 0 even when beforeEach/afterEach failed + # After: Exit code 1 when any hook fails, properly failing CI builds + ``` + +- **TypeScript Effects Support**: Added complete TypeScript definitions for effects functionality ([#5027](https://github.com/codeceptjs/CodeceptJS/issues/5027)) - by **[kobenguyent](https://github.com/kobenguyent)** + + ```typescript + // Import effects with full TypeScript type definitions + import { tryTo, retryTo, within } from 'codeceptjs/effects' + + // tryTo returns Promise for conditional actions + const success: boolean = await tryTo(async () => { + await I.see('Cookie banner') + await I.click('Accept') + }) + + // retryTo with typed parameters for reliability + await retryTo(() => { + I.click('Submit') + I.see('Success') + }, 3) // retry up to 3 times + ``` + + _Note: Replaces deprecated global plugins - import from 'codeceptjs/effects' module_ + +- **Mochawesome Screenshot Uniqueness**: Fixed screenshot naming to prevent test failures from being overwritten when multiple tests run at the same time ([#4959](https://github.com/codeceptjs/CodeceptJS/issues/4959)) - by **[Lando1n](https://github.com/Lando1n)** + + ```js + // Problem: When tests run in parallel, screenshots had identical names + // This caused later test screenshots to overwrite earlier ones + + // Before: All failed tests saved as "screenshot.png" + // Result: Only the last failure screenshot was kept + + // After: Each screenshot gets a unique name with timestamp + // Examples: + // - "login_test_1645123456.failed.png" + // - "checkout_test_1645123789.failed.png" + // - "profile_test_1645124012.failed.png" + + // Configuration in codecept.conf.js: + helpers: { + Mochawesome: { + uniqueScreenshotNames: true // Enable unique naming + } + } + ``` + + _Ensures every failed test keeps its own screenshot for easier debugging_ + +📖 _Documentation_ + +- Fixed Docker build issues and improved container deployment process ([#4980](https://github.com/codeceptjs/CodeceptJS/issues/4980)) - by **[thomashohn](https://github.com/thomashohn)** +- Updated dependency versions to maintain security and compatibility ([#4957](https://github.com/codeceptjs/CodeceptJS/issues/4957), [#4950](https://github.com/codeceptjs/CodeceptJS/issues/4950), [#4943](https://github.com/codeceptjs/CodeceptJS/issues/4943)) - by **[thomashohn](https://github.com/thomashohn)** +- Fixed automatic documentation generation system for custom plugins ([#4973](https://github.com/codeceptjs/CodeceptJS/issues/4973)) - by **[Lando1n](https://github.com/Lando1n)** + +## 3.7.3 + +❤️ Thanks all to those who contributed to make this release! ❤️ + +🛩️ _Features_ + +- feat(cli): improve info command to return installed browsers ([#4890](https://github.com/codeceptjs/CodeceptJS/issues/4890)) - by **[kobenguyent](https://github.com/kobenguyent)** + +``` +➜ helloworld npx codeceptjs info +Environment information: + +codeceptVersion: "3.7.2" +nodeInfo: 18.19.0 +osInfo: macOS 14.4 +cpuInfo: (8) x64 Apple M1 Pro +osBrowsers: "chrome: 133.0.6943.143, edge: 133.0.3065.92, firefox: not installed, safari: 17.4" +playwrightBrowsers: "chromium: 133.0.6943.16, firefox: 134.0, webkit: 18.2" +helpers: { +"Playwright": { +"url": "http://localhost", +... +``` + +🐛 _Bug Fixes_ + +- fix: resolving path inconsistency in container.js and appium.js ([#4866](https://github.com/codeceptjs/CodeceptJS/issues/4866)) - by **[mjalav](https://github.com/mjalav)** +- fix: broken screenshot links in mochawesome reports ([#4889](https://github.com/codeceptjs/CodeceptJS/issues/4889)) - by **[kobenguyent](https://github.com/kobenguyent)** +- some internal fixes to make UTs more stable by **[thomashohn](https://github.com/thomashohn)** +- dependencies upgrades by **[thomashohn](https://github.com/thomashohn)** + +## 3.7.2 + +❤️ Thanks all to those who contributed to make this release! ❤️ + +🛩️ _Features_ + +- feat(playwright): Clear cookie by name ([#4693](https://github.com/codeceptjs/CodeceptJS/issues/4693)) - by **[ngraf](https://github.com/ngraf)** + +🐛 _Bug Fixes_ + +- fix(stepByStepReport): no records html is generated when running with run-workers ([#4638](https://github.com/codeceptjs/CodeceptJS/issues/4638)) +- fix(webdriver): bidi error in log with webdriver ([#4850](https://github.com/codeceptjs/CodeceptJS/issues/4850)) +- fix(types): TS types of methods (Feature|Scenario)Config.config ([#4851](https://github.com/codeceptjs/CodeceptJS/issues/4851)) +- fix: redundant popup log ([#4830](https://github.com/codeceptjs/CodeceptJS/issues/4830)) +- fix(webdriver): grab browser logs using bidi protocol ([#4754](https://github.com/codeceptjs/CodeceptJS/issues/4754)) +- fix(webdriver): screenshots for sessions ([#4748](https://github.com/codeceptjs/CodeceptJS/issues/4748)) + +📖 _Documentation_ + +- fix(docs): mask sensitive data ([#4636](https://github.com/codeceptjs/CodeceptJS/issues/4636)) - by **[gkushang](https://github.com/gkushang)** + +## 3.7.1 + +- Fixed `reading charAt` error in `asyncWrapper.js` + ## 3.7.0 This release introduces major new features and internal refactoring. It is an important step toward the 4.0 release planned soon, which will remove all deprecations introduced in 3.7. @@ -434,7 +612,6 @@ I.flushSoftAssertions() // Throws an error if any soft assertions have failed. T ``` - feat(cli): print failed hooks ([#4476](https://github.com/codeceptjs/CodeceptJS/issues/4476)) - by **[kobenguyent](https://github.com/kobenguyent)** - - run command ![Screenshot 2024-09-02 at 15 25 20](https://github.com/user-attachments/assets/625c6b54-03f6-41c6-9d0c-cd699582404a) @@ -694,7 +871,6 @@ heal.addRecipe('reloadPageIfModalIsNotVisisble', { ``` - **Breaking Change** **AI** features refactored. Read updated [AI guide](./ai): - - **removed dependency on `openai`** - added support for **Azure OpenAI**, **Claude**, **Mistal**, or any AI via custom request function - `--ai` option added to explicitly enable AI features @@ -705,7 +881,6 @@ heal.addRecipe('reloadPageIfModalIsNotVisisble', { - `OpenAI` helper renamed to `AI` - feat(puppeteer): network traffic manipulation. See [#4263](https://github.com/codeceptjs/CodeceptJS/issues/4263) by **[KobeNguyenT](https://github.com/KobeNguyenT)** - - `startRecordingTraffic` - `grabRecordedNetworkTraffics` - `flushNetworkTraffics` @@ -2043,7 +2218,6 @@ await I.seeTraffic({ - **🪄 [AI Powered Test Automation](/ai)** - use OpenAI as a copilot for test automation. [#3713](https://github.com/codeceptjs/CodeceptJS/issues/3713) By **[davertmik](https://github.com/davertmik)** ![](https://user-images.githubusercontent.com/220264/250418764-c382709a-3ccb-4eb5-b6bc-538f3b3b3d35.png) - - [AI guide](/ai) added - added support for OpenAI in `pause()` - added [`heal` plugin](/plugins#heal) for self-healing tests @@ -2054,7 +2228,6 @@ await I.seeTraffic({ ![](https://user-images.githubusercontent.com/220264/250415226-a7620418-56a4-4837-b790-b15e91e5d1f0.png) - **[Playwright]** Support for APIs in Playwright ([#3665](https://github.com/codeceptjs/CodeceptJS/issues/3665)) - by Egor Bodnar - - `clearField` replaced to use new Playwright API - `blur` added - `focus` added @@ -3465,9 +3638,7 @@ I.seeFile(fileName) ## 2.0.0 - **[WebDriver]** **Breaking Change.** Updated to webdriverio v5. New helper **WebDriver** helper introduced. - - **Upgrade plan**: - 1. Install latest webdriverio ``` @@ -3484,9 +3655,7 @@ I.seeFile(fileName) - **[Appium]** **Breaking Change.** Updated to use webdriverio v5 as well. See upgrade plan ↑ - **[REST]** **Breaking Change.** Replaced `unirest` library with `axios`. - - **Upgrade plan**: - 1. Refer to [axios API](https://github.com/axios/axios). 2. If you were using `unirest` requests/responses in your tests change them to axios format. diff --git a/docs/commands.md b/docs/commands.md index 57bfd523e..c90595641 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -47,6 +47,12 @@ Run single test with steps printed npx codeceptjs run github_test.js --steps ``` +Run test files in shuffled order + +```sh +npx codeceptjs run --shuffle +``` + Run single test in debug mode (see more in [debugging](#Debugging) section) ```sh diff --git a/docs/parallel.md b/docs/parallel.md index 913eb2d6f..bea099046 100644 --- a/docs/parallel.md +++ b/docs/parallel.md @@ -364,37 +364,78 @@ workers.on(event.all.result, (status, completedTests, workerStats) => { ## Sharing Data Between Workers -NodeJS Workers can communicate between each other via messaging system. It may happen that you want to pass some data from one of the workers to other. For instance, you may want to share user credentials accross all tests. Data will be appended to a container. +NodeJS Workers can communicate between each other via messaging system. CodeceptJS allows you to share data between different worker processes using the `share()` and `inject()` functions. -However, you can't access uninitialized data from a container, so to start, you need to initialize data first. Inside `bootstrap` function of the config we execute the `share` to initialize value: +### Basic Usage +You can share data directly using the `share()` function and access it using `inject()`: + +```js +// In one test or worker +share({ userData: { name: 'user', password: '123456' } }); + +// In another test or worker +const testData = inject(); +console.log(testData.userData.name); // 'user' +console.log(testData.userData.password); // '123456' +``` + +### Initializing Data in Bootstrap + +For complex scenarios where you need to initialize shared data before tests run, you can use the bootstrap function: ```js // inside codecept.conf.js exports.config = { bootstrap() { - // append empty userData to container - share({ userData: false }); + // Initialize shared data container + share({ userData: null, config: { retries: 3 } }); } } ``` -Now each worker has `userData` inside a container. However, it is empty. -When you obtain real data in one of the tests you can now `share` this data accross tests. Use `inject` function to access data inside a container: +Then in your tests, you can check and update the shared data: ```js -// get current value of userData -let { userData } = inject(); -// if userData is still empty - update it -if (!userData) { - userData = { name: 'user', password: '123456' }; - // now new userData will be shared accross all workers - share({userData : userData}); +const testData = inject(); +if (!testData.userData) { + // Update shared data - both approaches work: + share({ userData: { name: 'user', password: '123456' } }); + // or mutate the injected object: + testData.userData = { name: 'user', password: '123456' }; } ``` -If you want to share data only within same worker, and not across all workers, you need to add option `local: true` every time you run `share` +### Working with Proxy Objects + +Since CodeceptJS 3.7.0+, shared data uses Proxy objects for synchronization between workers. The proxy system works seamlessly for most use cases: + +```js +// ✅ All of these work correctly: +const data = inject(); +console.log(data.userData.name); // Access nested properties +console.log(Object.keys(data)); // Enumerate shared keys +data.newProperty = 'value'; // Add new properties +Object.assign(data, { more: 'data' }); // Merge objects +``` + +**Important Note:** Avoid reassigning the entire injected object: + +```js +// ❌ AVOID: This breaks the proxy reference +let testData = inject(); +testData = someOtherObject; // This will NOT work as expected! + +// ✅ PREFERRED: Use share() to replace data or mutate properties +share({ userData: someOtherObject }); // This works! +// or +Object.assign(inject(), someOtherObject); // This works! +``` + +### Local Data (Worker-Specific) + +If you want to share data only within the same worker (not across all workers), use the `local` option: ```js -share({ userData: false }, {local: true }); +share({ localData: 'worker-specific' }, { local: true }); ``` diff --git a/docs/plugins.md b/docs/plugins.md index 682b24bd9..d726e636a 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -9,7 +9,7 @@ title: Plugins ## analyze -Uses AI to analyze test failures and provide insights. +Uses AI to analyze test failures and provide insights This plugin analyzes failed tests using AI to provide detailed explanations and group similar failures. When enabled with --ai flag, it generates reports after test execution. @@ -60,10 +60,12 @@ exports.config = { ### Parameters - `config` **[Object][1]** Plugin configuration (optional, default `{}`) - Returns **void** -## authLogs user in for the first test and reuses session for next tests. +Returns **void** +## auth + +Logs user in for the first test and reuses session for next tests. Works by saving cookies into memory or file. If a session expires automatically logs in again. @@ -294,13 +296,17 @@ Scenario('login', async ({ I, login }) => { - `config` -## autoDelaySometimes it takes some time for a page to respond to user's actions. +## autoDelay +Sometimes it takes some time for a page to respond to user's actions. Depending on app's performance this can be either slow or fast. + For instance, if you click a button and nothing happens - probably JS event is not attached to this button yet Also, if you fill field and input validation doesn't accept your input - maybe because you typed value too fast. + This plugin allows to slow down tests execution when a test running too fast. It puts a tiny delay for before and after action commands. + Commands affected (by default): - `click` @@ -332,110 +338,112 @@ Possible config options: ## commentStep -### Parameters +This plugin is **deprecated**, use `Section` instead. -- `config` +Add descriptive nested steps for your tests: -**Meta** +```js +Scenario('project update test', async I => { + __`Given` + const projectId = await I.have('project') -- **deprecated**: Add descriptive nested steps for your tests: + __`When` + projectPage.update(projectId, { title: 'new title' }) - ```js - Scenario('project update test', async I => { - __`Given` - const projectId = await I.have('project') + __`Then` + projectPage.open(projectId) + I.see('new title', 'h1') +}) +``` - __`When` - projectPage.update(projectId, { title: 'new title' }) +Steps prefixed with `__` will be printed as nested steps in `--steps` output: - __`Then` - projectPage.open(projectId) - I.see('new title', 'h1') - }) - ``` + Given + I have "project" + When + projectPage update + Then + projectPage open + I see "new title", "h1" - Steps prefixed with `__` will be printed as nested steps in `--steps` output: +Also those steps will be exported to allure reports. - Given - I have "project" - When - projectPage update - Then - projectPage open - I see "new title", "h1" +This plugin can be used - Also those steps will be exported to allure reports. +### Config - This plugin can be used +- `enabled` - (default: false) enable a plugin +- `registerGlobal` - (default: false) register `__` template literal function globally. You can override function global name by providing a name as a value. - ### Config +### Examples - - `enabled` - (default: false) enable a plugin - - `registerGlobal` - (default: false) register `__` template literal function globally. You can override function global name by providing a name as a value. +Registering `__` globally: - ### Examples +```js +plugins: { + commentStep: { + enabled: true, + registerGlobal: true + } +} +``` - Registering `__` globally: +Registering `Step` globally: - ```js - plugins: { - commentStep: { - enabled: true, - registerGlobal: true - } +```js +plugins: { + commentStep: { + enabled: true, + registerGlobal: 'Step' } - ``` +} +``` - Registering `Step` globally: +Using only local function names: - ```js - plugins: { - commentStep: { - enabled: true, - registerGlobal: 'Step' - } +```js +plugins: { + commentStep: { + enabled: true } - ``` +} +``` - Using only local function names: +Then inside a test import a comment function from a plugin. +For instance, you can prepare Given/When/Then functions to use them inside tests: - ```js - plugins: { - commentStep: { - enabled: true - } - } - ``` +```js +// inside a test +const step = codeceptjs.container.plugins('commentStep') - Then inside a test import a comment function from a plugin. - For instance, you can prepare Given/When/Then functions to use them inside tests: +const Given = () => step`Given` +const When = () => step`When` +const Then = () => step`Then` +``` - ```js - // inside a test - const step = codeceptjs.container.plugins('commentStep') +Scenario('project update test', async (I) => { +Given(); +const projectId = await I.have('project'); - const Given = () => step`Given` - const When = () => step`When` - const Then = () => step`Then` - ``` +When(); +projectPage.update(projectId, { title: 'new title' }); - Scenario('project update test', async (I) => { - Given(); - const projectId = await I.have('project'); +Then(); +projectPage.open(projectId); +I.see('new title', 'h1'); +}); - When(); - projectPage.update(projectId, { title: 'new title' }); +``` + +``` - Then(); - projectPage.open(projectId); - I.see('new title', 'h1'); - }); +### Parameters - ``` +- `config` - ``` +## coverage -## coverageDumps code coverage from Playwright/Puppeteer after every test. +Dumps code coverage from Playwright/Puppeteer after every test. #### Configuration @@ -462,7 +470,9 @@ Possible config options, More could be found at [monocart-coverage-reports][2] - `config` -## customLocatorCreates a [custom locator][3] by using special attributes in HTML. +## customLocator + +Creates a [custom locator][3] by using special attributes in HTML. If you have a convention to use `data-test-id` or `data-qa` attributes to mark active elements for e2e tests, you can enable this plugin to simplify matching elements with these attributes: @@ -569,20 +579,25 @@ I.click('=sign-up') // matches => [data-qa=sign-up],[data-test=sign-up] - `config` -## customReporterSample custom reporter for CodeceptJS. +## customReporter + +Sample custom reporter for CodeceptJS. ### Parameters - `config` -## eachElementProvides `eachElement` global function to iterate over found elements to perform actions on them. +## eachElement + +Provides `eachElement` global function to iterate over found elements to perform actions on them. `eachElement` takes following args: - `purpose` - the goal of an action. A comment text that will be displayed in output. - `locator` - a CSS/XPath locator to match elements - `fn(element, index)` - **asynchronous** function which will be executed for each matched element. - Example of usage: + +Example of usage: ```js // this example works with Playwright and Puppeteer helper @@ -611,6 +626,7 @@ await eachElement('check all items are visible', '.item', async el => { ``` This method works with WebDriver, Playwright, Puppeteer, Appium helpers. + Function parameter `el` represents a matched element. Depending on a helper API of `el` can be different. Refer to API of corresponding browser testing engine for a complete API list: @@ -621,7 +637,8 @@ Depending on a helper API of `el` can be different. Refer to API of correspondin #### Configuration - `registerGlobal` - to register `eachElement` function globally, true by default - If `registerGlobal` is false you can use eachElement from the plugin: + +If `registerGlobal` is false you can use eachElement from the plugin: ```js const eachElement = codeceptjs.container.plugins('eachElement') @@ -632,13 +649,23 @@ const eachElement = codeceptjs.container.plugins('eachElement') - `purpose` **[string][7]** - `locator` **CodeceptJS.LocatorOrString** - `fn` **[Function][8]** - Returns **([Promise][9]\ | [undefined][10])** -## fakerTransformUse the `@faker-js/faker` package to generate fake data inside examples on your gherkin tests +Returns **([Promise][9]\ | [undefined][10])** + +## fakerTransform + +Use the `@faker-js/faker` package to generate fake data inside examples on your gherkin tests #### Usage -To start please install `@faker-js/faker` package npm install -D @faker-js/faker yarn add -D @faker-js/faker +To start please install `@faker-js/faker` package + + npm install -D @faker-js/faker + + + + yarn add -D @faker-js/faker + Add this plugin to config file: ```js @@ -665,7 +692,9 @@ Scenario Outline: ... - `config` -## healSelf-healing tests with AI. +## heal + +Self-healing tests with AI. Read more about heaking in [Self-Healing Tests][11] @@ -685,11 +714,14 @@ More config options are available: - `config` (optional, default `{}`) -## pageInfoCollects information from web page after each failed test and adds it to the test as an artifact. +## pageInfo +Collects information from web page after each failed test and adds it to the test as an artifact. It is suggested to enable this plugin if you run tests on CI and you need to debug failed tests. This plugin can be paired with `analyze` plugin to provide more context. + It collects URL, HTML errors (by classes), and browser logs. + Enable this plugin in config: ```js @@ -708,7 +740,9 @@ Additional config options: - `config` (optional, default `{}`) -## pauseOnFailAutomatically launches [interactive pause][12] when a test fails. +## pauseOnFail + +Automatically launches [interactive pause][12] when a test fails. Useful for debugging flaky tests on local environment. Add this plugin to config file: @@ -720,9 +754,13 @@ plugins: { ``` Unlike other plugins, `pauseOnFail` is not recommended to be enabled by default. -Enable it manually on each run via `-p` option: npx codeceptjs run -p pauseOnFail +Enable it manually on each run via `-p` option: + + npx codeceptjs run -p pauseOnFail -## retryFailedStepRetries each failed step in a test. +## retryFailedStep + +Retries each failed step in a test. Add this plugin to config file: @@ -734,7 +772,9 @@ plugins: { } ``` -Run tests with plugin enabled: npx codeceptjs run --plugins retryFailedStep +Run tests with plugin enabled: + + npx codeceptjs run --plugins retryFailedStep #### Configuration: @@ -769,7 +809,9 @@ plugins: { } ``` -#### Disable Per TestThis plugin can be disabled per test. In this case you will need to stet `I.retry()` to all flaky steps: +#### Disable Per Test + +This plugin can be disabled per test. In this case you will need to stet `I.retry()` to all flaky steps: Use scenario configuration to disable plugin for a test @@ -783,9 +825,12 @@ Scenario('scenario tite', { disableRetryFailedStep: true }, () => { - `config` -## screenshotOnFailCreates screenshot on failure. Screenshot is saved into `output` directory. +## screenshotOnFail + +Creates screenshot on failure. Screenshot is saved into `output` directory. Initially this functionality was part of corresponding helper but has been moved into plugin since 1.4 + This plugin is **enabled by default**. #### Configuration @@ -809,8 +854,9 @@ Possible config options: - `config` -## selenoid[Selenoid][13] plugin automatically starts browsers and video recording. +## selenoid +[Selenoid][13] plugin automatically starts browsers and video recording. Works with WebDriver helper. ### Prerequisite @@ -829,6 +875,7 @@ Selenoid plugin can be started in two ways: #### Automatic If you are new to Selenoid and you want plug and play setup use automatic mode. + Add plugin configuration in `codecept.conf.js`: ```js @@ -847,6 +894,7 @@ plugins: { When `autoCreate` is enabled it will pull the [latest Selenoid from DockerHub][15] and start Selenoid automatically. It will also create `browsers.json` file required by Selenoid. + In automatic mode the latest version of browser will be used for tests. It is recommended to specify exact version of each browser inside `browsers.json` file. > **If you are using Windows machine or if `autoCreate` does not work properly, create container manually** @@ -860,7 +908,8 @@ This is especially useful for Continous Integration server as you can configure 1. Create `browsers.json` file in the same directory `codecept.conf.js` is located [Refer to Selenoid documentation][16] to know more about browsers.json. - _Sample browsers.json_ + +_Sample browsers.json_ ```js { @@ -878,8 +927,12 @@ This is especially useful for Continous Integration server as you can configure ``` > It is recommended to use specific versions of browsers in `browsers.json` instead of latest. This will prevent tests fail when browsers will be updated. -> **⚠ At first launch selenoid plugin takes extra time to download all Docker images before tests starts**. 2. Create Selenoid container -> Run the following command to create a container. To know more [refer here][17] + +**⚠ At first launch selenoid plugin takes extra time to download all Docker images before tests starts**. + +2. Create Selenoid container + +Run the following command to create a container. To know more [refer here][17] ```bash docker create \ @@ -895,8 +948,10 @@ aerokube/selenoid:latest-release ### Video Recording This plugin allows to record and save video per each executed tests. + When `enableVideo` is `true` this plugin saves video in `output/videos` directory with each test by name To save space videos for all succesful tests are deleted. This can be changed by `deletePassed` option. + When `allure` plugin is enabled a video is attached to report automatically. ### Options: @@ -916,12 +971,17 @@ When `allure` plugin is enabled a video is attached to report automatically. - `config` -## stepByStepReport![step-by-step-report][19] +## stepByStepReport + +![step-by-step-report][19] Generates step by step report for a test. After each step in a test a screenshot is created. After test executed screenshots are combined into slideshow. By default, reports are generated only for failed tests. -Run tests with plugin enabled: npx codeceptjs run --plugins stepByStepReport + +Run tests with plugin enabled: + + npx codeceptjs run --plugins stepByStepReport #### Configuration @@ -947,7 +1007,9 @@ Possible config options: - `config` **any** -## stepTimeoutSet timeout for test steps globally. +## stepTimeout + +Set timeout for test steps globally. Add this plugin to config file: @@ -959,7 +1021,9 @@ plugins: { } ``` -Run tests with plugin enabled: npx codeceptjs run --plugins stepTimeout +Run tests with plugin enabled: + + npx codeceptjs run --plugins stepTimeout #### Configuration: @@ -968,7 +1032,6 @@ Run tests with plugin enabled: npx codeceptjs run --plugins stepTimeout - `overrideStepLimits` - whether to use timeouts set in plugin config to override step timeouts set in code with I.limitTime(x).action(...), default false - `noTimeoutSteps` - an array of steps with no timeout. Default: - - `amOnPage` - `wait*` @@ -1000,7 +1063,9 @@ plugins: { - `config` -## subtitlesAutomatically captures steps as subtitle, and saves it as an artifact when a video is found for a failed test +## subtitles + +Automatically captures steps as subtitle, and saves it as an artifact when a video is found for a failed test #### Configuration @@ -1012,7 +1077,9 @@ plugins: { } ``` -## wdioWebdriverio services runner. +## wdio + +Webdriverio services runner. This plugin allows to run webdriverio services like: @@ -1021,19 +1088,22 @@ This plugin allows to run webdriverio services like: - testingbot - browserstack - appium - A complete list of all available services can be found on [webdriverio website][20]. + +A complete list of all available services can be found on [webdriverio website][20]. #### Setup 1. Install a webdriverio service 2. Enable `wdio` plugin in config 3. Add service name to `services` array inside wdio plugin config. - See examples below: + +See examples below: #### Selenium Standalone Service Install ` @wdio/selenium-standalone-service` package, as [described here][21]. It is important to make sure it is compatible with current webdriverio version. + Enable `wdio` plugin in plugins list and add `selenium-standalone` service: ```js @@ -1050,6 +1120,7 @@ plugins: { Install `@wdio/sauce-service` package, as [described here][22]. It is important to make sure it is compatible with current webdriverio version. + Enable `wdio` plugin in plugins list and add `sauce` service: ```js @@ -1064,7 +1135,9 @@ plugins: { } ``` -\*\*\*In the same manner additional services from webdriverio can be installed, enabled, and configured. +--- + +In the same manner additional services from webdriverio can be installed, enabled, and configured. #### Configuration diff --git a/lib/codecept.js b/lib/codecept.js index 7953b20a0..06752f593 100644 --- a/lib/codecept.js +++ b/lib/codecept.js @@ -1,5 +1,6 @@ const { existsSync, readFileSync } = require('fs') const { globSync } = require('glob') +const shuffle = require('lodash.shuffle') const fsPath = require('path') const { resolve } = require('path') @@ -180,6 +181,10 @@ class Codecept { }) } } + + if (this.opts.shuffle) { + this.testFiles = shuffle(this.testFiles) + } } /** diff --git a/lib/container.js b/lib/container.js index 68b26ecce..13a4337c4 100644 --- a/lib/container.js +++ b/lib/container.js @@ -28,6 +28,7 @@ let container = { translation: {}, /** @type {Result | null} */ result: null, + sharedKeys: new Set() // Track keys shared via share() function } /** @@ -174,6 +175,7 @@ class Container { container.translation = loadTranslation() container.proxySupport = createSupportObjects(newSupport) container.plugins = newPlugins + container.sharedKeys = new Set() // Clear shared keys asyncHelperPromise = Promise.resolve() store.actor = null debug('container cleared') @@ -197,7 +199,13 @@ class Container { * @param {Object} options - set {local: true} to not share among workers */ static share(data, options = {}) { - Container.append({ support: data }) + // Instead of using append which replaces the entire container, + // directly update the support object to maintain proxy references + Object.assign(container.support, data) + + // Track which keys were explicitly shared + Object.keys(data).forEach(key => container.sharedKeys.add(key)) + if (!options.local) { WorkerStorage.share(data) } @@ -396,10 +404,11 @@ function createSupportObjects(config) { {}, { has(target, key) { - return keys.includes(key) + return keys.includes(key) || container.sharedKeys.has(key) }, ownKeys() { - return keys + // Return both original config keys and explicitly shared keys + return [...new Set([...keys, ...container.sharedKeys])] }, getOwnPropertyDescriptor(target, prop) { return { @@ -409,6 +418,10 @@ function createSupportObjects(config) { } }, get(target, key) { + // First check if this is an explicitly shared property + if (container.sharedKeys.has(key) && key in container.support) { + return container.support[key] + } return lazyLoad(key) }, }, diff --git a/lib/helper/Mochawesome.js b/lib/helper/Mochawesome.js index 9c20cd1d7..0f45ff723 100644 --- a/lib/helper/Mochawesome.js +++ b/lib/helper/Mochawesome.js @@ -1,4 +1,3 @@ -let addMochawesomeContext let currentTest let currentSuite @@ -16,7 +15,8 @@ class Mochawesome extends Helper { disableScreenshots: false, } - addMochawesomeContext = require('mochawesome/addContext') + this._addContext = require('mochawesome/addContext') + this._createConfig(config) } @@ -44,28 +44,27 @@ class Mochawesome extends Helper { if (this.options.disableScreenshots) return let fileName // Get proper name if we are fail on hook - if (test.ctx.test.type === 'hook') { + if (test.ctx?.test?.type === 'hook') { currentTest = { test: test.ctx.test } // ignore retries if we are in hook test._retries = -1 fileName = clearString(`${test.title}_${currentTest.test.title}`) } else { currentTest = { test } - fileName = `${testToFileName(test)}` + fileName = testToFileName(test) } if (this.options.uniqueScreenshotNames) { - const uuid = test.uuid || test.ctx.test.uuid - fileName = `${fileName.substring(0, 10)}_${uuid}` + fileName = testToFileName(test, { unique: true }) } if (test._retries < 1 || test._retries === test.retryNum) { fileName = `${fileName}.failed.png` - return addMochawesomeContext(currentTest, fileName) + return this._addContext(currentTest, fileName) } } addMochawesomeContext(context) { if (currentTest === '') currentTest = { test: currentSuite.ctx.test } - return addMochawesomeContext(currentTest, context) + return this._addContext(currentTest, context) } } diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js index 194a17d16..dfc7b855d 100644 --- a/lib/helper/Playwright.js +++ b/lib/helper/Playwright.js @@ -2377,15 +2377,19 @@ class Playwright extends Helper { if (this.options.recordVideo && this.page && this.page.video()) { test.artifacts.video = saveVideoForPage(this.page, `${test.title}.failed`) for (const sessionName in this.sessionPages) { - test.artifacts[`video_${sessionName}`] = saveVideoForPage(this.sessionPages[sessionName], `${test.title}_${sessionName}.failed`) + if (sessionName === '') continue + test.artifacts[`video_${sessionName}`] = saveVideoForPage(this.sessionPages[sessionName], `${sessionName}_${test.title}.failed`) } } if (this.options.trace) { test.artifacts.trace = await saveTraceForContext(this.browserContext, `${test.title}.failed`) for (const sessionName in this.sessionPages) { - if (!this.sessionPages[sessionName].context) continue - test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context, `${test.title}_${sessionName}.failed`) + if (sessionName === '') continue + const sessionPage = this.sessionPages[sessionName] + const sessionContext = sessionPage.context() + if (!sessionContext || !sessionContext.tracing) continue + test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(sessionContext, `${sessionName}_${test.title}.failed`) } } @@ -2399,7 +2403,8 @@ class Playwright extends Helper { if (this.options.keepVideoForPassedTests) { test.artifacts.video = saveVideoForPage(this.page, `${test.title}.passed`) for (const sessionName of Object.keys(this.sessionPages)) { - test.artifacts[`video_${sessionName}`] = saveVideoForPage(this.sessionPages[sessionName], `${test.title}_${sessionName}.passed`) + if (sessionName === '') continue + test.artifacts[`video_${sessionName}`] = saveVideoForPage(this.sessionPages[sessionName], `${sessionName}_${test.title}.passed`) } } else { this.page @@ -2414,8 +2419,11 @@ class Playwright extends Helper { if (this.options.trace) { test.artifacts.trace = await saveTraceForContext(this.browserContext, `${test.title}.passed`) for (const sessionName in this.sessionPages) { - if (!this.sessionPages[sessionName].context) continue - test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context, `${test.title}_${sessionName}.passed`) + if (sessionName === '') continue + const sessionPage = this.sessionPages[sessionName] + const sessionContext = sessionPage.context() + if (!sessionContext || !sessionContext.tracing) continue + test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(sessionContext, `${sessionName}_${test.title}.passed`) } } } else { @@ -3883,9 +3891,18 @@ function saveVideoForPage(page, name) { async function saveTraceForContext(context, name) { if (!context) return if (!context.tracing) return - const fileName = `${`${global.output_dir}${pathSeparator}trace${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.zip` - await context.tracing.stop({ path: fileName }) - return fileName + try { + const fileName = `${`${global.output_dir}${pathSeparator}trace${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.zip` + await context.tracing.stop({ path: fileName }) + return fileName + } catch (err) { + // Handle the case where tracing was not started or context is invalid + if (err.message && err.message.includes('Must start tracing before stopping')) { + // Tracing was never started on this context, silently skip + return null + } + throw err + } } async function highlightActiveElement(element) { diff --git a/lib/mocha/asyncWrapper.js b/lib/mocha/asyncWrapper.js index 354cb35ed..a5061b0c2 100644 --- a/lib/mocha/asyncWrapper.js +++ b/lib/mocha/asyncWrapper.js @@ -121,9 +121,19 @@ module.exports.injected = function (fn, suite, hookName) { const errHandler = err => { recorder.session.start('teardown') recorder.cleanAsyncErr() - if (hookName == 'before' || hookName == 'beforeSuite') suiteTestFailedHookError(suite, err, hookName) - if (hookName === 'after') suite.eachTest(test => event.emit(event.test.after, test)) - if (hookName === 'afterSuite') event.emit(event.suite.after, suite) + if (['before', 'beforeSuite'].includes(hookName)) { + suiteTestFailedHookError(suite, err, hookName) + } + if (hookName === 'after') { + suiteTestFailedHookError(suite, err, hookName) + suite.eachTest(test => { + event.emit(event.test.after, test) + }) + } + if (hookName === 'afterSuite') { + suiteTestFailedHookError(suite, err, hookName) + event.emit(event.suite.after, suite) + } recorder.add(() => doneFn(err)) } diff --git a/lib/mocha/gherkin.js b/lib/mocha/gherkin.js index bcb3e63d8..904d51a9b 100644 --- a/lib/mocha/gherkin.js +++ b/lib/mocha/gherkin.js @@ -107,7 +107,7 @@ module.exports = (text, file) => { ) continue } - if (child.scenario && (currentLanguage ? currentLanguage.contexts.ScenarioOutline.includes(child.scenario.keyword) : child.scenario.keyword === 'Scenario Outline')) { + if (child.scenario && (currentLanguage ? currentLanguage.contexts.ScenarioOutline === child.scenario.keyword : child.scenario.keyword === 'Scenario Outline')) { for (const examples of child.scenario.examples) { const fields = examples.tableHeader.cells.map(c => c.value) for (const example of examples.tableBody) { diff --git a/lib/mocha/test.js b/lib/mocha/test.js index 14c202085..7ff53721d 100644 --- a/lib/mocha/test.js +++ b/lib/mocha/test.js @@ -135,9 +135,19 @@ function cloneTest(test) { return deserializeTest(serializeTest(test)) } -function testToFileName(test, suffix = '') { +/** + * Get a filename from the test object + * @param {CodeceptJS.Test} test + * @param {Object} options + * @param {string} options.suffix Add a suffix to the filename + * @param {boolean} options.unique Add a unique suffix to the file + * + * @returns {string} the filename + */ +function testToFileName(test, { suffix = '', unique = false } = {}) { let fileName = test.title + if (unique) fileName = `${fileName}_${test?.uid || Math.floor(new Date().getTime() / 1000)}` if (suffix) fileName = `${fileName}_${suffix}` // remove tags with empty string (disable for now) // fileName = fileName.replace(/\@\w+/g, '') @@ -151,6 +161,7 @@ function testToFileName(test, suffix = '') { // fileName = `${clearString(test.parent.title)}_${fileName}` // } fileName = clearString(fileName).slice(0, 100) + return fileName } diff --git a/lib/pause.js b/lib/pause.js index c4ba0a0e1..de887a08a 100644 --- a/lib/pause.js +++ b/lib/pause.js @@ -175,7 +175,12 @@ async function parseInput(cmd) { output.print(output.styles.success(' OK '), cmd) } if (cmd?.startsWith('I.grab')) { - output.print(output.styles.debug(val)) + try { + output.print(output.styles.debug(JSON.stringify(val, null, 2))) + } catch (err) { + output.print(output.styles.error(' ERROR '), 'Failed to stringify result:', err.message) + output.print(output.styles.error(' RAW VALUE '), String(val)) + } } history.push(cmd) // add command to history when successful diff --git a/lib/plugin/commentStep.js b/lib/plugin/commentStep.js index b368f3ca1..6d6601bb5 100644 --- a/lib/plugin/commentStep.js +++ b/lib/plugin/commentStep.js @@ -7,7 +7,7 @@ let currentCommentStep const defaultGlobalName = '__' /** - * @deprecated + * This plugin is **deprecated**, use `Section` instead. * * Add descriptive nested steps for your tests: * diff --git a/lib/plugin/screenshotOnFail.js b/lib/plugin/screenshotOnFail.js index aeba20834..04e855f92 100644 --- a/lib/plugin/screenshotOnFail.js +++ b/lib/plugin/screenshotOnFail.js @@ -86,7 +86,7 @@ module.exports = function (config) { let fileName if (options.uniqueScreenshotNames && test) { - fileName = `${testToFileName(test, _getUUID(test))}.failed.png` + fileName = `${testToFileName(test, { unique: true })}.failed.png` } else { fileName = `${testToFileName(test)}.failed.png` } @@ -137,12 +137,4 @@ module.exports = function (config) { true, ) }) - - function _getUUID(test) { - if (test.uid) { - return test.uid - } - - return Math.floor(new Date().getTime() / 1000) - } } diff --git a/lib/workerStorage.js b/lib/workerStorage.js index 8c5fdbf5e..2e7a5c6c5 100644 --- a/lib/workerStorage.js +++ b/lib/workerStorage.js @@ -7,7 +7,8 @@ const invokeWorkerListeners = (workerObj) => { const { threadId } = workerObj; workerObj.on('message', (messageData) => { if (messageData.event === shareEvent) { - share(messageData.data); + const Container = require('./container'); + Container.share(messageData.data); } }); workerObj.on('exit', () => { diff --git a/package.json b/package.json index 14ce78aca..3a09734e4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codeceptjs", - "version": "3.7.3", + "version": "3.7.4", "description": "Supercharged End 2 End Testing Framework for NodeJS", "keywords": [ "acceptance", @@ -75,15 +75,15 @@ "publish-beta": "./runok.js publish:next-beta-version" }, "dependencies": { - "@codeceptjs/configure": "1.0.3", + "@codeceptjs/configure": "1.0.6", "@codeceptjs/helper": "2.0.4", "@cucumber/cucumber-expressions": "18", - "@cucumber/gherkin": "32", + "@cucumber/gherkin": "32.1.2", "@cucumber/messages": "27.2.0", "@xmldom/xmldom": "0.9.8", "acorn": "8.14.1", "arrify": "3.0.0", - "axios": "1.8.3", + "axios": "1.11.0", "chalk": "4.1.2", "cheerio": "^1.0.0", "commander": "11.1.0", @@ -95,83 +95,85 @@ "figures": "3.2.0", "fn-args": "4.0.0", "fs-extra": "11.3.0", - "glob": ">=9.0.0 <12", "fuse.js": "^7.0.0", + "glob": ">=9.0.0 <12", "html-minifier-terser": "7.2.0", "inquirer": "8.2.6", "invisi-data": "^1.0.0", "joi": "17.13.3", "js-beautify": "1.15.4", "lodash.clonedeep": "4.5.0", + "lodash.shuffle": "4.2.0", "lodash.merge": "4.6.2", "mkdirp": "3.0.1", - "mocha": "11.1.0", - "monocart-coverage-reports": "2.12.3", + "mocha": "11.6.0", + "monocart-coverage-reports": "2.12.6", "ms": "2.1.3", "ora-classic": "5.4.2", "parse-function": "5.6.10", - "parse5": "7.2.1", + "parse5": "7.3.0", "promise-retry": "1.1.1", "resq": "1.11.0", "sprintf-js": "1.1.3", "uuid": "11.1.0" }, "optionalDependencies": { - "@codeceptjs/detox-helper": "1.1.7" + "@codeceptjs/detox-helper": "1.1.8" }, "devDependencies": { "@apollo/server": "^4", - "@codeceptjs/expect-helper": "^1.0.1", + "@codeceptjs/expect-helper": "^1.0.2", "@codeceptjs/mock-request": "0.3.1", - "@eslint/eslintrc": "3.3.0", - "@eslint/js": "9.22.0", - "@faker-js/faker": "9.6.0", + "@eslint/eslintrc": "3.3.1", + "@eslint/js": "9.31.0", + "@faker-js/faker": "9.8.0", "@pollyjs/adapter-puppeteer": "6.0.6", "@pollyjs/core": "6.0.6", - "@types/chai": "5.2.0", + "@types/chai": "5.2.2", "@types/inquirer": "9.0.7", - "@types/node": "22.13.10", - "@wdio/sauce-service": "9.12.0", + "@types/node": "24.0.10", + "@wdio/sauce-service": "9.12.5", "@wdio/selenium-standalone-service": "8.15.0", - "@wdio/utils": "9.11.0", + "@wdio/utils": "9.15.0", "@xmldom/xmldom": "0.9.8", "chai": "^4.0.0", "chai-as-promised": "7.1.2", "chai-subset": "1.6.0", "documentation": "14.0.3", - "electron": "35.0.1", - "eslint": "^9.21.0", - "eslint-plugin-import": "2.31.0", - "eslint-plugin-mocha": "10.5.0", - "expect": "29.7.0", - "express": "4.21.2", - "globals": "16.0.0", - "graphql": "16.10.0", + "electron": "37.2.3", + "eslint": "^9.24.0", + "eslint-plugin-import": "2.32.0", + "eslint-plugin-mocha": "11.1.0", + "expect": "30.0.5", + "express": "5.1.0", + "globals": "16.2.0", + "graphql": "16.11.0", "graphql-tag": "^2.12.6", "husky": "9.1.7", "inquirer-test": "2.0.1", "jsdoc": "^3.6.11", "jsdoc-typeof-plugin": "1.0.0", "json-server": "0.17.4", - "playwright": "1.51.0", + "mochawesome": "^7.1.3", + "playwright": "1.54.1", "prettier": "^3.3.2", - "puppeteer": "24.4.0", + "puppeteer": "24.15.0", "qrcode-terminal": "0.12.0", "rosie": "2.1.1", "runok": "0.9.3", - "semver": "7.7.1", - "sinon": "19.0.2", + "semver": "7.7.2", + "sinon": "21.0.0", "sinon-chai": "3.7.0", "testcafe": "3.7.2", - "ts-morph": "25.0.1", + "ts-morph": "26.0.0", "ts-node": "10.9.2", - "tsd": "^0.31.0", + "tsd": "^0.33.0", "tsd-jsdoc": "2.5.0", - "typedoc": "0.28.0", - "typedoc-plugin-markdown": "4.5.0", - "typescript": "5.8.2", + "typedoc": "0.28.10", + "typedoc-plugin-markdown": "4.8.1", + "typescript": "5.8.3", "wdio-docker-service": "3.2.1", - "webdriverio": "9.12.0", + "webdriverio": "9.12.5", "xml2js": "0.6.2", "xpath": "0.0.34" }, diff --git a/runok.js b/runok.js index 62fb7ca5c..07d2a0b4e 100755 --- a/runok.js +++ b/runok.js @@ -45,8 +45,6 @@ module.exports = { async docsPlugins() { // generate documentation for plugins - // broken for now - return await npx(`documentation build lib/plugin/*.js -o docs/plugins.md ${documentjsCliArgs}`) await replaceInFile('docs/plugins.md', cfg => { diff --git a/test/acceptance/config_test.js b/test/acceptance/config_test.js index 919a384e4..d8415aad5 100644 --- a/test/acceptance/config_test.js +++ b/test/acceptance/config_test.js @@ -32,7 +32,7 @@ Scenario('change config 5 @WebDriverIO @Puppeteer @Playwright', ({ I }) => { Scenario('make API call and check response @Playwright', ({ I }) => { I.amOnPage('/') - I.makeApiRequest('get', 'https://reqres.in/api/users?page=2') + I.makeApiRequest('get', 'https://reqres.in/api/users?page=2', { headers: {'x-api-key': 'reqres-free-v1'}}) I.seeResponseCodeIsSuccessful() }) diff --git a/test/data/sandbox/i18n/codecept.bdd.pt-br.js b/test/data/sandbox/i18n/codecept.bdd.pt-br.js new file mode 100644 index 000000000..0eb2d6020 --- /dev/null +++ b/test/data/sandbox/i18n/codecept.bdd.pt-br.js @@ -0,0 +1,19 @@ +exports.config = { + tests: './*_no_test.js', + timeout: 10000, + output: '../output', + helpers: { + BDD: { + require: '../support/bdd_helper.js', + }, + }, + gherkin: { + features: './features/examples.pt-br.feature', + steps: ['./features/step_definitions/my_steps.pt-br.js'], + }, + include: {}, + bootstrap: false, + mocha: {}, + name: 'sandbox', + translation: 'pt-BR', +} diff --git a/test/data/sandbox/i18n/features/examples.pt-br.feature b/test/data/sandbox/i18n/features/examples.pt-br.feature new file mode 100644 index 000000000..9c1523f58 --- /dev/null +++ b/test/data/sandbox/i18n/features/examples.pt-br.feature @@ -0,0 +1,19 @@ +#language: pt + +@i18n +Funcionalidade: Teste de Cenário e Esquema do Cenário + + Cenário: Cenário simples + Dado que inicio meu teste + Quando faço algo + Então acontece alguma coisa + + @i18n + Esquema do Cenário: Cenário com exemplos + Dado que estou com o usuário "" + Quando faço algo com o usuário + Então acontece alguma coisa + Exemplos: + | usuário | + | Um | + | Dois | diff --git a/test/data/sandbox/i18n/features/step_definitions/my_steps.pt-br.js b/test/data/sandbox/i18n/features/step_definitions/my_steps.pt-br.js new file mode 100644 index 000000000..b80224505 --- /dev/null +++ b/test/data/sandbox/i18n/features/step_definitions/my_steps.pt-br.js @@ -0,0 +1,25 @@ +const I = actor() + +Given('que inicio meu teste', function () { + // Simula início do teste +}) + +When('faço algo', function () { + // Simula ação +}) + +Then('acontece alguma coisa', function () { + // Simula verificação +}) + +Given('que estou com o usuário {string}', function (usuario) { + // Simula usuário +}) + +When('faço algo com o usuário', function () { + // Simula ação com usuário +}) + +Then('acontece alguma coisa', function () { + // Simula verificação +}) diff --git a/test/data/sandbox/workers-proxy-issue/README.md b/test/data/sandbox/workers-proxy-issue/README.md new file mode 100644 index 000000000..e876b60d8 --- /dev/null +++ b/test/data/sandbox/workers-proxy-issue/README.md @@ -0,0 +1,48 @@ +# Test Suite for Issue #5066 Fix + +This directory contains tests that validate the fix for **Issue #5066: Unable to inject data between workers because of proxy object**. + +## Test Files + +### `proxy_test.js` +Basic tests that verify the core functionality of `share()` and `inject()` functions: +- Basic data sharing with primitive types (strings, numbers) +- Complex nested data structures (objects, arrays) +- Property access patterns that should work after the fix + +### `final_test.js` +Comprehensive end-to-end validation test that covers: +- Multiple data types and structures +- Data overriding scenarios +- Deep nested property access +- Key enumeration functionality +- Real-world usage patterns + +## Running the Tests + +### Single-threaded execution: +```bash +npx codeceptjs run proxy_test.js +npx codeceptjs run final_test.js +``` + +### Multi-worker execution (tests worker communication): +```bash +npx codeceptjs run-workers 2 proxy_test.js +npx codeceptjs run-workers 2 final_test.js +``` + +## What the Fix Addresses + +1. **Circular Dependency Error**: Fixed "Support object undefined is not defined" error in `workerStorage.js` +2. **Proxy System Enhancement**: Updated container proxy system to handle dynamically shared data +3. **Worker Communication**: Ensured data sharing works correctly between worker threads +4. **Key Enumeration**: Made sure `Object.keys(inject())` shows shared properties + +## Expected Results + +All tests should pass in both single-threaded and multi-worker modes, demonstrating that: +- `share({ data })` correctly shares data between workers +- `inject()` returns a proxy object with proper access to shared data +- Both direct property access and nested object traversal work correctly +- Key enumeration shows all shared properties diff --git a/test/data/sandbox/workers-proxy-issue/codecept.conf.js b/test/data/sandbox/workers-proxy-issue/codecept.conf.js new file mode 100644 index 000000000..cc2767e51 --- /dev/null +++ b/test/data/sandbox/workers-proxy-issue/codecept.conf.js @@ -0,0 +1,10 @@ +exports.config = { + tests: './proxy_test.js', + output: './output', + helpers: { + FileSystem: {} + }, + include: {}, + mocha: {}, + name: 'workers-proxy-issue', +}; diff --git a/test/data/sandbox/workers-proxy-issue/final_test.js b/test/data/sandbox/workers-proxy-issue/final_test.js new file mode 100644 index 000000000..dd280654c --- /dev/null +++ b/test/data/sandbox/workers-proxy-issue/final_test.js @@ -0,0 +1,60 @@ +const assert = require('assert'); + +Feature('Complete validation for issue #5066 fix'); + +Scenario('End-to-end worker data sharing validation', () => { + console.log('=== Testing complete data sharing workflow ==='); + + // Test 1: Basic data sharing + share({ + message: 'Hello from main thread', + config: { timeout: 5000, retries: 3 }, + users: ['alice', 'bob', 'charlie'] + }); + + const data = inject(); + + // Verify all property types work correctly + assert.strictEqual(data.message, 'Hello from main thread', 'String property should work'); + assert.strictEqual(data.config.timeout, 5000, 'Nested object property should work'); + assert.strictEqual(data.config.retries, 3, 'Nested object property should work'); + assert(Array.isArray(data.users), 'Array property should work'); + assert.strictEqual(data.users.length, 3, 'Array length should work'); + assert.strictEqual(data.users[0], 'alice', 'Array access should work'); + + // Test 2: Data overriding + share({ message: 'Updated message' }); + const updatedData = inject(); + assert.strictEqual(updatedData.message, 'Updated message', 'Data override should work'); + assert.strictEqual(updatedData.config.timeout, 5000, 'Previous data should persist'); + + // Test 3: Complex nested structures + share({ + testSuite: { + name: 'E2E Tests', + tests: [ + { name: 'Login test', status: 'passed', data: { user: 'admin', pass: 'secret' } }, + { name: 'Checkout test', status: 'failed', error: 'Timeout occurred' } + ], + metadata: { + browser: 'chrome', + version: '91.0', + viewport: { width: 1920, height: 1080 } + } + } + }); + + const complexData = inject(); + assert.strictEqual(complexData.testSuite.name, 'E2E Tests', 'Deep nested string should work'); + assert.strictEqual(complexData.testSuite.tests[0].data.user, 'admin', 'Very deep nested access should work'); + assert.strictEqual(complexData.testSuite.metadata.viewport.width, 1920, 'Very deep nested number should work'); + + // Test 4: Key enumeration + const allKeys = Object.keys(inject()); + assert(allKeys.includes('message'), 'Keys should include shared properties'); + assert(allKeys.includes('testSuite'), 'Keys should include all shared properties'); + + console.log('✅ ALL TESTS PASSED - Issue #5066 is completely fixed!'); + console.log('✅ Workers can now share and inject data without circular dependency errors'); + console.log('✅ Proxy objects work correctly for both direct and nested property access'); +}); diff --git a/test/data/sandbox/workers-proxy-issue/proxy_test.js b/test/data/sandbox/workers-proxy-issue/proxy_test.js new file mode 100644 index 000000000..519e65498 --- /dev/null +++ b/test/data/sandbox/workers-proxy-issue/proxy_test.js @@ -0,0 +1,42 @@ +const assert = require('assert'); + +Feature('Fix for issue #5066: Unable to inject data between workers because of proxy object'); + +Scenario('Basic share and inject functionality', () => { + console.log('Testing basic share() and inject() functionality...'); + + // This is the basic pattern that should work after the fix + const originalData = { message: 'Hello', count: 42 }; + share(originalData); + + const injectedData = inject(); + console.log('Shared data keys:', Object.keys(originalData)); + console.log('Injected data keys:', Object.keys(injectedData)); + + // These assertions should pass after the fix + assert.strictEqual(injectedData.message, 'Hello', 'String property should be accessible'); + assert.strictEqual(injectedData.count, 42, 'Number property should be accessible'); + + console.log('✅ SUCCESS: Basic share/inject works!'); +}); + +Scenario('Complex nested data structures', () => { + console.log('Testing complex nested data sharing...'); + + const testDataJson = { + users: [{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }], + settings: { theme: 'dark', language: 'en' } + }; + + share({ testDataJson }); + + const data = inject(); + + // These should work after the fix + assert(data.testDataJson, 'testDataJson should be accessible'); + assert(Array.isArray(data.testDataJson.users), 'users should be an array'); + assert.strictEqual(data.testDataJson.users[0].name, 'John', 'Should access nested user data'); + assert.strictEqual(data.testDataJson.settings.theme, 'dark', 'Should access nested settings'); + + console.log('✅ SUCCESS: Complex nested data works!'); +}); diff --git a/test/helper/Playwright_test.js b/test/helper/Playwright_test.js index ea78a97f4..f02e4a6ec 100644 --- a/test/helper/Playwright_test.js +++ b/test/helper/Playwright_test.js @@ -1489,6 +1489,172 @@ describe('Playwright - Video & Trace & HAR', () => { expect(test.artifacts.trace).to.include(path.join(global.output_dir, 'trace')) expect(test.artifacts.har).to.include(path.join(global.output_dir, 'har')) }) + + it('checks that video and trace are recorded for sessions', async () => { + // Reset test artifacts + test.artifacts = {} + + await I.amOnPage('about:blank') + await I.executeScript(() => (document.title = 'Main Session')) + + // Create a session and perform actions + const session = I._session() + const sessionName = 'test_session' + I.activeSessionName = sessionName + + // Start session and get context + const sessionContext = await session.start(sessionName, {}) + I.sessionPages[sessionName] = (await sessionContext.pages())[0] + + // Simulate session actions + await I.sessionPages[sessionName].goto('about:blank') + await I.sessionPages[sessionName].evaluate(() => (document.title = 'Session Test')) + + // Trigger failure to save artifacts + await I._failed(test) + + // Check main session artifacts + assert(test.artifacts) + expect(Object.keys(test.artifacts)).to.include('trace') + expect(Object.keys(test.artifacts)).to.include('video') + + // Check session-specific artifacts with correct naming convention + const sessionVideoKey = `video_${sessionName}` + const sessionTraceKey = `trace_${sessionName}` + + expect(Object.keys(test.artifacts)).to.include(sessionVideoKey) + expect(Object.keys(test.artifacts)).to.include(sessionTraceKey) + + // Verify file naming convention: session name comes first + // The file names should contain the session name at the beginning + expect(test.artifacts[sessionVideoKey]).to.include(sessionName) + expect(test.artifacts[sessionTraceKey]).to.include(sessionName) + + // Cleanup + await sessionContext.close() + delete I.sessionPages[sessionName] + }) + + it('handles sessions with long test titles correctly', async () => { + // Create a test with a very long title to test truncation behavior + const longTest = { + title: + 'this_is_a_very_long_test_title_that_would_cause_issues_with_file_naming_when_session_names_are_appended_at_the_end_instead_of_the_beginning_which_could_lead_to_collisions_between_different_sessions_writing_to_the_same_file_path_due_to_truncation', + artifacts: {}, + } + + await I.amOnPage('about:blank') + + // Create multiple sessions with different names + const session1 = I._session() + const session2 = I._session() + const sessionName1 = 'session_one' + const sessionName2 = 'session_two' + + I.activeSessionName = sessionName1 + const sessionContext1 = await session1.start(sessionName1, {}) + I.sessionPages[sessionName1] = (await sessionContext1.pages())[0] + + I.activeSessionName = sessionName2 + const sessionContext2 = await session2.start(sessionName2, {}) + I.sessionPages[sessionName2] = (await sessionContext2.pages())[0] + + // Trigger failure to save artifacts + await I._failed(longTest) + + // Check that different sessions have different file paths + const session1VideoKey = `video_${sessionName1}` + const session2VideoKey = `video_${sessionName2}` + const session1TraceKey = `trace_${sessionName1}` + const session2TraceKey = `trace_${sessionName2}` + + expect(longTest.artifacts[session1VideoKey]).to.not.equal(longTest.artifacts[session2VideoKey]) + expect(longTest.artifacts[session1TraceKey]).to.not.equal(longTest.artifacts[session2TraceKey]) + + // Verify that session names are present in filenames (indicating the naming fix works) + expect(longTest.artifacts[session1VideoKey]).to.include(sessionName1) + expect(longTest.artifacts[session2VideoKey]).to.include(sessionName2) + expect(longTest.artifacts[session1TraceKey]).to.include(sessionName1) + expect(longTest.artifacts[session2TraceKey]).to.include(sessionName2) + + // Cleanup + await sessionContext1.close() + await sessionContext2.close() + delete I.sessionPages[sessionName1] + delete I.sessionPages[sessionName2] + }) + + it('skips main session in session artifacts processing', async () => { + // Reset test artifacts + test.artifacts = {} + + await I.amOnPage('about:blank') + + // Simulate having a main session (empty string name) in sessionPages + I.sessionPages[''] = I.page + + // Create a regular session + const session = I._session() + const sessionName = 'regular_session' + I.activeSessionName = sessionName + const sessionContext = await session.start(sessionName, {}) + I.sessionPages[sessionName] = (await sessionContext.pages())[0] + + // Trigger failure to save artifacts + await I._failed(test) + + // Check that main session artifacts are present (not duplicated) + expect(Object.keys(test.artifacts)).to.include('trace') + expect(Object.keys(test.artifacts)).to.include('video') + + // Check that regular session artifacts are present + expect(Object.keys(test.artifacts)).to.include(`video_${sessionName}`) + expect(Object.keys(test.artifacts)).to.include(`trace_${sessionName}`) + + // Check that there are no duplicate main session artifacts with empty key + expect(Object.keys(test.artifacts)).to.not.include('video_') + expect(Object.keys(test.artifacts)).to.not.include('trace_') + + // Cleanup + await sessionContext.close() + delete I.sessionPages[sessionName] + delete I.sessionPages[''] + }) + + it('gracefully handles tracing errors for invalid session contexts', async () => { + // Reset test artifacts + test.artifacts = {} + + await I.amOnPage('about:blank') + + // Create a real session that we can manipulate + const session = I._session() + const sessionName = 'error_session' + I.activeSessionName = sessionName + const sessionContext = await session.start(sessionName, {}) + I.sessionPages[sessionName] = (await sessionContext.pages())[0] + + // Manually stop tracing to create the error condition + try { + await sessionContext.tracing.stop() + } catch (e) { + // This may fail if tracing wasn't started, which is fine + } + + // Now when _failed is called, saveTraceForContext should handle the tracing error gracefully + await I._failed(test) + + // Main artifacts should still be created + expect(Object.keys(test.artifacts)).to.include('trace') + expect(Object.keys(test.artifacts)).to.include('video') + + // Session video should still be created despite tracing error + expect(Object.keys(test.artifacts)).to.include(`video_${sessionName}`) + + // Cleanup + await sessionContext.close() + delete I.sessionPages[sessionName] + }) }) describe('Playwright - HAR', () => { before(() => { diff --git a/test/rest/REST_test.js b/test/rest/REST_test.js index f996caa09..317fdd333 100644 --- a/test/rest/REST_test.js +++ b/test/rest/REST_test.js @@ -150,7 +150,7 @@ describe('REST', () => { }) it('should be able to parse JSON responses', async () => { - await I.sendGetRequest('https://reqres.in/api/comments/1') + await I.sendGetRequest('https://reqres.in/api/comments/1', { 'x-api-key': 'reqres-free-v1'}) await jsonResponse.seeResponseCodeIsSuccessful() await jsonResponse.seeResponseContainsKeys(['data', 'support']) }) diff --git a/test/runner/bdd_test.js b/test/runner/bdd_test.js index ae0fb37fd..863fe9fa4 100644 --- a/test/runner/bdd_test.js +++ b/test/runner/bdd_test.js @@ -356,7 +356,6 @@ When(/^I define a step with a \\( paren and a "(.*?)" string$/, () => { it('should run feature files in NL', done => { exec(config_run_config('codecept.bdd.nl.js') + ' --steps --grep "@i18n"', (err, stdout, stderr) => { - console.log(stdout) stdout.should.include('On Gegeven: ik heb een product met een prijs van 10$ in mijn winkelwagen') stdout.should.include('On En: de korting voor bestellingen van meer dan $20 is 10 %') stdout.should.include('On Wanneer: ik naar de kassa ga') @@ -373,5 +372,21 @@ When(/^I define a step with a \\( paren and a "(.*?)" string$/, () => { done() }) }) + + it('should run feature files in PT-BR', done => { + exec(config_run_config('codecept.bdd.pt-br.js') + ' --steps --grep "@i18n"', (err, stdout, stderr) => { + stdout.should.include('On Dado: que inicio meu teste') + stdout.should.include('On Quando: faço algo') + stdout.should.include('On Então: acontece alguma coisa') + stdout.should.include('On Dado: que estou com o usuário "um"') + stdout.should.include('On Quando: faço algo com o usuário') + stdout.should.include('On Dado: que estou com o usuário "dois"') + stdout.should.include('Cenário simples') + stdout.should.include('Cenário com exemplos') + stdout.should.match(/OK \| 3 passed/) + assert(!err) + done() + }) + }) }) }) diff --git a/test/unit/plugin/screenshotOnFail_test.js b/test/unit/plugin/screenshotOnFail_test.js index 1cda0ec3f..c17f47c5c 100644 --- a/test/unit/plugin/screenshotOnFail_test.js +++ b/test/unit/plugin/screenshotOnFail_test.js @@ -10,6 +10,8 @@ const event = require('../../../lib/event') const recorder = require('../../../lib/recorder') const { createTest } = require('../../../lib/mocha/test') const { deserializeSuite } = require('../../../lib/mocha/suite') +const MochawesomeHelper = require('../../../lib/helper/Mochawesome') + let screenshotSaved describe('screenshotOnFail', () => { @@ -101,5 +103,41 @@ describe('screenshotOnFail', () => { await recorder.promise() expect(!screenshotSaved.called).is.ok }) + + it('should have the same unique file name as the mochawesome helper when the uuid is present', async () => { + screenshotOnFail({ uniqueScreenshotNames: true }) + const test = createTest('test1') + test.uid = '1234' + + const helper = new MochawesomeHelper({ uniqueScreenshotNames: true }) + const spy = sinon.spy(helper, '_addContext') + helper._failed(test) + + event.dispatcher.emit(event.test.failed, test) + await recorder.promise() + + const screenshotFileName = screenshotSaved.getCall(0).args[0] + expect(spy.getCall(0).args[1]).to.equal(screenshotFileName) + }) + + it('should have the same unique file name as the mochawesome helper when the uuid is not present', async () => { + screenshotOnFail({ uniqueScreenshotNames: true }) + const test = createTest('test1') + + // Use sinon to stub Date.now to return consistent timestamp + const clock = sinon.useFakeTimers(1755596785000) // Fixed timestamp + + const helper = new MochawesomeHelper({ uniqueScreenshotNames: true }) + const spy = sinon.spy(helper, '_addContext') + helper._failed(test) + + event.dispatcher.emit(event.test.failed, test) + await recorder.promise() + + clock.restore() + + const screenshotFileName = screenshotSaved.getCall(0).args[0] + expect(spy.getCall(0).args[1]).to.equal(screenshotFileName) + }) // TODO: write more tests for different options }) diff --git a/test/unit/workerStorage_test.js b/test/unit/workerStorage_test.js new file mode 100644 index 000000000..8a1f95750 --- /dev/null +++ b/test/unit/workerStorage_test.js @@ -0,0 +1,35 @@ +const { expect } = require('expect'); +const WorkerStorage = require('../../lib/workerStorage'); +const { Worker } = require('worker_threads'); +const event = require('../../lib/event'); + +describe('WorkerStorage', () => { + it('should handle share message correctly without circular dependency', (done) => { + // Create a mock worker to test the functionality + const mockWorker = { + threadId: 'test-thread-1', + on: (eventName, callback) => { + if (eventName === 'message') { + // Simulate receiving a share message + setTimeout(() => { + callback({ event: 'share', data: { testKey: 'testValue' } }); + done(); + }, 10); + } + }, + postMessage: () => {} + }; + + // Add the mock worker to storage + WorkerStorage.addWorker(mockWorker); + }); + + it('should not crash when sharing data', () => { + const testData = { user: 'test', password: '123' }; + + // This should not throw an error + expect(() => { + WorkerStorage.share(testData); + }).not.toThrow(); + }); +}); diff --git a/typings/jsdoc.conf.js b/typings/jsdoc.conf.js index 6b38e805f..466b44834 100644 --- a/typings/jsdoc.conf.js +++ b/typings/jsdoc.conf.js @@ -9,6 +9,7 @@ module.exports = { './lib/container.js', './lib/data/table.js', './lib/data/dataTableArgument.js', + './lib/effects.js', './lib/event.js', './lib/index.js', './lib/locator.js', diff --git a/typings/tests/global-variables.types.ts b/typings/tests/global-variables.types.ts index aa21f2cca..2d2a0a512 100644 --- a/typings/tests/global-variables.types.ts +++ b/typings/tests/global-variables.types.ts @@ -83,3 +83,13 @@ expectType(AfterSuite((args) => { // @ts-ignore expectType(args.I) })) + +// @ts-ignore +expectType>(tryTo(() => { + return true; +})); + +// @ts-ignore +expectType>(tryTo(async () => { + return false; +}));