diff --git a/src/protocol/channels.ts b/src/protocol/channels.ts index 9a0ac27fda..ed2e560422 100644 --- a/src/protocol/channels.ts +++ b/src/protocol/channels.ts @@ -3668,6 +3668,7 @@ export const commandsWithTracingSnapshots = new Set([ 'Frame.addStyleTag', 'Frame.check', 'Frame.click', + 'Frame.dragAndDrop', 'Frame.dblclick', 'Frame.dispatchEvent', 'Frame.evaluateExpression', @@ -3683,6 +3684,8 @@ export const commandsWithTracingSnapshots = new Set([ 'Frame.isChecked', 'Frame.isDisabled', 'Frame.isEnabled', + 'Frame.isHidden', + 'Frame.isVisible', 'Frame.isEditable', 'Frame.press', 'Frame.selectOption', @@ -3706,14 +3709,50 @@ export const commandsWithTracingSnapshots = new Set([ 'ElementHandle.dispatchEvent', 'ElementHandle.fill', 'ElementHandle.hover', + 'ElementHandle.innerHTML', + 'ElementHandle.innerText', + 'ElementHandle.inputValue', + 'ElementHandle.isChecked', + 'ElementHandle.isDisabled', + 'ElementHandle.isEditable', + 'ElementHandle.isEnabled', + 'ElementHandle.isHidden', + 'ElementHandle.isVisible', 'ElementHandle.press', 'ElementHandle.scrollIntoViewIfNeeded', 'ElementHandle.selectOption', 'ElementHandle.selectText', 'ElementHandle.setInputFiles', 'ElementHandle.tap', + 'ElementHandle.textContent', 'ElementHandle.type', 'ElementHandle.uncheck', 'ElementHandle.waitForElementState', 'ElementHandle.waitForSelector' +]); + +export const pausesBeforeInputActions = new Set([ + 'Frame.check', + 'Frame.click', + 'Frame.dragAndDrop', + 'Frame.dblclick', + 'Frame.fill', + 'Frame.hover', + 'Frame.press', + 'Frame.selectOption', + 'Frame.setInputFiles', + 'Frame.tap', + 'Frame.type', + 'Frame.uncheck', + 'ElementHandle.check', + 'ElementHandle.click', + 'ElementHandle.dblclick', + 'ElementHandle.fill', + 'ElementHandle.hover', + 'ElementHandle.press', + 'ElementHandle.selectOption', + 'ElementHandle.setInputFiles', + 'ElementHandle.tap', + 'ElementHandle.type', + 'ElementHandle.uncheck' ]); \ No newline at end of file diff --git a/src/protocol/protocol.yml b/src/protocol/protocol.yml index d26670475f..1ab1d1de1a 100644 --- a/src/protocol/protocol.yml +++ b/src/protocol/protocol.yml @@ -1273,6 +1273,7 @@ Frame: trial: boolean? tracing: snapshot: true + pausesBeforeInput: true click: parameters: @@ -1302,6 +1303,7 @@ Frame: trial: boolean? tracing: snapshot: true + pausesBeforeInput: true content: returns: @@ -1317,6 +1319,9 @@ Frame: trial: boolean? sourcePosition: Point? targetPosition: Point? + tracing: + snapshot: true + pausesBeforeInput: true dblclick: parameters: @@ -1345,6 +1350,7 @@ Frame: trial: boolean? tracing: snapshot: true + pausesBeforeInput: true dispatchEvent: parameters: @@ -1386,6 +1392,7 @@ Frame: noWaitAfter: boolean? tracing: snapshot: true + pausesBeforeInput: true focus: parameters: @@ -1445,6 +1452,7 @@ Frame: trial: boolean? tracing: snapshot: true + pausesBeforeInput: true innerHTML: parameters: @@ -1512,6 +1520,8 @@ Frame: strict: boolean? returns: value: boolean + tracing: + snapshot: true isVisible: parameters: @@ -1519,6 +1529,8 @@ Frame: strict: boolean? returns: value: boolean + tracing: + snapshot: true isEditable: parameters: @@ -1540,6 +1552,7 @@ Frame: timeout: number? tracing: snapshot: true + pausesBeforeInput: true querySelector: parameters: @@ -1580,6 +1593,7 @@ Frame: items: string tracing: snapshot: true + pausesBeforeInput: true setContent: parameters: @@ -1610,6 +1624,7 @@ Frame: noWaitAfter: boolean? tracing: snapshot: true + pausesBeforeInput: true tap: parameters: @@ -1631,6 +1646,7 @@ Frame: trial: boolean? tracing: snapshot: true + pausesBeforeInput: true textContent: parameters: @@ -1656,6 +1672,7 @@ Frame: timeout: number? tracing: snapshot: true + pausesBeforeInput: true uncheck: parameters: @@ -1668,6 +1685,7 @@ Frame: trial: boolean? tracing: snapshot: true + pausesBeforeInput: true waitForFunction: parameters: @@ -1858,6 +1876,7 @@ ElementHandle: trial: boolean? tracing: snapshot: true + pausesBeforeInput: true click: parameters: @@ -1885,6 +1904,7 @@ ElementHandle: trial: boolean? tracing: snapshot: true + pausesBeforeInput: true contentFrame: returns: @@ -1915,6 +1935,7 @@ ElementHandle: trial: boolean? tracing: snapshot: true + pausesBeforeInput: true dispatchEvent: parameters: @@ -1931,6 +1952,7 @@ ElementHandle: noWaitAfter: boolean? tracing: snapshot: true + pausesBeforeInput: true focus: @@ -1957,42 +1979,61 @@ ElementHandle: trial: boolean? tracing: snapshot: true + pausesBeforeInput: true innerHTML: returns: value: string + tracing: + snapshot: true innerText: returns: value: string + tracing: + snapshot: true inputValue: returns: value: string + tracing: + snapshot: true isChecked: returns: value: boolean + tracing: + snapshot: true isDisabled: returns: value: boolean + tracing: + snapshot: true isEditable: returns: value: boolean + tracing: + snapshot: true isEnabled: returns: value: boolean + tracing: + snapshot: true isHidden: returns: value: boolean + tracing: + snapshot: true isVisible: returns: value: boolean + tracing: + snapshot: true ownerFrame: returns: @@ -2006,6 +2047,7 @@ ElementHandle: noWaitAfter: boolean? tracing: snapshot: true + pausesBeforeInput: true querySelector: parameters: @@ -2063,6 +2105,7 @@ ElementHandle: items: string tracing: snapshot: true + pausesBeforeInput: true selectText: parameters: @@ -2085,6 +2128,7 @@ ElementHandle: noWaitAfter: boolean? tracing: snapshot: true + pausesBeforeInput: true tap: parameters: @@ -2104,10 +2148,13 @@ ElementHandle: trial: boolean? tracing: snapshot: true + pausesBeforeInput: true textContent: returns: value: string? + tracing: + snapshot: true type: parameters: @@ -2117,6 +2164,7 @@ ElementHandle: timeout: number? tracing: snapshot: true + pausesBeforeInput: true uncheck: parameters: @@ -2127,6 +2175,7 @@ ElementHandle: trial: boolean? tracing: snapshot: true + pausesBeforeInput: true waitForElementState: parameters: diff --git a/src/server/dom.ts b/src/server/dom.ts index aa99152d66..76f55e8373 100644 --- a/src/server/dom.ts +++ b/src/server/dom.ts @@ -989,6 +989,7 @@ export function textContentTask(selector: SelectorInfo): SchedulableTask { - progress.log(` retrieving textContent from "${selector}"`); + progress.log(` waiting for selector "${selector}"\u2026`); return this._scheduleRerunnableTask(progress, info.world, task); }, this._page._timeoutSettings.timeout(options)); } diff --git a/src/server/supplements/debugger.ts b/src/server/supplements/debugger.ts index 3a75674ade..18ca277174 100644 --- a/src/server/supplements/debugger.ts +++ b/src/server/supplements/debugger.ts @@ -19,6 +19,7 @@ import { debugMode, isUnderTest, monotonicTime } from '../../utils/utils'; import { BrowserContext } from '../browserContext'; import { CallMetadata, InstrumentationListener, SdkObject } from '../instrumentation'; import { debugLogger } from '../../utils/debugLogger'; +import { commandsWithTracingSnapshots, pausesBeforeInputActions } from '../../protocol/channels'; const symbol = Symbol('Debugger'); @@ -55,7 +56,7 @@ export class Debugger extends EventEmitter implements InstrumentationListener { async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise { if (this._muted) return; - if (shouldPauseOnCall(sdkObject, metadata) || (this._pauseOnNextStatement && shouldPauseOnNonInputStep(sdkObject, metadata))) + if (shouldPauseOnCall(sdkObject, metadata) || (this._pauseOnNextStatement && shouldPauseBeforeStep(metadata))) await this.pause(sdkObject, metadata); } @@ -117,8 +118,14 @@ function shouldPauseOnCall(sdkObject: SdkObject, metadata: CallMetadata): boolea return metadata.method === 'pause'; } -const nonInputActionsToStep = new Set(['close', 'evaluate', 'evaluateHandle', 'goto', 'setContent']); - -function shouldPauseOnNonInputStep(sdkObject: SdkObject, metadata: CallMetadata): boolean { - return nonInputActionsToStep.has(metadata.method); +function shouldPauseBeforeStep(metadata: CallMetadata): boolean { + // Always stop on 'close' + if (metadata.method === 'close') + return true; + if (metadata.method === 'waitForSelector' || metadata.method === 'waitForEventInfo') + return false; // Never stop on those, primarily for the test harness. + const step = metadata.type + '.' + metadata.method; + // Stop before everything that generates snapshot. But don't stop before those marked as pausesBeforeInputActions + // since we stop in them on a separate instrumentation signal. + return commandsWithTracingSnapshots.has(step) && !pausesBeforeInputActions.has(metadata.type + '.' + metadata.method); } diff --git a/src/server/supplements/recorder/recorderUtils.ts b/src/server/supplements/recorder/recorderUtils.ts index aeed16d05c..81f66702bd 100644 --- a/src/server/supplements/recorder/recorderUtils.ts +++ b/src/server/supplements/recorder/recorderUtils.ts @@ -21,6 +21,7 @@ export function metadataToCallLog(metadata: CallMetadata, status: CallLogStatus) let title = metadata.apiName || metadata.method; if (metadata.method === 'waitForEventInfo') title += `(${metadata.params.info.event})`; + title = title.replace('object.expect', 'expect'); if (metadata.error) status = 'error'; const params = { diff --git a/src/test/expect.ts b/src/test/expect.ts index 7fa94025a8..f0fb688d4f 100644 --- a/src/test/expect.ts +++ b/src/test/expect.ts @@ -68,7 +68,7 @@ const customMatchers = { }; function wrap(matcherName: string, matcher: any) { - return function(this: any, ...args: any[]) { + const result = function(this: any, ...args: any[]) { const testInfo = currentTestInfo(); if (!testInfo) return matcher.call(this, ...args); @@ -107,6 +107,8 @@ function wrap(matcherName: string, matcher: any) { reportStepError(e); } }; + result.displayName = 'expect.' + matcherName; + return result; } const wrappedMatchers: any = {}; diff --git a/src/utils/stackTrace.ts b/src/utils/stackTrace.ts index 9110ec2f3e..21815165fc 100644 --- a/src/utils/stackTrace.ts +++ b/src/utils/stackTrace.ts @@ -33,6 +33,8 @@ export function rewriteErrorMessage(e: E, newMessage: string): const ROOT_DIR = path.resolve(__dirname, '..', '..'); const CLIENT_LIB = path.join(ROOT_DIR, 'lib', 'client'); const CLIENT_SRC = path.join(ROOT_DIR, 'src', 'client'); +const TEST_LIB = path.join(ROOT_DIR, 'lib', 'test'); +const TEST_SRC = path.join(ROOT_DIR, 'src', 'test'); export type ParsedStackTrace = { allFrames: StackFrame[]; @@ -60,9 +62,18 @@ export function captureStackTrace(): ParsedStackTrace { return null; if (frame.file.startsWith('internal')) return null; + if (frame.file.includes(path.join('node_modules', 'expect'))) + return null; const fileName = path.resolve(process.cwd(), frame.file); if (isTesting && fileName.includes(path.join('playwright', 'tests', 'config', 'coverage.js'))) return null; + const inClient = + // Allow fixtures in the reported stacks. + (!fileName.includes('test/index') && !fileName.includes('test\\index')) && ( + fileName.startsWith(CLIENT_LIB) + || fileName.startsWith(CLIENT_SRC) + || fileName.startsWith(TEST_LIB) + || fileName.startsWith(TEST_SRC)); const parsed: ParsedFrame = { frame: { file: fileName, @@ -71,10 +82,10 @@ export function captureStackTrace(): ParsedStackTrace { function: frame.function, }, frameText: line, - inClient: fileName.startsWith(CLIENT_LIB) || fileName.startsWith(CLIENT_SRC), + inClient }; return parsed; - }).filter(frame => !!frame) as ParsedFrame[]; + }).filter(Boolean) as ParsedFrame[]; let apiName = ''; // Deepest transition between non-client code calling into client code diff --git a/src/web/recorder/callLog.css b/src/web/recorder/callLog.css index b4cd38f6ff..8e6a5ac113 100644 --- a/src/web/recorder/callLog.css +++ b/src/web/recorder/callLog.css @@ -79,7 +79,7 @@ .call-log-selector { color: var(--orange); - white-space: normal; + white-space: nowrap; } .call-log-time { diff --git a/tests/playwright-test/playwright.spec.ts b/tests/playwright-test/playwright.spec.ts index be9073265a..bf9b70d675 100644 --- a/tests/playwright-test/playwright.spec.ts +++ b/tests/playwright-test/playwright.spec.ts @@ -268,7 +268,7 @@ test('should report error and pending operations on timeout', async ({ runInline expect(result.output).toContain('Pending operations:'); expect(result.output).toContain('- page.click at a.test.ts:9:16'); expect(result.output).toContain('- page.textContent at a.test.ts:10:16'); - expect(result.output).toContain('retrieving textContent from "text=More missing"'); + expect(result.output).toContain('waiting for selector'); expect(stripAscii(result.output)).toContain(`10 | page.textContent('text=More missing'),`); }); diff --git a/tests/playwright-test/reporter.spec.ts b/tests/playwright-test/reporter.spec.ts index 036996af78..097b98a835 100644 --- a/tests/playwright-test/reporter.spec.ts +++ b/tests/playwright-test/reporter.spec.ts @@ -229,9 +229,9 @@ test('should report expect steps', async ({ runInlineTest }) => { `%% end {\"title\":\"browserContext.newPage\",\"category\":\"pw:api\"}`, `%% end {\"title\":\"Before Hooks\",\"category\":\"hook\",\"steps\":[{\"title\":\"browserContext.newPage\",\"category\":\"pw:api\"}]}`, `%% begin {\"title\":\"expect.not.toHaveTitle\",\"category\":\"expect\"}`, - `%% begin {\"title\":\"page.title\",\"category\":\"pw:api\"}`, - `%% end {\"title\":\"page.title\",\"category\":\"pw:api\"}`, - `%% end {\"title\":\"expect.not.toHaveTitle\",\"category\":\"expect\",\"steps\":[{\"title\":\"page.title\",\"category\":\"pw:api\"}]}`, + `%% begin {\"title\":\"object.expect.toHaveTitle\",\"category\":\"pw:api\"}`, + `%% end {\"title\":\"object.expect.toHaveTitle\",\"category\":\"pw:api\"}`, + `%% end {\"title\":\"expect.not.toHaveTitle\",\"category\":\"expect\",\"steps\":[{\"title\":\"object.expect.toHaveTitle\",\"category\":\"pw:api\"}]}`, `%% begin {\"title\":\"After Hooks\",\"category\":\"hook\"}`, `%% begin {\"title\":\"browserContext.close\",\"category\":\"pw:api\"}`, `%% end {\"title\":\"browserContext.close\",\"category\":\"pw:api\"}`, diff --git a/utils/generate_channels.js b/utils/generate_channels.js index 057adc8530..9ca66cbcb2 100755 --- a/utils/generate_channels.js +++ b/utils/generate_channels.js @@ -170,6 +170,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { `]; const tracingSnapshots = []; +const pausesBeforeInputActions = []; const yml = fs.readFileSync(path.join(__dirname, '..', 'src', 'protocol', 'protocol.yml'), 'utf-8'); const protocol = yaml.parse(yml); @@ -232,6 +233,11 @@ for (const [name, item] of Object.entries(protocol)) { for (const derived of derivedClasses.get(name) || []) tracingSnapshots.push(derived + '.' + methodName); } + if (method.tracing && method.tracing.pausesBeforeInput) { + pausesBeforeInputActions.push(name + '.' + methodName); + for (const derived of derivedClasses.get(name) || []) + pausesBeforeInputActions.push(derived + '.' + methodName); + } const parameters = objectType(method.parameters || {}, ''); const paramsName = `${channelName}${titleCase(methodName)}Params`; const optionsName = `${channelName}${titleCase(methodName)}Options`; @@ -271,6 +277,10 @@ for (const [name, item] of Object.entries(protocol)) { channels_ts.push(`export const commandsWithTracingSnapshots = new Set([ '${tracingSnapshots.join(`',\n '`)}' ]);`); +channels_ts.push(''); +channels_ts.push(`export const pausesBeforeInputActions = new Set([ + '${pausesBeforeInputActions.join(`',\n '`)}' +]);`); validator_ts.push(` return scheme;