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'