feat(strict): list ambiguous matches when throwing strict exception (#8449)

This commit is contained in:
Pavel Feldman 2021-08-25 14:51:03 -07:00 committed by GitHub
parent 02bef1eded
commit 95be45967a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 26 additions and 6 deletions

View File

@ -22,6 +22,7 @@ export type ParsedSelectorPart = {
};
export type ParsedSelector = {
selector: string,
parts: ParsedSelectorPart[],
capture?: number,
};
@ -48,6 +49,7 @@ export function parseSelector(selector: string): ParsedSelector {
return part;
});
return {
selector,
capture: result.capture,
parts
};

View File

@ -949,7 +949,7 @@ export function waitForSelectorTask(selector: SelectorInfo, state: 'attached' |
} else {
if (elements.length > 1) {
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 ${visible ? 'visible' : 'hidden'} ${injected.previewNode(element)}`);

View File

@ -22,6 +22,7 @@ import { ParsedSelector, ParsedSelectorPart, parseSelector } from '../common/sel
import { FatalDOMError } from '../common/domErrors';
import { SelectorEvaluatorImpl, isVisible, parentElementOrShadowHost, elementMatchesText, TextMatcher, createRegexTextMatcher, createStrictTextMatcher, createLaxTextMatcher } from './selectorEvaluator';
import { CSSComplexSelectorList } from '../common/cssParser';
import { generateSelector } from './selectorGenerator';
type Predicate<T> = (progress: InjectedScriptProgress, continuePolling: symbol) => T | symbol;
@ -103,7 +104,7 @@ export class InjectedScript {
try {
const result = this._querySelectorRecursively([{ element: root as Element, capture: undefined }], selector, 0, new Map());
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;
} finally {
this._evaluator.end();
@ -737,6 +738,17 @@ export class InjectedScript {
text = text.substring(0, 49) + '\u2026';
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']);

View File

@ -14,8 +14,8 @@
* limitations under the License.
*/
import type InjectedScript from '../../injected/injectedScript';
import { elementText } from '../../injected/selectorEvaluator';
import type InjectedScript from './injectedScript';
import { elementText } from './selectorEvaluator';
type SelectorToken = {
engine: string;

View File

@ -15,7 +15,7 @@
*/
import type InjectedScript from '../../injected/injectedScript';
import { generateSelector } from './selectorGenerator';
import { generateSelector } from '../../injected/selectorGenerator';
type ConsoleAPIInterface = {
$: (selector: string) => void;

View File

@ -16,7 +16,7 @@
import type * as actions from '../recorder/recorderActions';
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 { UIState } from '../recorder/recorderTypes';

View File

@ -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>`);
const error = await page.textContent('span', { strict: true }).catch(e => e);
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 }) => {
@ -32,6 +34,8 @@ it('should fail page.fill in strict mode', async ({ page }) => {
await page.setContent(`<input></input><div><input></input></div>`);
const error = await page.fill('input', 'text', { strict: true }).catch(e => e);
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 }) => {
@ -50,4 +54,6 @@ it('should fail page.dispatchEvent in strict mode', async ({ page }) => {
await page.setContent(`<span></span><div><span></span></div>`);
const error = await page.dispatchEvent('span', 'click', {}, { strict: true }).catch(e => e);
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")');
});