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' } })`.
|
`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
|
## select-options-values
|
||||||
* langs: java, js, csharp
|
* langs: java, js, csharp
|
||||||
- `values` <[null]|[string]|[ElementHandle]|[Array]<[string]>|[Object]|[Array]<[ElementHandle]>|[Array]<[Object]>>
|
- `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-%%
|
||||||
- %%-context-option-recordvideo-dir-%%
|
- %%-context-option-recordvideo-dir-%%
|
||||||
- %%-context-option-recordvideo-size-%%
|
- %%-context-option-recordvideo-size-%%
|
||||||
|
- %%-context-option-strict-%%
|
||||||
|
|
||||||
## browser-option-args
|
## browser-option-args
|
||||||
- `args` <[Array]<[string]>>
|
- `args` <[Array]<[string]>>
|
||||||
|
|
|
||||||
|
|
@ -342,6 +342,7 @@ export type BrowserTypeLaunchPersistentContextParams = {
|
||||||
omitContent?: boolean,
|
omitContent?: boolean,
|
||||||
path: string,
|
path: string,
|
||||||
},
|
},
|
||||||
|
strictSelectors?: boolean,
|
||||||
userDataDir: string,
|
userDataDir: string,
|
||||||
slowMo?: number,
|
slowMo?: number,
|
||||||
};
|
};
|
||||||
|
|
@ -413,6 +414,7 @@ export type BrowserTypeLaunchPersistentContextOptions = {
|
||||||
omitContent?: boolean,
|
omitContent?: boolean,
|
||||||
path: string,
|
path: string,
|
||||||
},
|
},
|
||||||
|
strictSelectors?: boolean,
|
||||||
slowMo?: number,
|
slowMo?: number,
|
||||||
};
|
};
|
||||||
export type BrowserTypeLaunchPersistentContextResult = {
|
export type BrowserTypeLaunchPersistentContextResult = {
|
||||||
|
|
@ -504,6 +506,7 @@ export type BrowserNewContextParams = {
|
||||||
omitContent?: boolean,
|
omitContent?: boolean,
|
||||||
path: string,
|
path: string,
|
||||||
},
|
},
|
||||||
|
strictSelectors?: boolean,
|
||||||
proxy?: {
|
proxy?: {
|
||||||
server: string,
|
server: string,
|
||||||
bypass?: string,
|
bypass?: string,
|
||||||
|
|
@ -562,6 +565,7 @@ export type BrowserNewContextOptions = {
|
||||||
omitContent?: boolean,
|
omitContent?: boolean,
|
||||||
path: string,
|
path: string,
|
||||||
},
|
},
|
||||||
|
strictSelectors?: boolean,
|
||||||
proxy?: {
|
proxy?: {
|
||||||
server: string,
|
server: string,
|
||||||
bypass?: string,
|
bypass?: string,
|
||||||
|
|
@ -2768,6 +2772,7 @@ export type ElectronLaunchParams = {
|
||||||
height: number,
|
height: number,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
strictSelectors?: boolean,
|
||||||
timezoneId?: string,
|
timezoneId?: string,
|
||||||
};
|
};
|
||||||
export type ElectronLaunchOptions = {
|
export type ElectronLaunchOptions = {
|
||||||
|
|
@ -2803,6 +2808,7 @@ export type ElectronLaunchOptions = {
|
||||||
height: number,
|
height: number,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
strictSelectors?: boolean,
|
||||||
timezoneId?: string,
|
timezoneId?: string,
|
||||||
};
|
};
|
||||||
export type ElectronLaunchResult = {
|
export type ElectronLaunchResult = {
|
||||||
|
|
@ -3134,6 +3140,7 @@ export type AndroidDeviceLaunchBrowserParams = {
|
||||||
omitContent?: boolean,
|
omitContent?: boolean,
|
||||||
path: string,
|
path: string,
|
||||||
},
|
},
|
||||||
|
strictSelectors?: boolean,
|
||||||
proxy?: {
|
proxy?: {
|
||||||
server: string,
|
server: string,
|
||||||
bypass?: string,
|
bypass?: string,
|
||||||
|
|
@ -3179,6 +3186,7 @@ export type AndroidDeviceLaunchBrowserOptions = {
|
||||||
omitContent?: boolean,
|
omitContent?: boolean,
|
||||||
path: string,
|
path: string,
|
||||||
},
|
},
|
||||||
|
strictSelectors?: boolean,
|
||||||
proxy?: {
|
proxy?: {
|
||||||
server: string,
|
server: string,
|
||||||
bypass?: string,
|
bypass?: string,
|
||||||
|
|
|
||||||
|
|
@ -323,6 +323,7 @@ ContextOptions:
|
||||||
properties:
|
properties:
|
||||||
omitContent: boolean?
|
omitContent: boolean?
|
||||||
path: string
|
path: string
|
||||||
|
strictSelectors: boolean?
|
||||||
|
|
||||||
|
|
||||||
Playwright:
|
Playwright:
|
||||||
|
|
@ -2393,6 +2394,7 @@ Electron:
|
||||||
properties:
|
properties:
|
||||||
width: number
|
width: number
|
||||||
height: number
|
height: number
|
||||||
|
strictSelectors: boolean?
|
||||||
timezoneId: string?
|
timezoneId: string?
|
||||||
|
|
||||||
returns:
|
returns:
|
||||||
|
|
@ -2659,6 +2661,7 @@ AndroidDevice:
|
||||||
properties:
|
properties:
|
||||||
omitContent: boolean?
|
omitContent: boolean?
|
||||||
path: string
|
path: string
|
||||||
|
strictSelectors: boolean?
|
||||||
proxy:
|
proxy:
|
||||||
type: object?
|
type: object?
|
||||||
properties:
|
properties:
|
||||||
|
|
|
||||||
|
|
@ -251,6 +251,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
||||||
omitContent: tOptional(tBoolean),
|
omitContent: tOptional(tBoolean),
|
||||||
path: tString,
|
path: tString,
|
||||||
})),
|
})),
|
||||||
|
strictSelectors: tOptional(tBoolean),
|
||||||
userDataDir: tString,
|
userDataDir: tString,
|
||||||
slowMo: tOptional(tNumber),
|
slowMo: tOptional(tNumber),
|
||||||
});
|
});
|
||||||
|
|
@ -311,6 +312,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
||||||
omitContent: tOptional(tBoolean),
|
omitContent: tOptional(tBoolean),
|
||||||
path: tString,
|
path: tString,
|
||||||
})),
|
})),
|
||||||
|
strictSelectors: tOptional(tBoolean),
|
||||||
proxy: tOptional(tObject({
|
proxy: tOptional(tObject({
|
||||||
server: tString,
|
server: tString,
|
||||||
bypass: tOptional(tString),
|
bypass: tOptional(tString),
|
||||||
|
|
@ -1090,6 +1092,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
||||||
height: tNumber,
|
height: tNumber,
|
||||||
})),
|
})),
|
||||||
})),
|
})),
|
||||||
|
strictSelectors: tOptional(tBoolean),
|
||||||
timezoneId: tOptional(tString),
|
timezoneId: tOptional(tString),
|
||||||
});
|
});
|
||||||
scheme.ElectronApplicationBrowserWindowParams = tObject({
|
scheme.ElectronApplicationBrowserWindowParams = tObject({
|
||||||
|
|
@ -1232,6 +1235,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
||||||
omitContent: tOptional(tBoolean),
|
omitContent: tOptional(tBoolean),
|
||||||
path: tString,
|
path: tString,
|
||||||
})),
|
})),
|
||||||
|
strictSelectors: tOptional(tBoolean),
|
||||||
proxy: tOptional(tObject({
|
proxy: tOptional(tObject({
|
||||||
server: tString,
|
server: tString,
|
||||||
bypass: tOptional(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> {
|
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>[]> {
|
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> {
|
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)
|
if (!handle)
|
||||||
throw new Error(`Error: failed to find element matching selector "${selector}"`);
|
throw new Error(`Error: failed to find element matching selector "${selector}"`);
|
||||||
const result = await handle.evaluateExpressionAndWaitForSignals(expression, isFunction, true, arg);
|
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;
|
const { state = 'visible' } = options;
|
||||||
if (!['attached', 'detached', 'visible', 'hidden'].includes(state))
|
if (!['attached', 'detached', 'visible', 'hidden'].includes(state))
|
||||||
throw new Error(`state: expected one of (attached|detached|visible|hidden)`);
|
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 task = waitForSelectorTask(info, state, this);
|
||||||
const controller = new ProgressController(metadata, this);
|
const controller = new ProgressController(metadata, this);
|
||||||
return controller.run(async progress => {
|
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> {
|
async querySelector(selector: string, options: types.StrictOptions): Promise<dom.ElementHandle<Element> | null> {
|
||||||
debugLogger.log('api', ` finding element using the selector "${selector}"`);
|
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> {
|
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;
|
const { state = 'visible' } = options;
|
||||||
if (!['attached', 'detached', 'visible', 'hidden'].includes(state))
|
if (!['attached', 'detached', 'visible', 'hidden'].includes(state))
|
||||||
throw new Error(`state: expected one of (attached|detached|visible|hidden)`);
|
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);
|
const task = dom.waitForSelectorTask(info, state);
|
||||||
return controller.run(async progress => {
|
return controller.run(async progress => {
|
||||||
progress.log(`waiting for selector "${selector}"${state === 'attached' ? '' : ' to be ' + state}`);
|
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> {
|
async dispatchEvent(metadata: CallMetadata, selector: string, type: string, eventInit?: Object, options: types.QueryOnSelectorOptions = {}): Promise<void> {
|
||||||
const controller = new ProgressController(metadata, this);
|
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 || {});
|
const task = dom.dispatchEventTask(info, type, eventInit || {});
|
||||||
await controller.run(async progress => {
|
await controller.run(async progress => {
|
||||||
progress.log(`Dispatching "${type}" event on selector "${selector}"...`);
|
progress.log(`Dispatching "${type}" event on selector "${selector}"...`);
|
||||||
|
|
@ -938,7 +938,7 @@ export class Frame extends SdkObject {
|
||||||
selector: string,
|
selector: string,
|
||||||
strict: boolean,
|
strict: boolean,
|
||||||
action: (handle: dom.ElementHandle<Element>) => Promise<R | 'error:notconnected'>): Promise<R> {
|
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()) {
|
while (progress.isRunning()) {
|
||||||
progress.log(`waiting for selector "${selector}"`);
|
progress.log(`waiting for selector "${selector}"`);
|
||||||
const task = dom.waitForSelectorTask(info, 'attached');
|
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> {
|
async textContent(metadata: CallMetadata, selector: string, options: types.QueryOnSelectorOptions = {}): Promise<string | null> {
|
||||||
const controller = new ProgressController(metadata, this);
|
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);
|
const task = dom.textContentTask(info);
|
||||||
return controller.run(async progress => {
|
return controller.run(async progress => {
|
||||||
progress.log(` retrieving textContent from "${selector}"`);
|
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> {
|
async innerText(metadata: CallMetadata, selector: string, options: types.QueryOnSelectorOptions = {}): Promise<string> {
|
||||||
const controller = new ProgressController(metadata, this);
|
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);
|
const task = dom.innerTextTask(info);
|
||||||
return controller.run(async progress => {
|
return controller.run(async progress => {
|
||||||
progress.log(` retrieving innerText from "${selector}"`);
|
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> {
|
async innerHTML(metadata: CallMetadata, selector: string, options: types.QueryOnSelectorOptions = {}): Promise<string> {
|
||||||
const controller = new ProgressController(metadata, this);
|
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);
|
const task = dom.innerHTMLTask(info);
|
||||||
return controller.run(async progress => {
|
return controller.run(async progress => {
|
||||||
progress.log(` retrieving innerHTML from "${selector}"`);
|
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> {
|
async getAttribute(metadata: CallMetadata, selector: string, name: string, options: types.QueryOnSelectorOptions = {}): Promise<string | null> {
|
||||||
const controller = new ProgressController(metadata, this);
|
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);
|
const task = dom.getAttributeTask(info, name);
|
||||||
return controller.run(async progress => {
|
return controller.run(async progress => {
|
||||||
progress.log(` retrieving attribute "${name}" from "${selector}"`);
|
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> {
|
async inputValue(metadata: CallMetadata, selector: string, options: types.TimeoutOptions & types.StrictOptions = {}): Promise<string> {
|
||||||
const controller = new ProgressController(metadata, this);
|
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);
|
const task = dom.inputValueTask(info);
|
||||||
return controller.run(async progress => {
|
return controller.run(async progress => {
|
||||||
progress.log(` retrieving value from "${selector}"`);
|
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> {
|
private async _checkElementState(metadata: CallMetadata, selector: string, state: ElementStateWithoutStable, options: types.QueryOnSelectorOptions = {}): Promise<boolean> {
|
||||||
const controller = new ProgressController(metadata, this);
|
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 task = dom.elementStateTask(info, state);
|
||||||
const result = await controller.run(async progress => {
|
const result = await controller.run(async progress => {
|
||||||
progress.log(` checking "${state}" state of "${selector}"`);
|
progress.log(` checking "${state}" state of "${selector}"`);
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ import { FileChooser } from './fileChooser';
|
||||||
import { Progress, ProgressController } from './progress';
|
import { Progress, ProgressController } from './progress';
|
||||||
import { assert, isError } from '../utils/utils';
|
import { assert, isError } from '../utils/utils';
|
||||||
import { debugLogger } from '../utils/debugLogger';
|
import { debugLogger } from '../utils/debugLogger';
|
||||||
import { Selectors } from './selectors';
|
import { SelectorInfo, Selectors } from './selectors';
|
||||||
import { CallMetadata, SdkObject } from './instrumentation';
|
import { CallMetadata, SdkObject } from './instrumentation';
|
||||||
import { Artifact } from './artifact';
|
import { Artifact } from './artifact';
|
||||||
|
|
||||||
|
|
@ -506,6 +506,11 @@ export class Page extends SdkObject {
|
||||||
firePageError(error: Error) {
|
firePageError(error: Error) {
|
||||||
this.emit(Page.Events.PageError, 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 {
|
export class Worker extends SdkObject {
|
||||||
|
|
|
||||||
|
|
@ -68,13 +68,13 @@ export class Selectors {
|
||||||
this._engines.clear();
|
this._engines.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
async _query(frame: frames.Frame, selector: string, strict: boolean, scope?: dom.ElementHandle): Promise<dom.ElementHandle<Element> | null> {
|
async query(frame: frames.Frame, selector: string, options: { strict?: boolean }, scope?: dom.ElementHandle): Promise<dom.ElementHandle<Element> | null> {
|
||||||
const info = this._parseSelector(selector, strict);
|
const info = frame._page.parseSelector(selector, options);
|
||||||
const context = await frame._context(info.world);
|
const context = await frame._context(info.world);
|
||||||
const injectedScript = await context.injectedScript();
|
const injectedScript = await context.injectedScript();
|
||||||
const handle = await injectedScript.evaluateHandle((injected, { parsed, scope, strict }) => {
|
const handle = await injectedScript.evaluateHandle((injected, { parsed, scope, strict }) => {
|
||||||
return injected.querySelector(parsed, scope || document, 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;
|
const elementHandle = handle.asElement() as dom.ElementHandle<Element> | null;
|
||||||
if (!elementHandle) {
|
if (!elementHandle) {
|
||||||
handle.dispose();
|
handle.dispose();
|
||||||
|
|
@ -85,7 +85,7 @@ export class Selectors {
|
||||||
}
|
}
|
||||||
|
|
||||||
async _queryArray(frame: frames.Frame, selector: string, scope?: dom.ElementHandle): Promise<js.JSHandle<Element[]>> {
|
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 context = await frame._mainContext();
|
||||||
const injectedScript = await context.injectedScript();
|
const injectedScript = await context.injectedScript();
|
||||||
const arrayHandle = await injectedScript.evaluateHandle((injected, { parsed, scope }) => {
|
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>[]> {
|
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 context = await frame._context(info.world);
|
||||||
const injectedScript = await context.injectedScript();
|
const injectedScript = await context.injectedScript();
|
||||||
const arrayHandle = await injectedScript.evaluateHandle((injected, { parsed, scope }) => {
|
const arrayHandle = await injectedScript.evaluateHandle((injected, { parsed, scope }) => {
|
||||||
|
|
@ -127,7 +127,7 @@ export class Selectors {
|
||||||
return adopted;
|
return adopted;
|
||||||
}
|
}
|
||||||
|
|
||||||
_parseSelector(selector: string, strict: boolean): SelectorInfo {
|
parseSelector(selector: string, strict: boolean): SelectorInfo {
|
||||||
const parsed = parseSelector(selector);
|
const parsed = parseSelector(selector);
|
||||||
let needsMainWorld = false;
|
let needsMainWorld = false;
|
||||||
for (const part of parsed.parts) {
|
for (const part of parsed.parts) {
|
||||||
|
|
|
||||||
|
|
@ -274,6 +274,7 @@ export type BrowserContextOptions = {
|
||||||
omitContent?: boolean,
|
omitContent?: boolean,
|
||||||
path: string
|
path: string
|
||||||
},
|
},
|
||||||
|
strictSelectors?: boolean,
|
||||||
proxy?: ProxySettings,
|
proxy?: ProxySettings,
|
||||||
baseURL?: string,
|
baseURL?: string,
|
||||||
_debugName?: 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;
|
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
|
* Maximum time in milliseconds to wait for the browser instance to start. Defaults to `30000` (30 seconds). Pass `0` to
|
||||||
* disable timeout.
|
* disable timeout.
|
||||||
|
|
@ -9562,6 +9569,13 @@ export interface AndroidDevice {
|
||||||
height: number;
|
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
|
* 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)
|
* [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
|
* 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)
|
* [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
|
* 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)
|
* [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