diff --git a/__tests__/options.test.ts b/__tests__/options.test.ts index 7baa5a0d..b52f0d5c 100644 --- a/__tests__/options.test.ts +++ b/__tests__/options.test.ts @@ -48,6 +48,7 @@ describe('options', () => { params: {}, screenshots: 'on', }); + expect(await normalizeOptions(cliArgs)).toMatchObject({ dryRun: true, environment: 'test', @@ -187,4 +188,14 @@ describe('options', () => { expect(Buffer.isBuffer(t.passphrase)).toBeFalsy(); }); }); + + it('normalizes outputDom option', async () => { + // Test that outputDom is false by default + let options = await normalizeOptions({}); + expect(options.outputDom).toBe(false); + + // Test that outputDom is set when true in CLI args + options = await normalizeOptions({ outputDom: true }); + expect(options.outputDom).toBe(true); + }); }); diff --git a/__tests__/reporters/base.test.ts b/__tests__/reporters/base.test.ts index 0a5d7c63..8ad7dbf4 100644 --- a/__tests__/reporters/base.test.ts +++ b/__tests__/reporters/base.test.ts @@ -84,4 +84,40 @@ describe('base reporter', () => { reporter.onEnd(); expect((await readAndCloseStream()).toString()).toMatchSnapshot(); }); + + it('should not print DOM when outputDom is disabled', async () => { + const j1 = tJourney(); + reporter.onJourneyStart(j1, { timestamp }); + reporter.onJourneyEnd(j1, { + timestamp, + browserDelay: 0, + options: {}, + pageDom: 'Test DOM', + }); + reporter.onEnd(); + const output = await readAndCloseStream(); + expect(output).not.toContain('PAGE DOM START'); + expect(output).not.toContain('Test DOM'); + }); + + it('should print DOM when outputDom is enabled', async () => { + const reporter = new BaseReporter({ + fd: fs.openSync(dest, 'w'), + outputDom: true, + }); + stream = reporter.stream; + + const j1 = tJourney(); + reporter.onJourneyStart(j1, { timestamp }); + reporter.onJourneyEnd(j1, { + timestamp, + browserDelay: 0, + options: {}, + pageDom: 'Test DOM', + }); + reporter.onEnd(); + const output = await readAndCloseStream(); + expect(output).toContain('PAGE DOM START'); + expect(output).toContain('Test DOM'); + }); }); diff --git a/__tests__/utils/jest-global-setup.ts b/__tests__/utils/jest-global-setup.ts index 83719e5e..6400fd16 100644 --- a/__tests__/utils/jest-global-setup.ts +++ b/__tests__/utils/jest-global-setup.ts @@ -27,6 +27,12 @@ import { spawn } from 'child_process'; import { wsEndpoint } from './test-config'; module.exports = async () => { + // Unset the SYNTHETICS_API_KEY to ensure it doesn't affect tests + if (process.env.SYNTHETICS_API_KEY) { + console.log('Unsetting SYNTHETICS_API_KEY environment variable for tests'); + delete process.env.SYNTHETICS_API_KEY; + } + if (wsEndpoint) { return new Promise((resolve, reject) => { console.log(`\nRunning BrowserService ${wsEndpoint}`); diff --git a/src/cli.ts b/src/cli.ts index 8115b31d..54bbb2fc 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -122,6 +122,7 @@ program '--ignore-https-errors', 'ignores any HTTPS errors in sites being tested, including ones related to unrecognized certs or signatures. This can be insecure!' ) + .option('--output-dom', 'output the full page DOM when a journey ends') .option( '--quiet-exit-code', 'always return 0 as an exit code status, regardless of test pass / fail. Only return > 0 exit codes on internal errors where the suite could not be run' diff --git a/src/common_types.ts b/src/common_types.ts index 476ccb8e..3501a39e 100644 --- a/src/common_types.ts +++ b/src/common_types.ts @@ -238,6 +238,7 @@ export type CliArgs = BaseArgs & { headless?: boolean; capability?: Array; ignoreHttpsErrors?: boolean; + outputDom?: boolean; }; export type RunOptions = BaseArgs & { @@ -248,6 +249,7 @@ export type RunOptions = BaseArgs & { filmstrips?: boolean; environment?: string; networkConditions?: NetworkConditions; + outputDom?: boolean; reporter?: BuiltInReporterName | ReporterInstance; grepOpts?: GrepOptions; }; @@ -322,6 +324,7 @@ export type JourneyEndResult = JourneyStartResult & JourneyResult & { browserDelay: number; options: RunOptions; + pageDom?: string; }; export type StepEndResult = StepResult; diff --git a/src/core/runner.ts b/src/core/runner.ts index a1b6b486..51b294d2 100644 --- a/src/core/runner.ts +++ b/src/core/runner.ts @@ -178,12 +178,12 @@ export default class Runner implements RunnerInfo { * Set up the corresponding reporter and fallback * to default reporter if not provided */ - const { reporter, outfd, dryRun } = options; + const { reporter, outfd, dryRun, outputDom } = options; const Reporter = typeof reporter === 'function' ? reporter : reporters[reporter] || reporters['default']; - this.#reporter = new Reporter({ fd: outfd, dryRun }); + this.#reporter = new Reporter({ fd: outfd, dryRun, outputDom }); } async #runBeforeAllHook(args: HooksArgs) { @@ -338,12 +338,29 @@ export default class Runner implements RunnerInfo { pOutput.browserconsole, journey.status ); + + // Get the current page DOM if available + let pageDom: string | undefined; + if (this.#driver && this.#driver.page) { + try { + // Get the last active page + const pages = this.#driver.context.pages(); + const page = pages[pages.length - 1]; + if (page) { + pageDom = await page.content(); + } + } catch (error) { + log('Error capturing page DOM: ' + error); + } + } + await this.#reporter?.onJourneyEnd?.(journey, { browserDelay: this.#browserDelay, timestamp: getTimestamp(), options, networkinfo: pOutput.networkinfo, browserconsole: bConsole, + pageDom, }); await Gatherer.endRecording(); await Gatherer.dispose(this.#driver); diff --git a/src/options.ts b/src/options.ts index 355b5491..7a92717c 100644 --- a/src/options.ts +++ b/src/options.ts @@ -145,6 +145,7 @@ export async function normalizeOptions( } options.screenshots = cliArgs.screenshots ?? 'on'; + options.outputDom = cliArgs.outputDom ?? false; break; case 'push': /** diff --git a/src/reporters/base.ts b/src/reporters/base.ts index 69aa8d14..cce8d3d5 100644 --- a/src/reporters/base.ts +++ b/src/reporters/base.ts @@ -43,6 +43,7 @@ export default class BaseReporter implements Reporter { stream: SonicBoom; fd: number; dryRun: boolean; + outputDom: boolean; metrics = { succeeded: 0, failed: 0, @@ -53,6 +54,7 @@ export default class BaseReporter implements Reporter { constructor(options: ReporterOptions = {}) { this.fd = options.fd || process.stdout.fd; this.dryRun = options.dryRun ?? false; + this.outputDom = options.outputDom ?? false; /** * minLength is set to 1 byte to make sure we flush the * content even if its the last byte on the stream buffer @@ -84,7 +86,7 @@ export default class BaseReporter implements Reporter { } /* eslint-disable @typescript-eslint/no-unused-vars */ - onJourneyEnd(journey: Journey, {}: JourneyEndResult) { + onJourneyEnd(journey: Journey, { pageDom }: JourneyEndResult) { const { failed, succeeded, skipped } = this.metrics; const total = failed + succeeded + skipped; /** @@ -95,6 +97,24 @@ export default class BaseReporter implements Reporter { const message = renderError(serializeError(journey.error)); this.write(indent(message) + '\n'); } + + // Log the full page DOM when available and outputDom option is enabled + if (this.outputDom && pageDom) { + this.write(indent('\n--- PAGE DOM START ---')); + // If DOM is too large, truncate it to a reasonable size + const maxDomLength = 100000; // Limit to ~100KB + if (pageDom.length > maxDomLength) { + this.write( + indent( + pageDom.substring(0, maxDomLength) + + '\n... (DOM truncated due to large size)' + ) + ); + } else { + this.write(indent(pageDom)); + } + this.write(indent('--- PAGE DOM END ---\n')); + } } onEnd() { diff --git a/src/reporters/index.ts b/src/reporters/index.ts index 0cd42ae8..9a340f12 100644 --- a/src/reporters/index.ts +++ b/src/reporters/index.ts @@ -39,6 +39,7 @@ export type ReporterOptions = { fd?: number; colors?: boolean; dryRun?: boolean; + outputDom?: boolean; }; export type BuiltInReporterName = | 'default'