diff --git a/packages/playwright-core/src/server/common/cssParser.ts b/packages/playwright-core/src/server/common/cssParser.ts index 62edb9069d..c41df11cba 100644 --- a/packages/playwright-core/src/server/common/cssParser.ts +++ b/packages/playwright-core/src/server/common/cssParser.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { InvalidSelectorError } from './selectorErrors'; import * as css from './cssTokenizer'; // Note: '>=' is used internally for text engine to preserve backwards compatibility. @@ -61,13 +62,13 @@ export function parseCSS(selector: string, customNames: Set): { selector (token instanceof css.PercentageToken); }); if (unsupportedToken) - throw new Error(`Unsupported token "${unsupportedToken.toSource()}" while parsing selector "${selector}"`); + throw new InvalidSelectorError(`Unsupported token "${unsupportedToken.toSource()}" while parsing selector "${selector}"`); let pos = 0; const names = new Set(); function unexpected() { - return new Error(`Unexpected token "${tokens[pos].toSource()}" while parsing selector "${selector}"`); + return new InvalidSelectorError(`Unexpected token "${tokens[pos].toSource()}" while parsing selector "${selector}"`); } function skipWhitespace() { @@ -221,9 +222,9 @@ export function parseCSS(selector: string, customNames: Set): { selector const result = consumeFunctionArguments(); if (!isEOF()) - throw new Error(`Error while parsing selector "${selector}"`); + throw new InvalidSelectorError(`Error while parsing selector "${selector}"`); if (result.some(arg => typeof arg !== 'object' || !('simples' in arg))) - throw new Error(`Error while parsing selector "${selector}"`); + throw new InvalidSelectorError(`Error while parsing selector "${selector}"`); return { selector: result as CSSComplexSelector[], names: Array.from(names) }; } diff --git a/packages/playwright-core/src/server/common/selectorErrors.ts b/packages/playwright-core/src/server/common/selectorErrors.ts new file mode 100644 index 0000000000..a0d9f1e721 --- /dev/null +++ b/packages/playwright-core/src/server/common/selectorErrors.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * 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. + */ + +export class InvalidSelectorError extends Error { +} + +export function isInvalidSelectorError(error: Error) { + return error instanceof InvalidSelectorError; +} diff --git a/packages/playwright-core/src/server/common/selectorParser.ts b/packages/playwright-core/src/server/common/selectorParser.ts index f40b1ae940..6adede7b5b 100644 --- a/packages/playwright-core/src/server/common/selectorParser.ts +++ b/packages/playwright-core/src/server/common/selectorParser.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { InvalidSelectorError } from './selectorErrors'; import { CSSComplexSelectorList, parseCSS } from './cssParser'; export type ParsedSelectorPart = { @@ -66,7 +67,7 @@ export function splitSelectorByFrame(selectorText: string): ParsedSelector[] { const part = selector.parts[i]; if (part.name === 'control' && part.body === 'enter-frame') { if (!chunk.parts.length) - throw new Error('Selector cannot start with entering frame, select the iframe first'); + throw new InvalidSelectorError('Selector cannot start with entering frame, select the iframe first'); result.push(chunk); chunk = { parts: [] }; chunkStartIndex = i + 1; @@ -77,10 +78,10 @@ export function splitSelectorByFrame(selectorText: string): ParsedSelector[] { chunk.parts.push(part); } if (!chunk.parts.length) - throw new Error(`Selector cannot end with entering frame, while parsing selector ${selectorText}`); + throw new InvalidSelectorError(`Selector cannot end with entering frame, while parsing selector ${selectorText}`); result.push(chunk); if (typeof selector.capture === 'number' && typeof result[result.length - 1].capture !== 'number') - throw new Error(`Can not capture the selector before diving into the frame. Only use * after the last frame has been selected`); + throw new InvalidSelectorError(`Can not capture the selector before diving into the frame. Only use * after the last frame has been selected`); return result; } @@ -130,7 +131,7 @@ function parseSelectorString(selector: string): ParsedSelectorStrings { result.parts.push({ name, body }); if (capture) { if (result.capture !== undefined) - throw new Error(`Only one of the selectors can capture using * modifier`); + throw new InvalidSelectorError(`Only one of the selectors can capture using * modifier`); result.capture = result.parts.length - 1; } }; diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index 1613356b56..6a2f31d77b 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -35,6 +35,7 @@ import type { ElementStateWithoutStable, FrameExpectParams, InjectedScriptPoll, import { isSessionClosedError } from './common/protocolError'; import { splitSelectorByFrame, stringifySelector } from './common/selectorParser'; import { SelectorInfo } from './selectors'; +import { isInvalidSelectorError } from './common/selectorErrors'; type ContextData = { contextPromise: ManualPromise; @@ -1277,7 +1278,7 @@ export class Frame extends SdkObject { }, timeout).catch(e => { // Q: Why not throw upon isSessionClosedError(e) as in other places? // A: We want user to receive a friendly message containing the last intermediate result. - if (js.isJavaScriptErrorInEvaluate(e)) + if (js.isJavaScriptErrorInEvaluate(e) || isInvalidSelectorError(e)) throw e; return { received: controller.lastIntermediateResult(), matches: options.isNot, log: metadata.log }; }); diff --git a/packages/playwright-core/src/server/selectors.ts b/packages/playwright-core/src/server/selectors.ts index 20e5638e8b..d452666e41 100644 --- a/packages/playwright-core/src/server/selectors.ts +++ b/packages/playwright-core/src/server/selectors.ts @@ -19,6 +19,7 @@ import * as frames from './frames'; import * as js from './javascript'; import * as types from './types'; import { ParsedSelector, parseSelector, stringifySelector } from './common/selectorParser'; +import { InvalidSelectorError } from './common/selectorErrors'; import { createGuid } from '../utils/utils'; export type SelectorInfo = { @@ -130,7 +131,7 @@ export class Selectors { for (const part of parsed.parts) { const custom = this._engines.get(part.name); if (!custom && !this._builtinEngines.has(part.name)) - throw new Error(`Unknown engine "${part.name}" while parsing selector ${stringifySelector(parsed)}`); + throw new InvalidSelectorError(`Unknown engine "${part.name}" while parsing selector ${stringifySelector(parsed)}`); if (custom && !custom.contentScript) needsMainWorld = true; if (this._builtinEnginesInMainWorld.has(part.name)) diff --git a/tests/playwright-test/playwright.expect.true.spec.ts b/tests/playwright-test/playwright.expect.true.spec.ts index 0657acf6bf..50a8889c9a 100644 --- a/tests/playwright-test/playwright.expect.true.spec.ts +++ b/tests/playwright-test/playwright.expect.true.spec.ts @@ -294,3 +294,31 @@ test('should support toBeFocused', async ({ runInlineTest }) => { expect(result.passed).toBe(1); expect(result.exitCode).toBe(0); }); + +test('should print unknown engine error', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.ts': ` + const { test } = pwt; + test('focused', async ({ page }) => { + await expect(page.locator('row="row"]')).toBeVisible(); + }); + `, + }, { workers: 1 }); + expect(result.passed).toBe(0); + expect(result.exitCode).toBe(1); + expect(result.output).toContain(`Unknown engine "row" while parsing selector row="row"]`); +}); + +test('should print syntax error', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.ts': ` + const { test } = pwt; + test('focused', async ({ page }) => { + await expect(page.locator('row]')).toBeVisible(); + }); + `, + }, { workers: 1 }); + expect(result.passed).toBe(0); + expect(result.exitCode).toBe(1); + expect(result.output).toContain(`Unexpected token "]" while parsing selector "row]"`); +});