feat(strict): add context-level selector strictness (#8290)
This commit is contained in:
parent
1426f66ccd
commit
6ef76e333e
|
|
@ -511,6 +511,13 @@ contexts override the proxy, global proxy will be never used and can be any stri
|
|||
`launch({ proxy: { server: 'http://per-context' } })`.
|
||||
:::
|
||||
|
||||
## context-option-strict
|
||||
- `strictSelectors` <[boolean]>
|
||||
|
||||
It specified, enables strict selectors mode for this context. In the strict selectors mode all operations
|
||||
on selectors that imply single target DOM element will throw when more than one element matches the selector.
|
||||
See [Locator] to learn more about the strict mode.
|
||||
|
||||
## select-options-values
|
||||
* langs: java, js, csharp
|
||||
- `values` <[null]|[string]|[ElementHandle]|[Array]<[string]>|[Object]|[Array]<[ElementHandle]>|[Array]<[Object]>>
|
||||
|
|
@ -637,6 +644,7 @@ using the [`method: AndroidDevice.setDefaultTimeout`] method.
|
|||
- %%-context-option-recordvideo-%%
|
||||
- %%-context-option-recordvideo-dir-%%
|
||||
- %%-context-option-recordvideo-size-%%
|
||||
- %%-context-option-strict-%%
|
||||
|
||||
## browser-option-args
|
||||
- `args` <[Array]<[string]>>
|
||||
|
|
|
|||
|
|
@ -342,6 +342,7 @@ export type BrowserTypeLaunchPersistentContextParams = {
|
|||
omitContent?: boolean,
|
||||
path: string,
|
||||
},
|
||||
strictSelectors?: boolean,
|
||||
userDataDir: string,
|
||||
slowMo?: number,
|
||||
};
|
||||
|
|
@ -413,6 +414,7 @@ export type BrowserTypeLaunchPersistentContextOptions = {
|
|||
omitContent?: boolean,
|
||||
path: string,
|
||||
},
|
||||
strictSelectors?: boolean,
|
||||
slowMo?: number,
|
||||
};
|
||||
export type BrowserTypeLaunchPersistentContextResult = {
|
||||
|
|
@ -504,6 +506,7 @@ export type BrowserNewContextParams = {
|
|||
omitContent?: boolean,
|
||||
path: string,
|
||||
},
|
||||
strictSelectors?: boolean,
|
||||
proxy?: {
|
||||
server: string,
|
||||
bypass?: string,
|
||||
|
|
@ -562,6 +565,7 @@ export type BrowserNewContextOptions = {
|
|||
omitContent?: boolean,
|
||||
path: string,
|
||||
},
|
||||
strictSelectors?: boolean,
|
||||
proxy?: {
|
||||
server: string,
|
||||
bypass?: string,
|
||||
|
|
@ -2768,6 +2772,7 @@ export type ElectronLaunchParams = {
|
|||
height: number,
|
||||
},
|
||||
},
|
||||
strictSelectors?: boolean,
|
||||
timezoneId?: string,
|
||||
};
|
||||
export type ElectronLaunchOptions = {
|
||||
|
|
@ -2803,6 +2808,7 @@ export type ElectronLaunchOptions = {
|
|||
height: number,
|
||||
},
|
||||
},
|
||||
strictSelectors?: boolean,
|
||||
timezoneId?: string,
|
||||
};
|
||||
export type ElectronLaunchResult = {
|
||||
|
|
@ -3134,6 +3140,7 @@ export type AndroidDeviceLaunchBrowserParams = {
|
|||
omitContent?: boolean,
|
||||
path: string,
|
||||
},
|
||||
strictSelectors?: boolean,
|
||||
proxy?: {
|
||||
server: string,
|
||||
bypass?: string,
|
||||
|
|
@ -3179,6 +3186,7 @@ export type AndroidDeviceLaunchBrowserOptions = {
|
|||
omitContent?: boolean,
|
||||
path: string,
|
||||
},
|
||||
strictSelectors?: boolean,
|
||||
proxy?: {
|
||||
server: string,
|
||||
bypass?: string,
|
||||
|
|
|
|||
|
|
@ -323,6 +323,7 @@ ContextOptions:
|
|||
properties:
|
||||
omitContent: boolean?
|
||||
path: string
|
||||
strictSelectors: boolean?
|
||||
|
||||
|
||||
Playwright:
|
||||
|
|
@ -2393,6 +2394,7 @@ Electron:
|
|||
properties:
|
||||
width: number
|
||||
height: number
|
||||
strictSelectors: boolean?
|
||||
timezoneId: string?
|
||||
|
||||
returns:
|
||||
|
|
@ -2659,6 +2661,7 @@ AndroidDevice:
|
|||
properties:
|
||||
omitContent: boolean?
|
||||
path: string
|
||||
strictSelectors: boolean?
|
||||
proxy:
|
||||
type: object?
|
||||
properties:
|
||||
|
|
|
|||
|
|
@ -251,6 +251,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
|||
omitContent: tOptional(tBoolean),
|
||||
path: tString,
|
||||
})),
|
||||
strictSelectors: tOptional(tBoolean),
|
||||
userDataDir: tString,
|
||||
slowMo: tOptional(tNumber),
|
||||
});
|
||||
|
|
@ -311,6 +312,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
|||
omitContent: tOptional(tBoolean),
|
||||
path: tString,
|
||||
})),
|
||||
strictSelectors: tOptional(tBoolean),
|
||||
proxy: tOptional(tObject({
|
||||
server: tString,
|
||||
bypass: tOptional(tString),
|
||||
|
|
@ -1090,6 +1092,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
|||
height: tNumber,
|
||||
})),
|
||||
})),
|
||||
strictSelectors: tOptional(tBoolean),
|
||||
timezoneId: tOptional(tString),
|
||||
});
|
||||
scheme.ElectronApplicationBrowserWindowParams = tObject({
|
||||
|
|
@ -1232,6 +1235,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
|||
omitContent: tOptional(tBoolean),
|
||||
path: tString,
|
||||
})),
|
||||
strictSelectors: tOptional(tBoolean),
|
||||
proxy: tOptional(tObject({
|
||||
server: tString,
|
||||
bypass: tOptional(tString),
|
||||
|
|
|
|||
|
|
@ -672,7 +672,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
|||
}
|
||||
|
||||
async querySelector(selector: string, options: types.StrictOptions): Promise<ElementHandle | null> {
|
||||
return this._page.selectors._query(this._context.frame, selector, !!options.strict, this);
|
||||
return this._page.selectors.query(this._context.frame, selector, options, this);
|
||||
}
|
||||
|
||||
async querySelectorAll(selector: string): Promise<ElementHandle<Element>[]> {
|
||||
|
|
@ -680,7 +680,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
|||
}
|
||||
|
||||
async evalOnSelectorAndWaitForSignals(selector: string, strict: boolean, expression: string, isFunction: boolean | undefined, arg: any): Promise<any> {
|
||||
const handle = await this._page.selectors._query(this._context.frame, selector, strict, this);
|
||||
const handle = await this._page.selectors.query(this._context.frame, selector, { strict }, this);
|
||||
if (!handle)
|
||||
throw new Error(`Error: failed to find element matching selector "${selector}"`);
|
||||
const result = await handle.evaluateExpressionAndWaitForSignals(expression, isFunction, true, arg);
|
||||
|
|
@ -743,7 +743,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
|||
const { state = 'visible' } = options;
|
||||
if (!['attached', 'detached', 'visible', 'hidden'].includes(state))
|
||||
throw new Error(`state: expected one of (attached|detached|visible|hidden)`);
|
||||
const info = this._page.selectors._parseSelector(selector, !!options.strict);
|
||||
const info = this._page.parseSelector(selector, options);
|
||||
const task = waitForSelectorTask(info, state, this);
|
||||
const controller = new ProgressController(metadata, this);
|
||||
return controller.run(async progress => {
|
||||
|
|
|
|||
|
|
@ -684,7 +684,7 @@ export class Frame extends SdkObject {
|
|||
|
||||
async querySelector(selector: string, options: types.StrictOptions): Promise<dom.ElementHandle<Element> | null> {
|
||||
debugLogger.log('api', ` finding element using the selector "${selector}"`);
|
||||
return this._page.selectors._query(this, selector, !!options.strict);
|
||||
return this._page.selectors.query(this, selector, options);
|
||||
}
|
||||
|
||||
async waitForSelector(metadata: CallMetadata, selector: string, options: types.WaitForElementOptions = {}): Promise<dom.ElementHandle<Element> | null> {
|
||||
|
|
@ -696,7 +696,7 @@ export class Frame extends SdkObject {
|
|||
const { state = 'visible' } = options;
|
||||
if (!['attached', 'detached', 'visible', 'hidden'].includes(state))
|
||||
throw new Error(`state: expected one of (attached|detached|visible|hidden)`);
|
||||
const info = this._page.selectors._parseSelector(selector, !!options.strict);
|
||||
const info = this._page.parseSelector(selector, options);
|
||||
const task = dom.waitForSelectorTask(info, state);
|
||||
return controller.run(async progress => {
|
||||
progress.log(`waiting for selector "${selector}"${state === 'attached' ? '' : ' to be ' + state}`);
|
||||
|
|
@ -725,7 +725,7 @@ export class Frame extends SdkObject {
|
|||
|
||||
async dispatchEvent(metadata: CallMetadata, selector: string, type: string, eventInit?: Object, options: types.QueryOnSelectorOptions = {}): Promise<void> {
|
||||
const controller = new ProgressController(metadata, this);
|
||||
const info = this._page.selectors._parseSelector(selector, !!options.strict);
|
||||
const info = this._page.parseSelector(selector, options);
|
||||
const task = dom.dispatchEventTask(info, type, eventInit || {});
|
||||
await controller.run(async progress => {
|
||||
progress.log(`Dispatching "${type}" event on selector "${selector}"...`);
|
||||
|
|
@ -938,7 +938,7 @@ export class Frame extends SdkObject {
|
|||
selector: string,
|
||||
strict: boolean,
|
||||
action: (handle: dom.ElementHandle<Element>) => Promise<R | 'error:notconnected'>): Promise<R> {
|
||||
const info = this._page.selectors._parseSelector(selector, strict);
|
||||
const info = this._page.parseSelector(selector, { strict });
|
||||
while (progress.isRunning()) {
|
||||
progress.log(`waiting for selector "${selector}"`);
|
||||
const task = dom.waitForSelectorTask(info, 'attached');
|
||||
|
|
@ -1031,7 +1031,7 @@ export class Frame extends SdkObject {
|
|||
|
||||
async textContent(metadata: CallMetadata, selector: string, options: types.QueryOnSelectorOptions = {}): Promise<string | null> {
|
||||
const controller = new ProgressController(metadata, this);
|
||||
const info = this._page.selectors._parseSelector(selector, !!options.strict);
|
||||
const info = this._page.parseSelector(selector, options);
|
||||
const task = dom.textContentTask(info);
|
||||
return controller.run(async progress => {
|
||||
progress.log(` retrieving textContent from "${selector}"`);
|
||||
|
|
@ -1041,7 +1041,7 @@ export class Frame extends SdkObject {
|
|||
|
||||
async innerText(metadata: CallMetadata, selector: string, options: types.QueryOnSelectorOptions = {}): Promise<string> {
|
||||
const controller = new ProgressController(metadata, this);
|
||||
const info = this._page.selectors._parseSelector(selector, !!options.strict);
|
||||
const info = this._page.parseSelector(selector, options);
|
||||
const task = dom.innerTextTask(info);
|
||||
return controller.run(async progress => {
|
||||
progress.log(` retrieving innerText from "${selector}"`);
|
||||
|
|
@ -1052,7 +1052,7 @@ export class Frame extends SdkObject {
|
|||
|
||||
async innerHTML(metadata: CallMetadata, selector: string, options: types.QueryOnSelectorOptions = {}): Promise<string> {
|
||||
const controller = new ProgressController(metadata, this);
|
||||
const info = this._page.selectors._parseSelector(selector, !!options.strict);
|
||||
const info = this._page.parseSelector(selector, options);
|
||||
const task = dom.innerHTMLTask(info);
|
||||
return controller.run(async progress => {
|
||||
progress.log(` retrieving innerHTML from "${selector}"`);
|
||||
|
|
@ -1062,7 +1062,7 @@ export class Frame extends SdkObject {
|
|||
|
||||
async getAttribute(metadata: CallMetadata, selector: string, name: string, options: types.QueryOnSelectorOptions = {}): Promise<string | null> {
|
||||
const controller = new ProgressController(metadata, this);
|
||||
const info = this._page.selectors._parseSelector(selector, !!options.strict);
|
||||
const info = this._page.parseSelector(selector, options);
|
||||
const task = dom.getAttributeTask(info, name);
|
||||
return controller.run(async progress => {
|
||||
progress.log(` retrieving attribute "${name}" from "${selector}"`);
|
||||
|
|
@ -1072,7 +1072,7 @@ export class Frame extends SdkObject {
|
|||
|
||||
async inputValue(metadata: CallMetadata, selector: string, options: types.TimeoutOptions & types.StrictOptions = {}): Promise<string> {
|
||||
const controller = new ProgressController(metadata, this);
|
||||
const info = this._page.selectors._parseSelector(selector, !!options.strict);
|
||||
const info = this._page.parseSelector(selector, options);
|
||||
const task = dom.inputValueTask(info);
|
||||
return controller.run(async progress => {
|
||||
progress.log(` retrieving value from "${selector}"`);
|
||||
|
|
@ -1082,7 +1082,7 @@ export class Frame extends SdkObject {
|
|||
|
||||
private async _checkElementState(metadata: CallMetadata, selector: string, state: ElementStateWithoutStable, options: types.QueryOnSelectorOptions = {}): Promise<boolean> {
|
||||
const controller = new ProgressController(metadata, this);
|
||||
const info = this._page.selectors._parseSelector(selector, !!options.strict);
|
||||
const info = this._page.parseSelector(selector, options);
|
||||
const task = dom.elementStateTask(info, state);
|
||||
const result = await controller.run(async progress => {
|
||||
progress.log(` checking "${state}" state of "${selector}"`);
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ import { FileChooser } from './fileChooser';
|
|||
import { Progress, ProgressController } from './progress';
|
||||
import { assert, isError } from '../utils/utils';
|
||||
import { debugLogger } from '../utils/debugLogger';
|
||||
import { Selectors } from './selectors';
|
||||
import { SelectorInfo, Selectors } from './selectors';
|
||||
import { CallMetadata, SdkObject } from './instrumentation';
|
||||
import { Artifact } from './artifact';
|
||||
|
||||
|
|
@ -506,6 +506,11 @@ export class Page extends SdkObject {
|
|||
firePageError(error: Error) {
|
||||
this.emit(Page.Events.PageError, error);
|
||||
}
|
||||
|
||||
parseSelector(selector: string, options?: types.StrictOptions): SelectorInfo {
|
||||
const strict = typeof options?.strict === 'boolean' ? options.strict : !!this.context()._options.strictSelectors;
|
||||
return this.selectors.parseSelector(selector, strict);
|
||||
}
|
||||
}
|
||||
|
||||
export class Worker extends SdkObject {
|
||||
|
|
|
|||
|
|
@ -68,13 +68,13 @@ export class Selectors {
|
|||
this._engines.clear();
|
||||
}
|
||||
|
||||
async _query(frame: frames.Frame, selector: string, strict: boolean, scope?: dom.ElementHandle): Promise<dom.ElementHandle<Element> | null> {
|
||||
const info = this._parseSelector(selector, strict);
|
||||
async query(frame: frames.Frame, selector: string, options: { strict?: boolean }, scope?: dom.ElementHandle): Promise<dom.ElementHandle<Element> | null> {
|
||||
const info = frame._page.parseSelector(selector, options);
|
||||
const context = await frame._context(info.world);
|
||||
const injectedScript = await context.injectedScript();
|
||||
const handle = await injectedScript.evaluateHandle((injected, { parsed, scope, strict }) => {
|
||||
return injected.querySelector(parsed, scope || document, strict);
|
||||
}, { parsed: info.parsed, scope, strict });
|
||||
}, { parsed: info.parsed, scope, strict: info.strict });
|
||||
const elementHandle = handle.asElement() as dom.ElementHandle<Element> | null;
|
||||
if (!elementHandle) {
|
||||
handle.dispose();
|
||||
|
|
@ -85,7 +85,7 @@ export class Selectors {
|
|||
}
|
||||
|
||||
async _queryArray(frame: frames.Frame, selector: string, scope?: dom.ElementHandle): Promise<js.JSHandle<Element[]>> {
|
||||
const info = this._parseSelector(selector, false);
|
||||
const info = this.parseSelector(selector, false);
|
||||
const context = await frame._mainContext();
|
||||
const injectedScript = await context.injectedScript();
|
||||
const arrayHandle = await injectedScript.evaluateHandle((injected, { parsed, scope }) => {
|
||||
|
|
@ -95,7 +95,7 @@ export class Selectors {
|
|||
}
|
||||
|
||||
async _queryAll(frame: frames.Frame, selector: string, scope?: dom.ElementHandle, adoptToMain?: boolean): Promise<dom.ElementHandle<Element>[]> {
|
||||
const info = this._parseSelector(selector, false);
|
||||
const info = this.parseSelector(selector, false);
|
||||
const context = await frame._context(info.world);
|
||||
const injectedScript = await context.injectedScript();
|
||||
const arrayHandle = await injectedScript.evaluateHandle((injected, { parsed, scope }) => {
|
||||
|
|
@ -127,7 +127,7 @@ export class Selectors {
|
|||
return adopted;
|
||||
}
|
||||
|
||||
_parseSelector(selector: string, strict: boolean): SelectorInfo {
|
||||
parseSelector(selector: string, strict: boolean): SelectorInfo {
|
||||
const parsed = parseSelector(selector);
|
||||
let needsMainWorld = false;
|
||||
for (const part of parsed.parts) {
|
||||
|
|
|
|||
|
|
@ -274,6 +274,7 @@ export type BrowserContextOptions = {
|
|||
omitContent?: boolean,
|
||||
path: string
|
||||
},
|
||||
strictSelectors?: boolean,
|
||||
proxy?: ProxySettings,
|
||||
baseURL?: string,
|
||||
_debugName?: string,
|
||||
|
|
|
|||
42
tests/browsercontext-strict.spec.ts
Normal file
42
tests/browsercontext-strict.spec.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
/**
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { browserTest as it, expect } from './config/browserTest';
|
||||
|
||||
it('should not fail page.textContent in non-strict mode', async ({ page }) => {
|
||||
await page.setContent(`<span>span1</span><div><span>target</span></div>`);
|
||||
expect(await page.textContent('span', { strict: false })).toBe('span1');
|
||||
});
|
||||
|
||||
it.describe('strict context mode', () => {
|
||||
it.use({
|
||||
contextOptions: async ({ contextOptions }, use) => {
|
||||
const options = { ...contextOptions, strictSelectors: true };
|
||||
await use(options);
|
||||
}
|
||||
});
|
||||
|
||||
it('should fail page.textContent in strict mode', async ({ page }) => {
|
||||
await page.setContent(`<span>span1</span><div><span>target</span></div>`);
|
||||
const error = await page.textContent('span').catch(e => e);
|
||||
expect(error.message).toContain('strict mode violation');
|
||||
});
|
||||
|
||||
it('should opt out of strict mode', async ({ page }) => {
|
||||
await page.setContent(`<span>span1</span><div><span>target</span></div>`);
|
||||
expect(await page.textContent('span', { strict: false })).toBe('span1');
|
||||
});
|
||||
});
|
||||
28
types/types.d.ts
vendored
28
types/types.d.ts
vendored
|
|
@ -8510,6 +8510,13 @@ export interface BrowserType<Unused = {}> {
|
|||
*/
|
||||
slowMo?: number;
|
||||
|
||||
/**
|
||||
* It specified, enables strict selectors mode for this context. In the strict selectors mode all operations on selectors
|
||||
* that imply single target DOM element will throw when more than one element matches the selector. See [Locator] to learn
|
||||
* more about the strict mode.
|
||||
*/
|
||||
strictSelectors?: boolean;
|
||||
|
||||
/**
|
||||
* Maximum time in milliseconds to wait for the browser instance to start. Defaults to `30000` (30 seconds). Pass `0` to
|
||||
* disable timeout.
|
||||
|
|
@ -9562,6 +9569,13 @@ export interface AndroidDevice {
|
|||
height: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* It specified, enables strict selectors mode for this context. In the strict selectors mode all operations on selectors
|
||||
* that imply single target DOM element will throw when more than one element matches the selector. See [Locator] to learn
|
||||
* more about the strict mode.
|
||||
*/
|
||||
strictSelectors?: boolean;
|
||||
|
||||
/**
|
||||
* Changes the timezone of the context. See
|
||||
* [ICU's metaZones.txt](https://cs.chromium.org/chromium/src/third_party/icu/source/data/misc/metaZones.txt?rcl=faee8bc70570192d82d2978a71e2a615788597d1)
|
||||
|
|
@ -10417,6 +10431,13 @@ export interface Browser extends EventEmitter {
|
|||
}>;
|
||||
};
|
||||
|
||||
/**
|
||||
* It specified, enables strict selectors mode for this context. In the strict selectors mode all operations on selectors
|
||||
* that imply single target DOM element will throw when more than one element matches the selector. See [Locator] to learn
|
||||
* more about the strict mode.
|
||||
*/
|
||||
strictSelectors?: boolean;
|
||||
|
||||
/**
|
||||
* Changes the timezone of the context. See
|
||||
* [ICU's metaZones.txt](https://cs.chromium.org/chromium/src/third_party/icu/source/data/misc/metaZones.txt?rcl=faee8bc70570192d82d2978a71e2a615788597d1)
|
||||
|
|
@ -12535,6 +12556,13 @@ export interface BrowserContextOptions {
|
|||
}>;
|
||||
};
|
||||
|
||||
/**
|
||||
* It specified, enables strict selectors mode for this context. In the strict selectors mode all operations on selectors
|
||||
* that imply single target DOM element will throw when more than one element matches the selector. See [Locator] to learn
|
||||
* more about the strict mode.
|
||||
*/
|
||||
strictSelectors?: boolean;
|
||||
|
||||
/**
|
||||
* Changes the timezone of the context. See
|
||||
* [ICU's metaZones.txt](https://cs.chromium.org/chromium/src/third_party/icu/source/data/misc/metaZones.txt?rcl=faee8bc70570192d82d2978a71e2a615788597d1)
|
||||
|
|
|
|||
Loading…
Reference in a new issue