diff --git a/src/server/common/selectorParser.ts b/src/server/common/selectorParser.ts index 8b60fd20f9..7d0ae32a85 100644 --- a/src/server/common/selectorParser.ts +++ b/src/server/common/selectorParser.ts @@ -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 }; diff --git a/src/server/dom.ts b/src/server/dom.ts index 91fb017099..13f41e9a90 100644 --- a/src/server/dom.ts +++ b/src/server/dom.ts @@ -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)}`); diff --git a/src/server/injected/injectedScript.ts b/src/server/injected/injectedScript.ts index 74549e975b..d2d5c489f4 100644 --- a/src/server/injected/injectedScript.ts +++ b/src/server/injected/injectedScript.ts @@ -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 = (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}`); } + + 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']); diff --git a/src/server/supplements/injected/selectorGenerator.ts b/src/server/injected/selectorGenerator.ts similarity index 99% rename from src/server/supplements/injected/selectorGenerator.ts rename to src/server/injected/selectorGenerator.ts index 6fe7eccb01..0db5dddec8 100644 --- a/src/server/supplements/injected/selectorGenerator.ts +++ b/src/server/injected/selectorGenerator.ts @@ -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; diff --git a/src/server/supplements/injected/consoleApi.ts b/src/server/supplements/injected/consoleApi.ts index cd0da80cd9..9d7b947db1 100644 --- a/src/server/supplements/injected/consoleApi.ts +++ b/src/server/supplements/injected/consoleApi.ts @@ -15,7 +15,7 @@ */ import type InjectedScript from '../../injected/injectedScript'; -import { generateSelector } from './selectorGenerator'; +import { generateSelector } from '../../injected/selectorGenerator'; type ConsoleAPIInterface = { $: (selector: string) => void; diff --git a/src/server/supplements/injected/recorder.ts b/src/server/supplements/injected/recorder.ts index 6b1b741582..e612664973 100644 --- a/src/server/supplements/injected/recorder.ts +++ b/src/server/supplements/injected/recorder.ts @@ -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'; diff --git a/tests/page/page-strict.spec.ts b/tests/page/page-strict.spec.ts index 1ceb017147..8d0e12721a 100644 --- a/tests/page/page-strict.spec.ts +++ b/tests/page/page-strict.spec.ts @@ -20,6 +20,8 @@ it('should fail page.textContent in strict mode', async ({ page }) => { await page.setContent(`span1
target
`); const error = await page.textContent('span', { strict: true }).catch(e => e); expect(error.message).toContain('strict mode violation'); + expect(error.message).toContain('1) span1 aka playwright.$("text=span1")'); + expect(error.message).toContain('2) target 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(`
`); const error = await page.fill('input', 'text', { strict: true }).catch(e => e); expect(error.message).toContain('strict mode violation'); + expect(error.message).toContain('1) aka playwright.$("input")'); + expect(error.message).toContain('2) 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(`
`); const error = await page.dispatchEvent('span', 'click', {}, { strict: true }).catch(e => e); expect(error.message).toContain('strict mode violation'); + expect(error.message).toContain('1) aka playwright.$("span")'); + expect(error.message).toContain('2) aka playwright.$("div span")'); });