fix(highlight): fix the testing harness to be real (#18294)

This commit is contained in:
Pavel Feldman 2022-10-24 18:01:48 -04:00 committed by GitHub
parent be67189a54
commit 3f850d27e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 78 additions and 113 deletions

View File

@ -54,10 +54,7 @@ export class Highlight {
this._actionPointElement = document.createElement('x-pw-action-point');
this._actionPointElement.setAttribute('hidden', 'true');
// NOTE: do not use an open shadow root, event for test.
// Closed shadow root prevents selectors matching our internal previews.
this._glassPaneShadow = this._glassPaneElement.attachShadow({ mode: 'closed' });
this._glassPaneShadow = this._glassPaneElement.attachShadow({ mode: this._isUnderTest ? 'open' : 'closed' });
this._glassPaneShadow.appendChild(this._actionPointElement);
const styleElement = document.createElement('style');
styleElement.textContent = `
@ -182,8 +179,6 @@ export class Highlight {
tooltipElement.style.top = '0';
tooltipElement.style.left = '0';
tooltipElement.style.display = 'flex';
if (this._isUnderTest)
console.error('Highlight text for test: ' + JSON.stringify(tooltipElement.textContent)); // eslint-disable-line no-console
}
this._highlightEntries.push({ targetElement: elements[i], tooltipElement, highlightElement });
}

View File

@ -74,7 +74,7 @@ class Recorder {
addEventListener(document, 'mouseup', event => this._onMouseUp(event as MouseEvent), true),
addEventListener(document, 'mousemove', event => this._onMouseMove(event as MouseEvent), true),
addEventListener(document, 'mouseleave', event => this._onMouseLeave(event as MouseEvent), true),
addEventListener(document, 'focus', () => this._onFocus(), true),
addEventListener(document, 'focus', () => this._onFocus(true), true),
addEventListener(document, 'scroll', () => {
this._hoveredModel = null;
this._highlight.hideActionPoint();
@ -234,12 +234,13 @@ class Recorder {
}
}
private _onFocus() {
private _onFocus(userGesture: boolean) {
const activeElement = this._deepActiveElement(document);
const result = activeElement ? generateSelector(this._injectedScript, activeElement, true) : null;
this._activeModel = result && result.selector ? result : null;
if (this._injectedScript.isUnderTest)
console.error('Highlight updated for test: ' + (result ? result.selector : null)); // eslint-disable-line no-console
if (userGesture)
this._hoveredElement = activeElement as HTMLElement | null;
this._updateModelForHoveredElement();
}
private _updateModelForHoveredElement() {
@ -250,12 +251,10 @@ class Recorder {
}
const hoveredElement = this._hoveredElement;
const { selector, elements } = generateSelector(this._injectedScript, hoveredElement, true);
if ((this._hoveredModel && this._hoveredModel.selector === selector) || this._hoveredElement !== hoveredElement)
if ((this._hoveredModel && this._hoveredModel.selector === selector))
return;
this._hoveredModel = selector ? { selector, elements } : null;
this._updateHighlight();
if (this._injectedScript.isUnderTest)
console.error('Highlight updated for test: ' + selector); // eslint-disable-line no-console
}
private _updateHighlight() {
@ -392,10 +391,8 @@ class Recorder {
await globalThis.__pw_recorderPerformAction(action).catch(() => {});
this._performingAction = false;
// Action could have changed DOM, update hovered model selectors.
this._updateModelForHoveredElement();
// If that was a keyboard action, it similarly requires new selectors for active model.
this._onFocus();
this._onFocus(false);
if (this._injectedScript.isUnderTest) {
// Serialize all to string as we cannot attribute console message to isolated world

View File

@ -25,8 +25,8 @@ test.describe('cli codegen', () => {
await recorder.setContentAndWait(`<button onclick="console.log('click')">Submit</button>`);
const selector = await recorder.hoverOverElement('button');
expect(selector).toBe('internal:role=button[name=\"Submit\"]');
const locator = await recorder.hoverOverElement('button');
expect(locator).toBe(`getByRole('button', { name: 'Submit' })`);
const [message, sources] = await Promise.all([
page.waitForEvent('console', msg => msg.type() !== 'error'),
@ -68,8 +68,8 @@ test.describe('cli codegen', () => {
// the second unnecessary copy of the recorder script.
await page.waitForTimeout(1000);
const selector = await recorder.hoverOverElement('button');
expect(selector).toBe('internal:role=button[name=\"Submit\"]');
const locator = await recorder.hoverOverElement('button');
expect(locator).toBe(`getByRole('button', { name: 'Submit' })`);
const [message, sources] = await Promise.all([
page.waitForEvent('console', msg => msg.type() !== 'error'),
@ -95,10 +95,10 @@ test.describe('cli codegen', () => {
</script>
`);
const selector = await recorder.waitForHighlight(() => recorder.page.hover('canvas', {
const locator = await recorder.waitForHighlight(() => recorder.page.hover('canvas', {
position: { x: 250, y: 250 },
}));
expect(selector).toBe('canvas');
expect(locator).toBe(`locator('canvas')`);
const [message, sources] = await Promise.all([
page.waitForEvent('console', msg => msg.type() !== 'error'),
recorder.waitForOutput('JavaScript', 'click'),
@ -148,8 +148,8 @@ test.describe('cli codegen', () => {
<button onclick="console.log('click')">Submit</button>
</body>`);
const selector = await recorder.hoverOverElement('button');
expect(selector).toBe('internal:role=button[name=\"Submit\"]');
const locator = await recorder.hoverOverElement('button');
expect(locator).toBe(`getByRole('button', { name: 'Submit' })`);
const [message, sources] = await Promise.all([
page.waitForEvent('console', msg => msg.type() !== 'error'),
@ -191,11 +191,10 @@ test.describe('cli codegen', () => {
document.documentElement.appendChild(div);
});
const selector = await recorder.hoverOverElement('div');
expect(selector).toBe('internal:text="Some long text here"i');
const locator = await recorder.hoverOverElement('div');
expect(locator).toBe(`getByText('Some long text here')`);
// Sanity check that selector does not match our highlight.
const divContents = await page.$eval(selector, div => div.outerHTML);
const divContents = await page.$eval('div', div => div.outerHTML);
expect(divContents.replace(/\s__playwright_target__="[^"]+"/, '')).toBe(`<div onclick="console.log('click')"> Some long text here </div>`);
const [message, sources] = await Promise.all([
@ -212,8 +211,8 @@ test.describe('cli codegen', () => {
const recorder = await openRecorder();
await recorder.setContentAndWait(`<input id="input" name="name" oninput="console.log(input.value)"></input>`);
const selector = await recorder.focusElement('input');
expect(selector).toBe('input[name="name"]');
const locator = await recorder.focusElement('input');
expect(locator).toBe(`locator('input[name="name"]')`);
const [message, sources] = await Promise.all([
page.waitForEvent('console', msg => msg.type() !== 'error'),
@ -243,16 +242,16 @@ test.describe('cli codegen', () => {
// In Japanese, "てすと" or "テスト" means "test".
await recorder.setContentAndWait(`<input id="input" name="name" oninput="input.value === 'てすと' && console.log(input.value)"></input>`);
const selector = await recorder.focusElement('input');
expect(selector).toBe('input[name="name"]');
const locator = await recorder.focusElement('input');
expect(locator).toBe(`locator('input[name="name"]')`);
const [message, sources] = await Promise.all([
page.waitForEvent('console', msg => msg.type() !== 'error'),
recorder.waitForOutput('JavaScript', 'fill'),
(async () => {
await recorder.page.dispatchEvent(selector, 'keydown', { key: 'Process' });
await recorder.page.dispatchEvent('input', 'keydown', { key: 'Process' });
await recorder.page.keyboard.insertText('てすと');
await recorder.page.dispatchEvent(selector, 'keyup', { key: 'Process' });
await recorder.page.dispatchEvent('input', 'keyup', { key: 'Process' });
})()
]);
expect(sources.get('JavaScript').text).toContain(`
@ -276,8 +275,8 @@ test.describe('cli codegen', () => {
const recorder = await openRecorder();
await recorder.setContentAndWait(`<textarea id="textarea" name="name" oninput="console.log(textarea.value)"></textarea>`);
const selector = await recorder.focusElement('textarea');
expect(selector).toBe('textarea[name="name"]');
const locator = await recorder.focusElement('textarea');
expect(locator).toBe(`locator('textarea[name="name"]')`);
const [message, sources] = await Promise.all([
page.waitForEvent('console', msg => msg.type() !== 'error'),
@ -294,8 +293,8 @@ test.describe('cli codegen', () => {
await recorder.setContentAndWait(`<input name="name" onkeypress="console.log('press')"></input>`);
const selector = await recorder.focusElement('input');
expect(selector).toBe('input[name="name"]');
const locator = await recorder.focusElement('input');
expect(locator).toBe(`locator('input[name="name"]')`);
const messages: any[] = [];
page.on('console', message => messages.push(message));
@ -357,8 +356,8 @@ test.describe('cli codegen', () => {
await recorder.setContentAndWait(`<input name="name" onkeydown="console.log('press:' + event.key)"></input>`);
const selector = await recorder.focusElement('input');
expect(selector).toBe('input[name="name"]');
const locator = await recorder.focusElement('input');
expect(locator).toBe(`locator('input[name="name"]')`);
const messages: any[] = [];
page.on('console', message => {
@ -379,8 +378,8 @@ test.describe('cli codegen', () => {
await recorder.setContentAndWait(`<input name="name" onkeydown="console.log('down:' + event.key)" onkeyup="console.log('up:' + event.key)"></input>`);
const selector = await recorder.focusElement('input');
expect(selector).toBe('input[name="name"]');
const locator = await recorder.focusElement('input');
expect(locator).toBe(`locator('input[name="name"]')`);
const messages: any[] = [];
page.on('console', message => {
@ -404,8 +403,8 @@ test.describe('cli codegen', () => {
await recorder.setContentAndWait(`<input id="checkbox" type="checkbox" name="accept" onchange="console.log(checkbox.checked)"></input>`);
const selector = await recorder.focusElement('input');
expect(selector).toBe('input[name="accept"]');
const locator = await recorder.focusElement('input');
expect(locator).toBe(`locator('input[name="accept"]')`);
const [message, sources] = await Promise.all([
page.waitForEvent('console', msg => msg.type() !== 'error'),
@ -436,8 +435,8 @@ test.describe('cli codegen', () => {
await recorder.setContentAndWait(`<input id="checkbox" type="radio" name="accept" onchange="console.log(checkbox.checked)"></input>`);
const selector = await recorder.focusElement('input');
expect(selector).toBe('input[name="accept"]');
const locator = await recorder.focusElement('input');
expect(locator).toBe(`locator('input[name="accept"]')`);
const [message, sources] = await Promise.all([
page.waitForEvent('console', msg => msg.type() !== 'error'),
@ -455,8 +454,8 @@ test.describe('cli codegen', () => {
await recorder.setContentAndWait(`<input id="checkbox" type="checkbox" name="accept" onchange="console.log(checkbox.checked)"></input>`);
const selector = await recorder.focusElement('input');
expect(selector).toBe('input[name="accept"]');
const locator = await recorder.focusElement('input');
expect(locator).toBe(`locator('input[name="accept"]')`);
const [message, sources] = await Promise.all([
page.waitForEvent('console', msg => msg.type() !== 'error'),
@ -474,8 +473,8 @@ test.describe('cli codegen', () => {
await recorder.setContentAndWait(`<input id="checkbox" type="checkbox" checked name="accept" onchange="console.log(checkbox.checked)"></input>`);
const selector = await recorder.focusElement('input');
expect(selector).toBe('input[name="accept"]');
const locator = await recorder.focusElement('input');
expect(locator).toBe(`locator('input[name="accept"]')`);
const [message, sources] = await Promise.all([
page.waitForEvent('console', msg => msg.type() !== 'error'),
@ -506,8 +505,8 @@ test.describe('cli codegen', () => {
await recorder.setContentAndWait('<select id="age" onchange="console.log(age.selectedOptions[0].value)"><option value="1"><option value="2"></select>');
const selector = await recorder.hoverOverElement('select');
expect(selector).toBe('select');
const locator = await recorder.hoverOverElement('select');
expect(locator).toBe(`locator('select')`);
const [message, sources] = await Promise.all([
page.waitForEvent('console', msg => msg.type() !== 'error'),
@ -539,8 +538,8 @@ test.describe('cli codegen', () => {
const recorder = await openRecorder();
await recorder.setContentAndWait('<a target=_blank rel=noopener href="about:blank">link</a>');
const selector = await recorder.hoverOverElement('a');
expect(selector).toBe('internal:role=link[name=\"link\"]');
const locator = await recorder.hoverOverElement('a');
expect(locator).toBe(`getByRole('link', { name: 'link' })`);
const [popup, sources] = await Promise.all([
page.context().waitForEvent('page'),
@ -583,8 +582,8 @@ test.describe('cli codegen', () => {
await recorder.setContentAndWait(`<a onclick="window.location.href='about:blank#foo'">link</a>`);
const selector = await recorder.hoverOverElement('a');
expect(selector).toBe('internal:text="link"i');
const locator = await recorder.hoverOverElement('a');
expect(locator).toBe(`getByText('link')`);
const [, sources] = await Promise.all([
page.waitForNavigation(),
recorder.waitForOutput('JavaScript', '.click()'),

View File

@ -98,8 +98,7 @@ test.describe('cli codegen', () => {
const errors: any[] = [];
recorder.page.on('pageerror', e => errors.push(e));
await recorder.page.evaluate(() => document.querySelector('body').remove());
const selector = await recorder.hoverOverElement('html');
expect(selector).toBe('html');
await page.dispatchEvent('html', 'mousemove', { detail: 1 });
await recorder.page.close();
await recorder.waitForOutput('JavaScript', 'page.close();');
expect(errors.length).toBe(0);
@ -219,10 +218,10 @@ test.describe('cli codegen', () => {
await recorder.setContentAndWait(`
<a href="${server.PREFIX}/download" download>Download</a>
`, server.PREFIX);
await recorder.hoverOverElement('text=Download');
await recorder.hoverOverElement('a');
await Promise.all([
page.waitForEvent('download'),
page.click('text=Download')
page.click('a')
]);
const sources = await recorder.waitForOutput('JavaScript', 'waitForEvent');
@ -274,7 +273,7 @@ test.describe('cli codegen', () => {
page.once('dialog', async dialog => {
await dialog.dismiss();
});
await page.click('text=click me');
await page.click('button');
const sources = await recorder.waitForOutput('JavaScript', 'once');
@ -332,8 +331,8 @@ test.describe('cli codegen', () => {
const recorder = await openRecorder();
await recorder.setContentAndWait(`<a href="about:blank?foo">link</a>`);
const selector = await recorder.hoverOverElement('a');
expect(selector).toBe('internal:role=link[name=\"link\"]');
const locator = await recorder.hoverOverElement('a');
expect(locator).toBe(`getByRole('link', { name: 'link' })`);
await page.click('a', { modifiers: [platform === 'darwin' ? 'Meta' : 'Control'] });
const sources = await recorder.waitForOutput('JavaScript', 'page1');
@ -502,8 +501,8 @@ test.describe('cli codegen', () => {
const recorder = await openRecorder();
await recorder.setContentAndWait(`<textarea spellcheck=false id="textarea" name="name" oninput="console.log(textarea.value)"></textarea>`);
const selector = await recorder.focusElement('textarea');
expect(selector).toBe('textarea[name="name"]');
const locator = await recorder.focusElement('textarea');
expect(locator).toBe(`locator('textarea[name="name"]')`);
const [message, sources] = await Promise.all([
page.waitForEvent('console', msg => msg.type() !== 'error'),

View File

@ -27,8 +27,8 @@ test.describe('cli codegen', () => {
<button onclick="console.log('click2')">Submit</button>
`);
const selector = await recorder.hoverOverElement('button');
expect(selector).toBe('internal:role=button[name=\"Submit\"] >> nth=0');
const locator = await recorder.hoverOverElement('button');
expect(locator).toBe(`getByRole('button', { name: 'Submit' }).first()`);
const [message, sources] = await Promise.all([
page.waitForEvent('console', msg => msg.type() !== 'error'),
@ -62,8 +62,8 @@ test.describe('cli codegen', () => {
<button onclick="console.log('click2')">Submit</button>
`);
const selector = await recorder.hoverOverElement('button >> nth=1');
expect(selector).toBe('internal:role=button[name=\"Submit\"] >> nth=1');
const locator = await recorder.hoverOverElement('button >> nth=1');
expect(locator).toBe(`getByRole('button', { name: 'Submit' }).nth(1)`);
const [message, sources] = await Promise.all([
page.waitForEvent('console', msg => msg.type() !== 'error'),
@ -234,8 +234,8 @@ test.describe('cli codegen', () => {
await recorder.setContentAndWait(`<div data-testid=testid onclick="console.log('click')">Submit</div>`);
const selector = await recorder.hoverOverElement('div');
expect(selector).toBe('internal:attr=[data-testid="testid"]');
const locator = await recorder.hoverOverElement('div');
expect(locator).toBe(`getByTestId('testid')`);
const [message, sources] = await Promise.all([
page.waitForEvent('console', msg => msg.type() !== 'error'),
@ -266,8 +266,8 @@ test.describe('cli codegen', () => {
await recorder.setContentAndWait(`<input placeholder="Country"></input>`);
const selector = await recorder.hoverOverElement('input');
expect(selector).toBe('internal:attr=[placeholder="Country"i]');
const locator = await recorder.hoverOverElement('input');
expect(locator).toBe(`getByPlaceholder('Country')`);
const [sources] = await Promise.all([
recorder.waitForOutput('JavaScript', 'click'),
@ -295,8 +295,8 @@ test.describe('cli codegen', () => {
await recorder.setContentAndWait(`<input alt="Country"></input>`);
const selector = await recorder.hoverOverElement('input');
expect(selector).toBe('internal:attr=[alt="Country"i]');
const locator = await recorder.hoverOverElement('input');
expect(locator).toBe(`getByAltText('Country')`);
const [sources] = await Promise.all([
recorder.waitForOutput('JavaScript', 'click'),
@ -324,8 +324,8 @@ test.describe('cli codegen', () => {
await recorder.setContentAndWait(`<label for=target>Country</label><input id=target>`);
const selector = await recorder.hoverOverElement('input');
expect(selector).toBe('internal:label="Country"i');
const locator = await recorder.hoverOverElement('input');
expect(locator).toBe(`getByLabel('Country')`);
const [sources] = await Promise.all([
recorder.waitForOutput('JavaScript', 'click'),
@ -353,8 +353,8 @@ test.describe('cli codegen', () => {
await recorder.setContentAndWait(`<label for=target>Coun"try</label><input id=target>`);
const selector = await recorder.hoverOverElement('input');
expect(selector).toBe('internal:label="Coun\\\"try"i');
const locator = await recorder.hoverOverElement('input');
expect(locator).toBe(`getByLabel('Coun"try')`);
const [sources] = await Promise.all([
recorder.waitForOutput('JavaScript', 'click'),

View File

@ -15,7 +15,7 @@
*/
import { contextTest } from '../../config/browserTest';
import type { ConsoleMessage, Page } from 'playwright-core';
import type { Page } from 'playwright-core';
import * as path from 'path';
import type { Source } from '../../../packages/recorder/src/recorderTypes';
import type { CommonFixtures, TestChildProcess } from '../../config/commonFixtures';
@ -149,27 +149,10 @@ class Recorder {
}
async waitForHighlight(action: () => Promise<void>): Promise<string> {
// We get the last highlighted selector, because Firefox sometimes issues multiple
// focus events.
let generatedSelector: string | undefined;
let callback: Function | undefined;
const listener = async (msg: ConsoleMessage) => {
const prefix = 'Highlight updated for test: ';
if (msg.text().startsWith(prefix)) {
generatedSelector = msg.text().substr(prefix.length);
if (callback) {
this.page.off('console', listener);
callback(generatedSelector);
}
}
};
this.page.on('console', listener);
await action();
if (generatedSelector)
return generatedSelector;
return await new Promise<string>(f => callback = f);
await this.page.locator('x-pw-highlight').waitFor();
await this.page.locator('x-pw-tooltip').waitFor();
return this.page.locator('x-pw-tooltip').textContent();
}
async waitForActionPerformed(): Promise<{ hovered: string | null, active: string | null }> {

View File

@ -15,27 +15,19 @@
*/
import { test as it, expect } from './pageTest';
import { waitForTestLog } from '../config/utils';
import type { Locator } from 'playwright-core';
type BoundingBox = Awaited<ReturnType<Locator['boundingBox']>>;
it.skip(({ mode }) => mode !== 'default', 'Highlight element has a closed shadow-root on != default');
it('should highlight locator', async ({ page, isAndroid }) => {
it('should highlight locator', async ({ page }) => {
await page.setContent(`<input type='text' />`);
const textPromise = waitForTestLog<string>(page, 'Highlight text for test: ');
const boxPromise = waitForTestLog<{ x: number, y: number, width: number, height: number }>(page, 'Highlight box for test: ');
await page.locator('input').highlight();
expect(await textPromise).toBe('locator(\'input\')');
let box1 = await page.locator('input').boundingBox();
let box2 = await boxPromise;
if (isAndroid) {
box1 = roundBox(box1);
box2 = roundBox(box2);
}
await expect(page.locator('x-pw-tooltip')).toHaveText('locator(\'input\')');
await expect(page.locator('x-pw-highlight')).toBeVisible();
const box1 = roundBox(await page.locator('input').boundingBox());
const box2 = roundBox(await page.locator('x-pw-highlight').boundingBox());
expect(box1).toEqual(box2);
});