mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
fix(textContent): make page.textContent(selector) atomic (#2717)
We now query selector and take textContent synchronously. This avoids any issues with async processing: node being recycled, detached, etc. More methods will follow with the same atomic pattern. Drive-by: fixed selector engine names being sometimes case-sensitive and sometimes not.
This commit is contained in:
parent
43f70ab978
commit
b54303a386
@ -52,7 +52,6 @@ export function parseSelector(selector: string): ParsedSelector {
|
|||||||
name = 'css';
|
name = 'css';
|
||||||
body = part;
|
body = part;
|
||||||
}
|
}
|
||||||
name = name.toLowerCase();
|
|
||||||
let capture = false;
|
let capture = false;
|
||||||
if (name[0] === '*') {
|
if (name[0] === '*') {
|
||||||
capture = true;
|
capture = true;
|
||||||
|
|||||||
54
src/dom.ts
54
src/dom.ts
@ -25,7 +25,7 @@ import * as injectedScriptSource from './generated/injectedScriptSource';
|
|||||||
import * as debugScriptSource from './generated/debugScriptSource';
|
import * as debugScriptSource from './generated/debugScriptSource';
|
||||||
import * as js from './javascript';
|
import * as js from './javascript';
|
||||||
import { Page } from './page';
|
import { Page } from './page';
|
||||||
import { selectors } from './selectors';
|
import { selectors, SelectorInfo } from './selectors';
|
||||||
import * as types from './types';
|
import * as types from './types';
|
||||||
import { Progress } from './progress';
|
import { Progress } from './progress';
|
||||||
import DebugScript from './debug/injected/debugScript';
|
import DebugScript from './debug/injected/debugScript';
|
||||||
@ -790,3 +790,55 @@ function roundPoint(point: types.Point): types.Point {
|
|||||||
y: (point.y * 100 | 0) / 100,
|
y: (point.y * 100 | 0) / 100,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SchedulableTask<T> = (injectedScript: js.JSHandle<InjectedScript>) => Promise<js.JSHandle<types.InjectedScriptPoll<T>>>;
|
||||||
|
|
||||||
|
export function waitForSelectorTask(selector: SelectorInfo, state: 'attached' | 'detached' | 'visible' | 'hidden'): SchedulableTask<Element | undefined> {
|
||||||
|
return injectedScript => injectedScript.evaluateHandle((injected, { parsed, state }) => {
|
||||||
|
let lastElement: Element | undefined;
|
||||||
|
|
||||||
|
return injected.pollRaf((progress, continuePolling) => {
|
||||||
|
const element = injected.querySelector(parsed, document);
|
||||||
|
const visible = element ? injected.isVisible(element) : false;
|
||||||
|
|
||||||
|
if (lastElement !== element) {
|
||||||
|
lastElement = element;
|
||||||
|
if (!element)
|
||||||
|
progress.log(` selector did not resolve to any element`);
|
||||||
|
else
|
||||||
|
progress.log(` selector resolved to ${visible ? 'visible' : 'hidden'} ${injected.previewNode(element)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case 'attached':
|
||||||
|
return element ? element : continuePolling;
|
||||||
|
case 'detached':
|
||||||
|
return !element ? undefined : continuePolling;
|
||||||
|
case 'visible':
|
||||||
|
return visible ? element : continuePolling;
|
||||||
|
case 'hidden':
|
||||||
|
return !visible ? undefined : continuePolling;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, { parsed: selector.parsed, state });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dispatchEventTask(selector: SelectorInfo, type: string, eventInit: Object): SchedulableTask<undefined> {
|
||||||
|
return injectedScript => injectedScript.evaluateHandle((injected, { parsed, type, eventInit }) => {
|
||||||
|
return injected.pollRaf((progress, continuePolling) => {
|
||||||
|
const element = injected.querySelector(parsed, document);
|
||||||
|
if (element)
|
||||||
|
injected.dispatchEvent(element, type, eventInit);
|
||||||
|
return element ? undefined : continuePolling;
|
||||||
|
});
|
||||||
|
}, { parsed: selector.parsed, type, eventInit });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function textContentTask(selector: SelectorInfo): SchedulableTask<string | null> {
|
||||||
|
return injectedScript => injectedScript.evaluateHandle((injected, parsed) => {
|
||||||
|
return injected.pollRaf((progress, continuePolling) => {
|
||||||
|
const element = injected.querySelector(parsed, document);
|
||||||
|
return element ? element.textContent : continuePolling;
|
||||||
|
});
|
||||||
|
}, selector.parsed);
|
||||||
|
}
|
||||||
|
|||||||
@ -33,7 +33,7 @@ type ContextData = {
|
|||||||
contextPromise: Promise<dom.FrameExecutionContext>;
|
contextPromise: Promise<dom.FrameExecutionContext>;
|
||||||
contextResolveCallback: (c: dom.FrameExecutionContext) => void;
|
contextResolveCallback: (c: dom.FrameExecutionContext) => void;
|
||||||
context: dom.FrameExecutionContext | null;
|
context: dom.FrameExecutionContext | null;
|
||||||
rerunnableTasks: Set<RerunnableTask<any>>;
|
rerunnableTasks: Set<RerunnableTask>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GotoResult = {
|
export type GotoResult = {
|
||||||
@ -456,10 +456,10 @@ export class Frame {
|
|||||||
if (!['attached', 'detached', 'visible', 'hidden'].includes(state))
|
if (!['attached', 'detached', 'visible', 'hidden'].includes(state))
|
||||||
throw new Error(`Unsupported state option "${state}"`);
|
throw new Error(`Unsupported state option "${state}"`);
|
||||||
const info = selectors._parseSelector(selector);
|
const info = selectors._parseSelector(selector);
|
||||||
const task = selectors._waitForSelectorTask(info, state);
|
const task = dom.waitForSelectorTask(info, state);
|
||||||
return this._page._runAbortableTask(async progress => {
|
return this._page._runAbortableTask(async progress => {
|
||||||
progress.logger.info(`waiting for selector "${selector}"${state === 'attached' ? '' : ' to be ' + state}`);
|
progress.logger.info(`waiting for selector "${selector}"${state === 'attached' ? '' : ' to be ' + state}`);
|
||||||
const result = await this._scheduleRerunnableTask(progress, info.world, task);
|
const result = await this._scheduleRerunnableHandleTask(progress, info.world, task);
|
||||||
if (!result.asElement()) {
|
if (!result.asElement()) {
|
||||||
result.dispose();
|
result.dispose();
|
||||||
return null;
|
return null;
|
||||||
@ -477,11 +477,11 @@ export class Frame {
|
|||||||
|
|
||||||
async dispatchEvent(selector: string, type: string, eventInit?: Object, options: types.TimeoutOptions = {}): Promise<void> {
|
async dispatchEvent(selector: string, type: string, eventInit?: Object, options: types.TimeoutOptions = {}): Promise<void> {
|
||||||
const info = selectors._parseSelector(selector);
|
const info = selectors._parseSelector(selector);
|
||||||
const task = selectors._dispatchEventTask(info, type, eventInit || {});
|
const task = dom.dispatchEventTask(info, type, eventInit || {});
|
||||||
return this._page._runAbortableTask(async progress => {
|
return this._page._runAbortableTask(async progress => {
|
||||||
progress.logger.info(`Dispatching "${type}" event on selector "${selector}"...`);
|
progress.logger.info(`Dispatching "${type}" event on selector "${selector}"...`);
|
||||||
const result = await this._scheduleRerunnableTask(progress, 'main', task);
|
// Note: we always dispatch events in the main world.
|
||||||
result.dispose();
|
await this._scheduleRerunnableTask(progress, 'main', task);
|
||||||
}, this._page._timeoutSettings.timeout(options), this._apiName('dispatchEvent'));
|
}, this._page._timeoutSettings.timeout(options), this._apiName('dispatchEvent'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -723,8 +723,8 @@ export class Frame {
|
|||||||
return this._page._runAbortableTask(async progress => {
|
return this._page._runAbortableTask(async progress => {
|
||||||
while (progress.isRunning()) {
|
while (progress.isRunning()) {
|
||||||
progress.logger.info(`waiting for selector "${selector}"`);
|
progress.logger.info(`waiting for selector "${selector}"`);
|
||||||
const task = selectors._waitForSelectorTask(info, 'attached');
|
const task = dom.waitForSelectorTask(info, 'attached');
|
||||||
const handle = await this._scheduleRerunnableTask(progress, info.world, task);
|
const handle = await this._scheduleRerunnableHandleTask(progress, info.world, task);
|
||||||
const element = handle.asElement() as dom.ElementHandle<Element>;
|
const element = handle.asElement() as dom.ElementHandle<Element>;
|
||||||
progress.cleanupWhenAborted(() => element.dispose());
|
progress.cleanupWhenAborted(() => element.dispose());
|
||||||
const result = await action(progress, element);
|
const result = await action(progress, element);
|
||||||
@ -755,8 +755,13 @@ export class Frame {
|
|||||||
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._focus(progress), this._apiName('focus'));
|
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._focus(progress), this._apiName('focus'));
|
||||||
}
|
}
|
||||||
|
|
||||||
async textContent(selector: string, options: types.TimeoutOptions = {}): Promise<null|string> {
|
async textContent(selector: string, options: types.TimeoutOptions = {}): Promise<string | null> {
|
||||||
return await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle.textContent(), this._apiName('textContent'));
|
const info = selectors._parseSelector(selector);
|
||||||
|
const task = dom.textContentTask(info);
|
||||||
|
return this._page._runAbortableTask(async progress => {
|
||||||
|
progress.logger.info(`Retrieving text context from "${selector}"...`);
|
||||||
|
return this._scheduleRerunnableTask(progress, info.world, task);
|
||||||
|
}, this._page._timeoutSettings.timeout(options), this._apiName('textContent'));
|
||||||
}
|
}
|
||||||
|
|
||||||
async innerText(selector: string, options: types.TimeoutOptions = {}): Promise<string> {
|
async innerText(selector: string, options: types.TimeoutOptions = {}): Promise<string> {
|
||||||
@ -809,7 +814,6 @@ export class Frame {
|
|||||||
return this._waitForFunctionExpression(String(pageFunction), typeof pageFunction === 'function', arg, options);
|
return this._waitForFunctionExpression(String(pageFunction), typeof pageFunction === 'function', arg, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async _waitForFunctionExpression<R>(expression: string, isFunction: boolean, arg: any, options: types.WaitForFunctionOptions = {}): Promise<js.SmartHandle<R>> {
|
async _waitForFunctionExpression<R>(expression: string, isFunction: boolean, arg: any, options: types.WaitForFunctionOptions = {}): Promise<js.SmartHandle<R>> {
|
||||||
const { polling = 'raf' } = options;
|
const { polling = 'raf' } = options;
|
||||||
if (helper.isString(polling))
|
if (helper.isString(polling))
|
||||||
@ -819,17 +823,14 @@ export class Frame {
|
|||||||
else
|
else
|
||||||
throw new Error('Unknown polling option: ' + polling);
|
throw new Error('Unknown polling option: ' + polling);
|
||||||
const predicateBody = isFunction ? 'return (' + expression + ')(arg)' : 'return (' + expression + ')';
|
const predicateBody = isFunction ? 'return (' + expression + ')(arg)' : 'return (' + expression + ')';
|
||||||
const task = async (context: dom.FrameExecutionContext) => {
|
const task: dom.SchedulableTask<R> = injectedScript => injectedScript.evaluateHandle((injectedScript, { predicateBody, polling, arg }) => {
|
||||||
const injectedScript = await context.injectedScript();
|
|
||||||
return context.evaluateHandleInternal(({ injectedScript, predicateBody, polling, arg }) => {
|
|
||||||
const innerPredicate = new Function('arg', predicateBody) as (arg: any) => R;
|
const innerPredicate = new Function('arg', predicateBody) as (arg: any) => R;
|
||||||
if (polling === 'raf')
|
if (polling === 'raf')
|
||||||
return injectedScript.pollRaf((progress, continuePolling) => innerPredicate(arg) || continuePolling);
|
return injectedScript.pollRaf((progress, continuePolling) => innerPredicate(arg) || continuePolling);
|
||||||
return injectedScript.pollInterval(polling, (progress, continuePolling) => innerPredicate(arg) || continuePolling);
|
return injectedScript.pollInterval(polling, (progress, continuePolling) => innerPredicate(arg) || continuePolling);
|
||||||
}, { injectedScript, predicateBody, polling, arg });
|
}, { predicateBody, polling, arg });
|
||||||
};
|
|
||||||
return this._page._runAbortableTask(
|
return this._page._runAbortableTask(
|
||||||
progress => this._scheduleRerunnableTask(progress, 'main', task),
|
progress => this._scheduleRerunnableHandleTask(progress, 'main', task),
|
||||||
this._page._timeoutSettings.timeout(options), this._apiName('waitForFunction'));
|
this._page._timeoutSettings.timeout(options), this._apiName('waitForFunction'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -850,9 +851,19 @@ export class Frame {
|
|||||||
this._parentFrame = null;
|
this._parentFrame = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _scheduleRerunnableTask<T>(progress: Progress, world: types.World, task: SchedulableTask<T>): Promise<js.SmartHandle<T>> {
|
private _scheduleRerunnableTask<T>(progress: Progress, world: types.World, task: dom.SchedulableTask<T>): Promise<T> {
|
||||||
const data = this._contextData.get(world)!;
|
const data = this._contextData.get(world)!;
|
||||||
const rerunnableTask = new RerunnableTask(data, progress, task);
|
const rerunnableTask = new RerunnableTask(data, progress, task, true /* returnByValue */);
|
||||||
|
if (this._detached)
|
||||||
|
rerunnableTask.terminate(new Error('waitForFunction failed: frame got detached.'));
|
||||||
|
if (data.context)
|
||||||
|
rerunnableTask.rerun(data.context);
|
||||||
|
return rerunnableTask.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _scheduleRerunnableHandleTask<T>(progress: Progress, world: types.World, task: dom.SchedulableTask<T>): Promise<js.SmartHandle<T>> {
|
||||||
|
const data = this._contextData.get(world)!;
|
||||||
|
const rerunnableTask = new RerunnableTask(data, progress, task, false /* returnByValue */);
|
||||||
if (this._detached)
|
if (this._detached)
|
||||||
rerunnableTask.terminate(new Error('waitForFunction failed: frame got detached.'));
|
rerunnableTask.terminate(new Error('waitForFunction failed: frame got detached.'));
|
||||||
if (data.context)
|
if (data.context)
|
||||||
@ -905,20 +916,20 @@ export class Frame {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SchedulableTask<T> = (context: dom.FrameExecutionContext) => Promise<js.JSHandle<types.InjectedScriptPoll<T>>>;
|
class RerunnableTask {
|
||||||
|
readonly promise: Promise<any>;
|
||||||
class RerunnableTask<T> {
|
private _task: dom.SchedulableTask<any>;
|
||||||
readonly promise: Promise<js.SmartHandle<T>>;
|
private _resolve: (result: any) => void = () => {};
|
||||||
private _task: SchedulableTask<T>;
|
|
||||||
private _resolve: (result: js.SmartHandle<T>) => void = () => {};
|
|
||||||
private _reject: (reason: Error) => void = () => {};
|
private _reject: (reason: Error) => void = () => {};
|
||||||
private _progress: Progress;
|
private _progress: Progress;
|
||||||
|
private _returnByValue: boolean;
|
||||||
|
|
||||||
constructor(data: ContextData, progress: Progress, task: SchedulableTask<T>) {
|
constructor(data: ContextData, progress: Progress, task: dom.SchedulableTask<any>, returnByValue: boolean) {
|
||||||
this._task = task;
|
this._task = task;
|
||||||
this._progress = progress;
|
this._progress = progress;
|
||||||
|
this._returnByValue = returnByValue;
|
||||||
data.rerunnableTasks.add(this);
|
data.rerunnableTasks.add(this);
|
||||||
this.promise = new Promise<js.SmartHandle<T>>((resolve, reject) => {
|
this.promise = new Promise<any>((resolve, reject) => {
|
||||||
// The task is either resolved with a value, or rejected with a meaningful evaluation error.
|
// The task is either resolved with a value, or rejected with a meaningful evaluation error.
|
||||||
this._resolve = resolve;
|
this._resolve = resolve;
|
||||||
this._reject = reject;
|
this._reject = reject;
|
||||||
@ -931,8 +942,9 @@ class RerunnableTask<T> {
|
|||||||
|
|
||||||
async rerun(context: dom.FrameExecutionContext) {
|
async rerun(context: dom.FrameExecutionContext) {
|
||||||
try {
|
try {
|
||||||
const pollHandler = new dom.InjectedScriptPollHandler(this._progress, await this._task(context));
|
const injectedScript = await context.injectedScript();
|
||||||
const result = await pollHandler.finishHandle();
|
const pollHandler = new dom.InjectedScriptPollHandler(this._progress, await this._task(injectedScript));
|
||||||
|
const result = this._returnByValue ? await pollHandler.finish() : await pollHandler.finishHandle();
|
||||||
this._resolve(result);
|
this._resolve(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// When the page is navigated, the promise is rejected.
|
// When the page is navigated, the promise is rejected.
|
||||||
|
|||||||
@ -109,54 +109,6 @@ export class Selectors {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
_waitForSelectorTask(selector: SelectorInfo, state: 'attached' | 'detached' | 'visible' | 'hidden'): frames.SchedulableTask<Element | undefined> {
|
|
||||||
return async (context: dom.FrameExecutionContext) => {
|
|
||||||
const injectedScript = await context.injectedScript();
|
|
||||||
return injectedScript.evaluateHandle((injected, { parsed, state }) => {
|
|
||||||
let lastElement: Element | undefined;
|
|
||||||
|
|
||||||
return injected.pollRaf((progress, continuePolling) => {
|
|
||||||
const element = injected.querySelector(parsed, document);
|
|
||||||
const visible = element ? injected.isVisible(element) : false;
|
|
||||||
|
|
||||||
if (lastElement !== element) {
|
|
||||||
lastElement = element;
|
|
||||||
if (!element)
|
|
||||||
progress.log(` selector did not resolve to any element`);
|
|
||||||
else
|
|
||||||
progress.log(` selector resolved to ${visible ? 'visible' : 'hidden'} ${injected.previewNode(element)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (state) {
|
|
||||||
case 'attached':
|
|
||||||
return element ? element : continuePolling;
|
|
||||||
case 'detached':
|
|
||||||
return !element ? undefined : continuePolling;
|
|
||||||
case 'visible':
|
|
||||||
return visible ? element : continuePolling;
|
|
||||||
case 'hidden':
|
|
||||||
return !visible ? undefined : continuePolling;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, { parsed: selector.parsed, state });
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
_dispatchEventTask(selector: SelectorInfo, type: string, eventInit: Object): frames.SchedulableTask<undefined> {
|
|
||||||
const task = async (context: dom.FrameExecutionContext) => {
|
|
||||||
const injectedScript = await context.injectedScript();
|
|
||||||
return injectedScript.evaluateHandle((injected, { parsed, type, eventInit }) => {
|
|
||||||
return injected.pollRaf((progress, continuePolling) => {
|
|
||||||
const element = injected.querySelector(parsed, document);
|
|
||||||
if (element)
|
|
||||||
injected.dispatchEvent(element, type, eventInit);
|
|
||||||
return element ? undefined : continuePolling;
|
|
||||||
});
|
|
||||||
}, { parsed: selector.parsed, type, eventInit });
|
|
||||||
};
|
|
||||||
return task;
|
|
||||||
}
|
|
||||||
|
|
||||||
async _createSelector(name: string, handle: dom.ElementHandle<Element>): Promise<string | undefined> {
|
async _createSelector(name: string, handle: dom.ElementHandle<Element>): Promise<string | undefined> {
|
||||||
const mainContext = await handle._page.mainFrame()._mainContext();
|
const mainContext = await handle._page.mainFrame()._mainContext();
|
||||||
const injectedScript = await mainContext.injectedScript();
|
const injectedScript = await mainContext.injectedScript();
|
||||||
|
|||||||
@ -97,6 +97,27 @@ describe('Page.dispatchEvent(click)', function() {
|
|||||||
await watchdog;
|
await watchdog;
|
||||||
expect(await page.evaluate(() => window.clicked)).toBe(true);
|
expect(await page.evaluate(() => window.clicked)).toBe(true);
|
||||||
});
|
});
|
||||||
|
it('should be atomic', async({page}) => {
|
||||||
|
const createDummySelector = () => ({
|
||||||
|
create(root, target) {},
|
||||||
|
query(root, selector) {
|
||||||
|
const result = root.querySelector(selector);
|
||||||
|
if (result)
|
||||||
|
Promise.resolve().then(() => result.onclick = "");
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
queryAll(root, selector) {
|
||||||
|
const result = Array.from(root.querySelectorAll(selector));
|
||||||
|
for (const e of result)
|
||||||
|
Promise.resolve().then(() => result.onclick = "");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await utils.registerEngine('dispatchEvent', createDummySelector);
|
||||||
|
await page.setContent(`<div onclick="window._clicked=true">Hello</div>`);
|
||||||
|
await page.dispatchEvent('dispatchEvent=div', 'click');
|
||||||
|
expect(await page.evaluate(() => window._clicked)).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Page.dispatchEvent(drag)', function() {
|
describe('Page.dispatchEvent(drag)', function() {
|
||||||
|
|||||||
@ -476,6 +476,28 @@ describe('ElementHandle convenience API', function() {
|
|||||||
expect(await handle.textContent()).toBe('Text,\nmore text');
|
expect(await handle.textContent()).toBe('Text,\nmore text');
|
||||||
expect(await page.textContent('#inner')).toBe('Text,\nmore text');
|
expect(await page.textContent('#inner')).toBe('Text,\nmore text');
|
||||||
});
|
});
|
||||||
|
it('textContent should be atomic', async({page}) => {
|
||||||
|
const createDummySelector = () => ({
|
||||||
|
create(root, target) {},
|
||||||
|
query(root, selector) {
|
||||||
|
const result = root.querySelector(selector);
|
||||||
|
if (result)
|
||||||
|
Promise.resolve().then(() => result.textContent = 'modified');
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
queryAll(root, selector) {
|
||||||
|
const result = Array.from(root.querySelectorAll(selector));
|
||||||
|
for (const e of result)
|
||||||
|
Promise.resolve().then(() => result.textContent = 'modified');
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await utils.registerEngine('textContent', createDummySelector);
|
||||||
|
await page.setContent(`<div>Hello</div>`);
|
||||||
|
const tc = await page.textContent('textContent=div');
|
||||||
|
expect(tc).toBe('Hello');
|
||||||
|
expect(await page.evaluate(() => document.querySelector('div').textContent)).toBe('modified');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('ElementHandle.check', () => {
|
describe('ElementHandle.check', () => {
|
||||||
|
|||||||
@ -16,16 +16,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const {FFOX, CHROMIUM, WEBKIT} = require('./utils').testOptions(browserType);
|
const utils = require('./utils');
|
||||||
|
const {FFOX, CHROMIUM, WEBKIT} = utils.testOptions(browserType);
|
||||||
async function registerEngine(name, script, options) {
|
|
||||||
try {
|
|
||||||
await playwright.selectors.register(name, script, options);
|
|
||||||
} catch (e) {
|
|
||||||
if (!e.message.includes('has been already registered'))
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('Page.$eval', function() {
|
describe('Page.$eval', function() {
|
||||||
it('should work with css selector', async({page, server}) => {
|
it('should work with css selector', async({page, server}) => {
|
||||||
@ -764,15 +756,19 @@ describe('selectors.register', () => {
|
|||||||
return Array.from(root.querySelectorAll(selector));
|
return Array.from(root.querySelectorAll(selector));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
await registerEngine('tag', `(${createTagSelector.toString()})()`);
|
await utils.registerEngine('tag', `(${createTagSelector.toString()})()`);
|
||||||
await page.setContent('<div><span></span></div><div></div>');
|
await page.setContent('<div><span></span></div><div></div>');
|
||||||
expect(await playwright.selectors._createSelector('tag', await page.$('div'))).toBe('DIV');
|
expect(await playwright.selectors._createSelector('tag', await page.$('div'))).toBe('DIV');
|
||||||
expect(await page.$eval('tag=DIV', e => e.nodeName)).toBe('DIV');
|
expect(await page.$eval('tag=DIV', e => e.nodeName)).toBe('DIV');
|
||||||
expect(await page.$eval('tag=SPAN', e => e.nodeName)).toBe('SPAN');
|
expect(await page.$eval('tag=SPAN', e => e.nodeName)).toBe('SPAN');
|
||||||
expect(await page.$$eval('tag=DIV', es => es.length)).toBe(2);
|
expect(await page.$$eval('tag=DIV', es => es.length)).toBe(2);
|
||||||
|
|
||||||
|
// Selector names are case-sensitive.
|
||||||
|
const error = await page.$('tAG=DIV').catch(e => e);
|
||||||
|
expect(error.message).toBe('Unknown engine "tAG" while parsing selector tAG=DIV');
|
||||||
});
|
});
|
||||||
it('should work with path', async ({page}) => {
|
it('should work with path', async ({page}) => {
|
||||||
await registerEngine('foo', { path: path.join(__dirname, 'assets/sectionselectorengine.js') });
|
await utils.registerEngine('foo', { path: path.join(__dirname, 'assets/sectionselectorengine.js') });
|
||||||
await page.setContent('<section></section>');
|
await page.setContent('<section></section>');
|
||||||
expect(await page.$eval('foo=whatever', e => e.nodeName)).toBe('SECTION');
|
expect(await page.$eval('foo=whatever', e => e.nodeName)).toBe('SECTION');
|
||||||
});
|
});
|
||||||
@ -786,8 +782,8 @@ describe('selectors.register', () => {
|
|||||||
return [document.body, document.documentElement, window.__answer];
|
return [document.body, document.documentElement, window.__answer];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
await registerEngine('main', createDummySelector);
|
await utils.registerEngine('main', createDummySelector);
|
||||||
await registerEngine('isolated', createDummySelector, { contentScript: true });
|
await utils.registerEngine('isolated', createDummySelector, { contentScript: true });
|
||||||
await page.setContent('<div><span><section></section></span></div>');
|
await page.setContent('<div><span><section></section></span></div>');
|
||||||
await page.evaluate(() => window.__answer = document.querySelector('span'));
|
await page.evaluate(() => window.__answer = document.querySelector('span'));
|
||||||
// Works in main if asked.
|
// Works in main if asked.
|
||||||
@ -826,7 +822,9 @@ describe('selectors.register', () => {
|
|||||||
error = await playwright.selectors.register('$', createDummySelector).catch(e => e);
|
error = await playwright.selectors.register('$', createDummySelector).catch(e => e);
|
||||||
expect(error.message).toBe('Selector engine name may only contain [a-zA-Z0-9_] characters');
|
expect(error.message).toBe('Selector engine name may only contain [a-zA-Z0-9_] characters');
|
||||||
|
|
||||||
await registerEngine('dummy', createDummySelector);
|
// Selector names are case-sensitive.
|
||||||
|
await utils.registerEngine('dummy', createDummySelector);
|
||||||
|
await utils.registerEngine('duMMy', createDummySelector);
|
||||||
|
|
||||||
error = await playwright.selectors.register('dummy', createDummySelector).catch(e => e);
|
error = await playwright.selectors.register('dummy', createDummySelector).catch(e => e);
|
||||||
expect(error.message).toBe('"dummy" selector engine has been already registered');
|
expect(error.message).toBe('"dummy" selector engine has been already registered');
|
||||||
|
|||||||
@ -94,6 +94,15 @@ const utils = module.exports = {
|
|||||||
expect(await page.evaluate('window.innerHeight')).toBe(height);
|
expect(await page.evaluate('window.innerHeight')).toBe(height);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
registerEngine: async (name, script, options) => {
|
||||||
|
try {
|
||||||
|
await playwright.selectors.register(name, script, options);
|
||||||
|
} catch (e) {
|
||||||
|
if (!e.message.includes('has been already registered'))
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
initializeFlakinessDashboardIfNeeded: async function(testRunner) {
|
initializeFlakinessDashboardIfNeeded: async function(testRunner) {
|
||||||
// Generate testIDs for all tests and verify they don't clash.
|
// Generate testIDs for all tests and verify they don't clash.
|
||||||
// This will add |test.testId| for every test.
|
// This will add |test.testId| for every test.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user