fix(selectors): do not hide selector errors (#10595)

This commit is contained in:
Pavel Feldman 2021-11-29 17:13:24 -08:00 committed by GitHub
parent e6ef3e3680
commit 3997671ab7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 64 additions and 10 deletions

View file

@ -14,6 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { InvalidSelectorError } from './selectorErrors';
import * as css from './cssTokenizer'; import * as css from './cssTokenizer';
// Note: '>=' is used internally for text engine to preserve backwards compatibility. // Note: '>=' is used internally for text engine to preserve backwards compatibility.
@ -61,13 +62,13 @@ export function parseCSS(selector: string, customNames: Set<string>): { selector
(token instanceof css.PercentageToken); (token instanceof css.PercentageToken);
}); });
if (unsupportedToken) 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; let pos = 0;
const names = new Set<string>(); const names = new Set<string>();
function unexpected() { 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() { function skipWhitespace() {
@ -221,9 +222,9 @@ export function parseCSS(selector: string, customNames: Set<string>): { selector
const result = consumeFunctionArguments(); const result = consumeFunctionArguments();
if (!isEOF()) 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))) 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) }; return { selector: result as CSSComplexSelector[], names: Array.from(names) };
} }

View file

@ -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;
}

View file

@ -14,6 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { InvalidSelectorError } from './selectorErrors';
import { CSSComplexSelectorList, parseCSS } from './cssParser'; import { CSSComplexSelectorList, parseCSS } from './cssParser';
export type ParsedSelectorPart = { export type ParsedSelectorPart = {
@ -66,7 +67,7 @@ export function splitSelectorByFrame(selectorText: string): ParsedSelector[] {
const part = selector.parts[i]; const part = selector.parts[i];
if (part.name === 'control' && part.body === 'enter-frame') { if (part.name === 'control' && part.body === 'enter-frame') {
if (!chunk.parts.length) 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); result.push(chunk);
chunk = { parts: [] }; chunk = { parts: [] };
chunkStartIndex = i + 1; chunkStartIndex = i + 1;
@ -77,10 +78,10 @@ export function splitSelectorByFrame(selectorText: string): ParsedSelector[] {
chunk.parts.push(part); chunk.parts.push(part);
} }
if (!chunk.parts.length) 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); result.push(chunk);
if (typeof selector.capture === 'number' && typeof result[result.length - 1].capture !== 'number') 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; return result;
} }
@ -130,7 +131,7 @@ function parseSelectorString(selector: string): ParsedSelectorStrings {
result.parts.push({ name, body }); result.parts.push({ name, body });
if (capture) { if (capture) {
if (result.capture !== undefined) 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; result.capture = result.parts.length - 1;
} }
}; };

View file

@ -35,6 +35,7 @@ import type { ElementStateWithoutStable, FrameExpectParams, InjectedScriptPoll,
import { isSessionClosedError } from './common/protocolError'; import { isSessionClosedError } from './common/protocolError';
import { splitSelectorByFrame, stringifySelector } from './common/selectorParser'; import { splitSelectorByFrame, stringifySelector } from './common/selectorParser';
import { SelectorInfo } from './selectors'; import { SelectorInfo } from './selectors';
import { isInvalidSelectorError } from './common/selectorErrors';
type ContextData = { type ContextData = {
contextPromise: ManualPromise<dom.FrameExecutionContext | Error>; contextPromise: ManualPromise<dom.FrameExecutionContext | Error>;
@ -1277,7 +1278,7 @@ export class Frame extends SdkObject {
}, timeout).catch(e => { }, timeout).catch(e => {
// Q: Why not throw upon isSessionClosedError(e) as in other places? // 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. // 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; throw e;
return { received: controller.lastIntermediateResult(), matches: options.isNot, log: metadata.log }; return { received: controller.lastIntermediateResult(), matches: options.isNot, log: metadata.log };
}); });

View file

@ -19,6 +19,7 @@ import * as frames from './frames';
import * as js from './javascript'; import * as js from './javascript';
import * as types from './types'; import * as types from './types';
import { ParsedSelector, parseSelector, stringifySelector } from './common/selectorParser'; import { ParsedSelector, parseSelector, stringifySelector } from './common/selectorParser';
import { InvalidSelectorError } from './common/selectorErrors';
import { createGuid } from '../utils/utils'; import { createGuid } from '../utils/utils';
export type SelectorInfo = { export type SelectorInfo = {
@ -130,7 +131,7 @@ export class Selectors {
for (const part of parsed.parts) { for (const part of parsed.parts) {
const custom = this._engines.get(part.name); const custom = this._engines.get(part.name);
if (!custom && !this._builtinEngines.has(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) if (custom && !custom.contentScript)
needsMainWorld = true; needsMainWorld = true;
if (this._builtinEnginesInMainWorld.has(part.name)) if (this._builtinEnginesInMainWorld.has(part.name))

View file

@ -294,3 +294,31 @@ test('should support toBeFocused', async ({ runInlineTest }) => {
expect(result.passed).toBe(1); expect(result.passed).toBe(1);
expect(result.exitCode).toBe(0); 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]"`);
});