From 023bbc9f8a5d9cd826e908079907ca2ab1d5eb36 Mon Sep 17 00:00:00 2001 From: kobenguyent Date: Mon, 6 Nov 2023 15:02:43 +0100 Subject: [PATCH 1/3] feat(puppeteer): support trace recording --- docs/helpers/Puppeteer.md | 16 ++++++++++-- lib/helper/Puppeteer.js | 46 +++++++++++++++++++++++++++++++-- test/helper/Puppeteer_test.js | 48 +++++++++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+), 4 deletions(-) diff --git a/docs/helpers/Puppeteer.md b/docs/helpers/Puppeteer.md index 26b5e43b0..ede7bc4ff 100644 --- a/docs/helpers/Puppeteer.md +++ b/docs/helpers/Puppeteer.md @@ -44,6 +44,8 @@ Type: [object][4] - `disableScreenshots` **[boolean][20]?** don't save screenshot on failure. - `fullPageScreenshots` **[boolean][20]?** make full page screenshots on failure. - `uniqueScreenshotNames` **[boolean][20]?** option to prevent screenshot override if you have scenarios with the same name in different suites. +- `trace` **[boolean][20]?** record [tracing information][25] with screenshots. +- `keepTraceForPassedTests` **[boolean][20]?** save trace for passed tests. - `keepBrowserState` **[boolean][20]?** keep browser state between tests when `restart` is set to false. - `keepCookies` **[boolean][20]?** keep cookies between tests when `restart` is set to false. - `waitForAction` **[number][11]?** how long to wait after click, doubleClick or PressKey actions in ms. Default: 100. @@ -55,11 +57,19 @@ Type: [object][4] - `userAgent` **[string][6]?** user-agent string. - `manualStart` **[boolean][20]?** do not start browser before a test, start it manually inside a helper with `this.helpers["Puppeteer"]._startBrowser()`. - `browser` **[string][6]?** can be changed to `firefox` when using [puppeteer-firefox][2]. -- `chrome` **[object][4]?** pass additional [Puppeteer run options][25]. +- `chrome` **[object][4]?** pass additional [Puppeteer run options][26]. - `highlightElement` **[boolean][20]?** highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose). +#### Trace Recording Customization + +Trace recording provides complete information on test execution and includes screenshots, and network requests logged during run. +Traces will be saved to `output/trace` + +- `trace`: enables trace recording for failed tests; trace are saved into `output/trace` folder +- `keepTraceForPassedTests`: - save trace for passed tests + #### Example #1: Wait for 0 network connections. ```js @@ -2263,4 +2273,6 @@ Returns **[Promise][7]<void>** automatically synchronized promise through #re [24]: https://codecept.io/react -[25]: https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#puppeteerlaunchoptions +[25]: https://pptr.dev/api/puppeteer.tracing + +[26]: https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#puppeteerlaunchoptions diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js index eb9284f6c..05d96dfbb 100644 --- a/lib/helper/Puppeteer.js +++ b/lib/helper/Puppeteer.js @@ -4,6 +4,7 @@ const fsExtra = require('fs-extra'); const path = require('path'); const Helper = require('@codeceptjs/helper'); +const { v4: uuidv4 } = require('uuid'); const Locator = require('../locator'); const recorder = require('../recorder'); const store = require('../store'); @@ -19,6 +20,7 @@ const { fileExists, chunkArray, toCamelCase, + clearString, convertCssPropertiesToCamelCase, screenshotOutputFolder, getNormalizedKeyAttributeValue, @@ -57,6 +59,8 @@ const consoleLogStore = new Console(); * @prop {boolean} [disableScreenshots=false] - don't save screenshot on failure. * @prop {boolean} [fullPageScreenshots=false] - make full page screenshots on failure. * @prop {boolean} [uniqueScreenshotNames=false] - option to prevent screenshot override if you have scenarios with the same name in different suites. + * @prop {boolean} [trace=false] - record [tracing information](https://pptr.dev/api/puppeteer.tracing) with screenshots. + * @prop {boolean} [keepTraceForPassedTests=false] - save trace for passed tests. * @prop {boolean} [keepBrowserState=false] - keep browser state between tests when `restart` is set to false. * @prop {boolean} [keepCookies=false] - keep cookies between tests when `restart` is set to false. * @prop {number} [waitForAction=100] - how long to wait after click, doubleClick or PressKey actions in ms. Default: 100. @@ -92,6 +96,14 @@ const config = {}; * * * + * #### Trace Recording Customization + * + * Trace recording provides complete information on test execution and includes screenshots, and network requests logged during run. + * Traces will be saved to `output/trace` + * + * * `trace`: enables trace recording for failed tests; trace are saved into `output/trace` folder + * * `keepTraceForPassedTests`: - save trace for passed tests + * * #### Example #1: Wait for 0 network connections. * * ```js @@ -281,8 +293,9 @@ class Puppeteer extends Helper { } } - async _before() { + async _before(test) { this.sessionPages = {}; + this.currentRunningTest = test; recorder.retry({ retries: 3, when: err => { @@ -646,6 +659,13 @@ class Puppeteer extends Helper { this.isAuthenticated = true; } } + const fileName = `${`${global.output_dir}${path.sep}trace${path.sep}${uuidv4()}_${clearString(this.currentRunningTest.title)}`.slice(0, 245)}.json`; + const dir = path.dirname(fileName); + if (!fileExists(dir)) fs.mkdirSync(dir); + if (this.options.trace) { + await this.page.tracing.start({ screenshots: true, path: fileName }); + this.currentRunningTest.artifacts.trace = fileName; + } await this.page.goto(url, { waitUntil: this.options.waitForNavigation }); @@ -1898,8 +1918,30 @@ class Puppeteer extends Helper { } } - async _failed() { + async _failed(test) { await this._withinEnd(); + + if (this.options.trace) { + await this.page.tracing.stop(); + const _traceName = this.currentRunningTest.artifacts.trace.replace('.json', '.failed.json'); + fs.renameSync(this.currentRunningTest.artifacts.trace, _traceName); + test.artifacts.trace = _traceName; + } + } + + async _passed(test) { + await this._withinEnd(); + + if (this.options.trace) { + await this.page.tracing.stop(); + if (this.options.keepTraceForPassedTests) { + const _traceName = this.currentRunningTest.artifacts.trace.replace('.json', '.passed.json'); + fs.renameSync(this.currentRunningTest.artifacts.trace, _traceName); + test.artifacts.trace = _traceName; + } else { + fs.unlinkSync(this.currentRunningTest.artifacts.trace); + } + } } /** diff --git a/test/helper/Puppeteer_test.js b/test/helper/Puppeteer_test.js index befad29ef..f1c8b06e4 100644 --- a/test/helper/Puppeteer_test.js +++ b/test/helper/Puppeteer_test.js @@ -4,6 +4,7 @@ const path = require('path'); const puppeteer = require('puppeteer'); +const fs = require('fs'); const TestHelper = require('../support/TestHelper'); const Puppeteer = require('../../lib/helper/Puppeteer'); @@ -11,6 +12,8 @@ const AssertionFailedError = require('../../lib/assert/error'); const webApiTests = require('./webapi'); const FileSystem = require('../../lib/helper/FileSystem'); const Secret = require('../../lib/secret'); +const Playwright = require('../../lib/helper/Playwright'); +const { deleteDir } = require('../../lib/utils'); global.codeceptjs = require('../../lib'); let I; @@ -1037,3 +1040,48 @@ describe('Puppeteer (remote browser)', function () { }); }); }); + +describe('Puppeteer - Trace', () => { + const test = { title: 'a failed test', artifacts: {} }; + before(() => { + global.codecept_dir = path.join(__dirname, '/../data'); + global.output_dir = path.join(`${__dirname}/../data/output`); + + I = new Puppeteer({ + url: siteUrl, + windowSize: '500x700', + show: false, + trace: true, + }); + I._init(); + return I._beforeSuite(); + }); + + beforeEach(async () => { + webApiTests.init({ + I, siteUrl, + }); + deleteDir(path.join(global.output_dir, 'trace')); + return I._before(test).then(() => { + page = I.page; + browser = I.browser; + }); + }); + + afterEach(async () => { + return I._after(); + }); + + it('checks that trace is recorded', async () => { + await I.amOnPage('/'); + await I.dontSee('this should be an error'); + await I.click('More info'); + await I.dontSee('this should be an error'); + await I._failed(test); + assert(test.artifacts); + expect(Object.keys(test.artifacts)).to.include('trace'); + + assert.ok(fs.existsSync(test.artifacts.trace)); + expect(test.artifacts.trace).to.include(path.join(global.output_dir, 'trace')); + }); +}); From 06debd770ca3e3b73784b7e4fbb678c8165f3492 Mon Sep 17 00:00:00 2001 From: kobenguyent Date: Mon, 6 Nov 2023 15:26:24 +0100 Subject: [PATCH 2/3] fix: UTs --- lib/helper/Puppeteer.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js index 05d96dfbb..76f1ac2b5 100644 --- a/lib/helper/Puppeteer.js +++ b/lib/helper/Puppeteer.js @@ -659,10 +659,11 @@ class Puppeteer extends Helper { this.isAuthenticated = true; } } - const fileName = `${`${global.output_dir}${path.sep}trace${path.sep}${uuidv4()}_${clearString(this.currentRunningTest.title)}`.slice(0, 245)}.json`; - const dir = path.dirname(fileName); - if (!fileExists(dir)) fs.mkdirSync(dir); + if (this.options.trace) { + const fileName = `${`${global.output_dir}${path.sep}trace${path.sep}${uuidv4()}_${clearString(this.currentRunningTest.title)}`.slice(0, 245)}.json`; + const dir = path.dirname(fileName); + if (!fileExists(dir)) fs.mkdirSync(dir); await this.page.tracing.start({ screenshots: true, path: fileName }); this.currentRunningTest.artifacts.trace = fileName; } From 8d0fbae74ded8d71568b9ebd3f5be45f6ec30d2c Mon Sep 17 00:00:00 2001 From: KobeNguyenT <7845001+kobenguyent@users.noreply.github.com> Date: Mon, 6 Nov 2023 18:10:49 +0100 Subject: [PATCH 3/3] fix: delete unused import --- test/helper/Puppeteer_test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/helper/Puppeteer_test.js b/test/helper/Puppeteer_test.js index f1c8b06e4..e4ec3a50e 100644 --- a/test/helper/Puppeteer_test.js +++ b/test/helper/Puppeteer_test.js @@ -12,7 +12,6 @@ const AssertionFailedError = require('../../lib/assert/error'); const webApiTests = require('./webapi'); const FileSystem = require('../../lib/helper/FileSystem'); const Secret = require('../../lib/secret'); -const Playwright = require('../../lib/helper/Playwright'); const { deleteDir } = require('../../lib/utils'); global.codeceptjs = require('../../lib');