Skip to content

Commit 1c17929

Browse files
authored
chore: add input logging and timeout debugging hints (#2109)
1 parent 0bb1ba1 commit 1c17929

File tree

4 files changed

+44
-25
lines changed

4 files changed

+44
-25
lines changed

src/dom.ts

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -227,17 +227,18 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
227227
return point;
228228
}
229229

230-
async _retryPointerAction(action: (point: types.Point) => Promise<void>, options: PointerActionOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}): Promise<void> {
230+
async _retryPointerAction(actionName: string, action: (point: types.Point) => Promise<void>, options: PointerActionOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}): Promise<void> {
231+
this._page._log(inputLog, `elementHandle.${actionName}()`);
231232
const deadline = this._page._timeoutSettings.computeDeadline(options);
232233
while (!helper.isPastDeadline(deadline)) {
233-
const result = await this._performPointerAction(action, deadline, options);
234+
const result = await this._performPointerAction(actionName, action, deadline, options);
234235
if (result === 'done')
235236
return;
236237
}
237-
throw new TimeoutError(`waiting for element to receive pointer events failed: timeout exceeded`);
238+
throw new TimeoutError(`waiting for element to receive pointer events failed: timeout exceeded. Re-run with the DEBUG=pw:input env variable to see the debug log.`);
238239
}
239240

240-
async _performPointerAction(action: (point: types.Point) => Promise<void>, deadline: number, options: PointerActionOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}): Promise<'done' | 'retry'> {
241+
async _performPointerAction(actionName: string, action: (point: types.Point) => Promise<void>, deadline: number, options: PointerActionOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}): Promise<'done' | 'retry'> {
241242
const { force = false, position } = options;
242243
if (!force)
243244
await this._waitForDisplayedAtStablePosition(deadline);
@@ -271,10 +272,10 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
271272
let restoreModifiers: input.Modifier[] | undefined;
272273
if (options && options.modifiers)
273274
restoreModifiers = await this._page.keyboard._ensureModifiers(options.modifiers);
274-
this._page._log(inputLog, 'performing input action...');
275+
this._page._log(inputLog, `performing "${actionName}" action...`);
275276
await action(point);
276-
this._page._log(inputLog, '...input action done');
277-
this._page._log(inputLog, 'waiting for navigations to finish...');
277+
this._page._log(inputLog, `... "${actionName}" action done`);
278+
this._page._log(inputLog, 'waiting for scheduled navigations to finish...');
278279
await this._page._delegate.setActivityPaused(false);
279280
paused = false;
280281
if (restoreModifiers)
@@ -290,18 +291,19 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
290291
}
291292

292293
hover(options?: PointerActionOptions & types.PointerActionWaitOptions): Promise<void> {
293-
return this._retryPointerAction(point => this._page.mouse.move(point.x, point.y), options);
294+
return this._retryPointerAction('hover', point => this._page.mouse.move(point.x, point.y), options);
294295
}
295296

296297
click(options?: ClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<void> {
297-
return this._retryPointerAction(point => this._page.mouse.click(point.x, point.y, options), options);
298+
return this._retryPointerAction('click', point => this._page.mouse.click(point.x, point.y, options), options);
298299
}
299300

300301
dblclick(options?: MultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<void> {
301-
return this._retryPointerAction(point => this._page.mouse.dblclick(point.x, point.y, options), options);
302+
return this._retryPointerAction('dblclick', point => this._page.mouse.dblclick(point.x, point.y, options), options);
302303
}
303304

304305
async selectOption(values: string | ElementHandle | types.SelectOption | string[] | ElementHandle[] | types.SelectOption[], options?: types.NavigatingActionWaitOptions): Promise<string[]> {
306+
this._page._log(inputLog, `elementHandle.selectOption(%s)`, values);
305307
const deadline = this._page._timeoutSettings.computeDeadline(options);
306308
let vals: string[] | ElementHandle[] | types.SelectOption[];
307309
if (!Array.isArray(values))
@@ -326,6 +328,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
326328
}
327329

328330
async fill(value: string, options?: types.NavigatingActionWaitOptions): Promise<void> {
331+
this._page._log(inputLog, `elementHandle.fill(${value})`);
329332
assert(helper.isString(value), 'Value must be string. Found value "' + value + '" of type "' + (typeof value) + '"');
330333
const deadline = this._page._timeoutSettings.computeDeadline(options);
331334
await this._page._frameManager.waitForSignalsCreatedBy(async () => {
@@ -341,11 +344,13 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
341344
}
342345

343346
async selectText(): Promise<void> {
347+
this._page._log(inputLog, `elementHandle.selectText()`);
344348
const injectedResult = await this._evaluateInUtility(({ injected, node }) => injected.selectText(node), {});
345349
handleInjectedResult(injectedResult, '');
346350
}
347351

348352
async setInputFiles(files: string | types.FilePayload | string[] | types.FilePayload[], options?: types.NavigatingActionWaitOptions) {
353+
this._page._log(inputLog, `elementHandle.setInputFiles(...)`);
349354
const deadline = this._page._timeoutSettings.computeDeadline(options);
350355
const injectedResult = await this._evaluateInUtility(({ node }): InjectedResult<boolean> => {
351356
if (node.nodeType !== Node.ELEMENT_NODE || (node as Node as Element).tagName !== 'INPUT')
@@ -381,11 +386,13 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
381386
}
382387

383388
async focus() {
389+
this._page._log(inputLog, `elementHandle.focus()`);
384390
const injectedResult = await this._evaluateInUtility(({ injected, node }) => injected.focusNode(node), {});
385391
handleInjectedResult(injectedResult, '');
386392
}
387393

388394
async type(text: string, options?: { delay?: number } & types.NavigatingActionWaitOptions) {
395+
this._page._log(inputLog, `elementHandle.type("${text}")`);
389396
const deadline = this._page._timeoutSettings.computeDeadline(options);
390397
await this._page._frameManager.waitForSignalsCreatedBy(async () => {
391398
await this.focus();
@@ -394,6 +401,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
394401
}
395402

396403
async press(key: string, options?: { delay?: number } & types.NavigatingActionWaitOptions) {
404+
this._page._log(inputLog, `elementHandle.press("${key}")`);
397405
const deadline = this._page._timeoutSettings.computeDeadline(options);
398406
await this._page._frameManager.waitForSignalsCreatedBy(async () => {
399407
await this.focus();
@@ -402,10 +410,12 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
402410
}
403411

404412
async check(options?: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) {
413+
this._page._log(inputLog, `elementHandle.check()`);
405414
await this._setChecked(true, options);
406415
}
407416

408417
async uncheck(options?: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) {
418+
this._page._log(inputLog, `elementHandle.uncheck()`);
409419
await this._setChecked(false, options);
410420
}
411421

@@ -494,7 +504,7 @@ function handleInjectedResult<T = undefined>(injectedResult: InjectedResult<T>,
494504
if (injectedResult.status === 'notconnected')
495505
throw new NotConnectedError();
496506
if (injectedResult.status === 'timeout')
497-
throw new TimeoutError(`waiting for ${timeoutMessage} failed: timeout exceeded`);
507+
throw new TimeoutError(`waiting for ${timeoutMessage} failed: timeout exceeded. Re-run with the DEBUG=pw:input env variable to see the debug log.`);
498508
if (injectedResult.status === 'error')
499509
throw new Error(injectedResult.error);
500510
return injectedResult.value as T;

src/frames.ts

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -684,13 +684,17 @@ export class Frame {
684684
}
685685

686686
private async _retryWithSelectorIfNotConnected<R>(
687+
actionName: string,
687688
selector: string, options: types.TimeoutOptions,
688689
action: (handle: dom.ElementHandle<Element>, deadline: number) => Promise<R>): Promise<R> {
689690
const deadline = this._page._timeoutSettings.computeDeadline(options);
691+
this._page._log(dom.inputLog, `(page|frame).${actionName}("${selector}")`);
690692
while (!helper.isPastDeadline(deadline)) {
691693
try {
692694
const { world, task } = selectors._waitForSelectorTask(selector, 'attached', deadline);
695+
this._page._log(dom.inputLog, `waiting for the selector "${selector}"`);
693696
const handle = await this._scheduleRerunnableTask(task, world, deadline, `selector "${selector}"`);
697+
this._page._log(dom.inputLog, `...got element for the selector`);
694698
const element = handle.asElement() as dom.ElementHandle<Element>;
695699
try {
696700
return await action(element, deadline);
@@ -703,61 +707,61 @@ export class Frame {
703707
this._page._log(dom.inputLog, 'Element was detached from the DOM, retrying');
704708
}
705709
}
706-
throw new TimeoutError(`waiting for selector "${selector}" failed: timeout exceeded`);
710+
throw new TimeoutError(`waiting for selector "${selector}" failed: timeout exceeded. Re-run with the DEBUG=pw:input env variable to see the debug log.`);
707711
}
708712

709713
async click(selector: string, options: dom.ClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
710-
await this._retryWithSelectorIfNotConnected(selector, options,
714+
await this._retryWithSelectorIfNotConnected('click', selector, options,
711715
(handle, deadline) => handle.click(helper.optionsWithUpdatedTimeout(options, deadline)));
712716
}
713717

714718
async dblclick(selector: string, options: dom.MultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
715-
await this._retryWithSelectorIfNotConnected(selector, options,
719+
await this._retryWithSelectorIfNotConnected('dblclick', selector, options,
716720
(handle, deadline) => handle.dblclick(helper.optionsWithUpdatedTimeout(options, deadline)));
717721
}
718722

719723
async fill(selector: string, value: string, options: types.NavigatingActionWaitOptions = {}) {
720-
await this._retryWithSelectorIfNotConnected(selector, options,
724+
await this._retryWithSelectorIfNotConnected('fill', selector, options,
721725
(handle, deadline) => handle.fill(value, helper.optionsWithUpdatedTimeout(options, deadline)));
722726
}
723727

724728
async focus(selector: string, options: types.TimeoutOptions = {}) {
725-
await this._retryWithSelectorIfNotConnected(selector, options,
729+
await this._retryWithSelectorIfNotConnected('focus', selector, options,
726730
(handle, deadline) => handle.focus());
727731
}
728732

729733
async hover(selector: string, options: dom.PointerActionOptions & types.PointerActionWaitOptions = {}) {
730-
await this._retryWithSelectorIfNotConnected(selector, options,
734+
await this._retryWithSelectorIfNotConnected('hover', selector, options,
731735
(handle, deadline) => handle.hover(helper.optionsWithUpdatedTimeout(options, deadline)));
732736
}
733737

734738
async selectOption(selector: string, values: string | dom.ElementHandle | types.SelectOption | string[] | dom.ElementHandle[] | types.SelectOption[], options: types.NavigatingActionWaitOptions = {}): Promise<string[]> {
735-
return await this._retryWithSelectorIfNotConnected(selector, options,
739+
return await this._retryWithSelectorIfNotConnected('selectOption', selector, options,
736740
(handle, deadline) => handle.selectOption(values, helper.optionsWithUpdatedTimeout(options, deadline)));
737741
}
738742

739743
async setInputFiles(selector: string, files: string | types.FilePayload | string[] | types.FilePayload[], options: types.NavigatingActionWaitOptions = {}): Promise<void> {
740-
await this._retryWithSelectorIfNotConnected(selector, options,
744+
await this._retryWithSelectorIfNotConnected('setInputFiles', selector, options,
741745
(handle, deadline) => handle.setInputFiles(files, helper.optionsWithUpdatedTimeout(options, deadline)));
742746
}
743747

744748
async type(selector: string, text: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}) {
745-
await this._retryWithSelectorIfNotConnected(selector, options,
749+
await this._retryWithSelectorIfNotConnected('type', selector, options,
746750
(handle, deadline) => handle.type(text, helper.optionsWithUpdatedTimeout(options, deadline)));
747751
}
748752

749753
async press(selector: string, key: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}) {
750-
await this._retryWithSelectorIfNotConnected(selector, options,
754+
await this._retryWithSelectorIfNotConnected('press', selector, options,
751755
(handle, deadline) => handle.press(key, helper.optionsWithUpdatedTimeout(options, deadline)));
752756
}
753757

754758
async check(selector: string, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
755-
await this._retryWithSelectorIfNotConnected(selector, options,
759+
await this._retryWithSelectorIfNotConnected('check', selector, options,
756760
(handle, deadline) => handle.check(helper.optionsWithUpdatedTimeout(options, deadline)));
757761
}
758762

759763
async uncheck(selector: string, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
760-
await this._retryWithSelectorIfNotConnected(selector, options,
764+
await this._retryWithSelectorIfNotConnected('uncheck', selector, options,
761765
(handle, deadline) => handle.uncheck(helper.optionsWithUpdatedTimeout(options, deadline)));
762766
}
763767

@@ -879,7 +883,7 @@ class RerunnableTask {
879883
});
880884
// Since page navigation requires us to re-install the pageScript, we should track
881885
// timeout on our end.
882-
const timeoutError = new TimeoutError(`waiting for ${title || 'function'} failed: timeout exceeded`);
886+
const timeoutError = new TimeoutError(`waiting for ${title || 'function'} failed: timeout exceeded. Re-run with the DEBUG=pw:input env variable to see the debug log.`);
883887
this._timeoutTimer = setTimeout(() => this.terminate(timeoutError), helper.timeUntilDeadline(deadline));
884888
}
885889

src/helper.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ class Helper {
164164

165165
static async waitWithDeadline<T>(promise: Promise<T>, taskName: string, deadline: number): Promise<T> {
166166
let reject: (error: Error) => void;
167-
const timeoutError = new TimeoutError(`waiting for ${taskName} failed: timeout exceeded`);
167+
const timeoutError = new TimeoutError(`Waiting for ${taskName} failed: timeout exceeded. Re-run with the DEBUG=pw:input env variable to see the debug log.`);
168168
const timeoutPromise = new Promise<T>((resolve, x) => reject = x);
169169
const timeoutTimer = setTimeout(() => reject(timeoutError), helper.timeUntilDeadline(deadline));
170170
try {

test/click.spec.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,12 +183,14 @@ describe('Page.click', function() {
183183
await page.$eval('button', b => b.style.display = 'none');
184184
const error = await page.click('button', { timeout: 100 }).catch(e => e);
185185
expect(error.message).toContain('timeout exceeded');
186+
expect(error.message).toContain('DEBUG=pw:input');
186187
});
187188
it('should timeout waiting for visbility:hidden to be gone', async({page, server}) => {
188189
await page.goto(server.PREFIX + '/input/button.html');
189190
await page.$eval('button', b => b.style.visibility = 'hidden');
190191
const error = await page.click('button', { timeout: 100 }).catch(e => e);
191192
expect(error.message).toContain('timeout exceeded');
193+
expect(error.message).toContain('DEBUG=pw:input');
192194
});
193195
it('should waitFor visible when parent is hidden', async({page, server}) => {
194196
let done = false;
@@ -433,6 +435,7 @@ describe('Page.click', function() {
433435
});
434436
const error = await button.click({ timeout: 100 }).catch(e => e);
435437
expect(error.message).toContain('timeout exceeded');
438+
expect(error.message).toContain('DEBUG=pw:input');
436439
});
437440
it('should wait for becoming hit target', async({page, server}) => {
438441
await page.goto(server.PREFIX + '/input/button.html');
@@ -481,6 +484,7 @@ describe('Page.click', function() {
481484
});
482485
const error = await button.click({ timeout: 100 }).catch(e => e);
483486
expect(error.message).toContain('timeout exceeded');
487+
expect(error.message).toContain('DEBUG=pw:input');
484488
});
485489
it('should fail when obscured and not waiting for hit target', async({page, server}) => {
486490
await page.goto(server.PREFIX + '/input/button.html');
@@ -583,6 +587,7 @@ describe('Page.click', function() {
583587
expect(clicked).toBe(false);
584588
expect(await page.evaluate(() => window.clicked)).toBe(undefined);
585589
expect(error.message).toContain('timeout exceeded');
590+
expect(error.message).toContain('DEBUG=pw:input');
586591
});
587592
it.skip(true)('should pause animations', async({page}) => {
588593
// This test requires pausing the page.

0 commit comments

Comments
 (0)