chore: allow editing aria template in recorder (tests) (#33522)

This commit is contained in:
Pavel Feldman 2024-11-08 17:18:51 -08:00 committed by GitHub
parent c29f573243
commit 503f74da90
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 166 additions and 83 deletions

11
package-lock.json generated
View File

@ -2910,10 +2910,11 @@
"periscopic": "^3.1.0"
}
},
"node_modules/codemirror-shadow-1": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/codemirror-shadow-1/-/codemirror-shadow-1-0.0.1.tgz",
"integrity": "sha512-kD3OZpCCHr3LHRKfbGx5IogHTWq4Uo9jH2bXPVa7/n6ppkgI66rx4tniQY1BpqWp/JNhQmQsXhQoaZ1TH6t0xQ=="
"node_modules/codemirror": {
"version": "5.65.18",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.18.tgz",
"integrity": "sha512-Gaz4gHnkbHMGgahNt3CA5HBk5lLQBqmD/pBgeB4kQU6OedZmqMBjlRF0LSrp2tJ4wlLNPm2FfaUd1pDy0mdlpA==",
"license": "MIT"
},
"node_modules/color-convert": {
"version": "1.9.3",
@ -8112,7 +8113,7 @@
"packages/web": {
"version": "0.0.0",
"dependencies": {
"codemirror-shadow-1": "0.0.1",
"codemirror": "5.65.18",
"xterm": "^5.1.0",
"xterm-addon-fit": "^0.7.0"
}

View File

@ -10,7 +10,7 @@ This project incorporates components from the projects listed below. The origina
- balanced-match@1.0.2 (https://github.com/juliangruber/balanced-match)
- brace-expansion@1.1.11 (https://github.com/juliangruber/brace-expansion)
- buffer-crc32@0.2.13 (https://github.com/brianloveswords/buffer-crc32)
- codemirror-shadow-1@0.0.1 (https://github.com/codemirror/CodeMirror)
- codemirror@5.65.18 (https://github.com/codemirror/CodeMirror)
- colors@1.4.0 (https://github.com/Marak/colors.js)
- commander@8.3.0 (https://github.com/tj/commander.js)
- concat-map@0.0.1 (https://github.com/substack/node-concat-map)
@ -208,7 +208,7 @@ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEAL
=========================================
END OF buffer-crc32@0.2.13 AND INFORMATION
%% codemirror-shadow-1@0.0.1 NOTICES AND INFORMATION BEGIN HERE
%% codemirror@5.65.18 NOTICES AND INFORMATION BEGIN HERE
=========================================
MIT License
@ -232,7 +232,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
=========================================
END OF codemirror-shadow-1@0.0.1 AND INFORMATION
END OF codemirror@5.65.18 AND INFORMATION
%% colors@1.4.0 NOTICES AND INFORMATION BEGIN HERE
=========================================

View File

@ -204,7 +204,7 @@ export type MatcherReceived = {
export function matchesAriaTree(rootElement: Element, template: AriaTemplateNode): { matches: AriaNode[], received: MatcherReceived } {
const root = generateAriaTree(rootElement);
const matches = matchesNodeDeep(root, template);
const matches = matchesNodeDeep(root, template, false);
return {
matches,
received: {
@ -215,8 +215,9 @@ export function matchesAriaTree(rootElement: Element, template: AriaTemplateNode
}
export function getAllByAria(rootElement: Element, template: AriaTemplateNode): Element[] {
const result = matchesAriaTree(rootElement, template);
return result.matches.map(n => n.element);
const root = generateAriaTree(rootElement);
const matches = matchesNodeDeep(root, template, true);
return matches.map(n => n.element);
}
function matchesNode(node: AriaNode | string, template: AriaTemplateNode, depth: number): boolean {
@ -265,12 +266,12 @@ function containsList(children: (AriaNode | string)[], template: AriaTemplateNod
return true;
}
function matchesNodeDeep(root: AriaNode, template: AriaTemplateNode): AriaNode[] {
function matchesNodeDeep(root: AriaNode, template: AriaTemplateNode, collectAll: boolean): AriaNode[] {
const results: AriaNode[] = [];
const visit = (node: AriaNode | string): boolean => {
if (matchesNode(node, template, 0)) {
results.push(node as AriaNode);
return true;
return !collectAll;
}
if (typeof node === 'string')
return false;

View File

@ -207,9 +207,9 @@ class InspectTool implements RecorderTool {
class RecordActionTool implements RecorderTool {
private _recorder: Recorder;
private _performingActions = new Set<actions.PerformOnRecordAction>();
private _hoveredModel: HighlightModeWithSelector | null = null;
private _hoveredModel: HighlightModelWithSelector | null = null;
private _hoveredElement: HTMLElement | null = null;
private _activeModel: HighlightModeWithSelector | null = null;
private _activeModel: HighlightModelWithSelector | null = null;
private _expectProgrammaticKeyUp = false;
private _pendingClickAction: { action: actions.ClickAction, timeout: number } | undefined;
@ -605,7 +605,7 @@ class RecordActionTool implements RecorderTool {
class TextAssertionTool implements RecorderTool {
private _recorder: Recorder;
private _hoverHighlight: HighlightModeWithSelector | null = null;
private _hoverHighlight: HighlightModelWithSelector | null = null;
private _action: actions.AssertAction | null = null;
private _dialog: Dialog;
private _textCache = new Map<Element | ShadowRoot, ElementText>();
@ -1460,7 +1460,7 @@ type HighlightModel = HighlightOptions & {
elements: Element[];
};
type HighlightModeWithSelector = HighlightModel & {
type HighlightModelWithSelector = HighlightModel & {
selector: string;
};

View File

@ -52,6 +52,8 @@ 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;
}

View File

@ -97,7 +97,7 @@ This project incorporates components from the projects listed below. The origina
- chalk@4.1.2 (https://github.com/chalk/chalk)
- chokidar@3.6.0 (https://github.com/paulmillr/chokidar)
- ci-info@3.9.0 (https://github.com/watson/ci-info)
- codemirror-shadow-1@0.0.1 (https://github.com/codemirror/CodeMirror)
- codemirror@5.65.18 (https://github.com/codemirror/CodeMirror)
- color-convert@1.9.3 (https://github.com/Qix-/color-convert)
- color-convert@2.0.1 (https://github.com/Qix-/color-convert)
- color-name@1.1.3 (https://github.com/dfcreative/color-name)
@ -3103,7 +3103,7 @@ SOFTWARE.
=========================================
END OF ci-info@3.9.0 AND INFORMATION
%% codemirror-shadow-1@0.0.1 NOTICES AND INFORMATION BEGIN HERE
%% codemirror@5.65.18 NOTICES AND INFORMATION BEGIN HERE
=========================================
MIT License
@ -3127,7 +3127,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
=========================================
END OF codemirror-shadow-1@0.0.1 AND INFORMATION
END OF codemirror@5.65.18 AND INFORMATION
%% color-convert@1.9.3 NOTICES AND INFORMATION BEGIN HERE
=========================================

View File

@ -29,7 +29,6 @@ import { asLocator } from '@isomorphic/locatorGenerators';
import { toggleTheme } from '@web/theme';
import { copy } from '@web/uiUtils';
import yaml from 'yaml';
import type { YAMLError } from 'yaml';
import { parseAriaKey } from '@isomorphic/ariaSnapshot';
import type { AriaKeyError, ParsedYaml } from '@isomorphic/ariaSnapshot';
@ -199,7 +198,7 @@ export const Recorder: React.FC<RecorderProps> = ({
{
id: 'aria',
title: 'Aria snapshot',
render: () => <CodeMirrorWrapper text={ariaSnapshot || ''} language={'yaml'} readOnly={false} onChange={onAriaEditorChange} highlight={ariaSnapshotErrors} wrapLines={true} />
render: () => <CodeMirrorWrapper text={ariaSnapshot || ''} language={'yaml'} readOnly={false} onChange={onAriaEditorChange} highlight={ariaSnapshotErrors} wrapLines={false} />
},
]}
selectedTab={selectedTab}
@ -211,32 +210,31 @@ export const Recorder: React.FC<RecorderProps> = ({
function parseAriaSnapshot(ariaSnapshot: string): { fragment?: ParsedYaml, errors: SourceHighlight[] } {
const lineCounter = new yaml.LineCounter();
let yamlDoc: yaml.Document;
try {
yamlDoc = yaml.parseDocument(ariaSnapshot, {
keepSourceTokens: true,
lineCounter,
});
} catch (e) {
const error = e as YAMLError;
const pos = error.linePos?.[0];
return {
errors: [{
line: pos?.line || 0,
type: 'error',
message: error.message,
}],
};
}
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: '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;
errors.push({
message: keyError.message,
message: keyError.shortMessage,
line: lineCounter.linePos(key.srcToken!.offset + keyError.pos).line,
type: 'error',
});

View File

@ -4,7 +4,7 @@
"version": "0.0.0",
"scripts": {},
"dependencies": {
"codemirror-shadow-1": "0.0.1",
"codemirror": "5.65.18",
"xterm": "^5.1.0",
"xterm-addon-fit": "^0.7.0"
}

View File

@ -15,17 +15,17 @@
*/
// @ts-ignore
import codemirror from 'codemirror-shadow-1';
import codemirror from 'codemirror';
import type codemirrorType from 'codemirror';
import 'codemirror-shadow-1/lib/codemirror.css';
import 'codemirror-shadow-1/mode/css/css';
import 'codemirror-shadow-1/mode/htmlmixed/htmlmixed';
import 'codemirror-shadow-1/mode/javascript/javascript';
import 'codemirror-shadow-1/mode/python/python';
import 'codemirror-shadow-1/mode/clike/clike';
import 'codemirror-shadow-1/mode/markdown/markdown';
import 'codemirror-shadow-1/addon/mode/simple';
import 'codemirror-shadow-1/mode/yaml/yaml';
import 'codemirror/lib/codemirror.css';
import 'codemirror/mode/css/css';
import 'codemirror/mode/htmlmixed/htmlmixed';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/mode/python/python';
import 'codemirror/mode/clike/clike';
import 'codemirror/mode/markdown/markdown';
import 'codemirror/addon/mode/simple';
import 'codemirror/mode/yaml/yaml';
export type CodeMirror = typeof codemirrorType;
export default codemirror;

View File

@ -115,8 +115,13 @@ export const CodeMirrorWrapper: React.FC<SourceProps> = ({
return;
let valueChanged = false;
if (codemirror.getValue() !== text) {
codemirror.setValue(text);
// CodeMirror has a bug that renders cursor poorly on a last line.
let normalizedText = text;
if (!readOnly && !wrapLines && !normalizedText.endsWith('\n'))
normalizedText = normalizedText + '\n';
if (codemirror.getValue() !== normalizedText) {
codemirror.setValue(normalizedText);
valueChanged = true;
if (focusOnChange) {
codemirror.execCommand('selectAll');
@ -170,7 +175,7 @@ export const CodeMirrorWrapper: React.FC<SourceProps> = ({
if (changeListener)
codemirror.off('change', changeListener);
};
}, [codemirror, text, highlight, revealLine, focusOnChange, onChange]);
}, [codemirror, text, highlight, revealLine, focusOnChange, onChange, readOnly]);
return <div data-testid={dataTestId} className='cm-wrapper' ref={codemirrorElement} onClick={onCodeMirrorClick}></div>;
};

View File

@ -15,6 +15,7 @@
*/
import { test, expect } from './inspectorTest';
import { roundBox } from '../../page/pageTest';
test.describe(() => {
test.skip(({ mode }) => mode !== 'default');
@ -59,4 +60,90 @@ test.describe(() => {
await expect.poll(() =>
recorder.text('C#')).toContain(`await Expect(page.GetByRole(AriaRole.Button)).ToMatchAriaSnapshotAsync("- button /Submit \\\\d+/");`);
});
test('should inspect aria snapshot', async ({ openRecorder }) => {
const { recorder } = await openRecorder();
await recorder.setContentAndWait(`<main><button>Submit</button></main>`);
await recorder.page.click('x-pw-tool-item.pick-locator');
await recorder.page.hover('button');
await recorder.trustedClick();
await recorder.recorderPage.getByRole('tab', { name: 'Aria snapshot ' }).click();
await expect(recorder.recorderPage.locator('.tab-aria .CodeMirror')).toMatchAriaSnapshot(`
- textbox
- text: '- button "Submit"'
`);
});
test('should update aria snapshot highlight', async ({ openRecorder }) => {
const { recorder } = await openRecorder();
await recorder.setContentAndWait(`<main>
<button>Submit</button>
<button>Cancel</button>
</main>`);
const submitButton = recorder.page.getByRole('button', { name: 'Submit' });
const cancelButton = recorder.page.getByRole('button', { name: 'Cancel' });
await recorder.recorderPage.getByRole('button', { name: 'Record' }).click();
await recorder.page.click('x-pw-tool-item.pick-locator');
await submitButton.hover();
await recorder.trustedClick();
await recorder.recorderPage.getByRole('tab', { name: 'Aria snapshot ' }).click();
await expect(recorder.recorderPage.locator('.tab-aria .CodeMirror')).toMatchAriaSnapshot(`
- text: '- button "Submit"'
`);
await recorder.recorderPage.locator('.tab-aria .CodeMirror').click();
await recorder.recorderPage.keyboard.press('ArrowLeft');
for (let i = 0; i < '"Submit"'.length; i++)
await recorder.recorderPage.keyboard.press('Backspace');
{
// No accessible name => two boxes.
const box11 = roundBox(await submitButton.boundingBox());
const box12 = roundBox(await recorder.page.locator('x-pw-highlight').first().boundingBox());
expect(box11).toEqual(box12);
const box21 = roundBox(await cancelButton.boundingBox());
const box22 = roundBox(await recorder.page.locator('x-pw-highlight').last().boundingBox());
expect(box21).toEqual(box22);
}
{
// Different button.
await recorder.recorderPage.locator('.tab-aria .CodeMirror').pressSequentially('"Cancel"');
await expect(recorder.page.locator('x-pw-highlight')).toBeVisible();
const box1 = roundBox(await cancelButton.boundingBox());
const box2 = roundBox(await recorder.page.locator('x-pw-highlight').boundingBox());
expect(box1).toEqual(box2);
}
});
test('should show aria snapshot error', async ({ openRecorder }) => {
const { recorder } = await openRecorder();
await recorder.setContentAndWait(`<main>
<button>Submit</button>
<button>Cancel</button>
</main>`);
const submitButton = recorder.page.getByRole('button', { name: 'Submit' });
await recorder.recorderPage.getByRole('button', { name: 'Record' }).click();
await recorder.page.click('x-pw-tool-item.pick-locator');
await submitButton.hover();
await recorder.trustedClick();
await recorder.recorderPage.getByRole('tab', { name: 'Aria snapshot ' }).click();
await expect(recorder.recorderPage.locator('.tab-aria .CodeMirror')).toMatchAriaSnapshot(`
- text: '- button "Submit"'
`);
await recorder.recorderPage.locator('.tab-aria .CodeMirror').click();
await recorder.recorderPage.keyboard.press('ArrowLeft');
await recorder.recorderPage.keyboard.press('Backspace');
await expect(recorder.recorderPage.locator('.tab-aria .CodeMirror')).toMatchAriaSnapshot(`
- text: '- button "Submit Unterminated string'
`);
});
});

View File

@ -17,7 +17,8 @@
import type { Page } from 'playwright-core';
import { test as it, expect, Recorder } from './inspectorTest';
import { waitForTestLog } from '../../config/utils';
import { roundBox } from '../../page/pageTest';
import type { BoundingBox } from '../../page/pageTest';
it('should resume when closing inspector', async ({ page, recorderPageGetter, closeRecorder, mode }) => {
it.skip(mode !== 'default');
@ -385,7 +386,7 @@ it.describe('pause', () => {
})();
const recorderPage = await recorderPageGetter();
const box1Promise = waitForTestLog<Box>(page, 'Highlight box for test: ');
const box1Promise = waitForTestLog<BoundingBox>(page, 'Highlight box for test: ');
await recorderPage.getByText('Locator', { exact: true }).click();
await recorderPage.locator('.tabbed-pane .CodeMirror').click();
await recorderPage.keyboard.type('getByText(\'Submit\')');
@ -407,7 +408,7 @@ it.describe('pause', () => {
})();
const recorderPage = await recorderPageGetter();
const box1Promise = waitForTestLog<Box>(page, 'Highlight box for test: ');
const box1Promise = waitForTestLog<BoundingBox>(page, 'Highlight box for test: ');
await recorderPage.getByText('Locator', { exact: true }).click();
await recorderPage.locator('.tabbed-pane .CodeMirror').click();
await recorderPage.keyboard.type('GetByText("Submit")');
@ -472,7 +473,7 @@ it.describe('pause', () => {
})();
const recorderPage = await recorderPageGetter();
const box1Promise = waitForTestLog<Box>(page, 'Highlight box for test: ');
const box1Promise = waitForTestLog<BoundingBox>(page, 'Highlight box for test: ');
await recorderPage.click('[title="Step over (F10)"]');
const box2 = roundBox((await page.locator('#target').boundingBox())!);
const box1 = roundBox(await box1Promise);
@ -514,13 +515,3 @@ async function sanitizeLog(recorderPage: Page): Promise<string[]> {
}
return results;
}
type Box = { x: number, y: number, width: number, height: number };
function roundBox(box: Box): Box {
return {
x: Math.round(box.x * 1000),
y: Math.round(box.y * 1000),
width: Math.round(box.width * 1000),
height: Math.round(box.height * 1000),
};
}

View File

@ -14,10 +14,7 @@
* limitations under the License.
*/
import { test as it, expect } from './pageTest';
import type { Locator } from 'playwright-core';
type BoundingBox = Awaited<ReturnType<Locator['boundingBox']>>;
import { test as it, expect, roundBox } from './pageTest';
it.skip(({ mode }) => mode !== 'default', 'Highlight element has a closed shadow-root on != default');
@ -30,12 +27,3 @@ it('should highlight locator', async ({ page }) => {
const box2 = roundBox(await page.locator('x-pw-highlight').boundingBox());
expect(box1).toEqual(box2);
});
function roundBox(box: BoundingBox): BoundingBox {
return {
x: Math.round(box.x),
y: Math.round(box.y),
width: Math.round(box.width),
height: Math.round(box.height),
};
}

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
import type { Frame, Page, TestType } from '@playwright/test';
import type { Frame, Page, TestType, Locator } from '@playwright/test';
import type { PlatformWorkerFixtures } from '../config/platformFixtures';
import type { TestModeTestFixtures, TestModeWorkerFixtures, TestModeWorkerOptions } from '../config/testModeFixtures';
import { androidTest } from '../android/androidTest';
@ -26,6 +26,7 @@ import type { ServerFixtures, ServerWorkerOptions } from '../config/serverFixtur
export { expect } from '@playwright/test';
let impl: TestType<PageTestFixtures & ServerFixtures & TestModeTestFixtures, PageWorkerFixtures & PlatformWorkerFixtures & TestModeWorkerFixtures & TestModeWorkerOptions & ServerWorkerOptions> = browserTest;
export type BoundingBox = Awaited<ReturnType<Locator['boundingBox']>>;
if (process.env.PWPAGE_IMPL === 'android')
impl = androidTest;
@ -43,3 +44,12 @@ export async function rafraf(target: Page | Frame, count = 1) {
});
}
}
export function roundBox(box: BoundingBox): BoundingBox {
return {
x: Math.round(box.x),
y: Math.round(box.y),
width: Math.round(box.width),
height: Math.round(box.height),
};
}

View File

@ -61,7 +61,7 @@ This project incorporates components from the projects listed below. The origina
}
}
const packages = await checkDir('node_modules/codemirror-shadow-1');
const packages = await checkDir('node_modules/codemirror');
for (const [key, value] of Object.entries(packages)) {
if (value.licenseText)
allPackages[key] = value;