Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/playwright-core/src/client/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -817,7 +817,7 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
this._browserContext.setDefaultNavigationTimeout(0);
this._browserContext.setDefaultTimeout(0);
this._instrumentation?.onWillPause({ keepTestTimeout: !!_options?.__testHookKeepTestTimeout });
await this._closedOrCrashedScope.safeRace(this.context()._channel.pause());
await this._closedOrCrashedScope.safeRace(this.context()._channel.pause(_options));
this._browserContext.setDefaultNavigationTimeout(defaultNavigationTimeout);
this._browserContext.setDefaultTimeout(defaultTimeout);
}
Expand Down
1 change: 1 addition & 0 deletions packages/playwright-core/src/protocol/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1056,6 +1056,7 @@ scheme.BrowserContextEnableRecorderParams = tObject({
language: tOptional(tString),
mode: tOptional(tEnum(['inspecting', 'recording'])),
recorderMode: tOptional(tEnum(['default', 'api'])),
snippet: tOptional(tEnum(['standalone', 'addition'])),
pauseOnNextStatement: tOptional(tBoolean),
testIdAttributeName: tOptional(tString),
launchOptions: tOptional(tAny),
Expand Down
4 changes: 2 additions & 2 deletions packages/playwright-core/src/server/codegen/csharp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,9 +221,9 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
return formatter.format();
}

generateFooter(saveStorage: string | undefined): string {
generateFooter(options: LanguageGeneratorOptions): string {
const offset = this._mode === 'library' ? '' : ' ';
let storageStateLine = saveStorage ? `\n${offset}await context.StorageStateAsync(new()\n${offset}{\n${offset} Path = ${quote(saveStorage)}\n${offset}});\n` : '';
let storageStateLine = options.saveStorage ? `\n${offset}await context.StorageStateAsync(new()\n${offset}{\n${offset} Path = ${quote(options.saveStorage)}\n${offset}});\n` : '';
if (this._mode !== 'library')
storageStateLine += ` }\n}\n`;
return storageStateLine;
Expand Down
4 changes: 2 additions & 2 deletions packages/playwright-core/src/server/codegen/java.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,8 @@ export class JavaLanguageGenerator implements LanguageGenerator {
return formatter.format();
}

generateFooter(saveStorage: string | undefined): string {
const storageStateLine = saveStorage ? `\n context.storageState(new BrowserContext.StorageStateOptions().setPath(${quote(saveStorage)}));\n` : '';
generateFooter(options: LanguageGeneratorOptions): string {
const storageStateLine = options.saveStorage ? `\n context.storageState(new BrowserContext.StorageStateOptions().setPath(${quote(options.saveStorage)}));\n` : '';
if (this._mode === 'junit') {
return `${storageStateLine} }
}`;
Expand Down
16 changes: 10 additions & 6 deletions packages/playwright-core/src/server/codegen/javascript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,13 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
this._isTest = isTest;
}

generateAction(actionInContext: actions.ActionInContext): string {
generateAction(actionInContext: actions.ActionInContext, options: LanguageGeneratorOptions): string {
const action = actionInContext.action;
if (this._isTest && (action.name === 'openPage' || action.name === 'closePage'))
return '';

const pageAlias = actionInContext.frame.pageAlias;
const formatter = new JavaScriptFormatter(2);
const formatter = new JavaScriptFormatter(options.snippet === 'addition' ? 0 : 2);

if (action.name === 'openPage') {
formatter.add(`const ${pageAlias} = await context.newPage();`);
Expand Down Expand Up @@ -131,16 +131,20 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
return asLocator('javascript', selector);
}

generateHeader(options: LanguageGeneratorOptions): string {
generateHeader(options: LanguageGeneratorOptions): string | undefined {
if (options.snippet === 'addition')
return;
if (this._isTest)
return this.generateTestHeader(options);
return this.generateStandaloneHeader(options);
}

generateFooter(saveStorage: string | undefined): string {
generateFooter(options: LanguageGeneratorOptions): string | undefined {
if (options.snippet === 'addition')
return;
if (this._isTest)
return this.generateTestFooter(saveStorage);
return this.generateStandaloneFooter(saveStorage);
return this.generateTestFooter(options.saveStorage);
return this.generateStandaloneFooter(options.saveStorage);
}

generateTestHeader(options: LanguageGeneratorOptions): string {
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright-core/src/server/codegen/jsonl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export class JsonlLanguageGenerator implements LanguageGenerator {
return JSON.stringify(options);
}

generateFooter(saveStorage: string | undefined): string {
generateFooter(options: LanguageGeneratorOptions): string {
return '';
}
}
14 changes: 7 additions & 7 deletions packages/playwright-core/src/server/codegen/language.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,17 @@ import type * as actions from '@recorder/actions';

export function generateCode(actions: actions.ActionInContext[], languageGenerator: LanguageGenerator, options: LanguageGeneratorOptions) {
const header = languageGenerator.generateHeader(options);
const footer = languageGenerator.generateFooter(options.saveStorage);
const actionTexts = actions.map(a => generateActionText(languageGenerator, a, !!options.generateAutoExpect)).filter(Boolean) as string[];
const text = [header, ...actionTexts, footer].join('\n');
const footer = languageGenerator.generateFooter(options);
const actionTexts = actions.map(a => generateActionText(languageGenerator, a, options)).filter(Boolean) as string[];
const text = [header, ...actionTexts, footer].filter(Boolean).join('\n');
return { header, footer, actionTexts, text };
}

function generateActionText(generator: LanguageGenerator, action: actions.ActionInContext, generateAutoExpect: boolean): string | undefined {
let text = generator.generateAction(action);
function generateActionText(generator: LanguageGenerator, action: actions.ActionInContext, options: LanguageGeneratorOptions): string | undefined {
let text = generator.generateAction(action, options);
if (!text)
return;
if (generateAutoExpect && action.action.preconditionSelector) {
if (options.generateAutoExpect && action.action.preconditionSelector) {
const expectAction: actions.ActionInContext = {
frame: action.frame,
startTime: action.startTime,
Expand All @@ -42,7 +42,7 @@ function generateActionText(generator: LanguageGenerator, action: actions.Action
signals: [],
},
};
const expectText = generator.generateAction(expectAction);
const expectText = generator.generateAction(expectAction, options);
if (expectText)
text = expectText + '\n\n' + text;
}
Expand Down
6 changes: 3 additions & 3 deletions packages/playwright-core/src/server/codegen/python.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,11 +184,11 @@ def run(playwright: Playwright) -> None {
return formatter.format();
}

generateFooter(saveStorage: string | undefined): string {
generateFooter(options: LanguageGeneratorOptions): string {
if (this._isPyTest) {
return '';
} else if (this._isAsync) {
const storageStateLine = saveStorage ? `\n await context.storage_state(path=${quote(saveStorage)})` : '';
const storageStateLine = options.saveStorage ? `\n await context.storage_state(path=${quote(options.saveStorage)})` : '';
return `\n # ---------------------${storageStateLine}
await context.close()
await browser.close()
Expand All @@ -202,7 +202,7 @@ async def main() -> None:
asyncio.run(main())
`;
} else {
const storageStateLine = saveStorage ? `\n context.storage_state(path=${quote(saveStorage)})` : '';
const storageStateLine = options.saveStorage ? `\n context.storage_state(path=${quote(options.saveStorage)})` : '';
return `\n # ---------------------${storageStateLine}
context.close()
browser.close()
Expand Down
7 changes: 4 additions & 3 deletions packages/playwright-core/src/server/codegen/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,15 @@ export type LanguageGeneratorOptions = {
deviceName?: string;
saveStorage?: string;
generateAutoExpect?: boolean;
snippet?: 'standalone' | 'addition';
};

export interface LanguageGenerator {
id: string;
groupName: string;
name: string;
highlighter: Language;
generateHeader(options: LanguageGeneratorOptions): string;
generateAction(actionInContext: actions.ActionInContext): string;
generateFooter(saveStorage: string | undefined): string;
generateHeader(options: LanguageGeneratorOptions): string | undefined;
generateAction(actionInContext: actions.ActionInContext, options: LanguageGeneratorOptions): string;
generateFooter(options: LanguageGeneratorOptions): string | undefined;
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export class RecorderApp {
contextOptions: { ...params.contextOptions },
deviceName: params.device,
saveStorage: params.saveStorage,
snippet: params.snippet,
};

this._throttledOutputFile = params.outputFile ? new ThrottledFile(params.outputFile) : null;
Expand Down
1 change: 1 addition & 0 deletions packages/playwright/src/DEPS.list
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ common/
./mcp/sdk/
./mcp/test/
./transform/babelBundle.ts
./transform/babelHighlightUtils.ts
./agents/

[internalsForTest.ts]
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright/src/common/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export class FullConfigInternal {
fullyParallel: takeFirst(configCLIOverrides.fullyParallel, userConfig.fullyParallel, false),
globalSetup: this.globalSetups[0] ?? null,
globalTeardown: this.globalTeardowns[0] ?? null,
globalTimeout: takeFirst(configCLIOverrides.debug ? 0 : undefined, configCLIOverrides.globalTimeout, userConfig.globalTimeout, 0),
globalTimeout: takeFirst((configCLIOverrides.debug || configCLIOverrides.pause) ? 0 : undefined, configCLIOverrides.globalTimeout, userConfig.globalTimeout, 0),
grep: takeFirst(userConfig.grep, defaultGrep),
grepInvert: takeFirst(userConfig.grepInvert, null),
maxFailures: takeFirst(configCLIOverrides.debug ? 1 : undefined, configCLIOverrides.maxFailures, userConfig.maxFailures, 0),
Expand Down
1 change: 1 addition & 0 deletions packages/playwright/src/common/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type { SerializedCompilationCache } from '../transform/compilationCache'

export type ConfigCLIOverrides = {
debug?: boolean;
pause?: boolean;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could replace new TestRun(options=)

failOnFlakyTests?: boolean;
forbidOnly?: boolean;
fullyParallel?: boolean;
Expand Down
36 changes: 33 additions & 3 deletions packages/playwright/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ import { currentTestInfo } from './common/globals';
import { rootTestType } from './common/testType';
import { createCustomMessageHandler } from './mcp/test/browserBackend';
import { performTask } from './agents/performTask';
import { findTestEndPosition } from './transform/babelHighlightUtils';
import { filteredStackTrace } from './util';

import type { Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, ScreenshotMode, TestInfo, TestType, VideoMode } from '../types/test';
import type { Fixtures, Location, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, ScreenshotMode, TestInfo, TestType, VideoMode } from '../types/test';
import type { ContextReuseMode } from './common/config';
import type { TestInfoImpl, TestStepInternal } from './worker/testInfo';
import type { ClientInstrumentationListener } from '../../playwright-core/src/client/clientInstrumentation';
Expand Down Expand Up @@ -231,12 +233,37 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
});
}, { box: true }],

_setupContextOptions: [async ({ playwright, _combinedContextOptions, actionTimeout, navigationTimeout, testIdAttribute }, use, testInfo) => {
_setupContextOptions: [async ({ playwright, _combinedContextOptions, actionTimeout, navigationTimeout, testIdAttribute, headless }, use, _testInfo) => {
const testInfo = _testInfo as TestInfoImpl;
if (testIdAttribute)
playwrightLibrary.selectors.setTestIdAttribute(testIdAttribute);
testInfo.snapshotSuffix = process.platform;
if (debugMode() === 'inspector')
(testInfo as TestInfoImpl)._setDebugMode();
testInfo._setDebugMode();

if (testInfo._configInternal.configCLIOverrides.pause && !headless) {
testInfo._onTestPausedCallback = async () => {
const page = playwright._allPages()[0];
if (!page)
return;
await page.context()._enableRecorder({
snippet: 'addition',
testIdAttributeName: testIdAttribute,
});

let location: Location | undefined;
if (testInfo.error) {
if (testInfo.error.stack)
location = filteredStackTrace(testInfo.error.stack.split('\n'))[0];
} else {
const source = await fs.promises.readFile(testInfo.file, 'utf-8');
location = findTestEndPosition(source, testInfo);
}
location ??= testInfo;
await page.pause({ location: location } as any);
};
}


playwright._defaultContextOptions = _combinedContextOptions;
playwright._defaultContextTimeout = actionTimeout || 0;
Expand Down Expand Up @@ -276,6 +303,9 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
return;
}

if (data.apiName === 'page.pause' && data.frames.length === 0 && channel.params?.location)
Copy link
Member Author

@Skn0tt Skn0tt Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is hella hacky. i'm all ears for a better solution

data.frames.push(channel.params.location);

// In the general case, create a step for each api call and connect them through the stepId.
const step = testInfo._addStep({
location: data.frames[0],
Expand Down
5 changes: 4 additions & 1 deletion packages/playwright/src/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ function overridesFromOptions(options: { [key: string]: any }): ConfigCLIOverrid
globalTimeout: options.globalTimeout ? parseInt(options.globalTimeout, 10) : undefined,
maxFailures: options.x ? 1 : (options.maxFailures ? parseInt(options.maxFailures, 10) : undefined),
outputDir: options.output ? path.resolve(process.cwd(), options.output) : undefined,
pause: options.pause ? true : undefined,
quiet: options.quiet ? options.quiet : undefined,
repeatEach: options.repeatEach ? parseInt(options.repeatEach, 10) : undefined,
retries: options.retries ? parseInt(options.retries, 10) : undefined,
Expand Down Expand Up @@ -325,6 +326,7 @@ function overridesFromOptions(options: { [key: string]: any }): ConfigCLIOverrid
overrides.use = { headless: false };
if (!options.ui && options.debug) {
overrides.debug = true;
overrides.pause = true;
process.env.PWDEBUG = '1';
}
if (!options.ui && options.trace) {
Expand Down Expand Up @@ -387,7 +389,7 @@ const kTraceModes: TraceMode[] = ['on', 'off', 'on-first-retry', 'on-all-retries
const testOptions: [string, { description: string, choices?: string[], preset?: string }][] = [
/* deprecated */ ['--browser <browser>', { description: `Browser to use for tests, one of "all", "chromium", "firefox" or "webkit" (default: "chromium")` }],
['-c, --config <file>', { description: `Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}"` }],
['--debug', { description: `Run tests with Playwright Inspector. Shortcut for "PWDEBUG=1" environment variable and "--timeout=0 --max-failures=1 --headed --workers=1" options` }],
['--debug', { description: `Run tests with Playwright Inspector. Shortcut for "PWDEBUG=1" environment variable and "--timeout=0 --max-failures=1 --headed --workers=1 --pause" options` }],
['--fail-on-flaky-tests', { description: `Fail if any test is flagged as flaky (default: false)` }],
['--forbid-only', { description: `Fail if test.only is called (default: false)` }],
['--fully-parallel', { description: `Run all tests in parallel (default: false)` }],
Expand All @@ -403,6 +405,7 @@ const testOptions: [string, { description: string, choices?: string[], preset?:
['--output <dir>', { description: `Folder for output artifacts (default: "test-results")` }],
['--only-changed [ref]', { description: `Only run test files that have been changed between 'HEAD' and 'ref'. Defaults to running all uncommitted changes. Only supports Git.` }],
['--pass-with-no-tests', { description: `Makes test run succeed even if no tests were found` }],
['--pause', { description: `Pause test execution at end of test` }],
['--project <project-name...>', { description: `Only run tests from the specified list of projects, supports '*' wildcard (default: run all projects)` }],
['--quiet', { description: `Suppress stdio` }],
['--repeat-each <N>', { description: `Run each test N times (default: 1)` }],
Expand Down
7 changes: 6 additions & 1 deletion packages/playwright/src/reporters/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ export class TerminalReporter implements ReporterV2 {
suite!: Suite;
totalTestCount = 0;
result!: FullResult;
paused = new Set<TestResult>();
private fileDurations = new Map<string, { duration: number, workers: Set<number> }>();
private _options: TerminalReporterOptions;
private _fatalErrors: TestError[] = [];
Expand Down Expand Up @@ -191,6 +192,10 @@ export class TerminalReporter implements ReporterV2 {
(result as any)[kOutputSymbol].push(output);
}

onTestPaused(test: TestCase, result: TestResult) {
this.paused.add(result);
}

onTestEnd(test: TestCase, result: TestResult) {
if (result.status !== 'skipped' && result.status !== test.expectedStatus)
++this._failureCount;
Expand Down Expand Up @@ -312,7 +317,7 @@ export class TerminalReporter implements ReporterV2 {
epilogue(full: boolean) {
const summary = this.generateSummary();
const summaryMessage = this.generateSummaryMessage(summary);
if (full && summary.failuresToPrint.length && !this._options.omitFailures)
if (full && summary.failuresToPrint.length && !this._options.omitFailures && !this.paused.size)
this._printFailures(summary.failuresToPrint);
this._printSlowTests();
this._printSummary(summaryMessage);
Expand Down
5 changes: 5 additions & 0 deletions packages/playwright/src/reporters/internalReporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ export class InternalReporter implements ReporterV2 {
this._reporter.onStdErr?.(chunk, test, result);
}

onTestPaused(test: TestCase, result: TestResult) {
this._addSnippetToTestErrors(test, result);
this._reporter.onTestPaused?.(test, result);
}

onTestEnd(test: TestCase, result: TestResult) {
this._addSnippetToTestErrors(test, result);
this._reporter.onTestEnd?.(test, result);
Expand Down
17 changes: 16 additions & 1 deletion packages/playwright/src/reporters/line.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,24 @@ class LineReporter extends TerminalReporter {
this._updateLine(test, result, step.parent);
}

override onTestPaused(test: TestCase, result: TestResult) {
super.onTestPaused(test, result);
if (result.errors.length) {
if (!process.env.PW_TEST_DEBUG_REPORTERS)
this.screen.stdout.write(`\u001B[1A\u001B[2K`);
this.writeLine(this.formatFailure(test));
this.writeLine(this.screen.colors.yellow(' Test paused on error. Press Ctrl+C to exit.'));
} else {
this.writeLine();
this.writeLine(this.screen.colors.yellow(' Test paused at end. Press Ctrl+C to exit.'));
}
this.writeLine();
this.writeLine();
}

override onTestEnd(test: TestCase, result: TestResult) {
super.onTestEnd(test, result);
if (!this.willRetry(test) && (test.outcome() === 'flaky' || test.outcome() === 'unexpected' || result.status === 'interrupted')) {
if (!this.willRetry(test) && (test.outcome() === 'flaky' || test.outcome() === 'unexpected' || result.status === 'interrupted') && !this.paused.has(result)) {
if (!process.env.PW_TEST_DEBUG_REPORTERS)
this.screen.stdout.write(`\u001B[1A\u001B[2K`);
this.writeLine(this.formatFailure(test, ++this._failures));
Expand Down
16 changes: 16 additions & 0 deletions packages/playwright/src/reporters/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,9 +165,25 @@ class ListReporter extends TerminalReporter {
stream.write(chunk);
}

override onTestPaused(test: TestCase, result: TestResult) {
super.onTestPaused(test, result);
if (result.errors.length) {
if (!process.env.PW_TEST_DEBUG_REPORTERS)
this.screen.stdout.write(`\u001B[1A\u001B[2K`);
this.writeLine(this.formatFailure(test));
this.writeLine(this.screen.colors.yellow(' Test paused on error. Press Ctrl+C to exit.'));
} else {
this.writeLine();
this.writeLine(this.screen.colors.yellow(' Test paused at end. Press Ctrl+C to exit.'));
}
}

override onTestEnd(test: TestCase, result: TestResult) {
super.onTestEnd(test, result);

if (this.paused.has(result))
return;

const title = this.formatTestTitle(test);
let prefix = '';
let text = '';
Expand Down
5 changes: 5 additions & 0 deletions packages/playwright/src/reporters/multiplexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ export class Multiplexer implements ReporterV2 {
wrap(() => reporter.onStdErr?.(chunk, test, result));
}

onTestPaused(test: TestCase, result: TestResult) {
for (const reporter of this._reporters)
wrap(() => reporter.onTestPaused?.(test, result));
}

onTestEnd(test: TestCase, result: TestResult) {
for (const reporter of this._reporters)
wrap(() => reporter.onTestEnd?.(test, result));
Expand Down
Loading
Loading