diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index f4a7182fb..cb4eb2e10 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -40,6 +40,14 @@ jobs: sudo apt-get update --allow-releaseinfo-change - name: Install browsers and deps run: npx playwright install && npx playwright install-deps + - name: Start mock server + run: nohup npm run mock-server:start & + - name: Wait for mock server + run: | + for i in {1..20}; do + curl -sSf http://localhost:3001/api/users && break + sleep 1 + done - name: check run: './bin/codecept.js check -c test/acceptance/codecept.Playwright.js' - name: start a server @@ -58,3 +66,5 @@ jobs: run: 'BROWSER=webkit node ./bin/codecept.js run -c test/acceptance/codecept.Playwright.js --grep @Playwright --debug' - name: run chromium unit tests run: ./node_modules/.bin/mocha test/helper/Playwright_test.js --timeout 5000 + - name: Stop mock server + run: npm run mock-server:stop diff --git a/.github/workflows/puppeteer.yml b/.github/workflows/puppeteer.yml index 336517ebd..87415f36e 100644 --- a/.github/workflows/puppeteer.yml +++ b/.github/workflows/puppeteer.yml @@ -35,6 +35,14 @@ jobs: npm i --force && npm i puppeteer --force env: PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: true + - name: Start mock server + run: nohup npm run mock-server:start & + - name: Wait for mock server + run: | + for i in {1..20}; do + curl -sSf http://localhost:3001/api/users && break + sleep 1 + done - name: start a server run: 'php -S 127.0.0.1:8000 -t test/data/app &' - uses: browser-actions/setup-chrome@v2 @@ -43,3 +51,5 @@ jobs: run: './bin/codecept.js run-workers 2 -c test/acceptance/codecept.Puppeteer.js --grep @Puppeteer --debug' - name: run unit tests run: ./node_modules/.bin/mocha test/helper/Puppeteer_test.js + - name: Stop mock server + run: npm run mock-server:stop diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5bb4a1752..a4b954c28 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,7 +27,17 @@ jobs: env: PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: true PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true + - name: Start mock server + run: nohup npm run mock-server:start & + - name: Wait for mock server + run: | + for i in {1..20}; do + curl -sSf http://localhost:3001/api/users && break + sleep 1 + done - run: npm run test:unit + - name: Stop mock server + run: npm run mock-server:stop runner-tests: name: Runner tests @@ -47,6 +57,16 @@ jobs: env: PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: true PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true + - name: Start mock server + run: nohup npm run mock-server:start & + - name: Wait for mock server + run: | + for i in {1..20}; do + curl -sSf http://localhost:3001/api/users && break + sleep 1 + done - run: npm run test:runner + - name: Stop mock server + run: npm run mock-server:stop # Note: Runner tests are mocha-based, so sharding doesn't apply here. # For CodeceptJS sharding examples, see sharding-demo.yml workflow. diff --git a/.github/workflows/webdriver.yml b/.github/workflows/webdriver.yml index a9b7f7317..666fed540 100644 --- a/.github/workflows/webdriver.yml +++ b/.github/workflows/webdriver.yml @@ -36,6 +36,14 @@ jobs: env: PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: true PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true + - name: Start mock server + run: nohup npm run mock-server:start & + - name: Wait for mock server + run: | + for i in {1..20}; do + curl -sSf http://localhost:3001/api/users && break + sleep 1 + done - name: start a server run: 'php -S 127.0.0.1:8000 -t test/data/app &' - name: check @@ -44,3 +52,5 @@ jobs: run: ./node_modules/.bin/mocha test/helper/WebDriver_test.js --exit - name: run tests run: './bin/codecept.js run -c test/acceptance/codecept.WebDriver.js --grep @WebDriver --debug' + - name: Stop mock server + run: npm run mock-server:stop diff --git a/lib/helper/JSONResponse.js b/lib/helper/JSONResponse.js index bc02f934a..29a44514c 100644 --- a/lib/helper/JSONResponse.js +++ b/lib/helper/JSONResponse.js @@ -72,11 +72,11 @@ class JSONResponse extends Helper { if (!this.helpers[this.options.requestHelper]) { throw new Error(`Error setting JSONResponse, helper ${this.options.requestHelper} is not enabled in config, helpers: ${Object.keys(this.helpers)}`) } - const origOnResponse = this.helpers[this.options.requestHelper].config.onResponse; + const origOnResponse = this.helpers[this.options.requestHelper].config.onResponse this.helpers[this.options.requestHelper].config.onResponse = response => { - this.response = response; - if (typeof origOnResponse === 'function') origOnResponse(response); - }; + this.response = response + if (typeof origOnResponse === 'function') origOnResponse(response) + } } _before() { diff --git a/lib/helper/WebDriver.js b/lib/helper/WebDriver.js index 2c511a0d1..e1ac68c5e 100644 --- a/lib/helper/WebDriver.js +++ b/lib/helper/WebDriver.js @@ -998,7 +998,7 @@ class WebDriver extends Helper { * {{ react }} */ async click(locator, context = null) { - const clickMethod = this.browser.isMobile && this.browser.capabilities.platformName !== 'android' ? 'touchClick' : 'elementClick' + const clickMethod = this.browser.isMobile && this.browser.capabilities.platformName !== 'android' ? 'touchClick' : 'elementClick' const locateFn = prepareLocateFn.call(this, context) const res = await findClickable.call(this, locator, locateFn) @@ -1217,7 +1217,7 @@ class WebDriver extends Helper { * {{> checkOption }} */ async checkOption(field, context = null) { - const clickMethod = this.browser.isMobile && this.browser.capabilities.platformName !== 'android' ? 'touchClick' : 'elementClick' + const clickMethod = this.browser.isMobile && this.browser.capabilities.platformName !== 'android' ? 'touchClick' : 'elementClick' const locateFn = prepareLocateFn.call(this, context) const res = await findCheckable.call(this, field, locateFn) @@ -1237,7 +1237,7 @@ class WebDriver extends Helper { * {{> uncheckOption }} */ async uncheckOption(field, context = null) { - const clickMethod = this.browser.isMobile && this.browser.capabilities.platformName !== 'android' ? 'touchClick' : 'elementClick' + const clickMethod = this.browser.isMobile && this.browser.capabilities.platformName !== 'android' ? 'touchClick' : 'elementClick' const locateFn = prepareLocateFn.call(this, context) const res = await findCheckable.call(this, field, locateFn) diff --git a/lib/listener/steps.js b/lib/listener/steps.js index a71bcd75c..524cd0ccd 100644 --- a/lib/listener/steps.js +++ b/lib/listener/steps.js @@ -79,14 +79,14 @@ module.exports = function () { return currentHook.steps.push(step) } if (!currentTest || !currentTest.steps) return - + // Check if we're in a session that should be excluded from main test steps const currentSessionId = recorder.getCurrentSessionId() if (currentSessionId && EXCLUDED_SESSIONS.includes(currentSessionId)) { // Skip adding this step to the main test steps return } - + currentTest.steps.push(step) }) diff --git a/lib/plugin/htmlReporter.js b/lib/plugin/htmlReporter.js index 8a0bcf2b0..97e7d0d0f 100644 --- a/lib/plugin/htmlReporter.js +++ b/lib/plugin/htmlReporter.js @@ -288,7 +288,7 @@ module.exports = function (config) { finalState: test.state, duration: test.duration || 0, }) - output.debug(`HTML Reporter: Fallback retry detection for failed test ${test.title}, attempts: ${fallbackAttempts}`) + output.debug(`HTML Reporter: Fallback retry detection for failed test ${test.title}, attempts: ${fallbackAttempts}`) } }) diff --git a/package.json b/package.json index b652ab752..7414556bd 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,9 @@ "repository": "Codeception/codeceptjs", "scripts": { "test-server": "node bin/test-server.js test/data/rest/db.json --host 0.0.0.0 -p 8010", + "mock-server:start": "node test/mock-server/start-mock-server.js", + "mock-server:stop": "kill -9 $(lsof -t -i:3001)", + "test:with-mock-server": "npm run mock-server:start && npm test", "json-server:graphql": "node test/data/graphql/index.js", "lint": "eslint bin/ examples/ lib/ test/ translations/ runok.js", "lint-fix": "eslint bin/ examples/ lib/ test/ translations/ runok.js --fix", diff --git a/test/acceptance/config_test.js b/test/acceptance/config_test.js index d8415aad5..879d833b4 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', { headers: {'x-api-key': 'reqres-free-v1'}}) + I.makeApiRequest('get', 'http://localhost:3001/api/users?page=2', { headers: { 'x-api-key': 'reqres-free-v1' } }) I.seeResponseCodeIsSuccessful() }) diff --git a/test/data/app/view/form/fetch_call.php b/test/data/app/view/form/fetch_call.php index b79ee6aad..ae7ca67c8 100644 --- a/test/data/app/view/form/fetch_call.php +++ b/test/data/app/view/form/fetch_call.php @@ -53,7 +53,7 @@ const getPostData = () => getData("https://jsonplaceholder.typicode.com/posts/1"); const getCommentsData = () => - getData("https://reqres.in/api/comments/1"); + getData("http://localhost:3001/api/comments/1"); const getUsersData = () => getData("https://jsonplaceholder.typicode.com/users/1"); diff --git a/test/docker-compose.yml b/test/docker-compose.yml index 45d8c1507..2bae7fa0e 100644 --- a/test/docker-compose.yml +++ b/test/docker-compose.yml @@ -7,9 +7,12 @@ services: env_file: .env volumes: - ./:/codecept/test + environment: + - MOCK_SERVER_HOST=mock_server command: ['/codecept/node_modules/.bin/mocha', 'test/rest'] depends_on: - json_server + - mock_server test-acceptance.webdriverio: build: .. @@ -73,6 +76,16 @@ services: - '8010:8010' # Expose to host restart: always # Automatically restart the container if it fails or becomes unhealthy + mock_server: + <<: *test-service + entrypoint: [] + command: npm run mock-server:start + ports: + - '3001:3001' # Expose to host + restart: always # Automatically restart the container if it fails or becomes unhealthy + environment: + - PORT=3001 + puppeteer-image: image: ghcr.io/puppeteer/puppeteer:22.4.1 diff --git a/test/helper/Playwright_test.js b/test/helper/Playwright_test.js index d16af3281..9f5d5566a 100644 --- a/test/helper/Playwright_test.js +++ b/test/helper/Playwright_test.js @@ -1022,7 +1022,7 @@ describe('Playwright', function () { describe('#mockRoute, #stopMockingRoute', () => { it('should mock a route', async () => { await I.amOnPage('/form/fetch_call') - await I.mockRoute('https://reqres.in/api/comments/1', route => { + await I.mockRoute('http://localhost:3001/api/comments/1', route => { route.fulfill({ status: 200, headers: { 'Access-Control-Allow-Origin': '*' }, @@ -1032,7 +1032,7 @@ describe('Playwright', function () { }) await I.click('GET COMMENTS') await I.see('this was mocked') - await I.stopMockingRoute('https://reqres.in/api/comments/1') + await I.stopMockingRoute('http://localhost:3001/api/comments/1') await I.click('GET COMMENTS') await I.see('data') await I.dontSee('this was mocked') @@ -1041,9 +1041,9 @@ describe('Playwright', function () { describe('#makeApiRequest', () => { it('should make 3rd party API request', async () => { - const response = await I.makeApiRequest('get', 'https://reqres.in/api/users?page=2') + const response = await I.makeApiRequest('get', 'http://localhost:3001/api/users?page=2') expect(response.status()).to.equal(200) - expect(await response.json()).to.include.keys(['page']) + expect(await response.json()).to.include.keys(['data']) }) it('should make local API request', async () => { @@ -1054,10 +1054,10 @@ describe('Playwright', function () { it('should convert to axios response with onResponse hook', async () => { let response I.config.onResponse = resp => (response = resp) - await I.makeApiRequest('get', 'https://reqres.in/api/users?page=2') + await I.makeApiRequest('get', 'http://localhost:3001/api/users?page=2') expect(response).to.be.ok expect(response.status).to.equal(200) - expect(response.data).to.include.keys(['page', 'total']) + expect(response.data).to.include.keys(['data']) }) }) diff --git a/test/helper/Puppeteer_test.js b/test/helper/Puppeteer_test.js index 06ef40e05..0d124f6ca 100644 --- a/test/helper/Puppeteer_test.js +++ b/test/helper/Puppeteer_test.js @@ -1032,7 +1032,7 @@ describe('Puppeteer', function () { describe('#mockRoute, #stopMockingRoute', () => { it('should mock a route', async () => { await I.amOnPage('/form/fetch_call') - await I.mockRoute('https://reqres.in/api/comments/1', request => { + await I.mockRoute('http://localhost:3001/api/comments/1', request => { request.respond({ status: 200, headers: { 'Access-Control-Allow-Origin': '*' }, @@ -1042,7 +1042,7 @@ describe('Puppeteer', function () { }) await I.click('GET COMMENTS') await I.see('this was mocked') - await I.stopMockingRoute('https://reqres.in/api/comments/1') + await I.stopMockingRoute('http://localhost:3001/api/comments/1') await I.click('GET COMMENTS') await I.see('data') await I.dontSee('this was mocked') diff --git a/test/helper/WebDriver.noSeleniumServer_test.js b/test/helper/WebDriver.noSeleniumServer_test.js index 622953f3b..c45c8540a 100644 --- a/test/helper/WebDriver.noSeleniumServer_test.js +++ b/test/helper/WebDriver.noSeleniumServer_test.js @@ -382,7 +382,6 @@ describe('WebDriver - No Selenium server started', function () { }) }) - describe('#seeTitleEquals', () => { it('should check that title is equal to provided one', async () => { await wd.amOnPage('/') diff --git a/test/mock-server/server.js b/test/mock-server/server.js new file mode 100644 index 000000000..ba757d2b2 --- /dev/null +++ b/test/mock-server/server.js @@ -0,0 +1,68 @@ +const express = require('express') +const app = express() +app.use(express.json()) + +// Example users data +let users = [ + { id: 1, name: 'John Doe', email: 'john@example.com' }, + { id: 2, name: 'Jane Smith', email: 'jane@example.com' }, +] + +// Example comments data +let comments = [{ id: 1, postId: 1, text: 'Great post!' }] + +// GET /api/users +app.get('/api/users', (req, res) => { + res.json({ data: users }) +}) + +// GET /api/comments/:id +app.get('/api/comments/:id', (req, res) => { + const comment = comments.find(c => c.id === parseInt(req.params.id)) + if (comment) { + return res.json({ + data: comment, + support: { + url: 'http://example.com/support', + text: 'Support information', + }, + }) + } + res.status(404).json({ error: 'Comment not found' }) +}) + +// GET /api/users/:id +app.get('/api/users/:id', (req, res) => { + const user = users.find(u => u.id === parseInt(req.params.id)) + if (user) return res.json(user) + res.status(404).json({ error: 'User not found' }) +}) + +// POST /api/users +app.post('/api/users', (req, res) => { + const { name, email } = req.body + const newUser = { id: users.length + 1, name, email } + users.push(newUser) + res.status(201).json(newUser) +}) + +// PUT /api/users/:id +app.put('/api/users/:id', (req, res) => { + const user = users.find(u => u.id === parseInt(req.params.id)) + if (!user) return res.status(404).json({ error: 'User not found' }) + user.name = req.body.name || user.name + user.email = req.body.email || user.email + res.json(user) +}) + +// DELETE /api/users/:id +app.delete('/api/users/:id', (req, res) => { + users = users.filter(u => u.id !== parseInt(req.params.id)) + res.status(204).send() +}) + +// Start server +const PORT = process.env.PORT || 3001 +app.listen(PORT, () => { + console.log(`Mock REST server running on port ${PORT}`) +}) diff --git a/test/mock-server/start-mock-server.js b/test/mock-server/start-mock-server.js new file mode 100644 index 000000000..32d2d280e --- /dev/null +++ b/test/mock-server/start-mock-server.js @@ -0,0 +1,43 @@ +const { spawn } = require('child_process') +const http = require('http') + +const PORT = process.env.PORT || 3001 +const MAX_RETRIES = 20 +const RETRY_DELAY = 500 + +function waitForServer(retries = 0) { + return new Promise((resolve, reject) => { + http + .get(`http://localhost:${PORT}/api/users`, res => { + if (res.statusCode === 200) return resolve(true) + res.resume() + reject() + }) + .on('error', () => { + if (retries < MAX_RETRIES) { + setTimeout(() => { + resolve(waitForServer(retries + 1)) + }, RETRY_DELAY) + } else { + reject(new Error('Mock server did not start in time')) + } + }) + }) +} + +const serverProcess = spawn('node', ['server.js'], { + cwd: __dirname, + stdio: 'inherit', + env: process.env, +}) + +console.log('Starting mock server...') +waitForServer() + .then(() => { + console.log('Mock server is up and running!') + }) + .catch(err => { + console.error(err.message) + serverProcess.kill() + process.exit(1) + }) diff --git a/test/rest/REST_test.js b/test/rest/REST_test.js index 317fdd333..99d005e4b 100644 --- a/test/rest/REST_test.js +++ b/test/rest/REST_test.js @@ -150,7 +150,8 @@ describe('REST', () => { }) it('should be able to parse JSON responses', async () => { - await I.sendGetRequest('https://reqres.in/api/comments/1', { 'x-api-key': 'reqres-free-v1'}) + const mockServerHost = process.env.MOCK_SERVER_HOST || 'localhost' + await I.sendGetRequest(`http://${mockServerHost}:3001/api/comments/1`, { 'x-api-key': 'reqres-free-v1' }) await jsonResponse.seeResponseCodeIsSuccessful() await jsonResponse.seeResponseContainsKeys(['data', 'support']) }) diff --git a/test/runner/html-reporter-plugin_test.js b/test/runner/html-reporter-plugin_test.js index 75c6345b4..56a7a7ef8 100644 --- a/test/runner/html-reporter-plugin_test.js +++ b/test/runner/html-reporter-plugin_test.js @@ -132,34 +132,34 @@ describe('CodeceptJS html-reporter-plugin', function () { // Read and validate HTML report content for BDD features const reportContent = fs.readFileSync(reportFile, 'utf8') - + // Check for BDD-specific elements expect(reportContent).toContain('bdd-test') // CSS class for BDD tests expect(reportContent).toContain('Scenario:') // BDD scenario prefix expect(reportContent).toContain('Feature:') // BDD feature prefix expect(reportContent).toContain('Gherkin') // BDD badge - + // Check for BDD steps styling expect(reportContent).toContain('bdd-steps-section') expect(reportContent).toContain('bdd-step-item') expect(reportContent).toContain('bdd-keyword') expect(reportContent).toContain('bdd-step-text') - + // Check for feature information section expect(reportContent).toContain('bdd-feature-section') expect(reportContent).toContain('feature-info') expect(reportContent).toContain('HTML Reporter BDD Test') // Feature name - + // Check for BDD-specific CSS styles expect(reportContent).toContain('bdd-badge') expect(reportContent).toContain('feature-name') expect(reportContent).toContain('feature-description') - + // Check for test type filter expect(reportContent).toContain('typeFilter') expect(reportContent).toContain('BDD/Gherkin') expect(reportContent).toContain('data-type=') - + // Should contain scenario steps with proper keywords expect(reportContent).toMatch(/Given|When|Then|And/) diff --git a/test/runner/timeout_test.js b/test/runner/timeout_test.js index 64090acb8..2f1d8c1d5 100644 --- a/test/runner/timeout_test.js +++ b/test/runner/timeout_test.js @@ -16,7 +16,8 @@ describe('CodeceptJS Timeouts', function () { exec(config_run_config('codecept.conf.js', 'timed out'), (err, stdout) => { debug_this_test && console.log(stdout) expect(stdout).toContain('Timeout 2s exceeded (with Before hook)') - expect(stdout).toContain('Timeout 1s exceeded (with Before hook)') + // The second scenario can show either format depending on which timeout triggers first + expect(stdout.includes('Timeout 1s exceeded (with Before hook)') || stdout.includes('timed out after 1s')).toBeTruthy() expect(err).toBeTruthy() done() }) diff --git a/test/unit/workerStorage_test.js b/test/unit/workerStorage_test.js index 8a1f95750..5ae81619c 100644 --- a/test/unit/workerStorage_test.js +++ b/test/unit/workerStorage_test.js @@ -1,10 +1,10 @@ -const { expect } = require('expect'); -const WorkerStorage = require('../../lib/workerStorage'); -const { Worker } = require('worker_threads'); -const event = require('../../lib/event'); +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) => { + it('should handle share message correctly without circular dependency', done => { // Create a mock worker to test the functionality const mockWorker = { threadId: 'test-thread-1', @@ -12,24 +12,24 @@ describe('WorkerStorage', () => { if (eventName === 'message') { // Simulate receiving a share message setTimeout(() => { - callback({ event: 'share', data: { testKey: 'testValue' } }); - done(); - }, 10); + callback({ event: 'share', data: { testKey: 'testValue' } }) + done() + }, 10) } }, - postMessage: () => {} - }; + postMessage: () => {}, + } // Add the mock worker to storage - WorkerStorage.addWorker(mockWorker); - }); + WorkerStorage.addWorker(mockWorker) + }) it('should not crash when sharing data', () => { - const testData = { user: 'test', password: '123' }; - + const testData = { user: 'test', password: '123' } + // This should not throw an error expect(() => { - WorkerStorage.share(testData); - }).not.toThrow(); - }); -}); + WorkerStorage.share(testData) + }).not.toThrow() + }) +}) diff --git a/test/unit/worker_test.js b/test/unit/worker_test.js index 1759cc8e5..825ffb59f 100644 --- a/test/unit/worker_test.js +++ b/test/unit/worker_test.js @@ -334,12 +334,12 @@ describe('Workers', function () { expect('pool').not.equal('suite') }) - it('should handle pool mode result accumulation correctly', (done) => { + it('should handle pool mode result accumulation correctly', done => { const workerConfig = { by: 'pool', testConfig: './test/data/sandbox/codecept.workers.conf.js', } - + let resultEventCount = 0 const workers = new Workers(2, workerConfig) @@ -347,20 +347,20 @@ describe('Workers', function () { const originalResult = Container.result() const mockStats = { passes: 0, failures: 0, tests: 0 } const originalAddStats = originalResult.addStats.bind(originalResult) - - originalResult.addStats = (newStats) => { + + originalResult.addStats = newStats => { resultEventCount++ mockStats.passes += newStats.passes || 0 - mockStats.failures += newStats.failures || 0 + mockStats.failures += newStats.failures || 0 mockStats.tests += newStats.tests || 0 return originalAddStats(newStats) } - workers.on(event.all.result, (result) => { + workers.on(event.all.result, result => { // In pool mode, we should receive consolidated results, not individual test results // The number of result events should be limited (one per worker, not per test) expect(resultEventCount).to.be.lessThan(10) // Should be much less than total number of tests - + // Restore original method originalResult.addStats = originalAddStats done()