From 5c3a275270c5bf139e7be85e18d9244d9130a925 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Sat, 6 Jun 2020 20:59:06 -0700 Subject: [PATCH] feat(debug): improve api logs (#2481) --- src/dom.ts | 76 ++++++++++++++++++++++++---------- src/frames.ts | 20 +++------ src/injected/injectedScript.ts | 29 +++++++++++-- src/progress.ts | 12 ++++-- src/selectors.ts | 35 +++++----------- src/types.ts | 1 + test/autowaiting.spec.js | 2 +- test/click.spec.js | 12 +++--- test/navigation.spec.js | 2 +- test/waittask.spec.js | 22 +++++----- 10 files changed, 127 insertions(+), 84 deletions(-) diff --git a/src/dom.ts b/src/dom.ts index 2c4c1f4adb..c24fd5d63e 100644 --- a/src/dom.ts +++ b/src/dom.ts @@ -246,10 +246,13 @@ export class ElementHandle extends js.JSHandle { } async _retryPointerAction(progress: Progress, action: (point: types.Point) => Promise, options: PointerActionOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise { + let first = true; while (progress.isRunning()) { + progress.log(apiLog, `${first ? 'attempting' : 'retrying'} ${progress.apiName} action`); const result = await this._performPointerAction(progress, action, options); if (result === 'done') return; + first = false; } } @@ -258,27 +261,27 @@ export class ElementHandle extends js.JSHandle { if (!force) await this._waitForDisplayedAtStablePositionAndEnabled(progress); - progress.log(apiLog, 'scrolling into view if needed...'); + progress.log(apiLog, ' scrolling into view if needed'); const scrolled = await this._scrollRectIntoViewIfNeeded(position ? { x: position.x, y: position.y, width: 0, height: 0 } : undefined); if (scrolled === 'invisible') { if (force) throw new Error('Element is not visible'); - progress.log(apiLog, '...element is not visible, retrying input action'); + progress.log(apiLog, ' element is not visible'); return 'retry'; } - progress.log(apiLog, '...done scrolling'); + progress.log(apiLog, ' done scrolling'); const maybePoint = position ? await this._offsetPoint(position) : await this._clickablePoint(); if (maybePoint === 'invisible') { if (force) throw new Error('Element is not visible'); - progress.log(apiLog, 'element is not visibile, retrying input action'); + progress.log(apiLog, ' element is not visibile'); return 'retry'; } if (maybePoint === 'outsideviewport') { if (force) throw new Error('Element is outside of the viewport'); - progress.log(apiLog, 'element is outside of the viewport, retrying input action'); + progress.log(apiLog, ' element is outside of the viewport'); return 'retry'; } const point = roundPoint(maybePoint); @@ -286,29 +289,29 @@ export class ElementHandle extends js.JSHandle { if (!force) { if ((options as any).__testHookBeforeHitTarget) await (options as any).__testHookBeforeHitTarget(); - progress.log(apiLog, `checking that element receives pointer events at (${point.x},${point.y})...`); + progress.log(apiLog, ` checking that element receives pointer events at (${point.x},${point.y})`); const matchesHitTarget = await this._checkHitTargetAt(point); if (!matchesHitTarget) { - progress.log(apiLog, '...element does not receive pointer events, retrying input action'); + progress.log(apiLog, ' element does not receive pointer events'); return 'retry'; } - progress.log(apiLog, `...element does receive pointer events, continuing input action`); + progress.log(apiLog, ` element does receive pointer events, continuing input action`); } await this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => { let restoreModifiers: input.Modifier[] | undefined; if (options && options.modifiers) restoreModifiers = await this._page.keyboard._ensureModifiers(options.modifiers); - progress.log(apiLog, `performing ${progress.apiName} action...`); + progress.log(apiLog, ` performing ${progress.apiName} action`); await action(point); - progress.log(apiLog, `...${progress.apiName} action done`); - progress.log(apiLog, 'waiting for scheduled navigations to finish...'); + progress.log(apiLog, ` ${progress.apiName} action done`); + progress.log(apiLog, ' waiting for scheduled navigations to finish'); if ((options as any).__testHookAfterPointerAction) await (options as any).__testHookAfterPointerAction(); if (restoreModifiers) await this._page.keyboard._ensureModifiers(restoreModifiers); }, 'input'); - progress.log(apiLog, '...navigations have finished'); + progress.log(apiLog, ' navigations have finished'); return 'done'; } @@ -342,7 +345,6 @@ export class ElementHandle extends js.JSHandle { } async _selectOption(progress: Progress, values: string | ElementHandle | types.SelectOption | string[] | ElementHandle[] | types.SelectOption[], options: types.NavigatingActionWaitOptions): Promise { - progress.log(apiLog, progress.apiName); let vals: string[] | ElementHandle[] | types.SelectOption[]; if (!Array.isArray(values)) vals = [ values ] as (string[] | ElementHandle[] | types.SelectOption[]); @@ -376,8 +378,8 @@ export class ElementHandle extends js.JSHandle { const poll = await this._evaluateHandleInUtility(([injected, node, value]) => { return injected.waitForEnabledAndFill(node, value); }, value); - new InjectedScriptPollHandler(progress, poll); - const injectedResult = await poll.evaluate(poll => poll.result); + const pollHandler = new InjectedScriptPollHandler(progress, poll); + const injectedResult = await pollHandler.finish(); const needsInput = handleInjectedResult(injectedResult); if (needsInput) { if (value) @@ -399,7 +401,6 @@ export class ElementHandle extends js.JSHandle { } async _setInputFiles(progress: Progress, files: string | types.FilePayload | string[] | types.FilePayload[], options: types.NavigatingActionWaitOptions) { - progress.log(apiLog, progress.apiName); const injectedResult = await this._evaluateInUtility(([injected, node]): types.InjectedScriptResult => { if (node.nodeType !== Node.ELEMENT_NODE || (node as Node as Element).tagName !== 'INPUT') return { status: 'error', error: 'Node is not an HTMLInputElement' }; @@ -522,15 +523,15 @@ export class ElementHandle extends js.JSHandle { } async _waitForDisplayedAtStablePositionAndEnabled(progress: Progress): Promise { - progress.log(apiLog, 'waiting for element to be displayed, enabled and not moving...'); + progress.log(apiLog, ' waiting for element to be displayed, enabled and not moving'); const rafCount = this._page._delegate.rafCountForStablePosition(); const poll = await this._evaluateHandleInUtility(([injected, node, rafCount]) => { return injected.waitForDisplayedAtStablePositionAndEnabled(node, rafCount); }, rafCount); - new InjectedScriptPollHandler(progress, poll); - const injectedResult = await poll.evaluate(poll => poll.result); + const pollHandler = new InjectedScriptPollHandler(progress, poll); + const injectedResult = await pollHandler.finish(); handleInjectedResult(injectedResult); - progress.log(apiLog, '...element is displayed and does not move'); + progress.log(apiLog, ' element is displayed and does not move'); } async _checkHitTargetAt(point: types.Point): Promise { @@ -553,11 +554,11 @@ export class ElementHandle extends js.JSHandle { // Handles an InjectedScriptPoll running in injected script: // - streams logs into progress; // - cancels the poll when progress cancels. -export class InjectedScriptPollHandler { +export class InjectedScriptPollHandler { private _progress: Progress; - private _poll: js.JSHandle> | null; + private _poll: js.JSHandle> | null; - constructor(progress: Progress, poll: js.JSHandle>) { + constructor(progress: Progress, poll: js.JSHandle>) { this._progress = progress; this._poll = poll; this._progress.cleanupWhenAborted(() => this.cancel()); @@ -577,6 +578,35 @@ export class InjectedScriptPollHandler { }); } + async finishHandle(): Promise> { + try { + const result = await this._poll!.evaluateHandle(poll => poll.result); + await this._finishInternal(); + return result; + } finally { + this.cancel(); + } + } + + async finish(): Promise { + try { + const result = await this._poll!.evaluate(poll => poll.result); + await this._finishInternal(); + return result; + } finally { + this.cancel(); + } + } + + private async _finishInternal() { + if (!this._poll) + return; + // Retrieve all the logs before continuing. + const messages = await this._poll.evaluate(poll => poll.takeLastLogs()).catch(e => [] as string[]); + for (const message of messages) + this._progress.log(apiLog, message); + } + cancel() { if (!this._poll) return; diff --git a/src/frames.ts b/src/frames.ts index 0b03c6be0b..bb2ac21d09 100644 --- a/src/frames.ts +++ b/src/frames.ts @@ -345,7 +345,7 @@ export class Frame { const progressController = new ProgressController(this._page, this._page._timeoutSettings.navigationTimeout(options)); abortProgressOnFrameDetach(progressController, this); return progressController.run(async progress => { - progress.log(apiLog, `${progress.apiName}("${url}"), waiting until "${options.waitUntil || 'load'}"`); + progress.log(apiLog, `navigating to "${url}", waiting until "${options.waitUntil || 'load'}"`); const headers = (this._page._state.extraHTTPHeaders || {}); let referer = headers['referer'] || headers['Referer']; if (options.referer !== undefined) { @@ -455,7 +455,7 @@ export class Frame { throw new Error(`Unsupported waitFor option "${state}"`); const { world, task } = selectors._waitForSelectorTask(selector, state); return runAbortableTask(async progress => { - progress.log(apiLog, `Waiting for selector "${selector}"${state === 'attached' ? '' : ' to be ' + state}...`); + progress.log(apiLog, `waiting for selector "${selector}"${state === 'attached' ? '' : ' to be ' + state}`); const result = await this._scheduleRerunnableTask(progress, world, task); if (!result.asElement()) { result.dispose(); @@ -524,12 +524,11 @@ export class Frame { abortProgressOnFrameDetach(progressController, this); return progressController.run(async progress => { const waitUntil = options.waitUntil === undefined ? 'load' : options.waitUntil; - progress.log(apiLog, `${progress.apiName}(), waiting until "${waitUntil}"`); + progress.log(apiLog, `setting frame content, waiting until "${waitUntil}"`); const tag = `--playwright--set--content--${this._id}--${++this._setContentCounter}--`; const context = await this._utilityContext(); const lifecyclePromise = new Promise((resolve, reject) => { this._page._frameManager._consoleMessageTags.set(tag, () => { - progress.log(apiLog, 'content written'); // Clear lifecycle right after document.open() - see 'tag' below. this._page._frameManager.clearFrameLifecycle(this); this._waitForLoadState(progress, waitUntil).then(resolve).catch(reject); @@ -710,13 +709,11 @@ export class Frame { selector: string, options: types.TimeoutOptions, action: (progress: Progress, handle: dom.ElementHandle) => Promise): Promise { return runAbortableTask(async progress => { - progress.log(apiLog, `${progress.apiName}("${selector}")`); while (progress.isRunning()) { try { + progress.log(apiLog, `waiting for selector "${selector}"`); const { world, task } = selectors._waitForSelectorTask(selector, 'attached'); - progress.log(apiLog, `waiting for the selector "${selector}"`); const handle = await this._scheduleRerunnableTask(progress, world, task); - progress.log(apiLog, `...got element for the selector`); const element = handle.asElement() as dom.ElementHandle; progress.cleanupWhenAborted(() => element.dispose()); const result = await action(progress, element); @@ -915,16 +912,11 @@ class RerunnableTask { } async rerun(context: dom.FrameExecutionContext) { - let pollHandler: dom.InjectedScriptPollHandler | null = null; try { - const poll = await this._task(context); - pollHandler = new dom.InjectedScriptPollHandler(this._progress, poll); - const result = await poll.evaluateHandle(poll => poll.result); + const pollHandler = new dom.InjectedScriptPollHandler(this._progress, await this._task(context)); + const result = await pollHandler.finishHandle(); this._resolve(result); } catch (e) { - if (pollHandler) - pollHandler.cancel(); - // When the page is navigated, the promise is rejected. // We will try again in the new execution context. if (e.message.includes('Execution context was destroyed')) diff --git a/src/injected/injectedScript.ts b/src/injected/injectedScript.ts index ec2faf3c48..2c480188ac 100644 --- a/src/injected/injectedScript.ts +++ b/src/injected/injectedScript.ts @@ -165,6 +165,7 @@ export default class InjectedScript { logs, result: poll(progress), cancel: () => { progress.canceled = true; }, + takeLastLogs: () => currentLogs, }; } @@ -448,12 +449,34 @@ export default class InjectedScript { } previewElement(element: Element): string { - const id = element.id ? '#' + element.id : ''; - const classes = Array.from(element.classList).map(c => '.' + c).join(''); - return `${element.nodeName.toLowerCase()}${id}${classes}`; + const attrs = []; + for (let i = 0; i < element.attributes.length; i++) { + if (element.attributes[i].name !== 'style') + attrs.push(` ${element.attributes[i].name}="${element.attributes[i].value}"`); + } + attrs.sort((a, b) => a.length - b.length); + let attrText = attrs.join(''); + if (attrText.length > 50) + attrText = attrText.substring(0, 49) + '\u2026'; + if (autoClosingTags.has(element.nodeName)) + return `<${element.nodeName.toLowerCase()}${attrText}/>`; + + const children = element.childNodes; + let onlyText = false; + if (children.length <= 5) { + onlyText = true; + for (let i = 0; i < children.length; i++) + onlyText = onlyText && children[i].nodeType === Node.TEXT_NODE; + } + let text = onlyText ? (element.textContent || '') : ''; + if (text.length > 50) + text = text.substring(0, 49) + '\u2026'; + return `<${element.nodeName.toLowerCase()}${attrText}>${text}`; } } +const autoClosingTags = new Set(['AREA', 'BASE', 'BR', 'COL', 'COMMAND', 'EMBED', 'HR', 'IMG', 'INPUT', 'KEYGEN', 'LINK', 'MENUITEM', 'META', 'PARAM', 'SOURCE', 'TRACK', 'WBR']); + const eventType = new Map([ ['auxclick', 'mouse'], ['click', 'mouse'], diff --git a/src/progress.ts b/src/progress.ts index c9fe9f9c27..fae37a324b 100644 --- a/src/progress.ts +++ b/src/progress.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { InnerLogger, Log } from './logger'; +import { InnerLogger, Log, apiLog } from './logger'; import { TimeoutError } from './errors'; import { assert } from './helper'; import { getCurrentApiCall, rewriteErrorMessage } from './debug/stackTrace'; @@ -82,11 +82,15 @@ export class ProgressController { runCleanup(cleanup); }, log: (log: Log, message: string | Error) => { - if (this._state === 'running') + if (this._state === 'running') { this._logRecording.push(message.toString()); - this._logger._log(log, message); + this._logger._log(log, ' ' + message); + } else { + this._logger._log(log, message); + } }, }; + this._logger._log(apiLog, `=> ${this._apiName} started`); const timeoutError = new TimeoutError(`Timeout ${this._timeout}ms exceeded during ${this._apiName}.`); const timer = setTimeout(() => this._forceAbort(timeoutError), progress.timeUntilDeadline()); @@ -96,6 +100,7 @@ export class ProgressController { clearTimeout(timer); this._state = 'finished'; this._logRecording = []; + this._logger._log(apiLog, `<= ${this._apiName} succeeded`); return result; } catch (e) { this._aborted(); @@ -103,6 +108,7 @@ export class ProgressController { clearTimeout(timer); this._state = 'aborted'; this._logRecording = []; + this._logger._log(apiLog, `<= ${this._apiName} failed`); await Promise.all(this._cleanups.splice(0).map(cleanup => runCleanup(cleanup))); throw e; } diff --git a/src/selectors.ts b/src/selectors.ts index 2c4b8a14f0..158bf52378 100644 --- a/src/selectors.ts +++ b/src/selectors.ts @@ -120,38 +120,25 @@ export class Selectors { return injected.poll('raf', (progress: types.InjectedScriptProgress) => { const element = injected.querySelector(parsed, document); + const visible = element ? injected.isVisible(element) : false; - const log = (suffix: string) => { - if (lastElement === element) - return; + if (lastElement !== element) { lastElement = element; if (!element) - progress.log(`selector did not resolve to any element`); + progress.log(` selector did not resolve to any element`); else - progress.log(`selector resolved to "${injected.previewElement(element)}"${suffix ? ' ' + suffix : ''}`); - }; + progress.log(` selector resolved to ${visible ? 'visible' : 'hidden'} ${injected.previewElement(element)}`); + } switch (state) { - case 'attached': { + case 'attached': return element || false; - } - case 'detached': { - if (element) - log(''); + case 'detached': return !element; - } - case 'visible': { - const result = element && injected.isVisible(element) ? element : false; - if (!result) - log('that is not visible'); - return result; - } - case 'hidden': { - const result = !element || !injected.isVisible(element); - if (!result) - log('that is still visible'); - return result; - } + case 'visible': + return visible ? element : false; + case 'hidden': + return !visible; } }); }, { parsed, state }); diff --git a/src/types.ts b/src/types.ts index efc777eeac..9fe159a577 100644 --- a/src/types.ts +++ b/src/types.ts @@ -176,6 +176,7 @@ export type InjectedScriptLogs = { current: string[], next: Promise = { result: Promise, logs: Promise, + takeLastLogs: () => string[], cancel: () => void, }; diff --git a/test/autowaiting.spec.js b/test/autowaiting.spec.js index 025b0621aa..42b706e249 100644 --- a/test/autowaiting.spec.js +++ b/test/autowaiting.spec.js @@ -192,7 +192,7 @@ describe('Auto waiting', () => { const __testHookAfterPointerAction = () => new Promise(f => setTimeout(f, 6000)); const error = await page.click('a', { timeout: 5000, __testHookAfterPointerAction }).catch(e => e); expect(error.message).toContain('Timeout 5000ms exceeded during page.click.'); - expect(error.message).toContain('waiting for scheduled navigations to finish...'); + expect(error.message).toContain('waiting for scheduled navigations to finish'); expect(error.message).toContain(`navigated to "${server.PREFIX + '/frames/one-frame.html'}"`); }); }); diff --git a/test/click.spec.js b/test/click.spec.js index 349f2748b6..4569ca74cc 100644 --- a/test/click.spec.js +++ b/test/click.spec.js @@ -182,14 +182,14 @@ describe('Page.click', function() { await page.$eval('button', b => b.style.display = 'none'); const error = await page.click('button', { timeout: 5000 }).catch(e => e); expect(error.message).toContain('Timeout 5000ms exceeded during page.click.'); - expect(error.message).toContain('waiting for element to be displayed, enabled and not moving...'); + expect(error.message).toContain('waiting for element to be displayed, enabled and not moving'); }); it('should timeout waiting for visbility:hidden to be gone', async({page, server}) => { await page.goto(server.PREFIX + '/input/button.html'); await page.$eval('button', b => b.style.visibility = 'hidden'); const error = await page.click('button', { timeout: 5000 }).catch(e => e); expect(error.message).toContain('Timeout 5000ms exceeded during page.click.'); - expect(error.message).toContain('waiting for element to be displayed, enabled and not moving...'); + expect(error.message).toContain('waiting for element to be displayed, enabled and not moving'); }); it('should waitFor visible when parent is hidden', async({page, server}) => { let done = false; @@ -431,7 +431,7 @@ describe('Page.click', function() { }); const error = await button.click({ timeout: 5000 }).catch(e => e); expect(error.message).toContain('Timeout 5000ms exceeded during elementHandle.click.'); - expect(error.message).toContain('waiting for element to be displayed, enabled and not moving...'); + expect(error.message).toContain('waiting for element to be displayed, enabled and not moving'); }); it('should wait for becoming hit target', async({page, server}) => { await page.goto(server.PREFIX + '/input/button.html'); @@ -479,7 +479,8 @@ describe('Page.click', function() { }); const error = await button.click({ timeout: 5000 }).catch(e => e); expect(error.message).toContain('Timeout 5000ms exceeded during elementHandle.click.'); - expect(error.message).toContain('...element does not receive pointer events, retrying input action'); + expect(error.message).toContain('element does not receive pointer events'); + expect(error.message).toContain('retrying elementHandle.click action'); }); it('should fail when obscured and not waiting for hit target', async({page, server}) => { await page.goto(server.PREFIX + '/input/button.html'); @@ -704,7 +705,8 @@ describe('Page.click', function() { expect(clicked).toBe(false); expect(await page.evaluate(() => window.clicked)).toBe(undefined); expect(error.message).toContain('Timeout 5000ms exceeded during elementHandle.click.'); - expect(error.message).toContain('...element does not receive pointer events, retrying input action'); + expect(error.message).toContain('element does not receive pointer events'); + expect(error.message).toContain('retrying elementHandle.click action'); }); it('should dispatch microtasks in order', async({page, server}) => { await page.setContent(` diff --git a/test/navigation.spec.js b/test/navigation.spec.js index 14c31f96c9..211d07c1e2 100644 --- a/test/navigation.spec.js +++ b/test/navigation.spec.js @@ -919,7 +919,7 @@ describe('Frame.goto', function() { const url = server.PREFIX + '/frames/child-redirect.html'; const error = await page.goto(url, { timeout: 5000, waitUntil: 'networkidle' }).catch(e => e); expect(error.message).toContain('Timeout 5000ms exceeded during page.goto.'); - expect(error.message).toContain(`page.goto("${url}"), waiting until "networkidle"`); + expect(error.message).toContain(`navigating to "${url}", waiting until "networkidle"`); expect(error.message).toContain(`navigated to "${url}"`); expect(error.message).toContain(`navigated to "${server.PREFIX + '/frames/one-frame.html'}"`); expect(error.message).toContain(`"domcontentloaded" event fired`); diff --git a/test/waittask.spec.js b/test/waittask.spec.js index e2de76b273..0d7624ce7a 100644 --- a/test/waittask.spec.js +++ b/test/waittask.spec.js @@ -211,7 +211,9 @@ describe('Frame.waitForSelector', function() { const div = document.createElement('div'); div.className = 'foo bar'; div.id = 'mydiv'; - div.style.display = 'none'; + div.setAttribute('style', 'display: none'); + div.setAttribute('foo', '123456789012345678901234567890123456789012345678901234567890'); + div.textContent = 'abcdefghijklmnopqrstuvwyxzabcdefghijklmnopqrstuvwyxzabcdefghijklmnopqrstuvwyxz'; document.body.appendChild(div); }); await giveItTimeToLog(frame); @@ -229,10 +231,10 @@ describe('Frame.waitForSelector', function() { const error = await watchdog.catch(e => e); expect(error.message).toContain(`Timeout 5000ms exceeded during frame.waitForSelector.`); - expect(error.message).toContain(`Waiting for selector "div" to be visible...`); - expect(error.message).toContain(`selector resolved to "div#mydiv.foo.bar" that is not visible`); + expect(error.message).toContain(`waiting for selector "div" to be visible`); + expect(error.message).toContain(`selector resolved to hidden `); }); it('should report logs while waiting for hidden', async({page, server}) => { await page.goto(server.EMPTY_PAGE); @@ -259,9 +261,9 @@ describe('Frame.waitForSelector', function() { const error = await watchdog.catch(e => e); expect(error.message).toContain(`Timeout 5000ms exceeded during frame.waitForSelector.`); - expect(error.message).toContain(`Waiting for selector "div" to be hidden...`); - expect(error.message).toContain(`selector resolved to "div#mydiv.foo.bar" that is still visible`); - expect(error.message).toContain(`selector resolved to "div.another" that is still visible`); + expect(error.message).toContain(`waiting for selector "div" to be hidden`); + expect(error.message).toContain(`selector resolved to visible
hello
`); + expect(error.message).toContain(`selector resolved to visible
hello
`); }); it('should resolve promise when node is added in shadow dom', async({page, server}) => { await page.goto(server.EMPTY_PAGE); @@ -400,7 +402,7 @@ describe('Frame.waitForSelector', function() { await page.waitForSelector('div', { timeout: 10, state: 'attached' }).catch(e => error = e); expect(error).toBeTruthy(); expect(error.message).toContain('Timeout 10ms exceeded during page.waitForSelector'); - expect(error.message).toContain('Waiting for selector "div"...'); + expect(error.message).toContain('waiting for selector "div"'); expect(error).toBeInstanceOf(playwright.errors.TimeoutError); }); it('should have an error message specifically for awaiting an element to be hidden', async({page, server}) => { @@ -409,7 +411,7 @@ describe('Frame.waitForSelector', function() { await page.waitForSelector('div', { state: 'hidden', timeout: 1000 }).catch(e => error = e); expect(error).toBeTruthy(); expect(error.message).toContain('Timeout 1000ms exceeded during page.waitForSelector'); - expect(error.message).toContain('Waiting for selector "div" to be hidden...'); + expect(error.message).toContain('waiting for selector "div" to be hidden'); }); it('should respond to node attribute mutation', async({page, server}) => { let divFound = false; @@ -490,7 +492,7 @@ describe('Frame.waitForSelector xpath', function() { await page.waitForSelector('//div', { state: 'attached', timeout: 10 }).catch(e => error = e); expect(error).toBeTruthy(); expect(error.message).toContain('Timeout 10ms exceeded during page.waitForSelector'); - expect(error.message).toContain('Waiting for selector "//div"...'); + expect(error.message).toContain('waiting for selector "//div"'); expect(error).toBeInstanceOf(playwright.errors.TimeoutError); }); it('should run in specified frame', async({page, server}) => {