chore: consolidate aria parser in isomorphic bundle (#34298)
This commit is contained in:
parent
4bb464197f
commit
0c8a6b80fb
|
@ -1,30 +0,0 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { parseYamlTemplate } from '../utils/isomorphic/ariaSnapshot';
|
||||
import type { AriaTemplateNode, ParsedYaml } from '@isomorphic/ariaSnapshot';
|
||||
import { yaml } from '../utilsBundle';
|
||||
|
||||
export function parseAriaSnapshot(text: string): AriaTemplateNode {
|
||||
return parseYamlTemplate(parseYamlForAriaSnapshot(text));
|
||||
}
|
||||
|
||||
export function parseYamlForAriaSnapshot(text: string): ParsedYaml {
|
||||
const parsed = yaml.parse(text);
|
||||
if (!Array.isArray(parsed))
|
||||
throw new Error('Expected object key starting with "- ":\n\n' + text + '\n');
|
||||
return parsed;
|
||||
}
|
|
@ -24,10 +24,9 @@ import type { Playwright } from './playwright';
|
|||
import { Recorder } from './recorder';
|
||||
import { EmptyRecorderApp } from './recorder/recorderApp';
|
||||
import { asLocator, type Language } from '../utils';
|
||||
import { parseYamlForAriaSnapshot } from './ariaSnapshot';
|
||||
import type { ParsedYaml } from '../utils/isomorphic/ariaSnapshot';
|
||||
import { parseYamlTemplate } from '../utils/isomorphic/ariaSnapshot';
|
||||
import { yaml } from '../utilsBundle';
|
||||
import { unsafeLocatorOrSelectorAsSelector } from '../utils/isomorphic/locatorParser';
|
||||
import { parseAriaSnapshotUnsafe } from '../utils/isomorphic/ariaSnapshot';
|
||||
|
||||
const internalMetadata = serverSideCallMetadata();
|
||||
|
||||
|
@ -125,14 +124,10 @@ export class DebugController extends SdkObject {
|
|||
// Assert parameters validity.
|
||||
if (params.selector)
|
||||
unsafeLocatorOrSelectorAsSelector(this._sdkLanguage, params.selector, 'data-testid');
|
||||
let parsedYaml: ParsedYaml | undefined;
|
||||
if (params.ariaTemplate) {
|
||||
parsedYaml = parseYamlForAriaSnapshot(params.ariaTemplate);
|
||||
parseYamlTemplate(parsedYaml);
|
||||
}
|
||||
const ariaTemplate = params.ariaTemplate ? parseAriaSnapshotUnsafe(yaml, params.ariaTemplate) : undefined;
|
||||
for (const recorder of await this._allRecorders()) {
|
||||
if (parsedYaml)
|
||||
recorder.setHighlightedAriaTemplate(parsedYaml);
|
||||
if (ariaTemplate)
|
||||
recorder.setHighlightedAriaTemplate(ariaTemplate);
|
||||
else if (params.selector)
|
||||
recorder.setHighlightedSelector(this._sdkLanguage, params.selector);
|
||||
}
|
||||
|
|
|
@ -3,5 +3,7 @@
|
|||
../../generated/
|
||||
../../protocol/
|
||||
../../utils/
|
||||
../../utils/isomorphic
|
||||
../../utilsBundle.ts
|
||||
../../zipBundle.ts
|
||||
../**
|
||||
|
|
|
@ -26,7 +26,8 @@ import type { CallMetadata } from '../instrumentation';
|
|||
import type { BrowserContextDispatcher } from './browserContextDispatcher';
|
||||
import type { PageDispatcher } from './pageDispatcher';
|
||||
import { debugAssert } from '../../utils';
|
||||
import { parseAriaSnapshot } from '../ariaSnapshot';
|
||||
import { parseAriaSnapshotUnsafe } from '../../utils/isomorphic/ariaSnapshot';
|
||||
import { yaml } from '../../utilsBundle';
|
||||
|
||||
export class FrameDispatcher extends Dispatcher<Frame, channels.FrameChannel, BrowserContextDispatcher | PageDispatcher> implements channels.FrameChannel {
|
||||
_type_Frame = true;
|
||||
|
@ -261,7 +262,7 @@ export class FrameDispatcher extends Dispatcher<Frame, channels.FrameChannel, Br
|
|||
metadata.potentiallyClosesScope = true;
|
||||
let expectedValue = params.expectedValue ? parseArgument(params.expectedValue) : undefined;
|
||||
if (params.expression === 'to.match.aria' && expectedValue)
|
||||
expectedValue = parseAriaSnapshot(expectedValue);
|
||||
expectedValue = parseAriaSnapshotUnsafe(yaml, expectedValue);
|
||||
const result = await this._frame.expect(metadata, params.selector, { ...params, expectedValue });
|
||||
if (result.received !== undefined)
|
||||
result.received = serializeResult(result.received);
|
||||
|
|
|
@ -18,7 +18,7 @@ import * as roleUtils from './roleUtils';
|
|||
import { getElementComputedStyle } from './domUtils';
|
||||
import { escapeRegExp, longestCommonSubstring, normalizeWhiteSpace } from '@isomorphic/stringUtils';
|
||||
import { yamlEscapeKeyIfNeeded, yamlEscapeValueIfNeeded } from './yaml';
|
||||
import type { AriaProps, AriaRole, AriaTemplateNode, AriaTemplateRoleNode, AriaTemplateTextNode } from '@isomorphic/ariaSnapshot';
|
||||
import type { AriaProps, AriaRegex, AriaRole, AriaTemplateNode, AriaTemplateRoleNode, AriaTemplateTextNode } from '@isomorphic/ariaSnapshot';
|
||||
|
||||
export type AriaNode = AriaProps & {
|
||||
role: AriaRole | 'fragment';
|
||||
|
@ -196,14 +196,14 @@ function normalizeStringChildren(rootA11yNode: AriaNode) {
|
|||
visit(rootA11yNode);
|
||||
}
|
||||
|
||||
function matchesText(text: string, template: RegExp | string | undefined): boolean {
|
||||
function matchesText(text: string, template: AriaRegex | string | undefined): boolean {
|
||||
if (!template)
|
||||
return true;
|
||||
if (!text)
|
||||
return false;
|
||||
if (typeof template === 'string')
|
||||
return text === template;
|
||||
return !!text.match(template);
|
||||
return !!text.match(new RegExp(template.pattern));
|
||||
}
|
||||
|
||||
function matchesTextNode(text: string, template: AriaTemplateTextNode) {
|
||||
|
|
|
@ -37,7 +37,7 @@ import { cacheNormalizedWhitespaces, normalizeWhiteSpace, trimStringWithEllipsis
|
|||
import { matchesAriaTree, getAllByAria, generateAriaTree, renderAriaTree } from './ariaSnapshot';
|
||||
import type { AriaNode, AriaSnapshot } from './ariaSnapshot';
|
||||
import type { AriaTemplateNode } from '@isomorphic/ariaSnapshot';
|
||||
import { parseYamlTemplate } from '@isomorphic/ariaSnapshot';
|
||||
import { parseAriaSnapshot } from '@isomorphic/ariaSnapshot';
|
||||
|
||||
export type FrameExpectParams = Omit<channels.FrameExpectParams, 'expectedValue'> & { expectedValue?: any };
|
||||
|
||||
|
@ -86,7 +86,7 @@ export class InjectedScript {
|
|||
isElementVisible,
|
||||
isInsideScope,
|
||||
normalizeWhiteSpace,
|
||||
parseYamlTemplate,
|
||||
parseAriaSnapshot,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
|
|
|
@ -1146,8 +1146,7 @@ export class Recorder {
|
|||
const ariaTemplateJSON = JSON.stringify(state.ariaTemplate);
|
||||
if (this._lastHighlightedAriaTemplateJSON !== ariaTemplateJSON) {
|
||||
this._lastHighlightedAriaTemplateJSON = ariaTemplateJSON;
|
||||
const template = state.ariaTemplate ? this.injectedScript.utils.parseYamlTemplate(state.ariaTemplate) : undefined;
|
||||
const elements = template ? this.injectedScript.getAllByAria(this.document, template) : [];
|
||||
const elements = state.ariaTemplate ? this.injectedScript.getAllByAria(this.document, state.ariaTemplate) : [];
|
||||
if (elements.length)
|
||||
highlight = { elements };
|
||||
else
|
||||
|
|
|
@ -32,7 +32,7 @@ import type * as actions from '@recorder/actions';
|
|||
import { buildFullSelector } from '../utils/isomorphic/recorderUtils';
|
||||
import { stringifySelector } from '../utils/isomorphic/selectorParser';
|
||||
import type { Frame } from './frames';
|
||||
import type { ParsedYaml } from '@isomorphic/ariaSnapshot';
|
||||
import type { AriaTemplateNode } from '@isomorphic/ariaSnapshot';
|
||||
|
||||
const recorderSymbol = Symbol('recorderSymbol');
|
||||
|
||||
|
@ -40,7 +40,7 @@ export class Recorder implements InstrumentationListener, IRecorder {
|
|||
readonly handleSIGINT: boolean | undefined;
|
||||
private _context: BrowserContext;
|
||||
private _mode: Mode;
|
||||
private _highlightedElement: { selector?: string, ariaTemplate?: ParsedYaml } = {};
|
||||
private _highlightedElement: { selector?: string, ariaTemplate?: AriaTemplateNode } = {};
|
||||
private _overlayState: OverlayState = { offsetX: 0 };
|
||||
private _recorderApp: IRecorderApp | null = null;
|
||||
private _currentCallsMetadata = new Map<CallMetadata, SdkObject>();
|
||||
|
@ -249,7 +249,7 @@ export class Recorder implements InstrumentationListener, IRecorder {
|
|||
this._refreshOverlay();
|
||||
}
|
||||
|
||||
setHighlightedAriaTemplate(ariaTemplate: ParsedYaml) {
|
||||
setHighlightedAriaTemplate(ariaTemplate: AriaTemplateNode) {
|
||||
this._highlightedElement = { ariaTemplate };
|
||||
this._refreshOverlay();
|
||||
}
|
||||
|
|
|
@ -24,8 +24,6 @@ export type AriaRole = 'alert' | 'alertdialog' | 'application' | 'article' | 'ba
|
|||
'spinbutton' | 'status' | 'strong' | 'subscript' | 'superscript' | 'switch' | 'tab' | 'table' | 'tablist' | 'tabpanel' | 'term' | 'textbox' | 'time' | 'timer' |
|
||||
'toolbar' | 'tooltip' | 'tree' | 'treegrid' | 'treeitem';
|
||||
|
||||
export type ParsedYaml = Array<any>;
|
||||
|
||||
export type AriaProps = {
|
||||
checked?: boolean | 'mixed';
|
||||
disabled?: boolean;
|
||||
|
@ -35,89 +33,209 @@ export type AriaProps = {
|
|||
selected?: boolean;
|
||||
};
|
||||
|
||||
// We pass parsed template between worlds using JSON, make it easy.
|
||||
export type AriaRegex = { pattern: string };
|
||||
|
||||
export type AriaTemplateTextNode = {
|
||||
kind: 'text';
|
||||
text: RegExp | string;
|
||||
text: AriaRegex | string;
|
||||
};
|
||||
|
||||
export type AriaTemplateRoleNode = AriaProps & {
|
||||
kind: 'role';
|
||||
role: AriaRole | 'fragment';
|
||||
name?: RegExp | string;
|
||||
name?: AriaRegex | string;
|
||||
children?: AriaTemplateNode[];
|
||||
};
|
||||
|
||||
export type AriaTemplateNode = AriaTemplateRoleNode | AriaTemplateTextNode;
|
||||
|
||||
export function parseYamlTemplate(fragment: ParsedYaml): AriaTemplateNode {
|
||||
const result: AriaTemplateNode = { kind: 'role', role: 'fragment' };
|
||||
populateNode(result, fragment);
|
||||
if (result.children && result.children.length === 1)
|
||||
return result.children[0];
|
||||
return result;
|
||||
import type * as yamlTypes from 'yaml';
|
||||
|
||||
type YamlLibrary = {
|
||||
parseDocument: typeof yamlTypes.parseDocument;
|
||||
Scalar: typeof yamlTypes.Scalar;
|
||||
YAMLMap: typeof yamlTypes.YAMLMap;
|
||||
YAMLSeq: typeof yamlTypes.YAMLSeq;
|
||||
LineCounter: typeof yamlTypes.LineCounter;
|
||||
};
|
||||
|
||||
type ParsedYamlPosition = { line: number; col: number; };
|
||||
|
||||
export type ParsedYamlError = {
|
||||
message: string;
|
||||
range: [ParsedYamlPosition, ParsedYamlPosition];
|
||||
};
|
||||
|
||||
export function parseAriaSnapshotUnsafe(yaml: YamlLibrary, text: string): AriaTemplateNode {
|
||||
const result = parseAriaSnapshot(yaml, text);
|
||||
if (result.errors.length)
|
||||
throw new Error(result.errors[0].message);
|
||||
return result.fragment;
|
||||
}
|
||||
|
||||
function populateNode(node: AriaTemplateRoleNode, container: ParsedYaml) {
|
||||
for (const object of container) {
|
||||
if (typeof object === 'string') {
|
||||
const childNode = KeyParser.parse(object);
|
||||
node.children = node.children || [];
|
||||
node.children.push(childNode);
|
||||
continue;
|
||||
export function parseAriaSnapshot(yaml: YamlLibrary, text: string, options: yamlTypes.ParseOptions = {}): { fragment: AriaTemplateNode, errors: ParsedYamlError[] } {
|
||||
const lineCounter = new yaml.LineCounter();
|
||||
const parseOptions: yamlTypes.ParseOptions = {
|
||||
keepSourceTokens: true,
|
||||
lineCounter,
|
||||
...options,
|
||||
};
|
||||
const yamlDoc = yaml.parseDocument(text, parseOptions);
|
||||
const errors: ParsedYamlError[] = [];
|
||||
|
||||
const convertRange = (range: [number, number] | yamlTypes.Range): [ParsedYamlPosition, ParsedYamlPosition] => {
|
||||
return [lineCounter.linePos(range[0]), lineCounter.linePos(range[1])];
|
||||
};
|
||||
|
||||
const addError = (error: yamlTypes.YAMLError) => {
|
||||
errors.push({
|
||||
message: error.message,
|
||||
range: [lineCounter.linePos(error.pos[0]), lineCounter.linePos(error.pos[1])],
|
||||
});
|
||||
};
|
||||
|
||||
const convertSeq = (container: AriaTemplateRoleNode, seq: yamlTypes.YAMLSeq) => {
|
||||
for (const item of seq.items) {
|
||||
const itemIsString = item instanceof yaml.Scalar && typeof item.value === 'string';
|
||||
if (itemIsString) {
|
||||
const childNode = KeyParser.parse(item, parseOptions, errors);
|
||||
if (childNode) {
|
||||
container.children = container.children || [];
|
||||
container.children.push(childNode);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const itemIsMap = item instanceof yaml.YAMLMap;
|
||||
if (itemIsMap) {
|
||||
convertMap(container, item);
|
||||
continue;
|
||||
}
|
||||
errors.push({
|
||||
message: 'Sequence items should be strings or maps',
|
||||
range: convertRange((item as any).range || seq.range),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
for (const key of Object.keys(object)) {
|
||||
node.children = node.children || [];
|
||||
const value = object[key];
|
||||
|
||||
if (key === 'text') {
|
||||
node.children.push({
|
||||
kind: 'text',
|
||||
text: valueOrRegex(value)
|
||||
const convertMap = (container: AriaTemplateRoleNode, map: yamlTypes.YAMLMap) => {
|
||||
for (const entry of map.items) {
|
||||
container.children = container.children || [];
|
||||
// Key must by a string
|
||||
const keyIsString = entry.key instanceof yaml.Scalar && typeof entry.key.value === 'string';
|
||||
if (!keyIsString) {
|
||||
errors.push({
|
||||
message: 'Only string keys are supported',
|
||||
range: convertRange((entry.key as any).range || map.range),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const childNode = KeyParser.parse(key);
|
||||
if (childNode.kind === 'text') {
|
||||
node.children.push({
|
||||
const key: yamlTypes.Scalar<string> = entry.key as yamlTypes.Scalar<string>;
|
||||
const value = entry.value;
|
||||
|
||||
// - text: "text"
|
||||
if (key.value === 'text') {
|
||||
const valueIsString = value instanceof yaml.Scalar && typeof value.value === 'string';
|
||||
if (!valueIsString) {
|
||||
errors.push({
|
||||
message: 'Text value should be a string',
|
||||
range: convertRange(((entry.value as any).range || map.range)),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
container.children.push({
|
||||
kind: 'text',
|
||||
text: valueOrRegex(value)
|
||||
text: valueOrRegex(value.value)
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
node.children.push({
|
||||
...childNode, children: [{
|
||||
// role "name": ...
|
||||
const childNode = KeyParser.parse(key, parseOptions, errors);
|
||||
if (!childNode)
|
||||
continue;
|
||||
|
||||
// - role "name": "text"
|
||||
const valueIsScalar = value instanceof yaml.Scalar;
|
||||
if (valueIsScalar) {
|
||||
container.children.push({
|
||||
...childNode,
|
||||
children: [{
|
||||
kind: 'text',
|
||||
text: valueOrRegex(value)
|
||||
text: valueOrRegex(String(value.value))
|
||||
}]
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
node.children.push(childNode);
|
||||
populateNode(childNode, value);
|
||||
// - role "name":
|
||||
// - child
|
||||
const valueIsSequence = value instanceof yaml.YAMLSeq ;
|
||||
if (valueIsSequence) {
|
||||
convertSeq(childNode, value as yamlTypes.YAMLSeq);
|
||||
continue;
|
||||
}
|
||||
|
||||
errors.push({
|
||||
message: 'Map values should be strings or sequences',
|
||||
range: convertRange((entry.value as any).range || map.range),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const fragment: AriaTemplateNode = { kind: 'role', role: 'fragment' };
|
||||
|
||||
yamlDoc.errors.forEach(addError);
|
||||
if (errors.length)
|
||||
return { errors, fragment };
|
||||
|
||||
if (!(yamlDoc.contents instanceof yaml.YAMLSeq)) {
|
||||
errors.push({
|
||||
message: 'Aria snapshot must be a YAML sequence, elements starting with " -"',
|
||||
range: convertRange(yamlDoc.contents!.range),
|
||||
});
|
||||
}
|
||||
if (errors.length)
|
||||
return { errors, fragment };
|
||||
|
||||
convertSeq(fragment, yamlDoc.contents as yamlTypes.YAMLSeq);
|
||||
if (errors.length)
|
||||
return { errors, fragment: emptyFragment };
|
||||
if (fragment.children?.length === 1)
|
||||
return { fragment: fragment.children[0], errors };
|
||||
return { fragment, errors };
|
||||
}
|
||||
|
||||
const emptyFragment: AriaTemplateRoleNode = { kind: 'role', role: 'fragment' };
|
||||
|
||||
function normalizeWhitespace(text: string) {
|
||||
return text.replace(/[\r\n\s\t]+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function valueOrRegex(value: string): string | RegExp {
|
||||
return value.startsWith('/') && value.endsWith('/') ? new RegExp(value.slice(1, -1)) : normalizeWhitespace(value);
|
||||
export function valueOrRegex(value: string): string | AriaRegex {
|
||||
return value.startsWith('/') && value.endsWith('/') ? { pattern: value.slice(1, -1) } : normalizeWhitespace(value);
|
||||
}
|
||||
|
||||
class KeyParser {
|
||||
export class KeyParser {
|
||||
private _input: string;
|
||||
private _pos: number;
|
||||
private _length: number;
|
||||
|
||||
static parse(input: string): AriaTemplateNode {
|
||||
return new KeyParser(input)._parse();
|
||||
static parse(text: yamlTypes.Scalar<string>, options: yamlTypes.ParseOptions, errors: ParsedYamlError[]): AriaTemplateRoleNode | null {
|
||||
try {
|
||||
return new KeyParser(text.value)._parse();
|
||||
} catch (e) {
|
||||
if (e instanceof ParserError) {
|
||||
const message = options.prettyErrors === false ? e.message : e.message + ':\n\n' + text.value + '\n' + ' '.repeat(e.pos) + '^\n';
|
||||
errors.push({
|
||||
message,
|
||||
range: [options.lineCounter!.linePos(text.range![0]), options.lineCounter!.linePos(text.range![0] + e.pos)],
|
||||
});
|
||||
return null;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
constructor(input: string) {
|
||||
|
@ -177,11 +295,11 @@ class KeyParser {
|
|||
this._throwError('Unterminated string');
|
||||
}
|
||||
|
||||
private _throwError(message: string, pos?: number): never {
|
||||
throw new AriaKeyError(message, this._input, pos || this._pos);
|
||||
private _throwError(message: string, offset: number = 0): never {
|
||||
throw new ParserError(message, offset || this._pos);
|
||||
}
|
||||
|
||||
private _readRegex(): string {
|
||||
private _readRegex(): AriaRegex {
|
||||
let result = '';
|
||||
let escaped = false;
|
||||
let insideClass = false;
|
||||
|
@ -194,7 +312,7 @@ class KeyParser {
|
|||
escaped = true;
|
||||
result += ch;
|
||||
} else if (ch === '/' && !insideClass) {
|
||||
return result;
|
||||
return { pattern: result };
|
||||
} else if (ch === '[') {
|
||||
insideClass = true;
|
||||
result += ch;
|
||||
|
@ -208,7 +326,7 @@ class KeyParser {
|
|||
this._throwError('Unterminated regex');
|
||||
}
|
||||
|
||||
private _readStringOrRegex(): string | RegExp | null {
|
||||
private _readStringOrRegex(): string | AriaRegex | null {
|
||||
const ch = this._peek();
|
||||
if (ch === '"') {
|
||||
this._next();
|
||||
|
@ -217,7 +335,7 @@ class KeyParser {
|
|||
|
||||
if (ch === '/') {
|
||||
this._next();
|
||||
return new RegExp(this._readRegex());
|
||||
return this._readRegex();
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -253,7 +371,7 @@ class KeyParser {
|
|||
}
|
||||
}
|
||||
|
||||
_parse(): AriaTemplateNode {
|
||||
_parse(): AriaTemplateRoleNode {
|
||||
this._skipWhitespace();
|
||||
|
||||
const role = this._readIdentifier('role') as AriaTemplateRoleNode['role'];
|
||||
|
@ -307,18 +425,11 @@ class KeyParser {
|
|||
}
|
||||
}
|
||||
|
||||
export function parseAriaKey(key: string) {
|
||||
return KeyParser.parse(key);
|
||||
}
|
||||
|
||||
export class AriaKeyError extends Error {
|
||||
readonly shortMessage: string;
|
||||
export class ParserError extends Error {
|
||||
readonly pos: number;
|
||||
|
||||
constructor(message: string, input: string, pos: number) {
|
||||
super(message + ':\n\n' + input + '\n' + ' '.repeat(pos) + '^\n');
|
||||
this.shortMessage = message;
|
||||
constructor(message: string, pos: number) {
|
||||
super(message);
|
||||
this.pos = pos;
|
||||
this.stack = undefined;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ export const program: typeof import('../bundles/utils/node_modules/commander').p
|
|||
export const progress: typeof import('../bundles/utils/node_modules/@types/progress') = require('./utilsBundleImpl').progress;
|
||||
export const SocksProxyAgent: typeof import('../bundles/utils/node_modules/socks-proxy-agent').SocksProxyAgent = require('./utilsBundleImpl').SocksProxyAgent;
|
||||
export const yaml: typeof import('../bundles/utils/node_modules/yaml') = require('./utilsBundleImpl').yaml;
|
||||
export type { Scalar as YAMLScalar, YAMLSeq, YAMLMap, YAMLError, Range as YAMLRange } from '../bundles/utils/node_modules/yaml';
|
||||
export const ws: typeof import('../bundles/utils/node_modules/@types/ws') = require('./utilsBundleImpl').ws;
|
||||
export const wsServer: typeof import('../bundles/utils/node_modules/@types/ws').WebSocketServer = require('./utilsBundleImpl').wsServer;
|
||||
export const wsReceiver = require('./utilsBundleImpl').wsReceiver;
|
||||
|
|
|
@ -29,8 +29,7 @@ import { asLocator } from '@isomorphic/locatorGenerators';
|
|||
import { toggleTheme } from '@web/theme';
|
||||
import { copy, useSetting } from '@web/uiUtils';
|
||||
import yaml from 'yaml';
|
||||
import { parseAriaKey } from '@isomorphic/ariaSnapshot';
|
||||
import type { AriaKeyError, ParsedYaml } from '@isomorphic/ariaSnapshot';
|
||||
import { parseAriaSnapshot } from '@isomorphic/ariaSnapshot';
|
||||
|
||||
export interface RecorderProps {
|
||||
sources: Source[],
|
||||
|
@ -117,8 +116,17 @@ export const Recorder: React.FC<RecorderProps> = ({
|
|||
const onAriaEditorChange = React.useCallback((ariaSnapshot: string) => {
|
||||
if (mode === 'none' || mode === 'inspecting')
|
||||
window.dispatch({ event: 'setMode', params: { mode: 'standby' } });
|
||||
const { fragment, errors } = parseAriaSnapshot(ariaSnapshot);
|
||||
setAriaSnapshotErrors(errors);
|
||||
const { fragment, errors } = parseAriaSnapshot(yaml, ariaSnapshot, { prettyErrors: false });
|
||||
const highlights = errors.map(error => {
|
||||
const highlight: SourceHighlight = {
|
||||
message: error.message,
|
||||
line: error.range[1].line,
|
||||
column: error.range[1].col,
|
||||
type: 'subtle-error',
|
||||
};
|
||||
return highlight;
|
||||
});
|
||||
setAriaSnapshotErrors(highlights);
|
||||
setAriaSnapshot(ariaSnapshot);
|
||||
if (!errors.length)
|
||||
window.dispatch({ event: 'highlightRequested', params: { ariaTemplate: fragment } });
|
||||
|
@ -208,57 +216,3 @@ export const Recorder: React.FC<RecorderProps> = ({
|
|||
/>
|
||||
</div>;
|
||||
};
|
||||
|
||||
function parseAriaSnapshot(ariaSnapshot: string): { fragment?: ParsedYaml, errors: SourceHighlight[] } {
|
||||
const lineCounter = new yaml.LineCounter();
|
||||
const yamlDoc = yaml.parseDocument(ariaSnapshot, {
|
||||
keepSourceTokens: true,
|
||||
lineCounter,
|
||||
prettyErrors: false,
|
||||
});
|
||||
|
||||
const errors: SourceHighlight[] = [];
|
||||
for (const error of yamlDoc.errors) {
|
||||
errors.push({
|
||||
line: lineCounter.linePos(error.pos[0]).line,
|
||||
type: 'subtle-error',
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
if (yamlDoc.errors.length)
|
||||
return { errors };
|
||||
|
||||
const handleKey = (key: yaml.Scalar<string>) => {
|
||||
try {
|
||||
parseAriaKey(key.value);
|
||||
} catch (e) {
|
||||
const keyError = e as AriaKeyError;
|
||||
const linePos = lineCounter.linePos(key.srcToken!.offset + keyError.pos);
|
||||
errors.push({
|
||||
message: keyError.shortMessage,
|
||||
line: linePos.line,
|
||||
column: linePos.col,
|
||||
type: 'subtle-error',
|
||||
});
|
||||
}
|
||||
};
|
||||
const visitSeq = (seq: yaml.YAMLSeq) => {
|
||||
for (const item of seq.items) {
|
||||
if (item instanceof yaml.YAMLMap) {
|
||||
const map = item as yaml.YAMLMap;
|
||||
for (const entry of map.items) {
|
||||
if (entry.key instanceof yaml.Scalar)
|
||||
handleKey(entry.key);
|
||||
if (entry.value instanceof yaml.YAMLSeq)
|
||||
visitSeq(entry.value);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (item instanceof yaml.Scalar)
|
||||
handleKey(item);
|
||||
}
|
||||
};
|
||||
visitSeq(yamlDoc.contents as yaml.YAMLSeq);
|
||||
return errors.length ? { errors } : { fragment: yamlDoc.toJSON(), errors };
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
*/
|
||||
|
||||
import type { Language } from '../../playwright-core/src/utils/isomorphic/locatorGenerators';
|
||||
import type { ParsedYaml } from '@isomorphic/ariaSnapshot';
|
||||
import type { AriaTemplateNode } from '@isomorphic/ariaSnapshot';
|
||||
|
||||
export type Point = { x: number; y: number };
|
||||
|
||||
|
@ -55,7 +55,7 @@ export type UIState = {
|
|||
mode: Mode;
|
||||
actionPoint?: Point;
|
||||
actionSelector?: string;
|
||||
ariaTemplate?: ParsedYaml;
|
||||
ariaTemplate?: AriaTemplateNode;
|
||||
language: Language;
|
||||
testIdAttributeName: string;
|
||||
overlay: OverlayState;
|
||||
|
|
|
@ -521,10 +521,7 @@ test('should report error in YAML', async ({ page }) => {
|
|||
const error = await expect(page.locator('body')).toMatchAriaSnapshot(`
|
||||
heading "title"
|
||||
`).catch(e => e);
|
||||
expect.soft(error.message).toBe(`expect.toMatchAriaSnapshot: Expected object key starting with "- ":
|
||||
|
||||
heading "title"
|
||||
`);
|
||||
expect.soft(error.message).toBe(`expect.toMatchAriaSnapshot: Aria snapshot must be a YAML sequence, elements starting with " -"`);
|
||||
}
|
||||
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue