mirror of
https://github.com/microsoft/playwright.git
synced 2024-10-27 05:46:28 +03:00
feat(strict): list ambiguous matches when throwing strict exception (#8449)
This commit is contained in:
parent
02bef1eded
commit
95be45967a
@ -22,6 +22,7 @@ export type ParsedSelectorPart = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type ParsedSelector = {
|
export type ParsedSelector = {
|
||||||
|
selector: string,
|
||||||
parts: ParsedSelectorPart[],
|
parts: ParsedSelectorPart[],
|
||||||
capture?: number,
|
capture?: number,
|
||||||
};
|
};
|
||||||
@ -48,6 +49,7 @@ export function parseSelector(selector: string): ParsedSelector {
|
|||||||
return part;
|
return part;
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
|
selector,
|
||||||
capture: result.capture,
|
capture: result.capture,
|
||||||
parts
|
parts
|
||||||
};
|
};
|
||||||
|
@ -949,7 +949,7 @@ export function waitForSelectorTask(selector: SelectorInfo, state: 'attached' |
|
|||||||
} else {
|
} else {
|
||||||
if (elements.length > 1) {
|
if (elements.length > 1) {
|
||||||
if (strict)
|
if (strict)
|
||||||
throw new Error(`strict mode violation: selector resolved to ${elements.length} elements.`);
|
throw new Error(injected.strictModeViolationErrorMessage(parsed, elements));
|
||||||
progress.log(` selector resolved to ${elements.length} elements. Proceeding with the first one.`);
|
progress.log(` selector resolved to ${elements.length} elements. Proceeding with the first one.`);
|
||||||
}
|
}
|
||||||
progress.log(` selector resolved to ${visible ? 'visible' : 'hidden'} ${injected.previewNode(element)}`);
|
progress.log(` selector resolved to ${visible ? 'visible' : 'hidden'} ${injected.previewNode(element)}`);
|
||||||
|
@ -22,6 +22,7 @@ import { ParsedSelector, ParsedSelectorPart, parseSelector } from '../common/sel
|
|||||||
import { FatalDOMError } from '../common/domErrors';
|
import { FatalDOMError } from '../common/domErrors';
|
||||||
import { SelectorEvaluatorImpl, isVisible, parentElementOrShadowHost, elementMatchesText, TextMatcher, createRegexTextMatcher, createStrictTextMatcher, createLaxTextMatcher } from './selectorEvaluator';
|
import { SelectorEvaluatorImpl, isVisible, parentElementOrShadowHost, elementMatchesText, TextMatcher, createRegexTextMatcher, createStrictTextMatcher, createLaxTextMatcher } from './selectorEvaluator';
|
||||||
import { CSSComplexSelectorList } from '../common/cssParser';
|
import { CSSComplexSelectorList } from '../common/cssParser';
|
||||||
|
import { generateSelector } from './selectorGenerator';
|
||||||
|
|
||||||
type Predicate<T> = (progress: InjectedScriptProgress, continuePolling: symbol) => T | symbol;
|
type Predicate<T> = (progress: InjectedScriptProgress, continuePolling: symbol) => T | symbol;
|
||||||
|
|
||||||
@ -103,7 +104,7 @@ export class InjectedScript {
|
|||||||
try {
|
try {
|
||||||
const result = this._querySelectorRecursively([{ element: root as Element, capture: undefined }], selector, 0, new Map());
|
const result = this._querySelectorRecursively([{ element: root as Element, capture: undefined }], selector, 0, new Map());
|
||||||
if (strict && result.length > 1)
|
if (strict && result.length > 1)
|
||||||
throw new Error(`strict mode violation: selector resolved to ${result.length} elements.`);
|
throw new Error(this.strictModeViolationErrorMessage(selector, result.map(r => r.element)));
|
||||||
return result[0]?.capture || result[0]?.element;
|
return result[0]?.capture || result[0]?.element;
|
||||||
} finally {
|
} finally {
|
||||||
this._evaluator.end();
|
this._evaluator.end();
|
||||||
@ -737,6 +738,17 @@ export class InjectedScript {
|
|||||||
text = text.substring(0, 49) + '\u2026';
|
text = text.substring(0, 49) + '\u2026';
|
||||||
return oneLine(`<${element.nodeName.toLowerCase()}${attrText}>${text}</${element.nodeName.toLowerCase()}>`);
|
return oneLine(`<${element.nodeName.toLowerCase()}${attrText}>${text}</${element.nodeName.toLowerCase()}>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
strictModeViolationErrorMessage(selector: ParsedSelector, matches: Element[]): string {
|
||||||
|
const infos = matches.slice(0, 10).map(m => ({
|
||||||
|
preview: this.previewNode(m),
|
||||||
|
selector: generateSelector(this, m).selector
|
||||||
|
}));
|
||||||
|
const lines = infos.map((info, i) => `\n ${i + 1}) ${info.preview} aka playwright.$("${info.selector}")`);
|
||||||
|
if (infos.length < matches.length)
|
||||||
|
lines.push('\n ...');
|
||||||
|
return `strict mode violation: "${selector.selector}" resolved to ${matches.length} elements:${lines.join('')}\n`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const autoClosingTags = new Set(['AREA', 'BASE', 'BR', 'COL', 'COMMAND', 'EMBED', 'HR', 'IMG', 'INPUT', 'KEYGEN', 'LINK', 'MENUITEM', 'META', 'PARAM', 'SOURCE', 'TRACK', 'WBR']);
|
const autoClosingTags = new Set(['AREA', 'BASE', 'BR', 'COL', 'COMMAND', 'EMBED', 'HR', 'IMG', 'INPUT', 'KEYGEN', 'LINK', 'MENUITEM', 'META', 'PARAM', 'SOURCE', 'TRACK', 'WBR']);
|
||||||
|
@ -14,8 +14,8 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type InjectedScript from '../../injected/injectedScript';
|
import type InjectedScript from './injectedScript';
|
||||||
import { elementText } from '../../injected/selectorEvaluator';
|
import { elementText } from './selectorEvaluator';
|
||||||
|
|
||||||
type SelectorToken = {
|
type SelectorToken = {
|
||||||
engine: string;
|
engine: string;
|
@ -15,7 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type InjectedScript from '../../injected/injectedScript';
|
import type InjectedScript from '../../injected/injectedScript';
|
||||||
import { generateSelector } from './selectorGenerator';
|
import { generateSelector } from '../../injected/selectorGenerator';
|
||||||
|
|
||||||
type ConsoleAPIInterface = {
|
type ConsoleAPIInterface = {
|
||||||
$: (selector: string) => void;
|
$: (selector: string) => void;
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
import type * as actions from '../recorder/recorderActions';
|
import type * as actions from '../recorder/recorderActions';
|
||||||
import type InjectedScript from '../../injected/injectedScript';
|
import type InjectedScript from '../../injected/injectedScript';
|
||||||
import { generateSelector, querySelector } from './selectorGenerator';
|
import { generateSelector, querySelector } from '../../injected/selectorGenerator';
|
||||||
import type { Point } from '../../../common/types';
|
import type { Point } from '../../../common/types';
|
||||||
import type { UIState } from '../recorder/recorderTypes';
|
import type { UIState } from '../recorder/recorderTypes';
|
||||||
|
|
||||||
|
@ -20,6 +20,8 @@ it('should fail page.textContent in strict mode', async ({ page }) => {
|
|||||||
await page.setContent(`<span>span1</span><div><span>target</span></div>`);
|
await page.setContent(`<span>span1</span><div><span>target</span></div>`);
|
||||||
const error = await page.textContent('span', { strict: true }).catch(e => e);
|
const error = await page.textContent('span', { strict: true }).catch(e => e);
|
||||||
expect(error.message).toContain('strict mode violation');
|
expect(error.message).toContain('strict mode violation');
|
||||||
|
expect(error.message).toContain('1) <span>span1</span> aka playwright.$("text=span1")');
|
||||||
|
expect(error.message).toContain('2) <span>target</span> aka playwright.$("text=target")');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail page.getAttribute in strict mode', async ({ page }) => {
|
it('should fail page.getAttribute in strict mode', async ({ page }) => {
|
||||||
@ -32,6 +34,8 @@ it('should fail page.fill in strict mode', async ({ page }) => {
|
|||||||
await page.setContent(`<input></input><div><input></input></div>`);
|
await page.setContent(`<input></input><div><input></input></div>`);
|
||||||
const error = await page.fill('input', 'text', { strict: true }).catch(e => e);
|
const error = await page.fill('input', 'text', { strict: true }).catch(e => e);
|
||||||
expect(error.message).toContain('strict mode violation');
|
expect(error.message).toContain('strict mode violation');
|
||||||
|
expect(error.message).toContain('1) <input/> aka playwright.$("input")');
|
||||||
|
expect(error.message).toContain('2) <input/> aka playwright.$("div input")');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail page.$ in strict mode', async ({ page }) => {
|
it('should fail page.$ in strict mode', async ({ page }) => {
|
||||||
@ -50,4 +54,6 @@ it('should fail page.dispatchEvent in strict mode', async ({ page }) => {
|
|||||||
await page.setContent(`<span></span><div><span></span></div>`);
|
await page.setContent(`<span></span><div><span></span></div>`);
|
||||||
const error = await page.dispatchEvent('span', 'click', {}, { strict: true }).catch(e => e);
|
const error = await page.dispatchEvent('span', 'click', {}, { strict: true }).catch(e => e);
|
||||||
expect(error.message).toContain('strict mode violation');
|
expect(error.message).toContain('strict mode violation');
|
||||||
|
expect(error.message).toContain('1) <span></span> aka playwright.$("span")');
|
||||||
|
expect(error.message).toContain('2) <span></span> aka playwright.$("div span")');
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user