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.
*/
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<string>): { 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<string>();
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<string>): { 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) };
}

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.
*/
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;
}
};

View file

@ -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<dom.FrameExecutionContext | Error>;
@ -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 };
});

View file

@ -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))

View file

@ -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]"`);
});