chore: use api selectors in codegen hover (#17855)

This commit is contained in:
Pavel Feldman 2022-10-05 16:59:34 -08:00 committed by GitHub
parent ed6ecbca2a
commit f2685cab95
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 58 additions and 22 deletions

View file

@ -90,8 +90,8 @@ class ProtocolHandler {
this._controller.on(DebugController.Events.BrowsersChanged, browsers => { this._controller.on(DebugController.Events.BrowsersChanged, browsers => {
process.send!({ method: 'browsersChanged', params: { browsers } }); process.send!({ method: 'browsersChanged', params: { browsers } });
}); });
this._controller.on(DebugController.Events.InspectRequested, selector => { this._controller.on(DebugController.Events.InspectRequested, ({ selector, locators }) => {
process.send!({ method: 'inspectRequested', params: { selector } }); process.send!({ method: 'inspectRequested', params: { selector, locators } });
}); });
} }

View file

@ -334,6 +334,7 @@ scheme.RecorderSource = tObject({
scheme.DebugControllerInitializer = tOptional(tObject({})); scheme.DebugControllerInitializer = tOptional(tObject({}));
scheme.DebugControllerInspectRequestedEvent = tObject({ scheme.DebugControllerInspectRequestedEvent = tObject({
selector: tString, selector: tString,
locators: tArray(tType('NameValue')),
}); });
scheme.DebugControllerBrowsersChangedEvent = tObject({ scheme.DebugControllerBrowsersChangedEvent = tObject({
browsers: tArray(tObject({ browsers: tArray(tObject({

View file

@ -23,6 +23,9 @@ import type { InstrumentationListener } from './instrumentation';
import type { Playwright } from './playwright'; import type { Playwright } from './playwright';
import { Recorder } from './recorder'; import { Recorder } from './recorder';
import { EmptyRecorderApp } from './recorder/recorderApp'; import { EmptyRecorderApp } from './recorder/recorderApp';
import { asLocator } from './isomorphic/locatorGenerators';
import type { Language } from './isomorphic/locatorGenerators';
import type { NameValue } from '../common/types';
const internalMetadata = serverSideCallMetadata(); const internalMetadata = serverSideCallMetadata();
@ -215,7 +218,8 @@ class InspectingRecorderApp extends EmptyRecorderApp {
} }
override async setSelector(selector: string): Promise<void> { override async setSelector(selector: string): Promise<void> {
this._debugController.emit(DebugController.Events.InspectRequested, selector); const locators: NameValue[] = ['javascript', 'python', 'java', 'csharp'].map(l => ({ name: l, value: asLocator(l as Language, selector) }));
this._debugController.emit(DebugController.Events.InspectRequested, { selector, locators });
} }
override async setSources(sources: Source[]): Promise<void> { override async setSources(sources: Source[]): Promise<void> {

View file

@ -28,8 +28,8 @@ export class DebugControllerDispatcher extends Dispatcher<DebugController, chann
this._object.on(DebugController.Events.BrowsersChanged, browsers => { this._object.on(DebugController.Events.BrowsersChanged, browsers => {
this._dispatchEvent('browsersChanged', { browsers }); this._dispatchEvent('browsersChanged', { browsers });
}); });
this._object.on(DebugController.Events.InspectRequested, selector => { this._object.on(DebugController.Events.InspectRequested, ({ selector, locators }) => {
this._dispatchEvent('inspectRequested', { selector }); this._dispatchEvent('inspectRequested', { selector, locators });
}); });
this._object.on(DebugController.Events.SourcesChanged, sources => { this._object.on(DebugController.Events.SourcesChanged, sources => {
this._dispatchEvent('sourcesChanged', { sources }); this._dispatchEvent('sourcesChanged', { sources });

View file

@ -17,6 +17,8 @@
import { stringifySelector } from '../isomorphic/selectorParser'; import { stringifySelector } from '../isomorphic/selectorParser';
import type { ParsedSelector } from '../isomorphic/selectorParser'; import type { ParsedSelector } from '../isomorphic/selectorParser';
import type { InjectedScript } from './injectedScript'; import type { InjectedScript } from './injectedScript';
import { asLocator } from '../isomorphic/locatorGenerators';
import type { Language } from '../isomorphic/locatorGenerators';
type HighlightEntry = { type HighlightEntry = {
targetElement: Element, targetElement: Element,
@ -35,6 +37,7 @@ export class Highlight {
private _isUnderTest: boolean; private _isUnderTest: boolean;
private _injectedScript: InjectedScript; private _injectedScript: InjectedScript;
private _rafRequest: number | undefined; private _rafRequest: number | undefined;
private _language: Language = 'javascript';
constructor(injectedScript: InjectedScript) { constructor(injectedScript: InjectedScript) {
this._injectedScript = injectedScript; this._injectedScript = injectedScript;
@ -102,6 +105,10 @@ export class Highlight {
document.documentElement.appendChild(this._glassPaneElement); document.documentElement.appendChild(this._glassPaneElement);
} }
setLanguage(language: Language) {
this._language = language;
}
runHighlightOnRaf(selector: ParsedSelector) { runHighlightOnRaf(selector: ParsedSelector) {
if (this._rafRequest) if (this._rafRequest)
cancelAnimationFrame(this._rafRequest); cancelAnimationFrame(this._rafRequest);
@ -145,7 +152,7 @@ export class Highlight {
color = '#dc6f6f7f'; color = '#dc6f6f7f';
else else
color = elements.length > 1 ? '#f6b26b7f' : '#6fa8dc7f'; color = elements.length > 1 ? '#f6b26b7f' : '#6fa8dc7f';
this._innerUpdateHighlight(elements, { color, tooltipText: selector }); this._innerUpdateHighlight(elements, { color, tooltipText: selector ? asLocator(this._language, selector) : '' });
} }
maskElements(elements: Element[]) { maskElements(elements: Element[]) {

View file

@ -94,7 +94,8 @@ class Recorder {
return; return;
} }
const { mode, actionPoint, actionSelector } = state; const { mode, actionPoint, actionSelector, language } = state;
this._highlight.setLanguage(language);
if (mode !== this._mode) { if (mode !== this._mode) {
this._mode = mode; this._mode = mode;
this._clearHighlight(); this._clearHighlight();

View file

@ -19,7 +19,7 @@ import type { CSSComplexSelectorList } from '../isomorphic/cssParser';
import { parseAttributeSelector, parseSelector, stringifySelector } from '../isomorphic/selectorParser'; import { parseAttributeSelector, parseSelector, stringifySelector } from '../isomorphic/selectorParser';
import type { ParsedSelector } from '../isomorphic/selectorParser'; import type { ParsedSelector } from '../isomorphic/selectorParser';
type Language = 'javascript' | 'python' | 'java' | 'csharp'; export type Language = 'javascript' | 'python' | 'java' | 'csharp';
export type LocatorType = 'default' | 'role' | 'text' | 'label' | 'placeholder' | 'alt' | 'title' | 'test-id' | 'nth' | 'first' | 'last' | 'has-text'; export type LocatorType = 'default' | 'role' | 'text' | 'label' | 'placeholder' | 'alt' | 'title' | 'test-id' | 'nth' | 'first' | 'last' | 'has-text';
export type LocatorBase = 'page' | 'locator' | 'frame-locator'; export type LocatorBase = 'page' | 'locator' | 'frame-locator';

View file

@ -40,7 +40,7 @@ import { metadataToCallLog } from './recorder/recorderUtils';
import { Debugger } from './debugger'; import { Debugger } from './debugger';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { raceAgainstTimeout } from '../utils/timeoutRunner'; import { raceAgainstTimeout } from '../utils/timeoutRunner';
import type { LanguageGenerator } from './recorder/language'; import type { Language, LanguageGenerator } from './recorder/language';
type BindingSource = { frame: Frame, page: Page }; type BindingSource = { frame: Frame, page: Page };
@ -59,6 +59,7 @@ export class Recorder implements InstrumentationListener {
private _handleSIGINT: boolean | undefined; private _handleSIGINT: boolean | undefined;
private _recorderAppFactory: (recorder: Recorder) => Promise<IRecorderApp>; private _recorderAppFactory: (recorder: Recorder) => Promise<IRecorderApp>;
private _omitCallTracking = false; private _omitCallTracking = false;
private _currentLanguage: Language;
static showInspector(context: BrowserContext) { static showInspector(context: BrowserContext) {
Recorder.show(context, {}).catch(() => {}); Recorder.show(context, {}).catch(() => {});
@ -83,6 +84,7 @@ export class Recorder implements InstrumentationListener {
this._debugger = Debugger.lookup(context)!; this._debugger = Debugger.lookup(context)!;
this._handleSIGINT = params.handleSIGINT; this._handleSIGINT = params.handleSIGINT;
context.instrumentation.addListener(this, context); context.instrumentation.addListener(this, context);
this._currentLanguage = this._contextRecorder.languageName();
} }
private static async defaultRecorderAppFactory(recorder: Recorder) { private static async defaultRecorderAppFactory(recorder: Recorder) {
@ -111,6 +113,11 @@ export class Recorder implements InstrumentationListener {
this._debugger.resume(true); this._debugger.resume(true);
return; return;
} }
if (data.event === 'fileChanged') {
this._currentLanguage = this._contextRecorder.languageName(data.params.file);
this._refreshOverlay();
return;
}
if (data.event === 'resume') { if (data.event === 'resume') {
this._debugger.resume(false); this._debugger.resume(false);
return; return;
@ -155,6 +162,7 @@ export class Recorder implements InstrumentationListener {
mode: this._mode, mode: this._mode,
actionPoint, actionPoint,
actionSelector, actionSelector,
language: this._currentLanguage
}; };
return uiState; return uiState;
}); });
@ -381,6 +389,14 @@ class ContextRecorder extends EventEmitter {
this._generator?.restart(); this._generator?.restart();
} }
languageName(id?: string): Language {
for (const lang of this._orderedLanguages) {
if (!id || lang.id === id)
return lang.highlighter;
}
return 'javascript';
}
async install() { async install() {
this._context.on(BrowserContext.Events.Page, page => this._onPage(page)); this._context.on(BrowserContext.Events.Page, page => this._onPage(page));
for (const page of this._context.pages()) for (const page of this._context.pages())

View file

@ -15,7 +15,7 @@
*/ */
import type { BrowserContextOptions } from '../../..'; import type { BrowserContextOptions } from '../../..';
import type { LanguageGenerator, LanguageGeneratorOptions } from './language'; import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language';
import { sanitizeDeviceOptions, toSignalMap } from './language'; import { sanitizeDeviceOptions, toSignalMap } from './language';
import type { ActionInContext } from './codeGenerator'; import type { ActionInContext } from './codeGenerator';
import type { Action } from './recorderActions'; import type { Action } from './recorderActions';
@ -31,7 +31,7 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
id: string; id: string;
groupName = '.NET C#'; groupName = '.NET C#';
name: string; name: string;
highlighter = 'csharp'; highlighter = 'csharp' as Language;
_mode: CSharpLanguageMode; _mode: CSharpLanguageMode;
constructor(mode: CSharpLanguageMode) { constructor(mode: CSharpLanguageMode) {

View file

@ -15,7 +15,7 @@
*/ */
import type { BrowserContextOptions } from '../../..'; import type { BrowserContextOptions } from '../../..';
import type { LanguageGenerator, LanguageGeneratorOptions } from './language'; import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language';
import { toSignalMap } from './language'; import { toSignalMap } from './language';
import type { ActionInContext } from './codeGenerator'; import type { ActionInContext } from './codeGenerator';
import type { Action } from './recorderActions'; import type { Action } from './recorderActions';
@ -30,7 +30,7 @@ export class JavaLanguageGenerator implements LanguageGenerator {
id = 'java'; id = 'java';
groupName = 'Java'; groupName = 'Java';
name = 'Library'; name = 'Library';
highlighter = 'java'; highlighter = 'java' as Language;
generateAction(actionInContext: ActionInContext): string { generateAction(actionInContext: ActionInContext): string {
const action = actionInContext.action; const action = actionInContext.action;

View file

@ -15,7 +15,7 @@
*/ */
import type { BrowserContextOptions } from '../../..'; import type { BrowserContextOptions } from '../../..';
import type { LanguageGenerator, LanguageGeneratorOptions } from './language'; import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language';
import { sanitizeDeviceOptions, toSignalMap } from './language'; import { sanitizeDeviceOptions, toSignalMap } from './language';
import type { ActionInContext } from './codeGenerator'; import type { ActionInContext } from './codeGenerator';
import type { Action } from './recorderActions'; import type { Action } from './recorderActions';
@ -29,7 +29,7 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
id: string; id: string;
groupName = 'Node.js'; groupName = 'Node.js';
name: string; name: string;
highlighter = 'javascript'; highlighter = 'javascript' as Language;
private _isTest: boolean; private _isTest: boolean;
constructor(isTest: boolean) { constructor(isTest: boolean) {

View file

@ -15,8 +15,10 @@
*/ */
import type { BrowserContextOptions, LaunchOptions } from '../../..'; import type { BrowserContextOptions, LaunchOptions } from '../../..';
import type { Language } from '../isomorphic/locatorGenerators';
import type { ActionInContext } from './codeGenerator'; import type { ActionInContext } from './codeGenerator';
import type { Action, DialogSignal, DownloadSignal, NavigationSignal, PopupSignal } from './recorderActions'; import type { Action, DialogSignal, DownloadSignal, NavigationSignal, PopupSignal } from './recorderActions';
export type { Language } from '../isomorphic/locatorGenerators';
export type LanguageGeneratorOptions = { export type LanguageGeneratorOptions = {
browserName: string; browserName: string;
@ -33,7 +35,7 @@ export interface LanguageGenerator {
id: string; id: string;
groupName: string; groupName: string;
name: string; name: string;
highlighter: string; highlighter: Language;
generateHeader(options: LanguageGeneratorOptions): string; generateHeader(options: LanguageGeneratorOptions): string;
generateAction(actionInContext: ActionInContext): string; generateAction(actionInContext: ActionInContext): string;
generateFooter(saveStorage: string | undefined): string; generateFooter(saveStorage: string | undefined): string;

View file

@ -15,7 +15,7 @@
*/ */
import type { BrowserContextOptions } from '../../..'; import type { BrowserContextOptions } from '../../..';
import type { LanguageGenerator, LanguageGeneratorOptions } from './language'; import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language';
import { sanitizeDeviceOptions, toSignalMap } from './language'; import { sanitizeDeviceOptions, toSignalMap } from './language';
import type { ActionInContext } from './codeGenerator'; import type { ActionInContext } from './codeGenerator';
import type { Action } from './recorderActions'; import type { Action } from './recorderActions';
@ -29,7 +29,7 @@ export class PythonLanguageGenerator implements LanguageGenerator {
id: string; id: string;
groupName = 'Python'; groupName = 'Python';
name: string; name: string;
highlighter = 'python'; highlighter = 'python' as Language;
private _awaitPrefix: '' | 'await '; private _awaitPrefix: '' | 'await ';
private _asyncPrefix: '' | 'async '; private _asyncPrefix: '' | 'async ';

View file

@ -32,8 +32,7 @@ export function toTitleCase(name: string) {
} }
export function toSnakeCase(name: string): string { export function toSnakeCase(name: string): string {
const toSnakeCaseRegex = /((?<=[a-z0-9])[A-Z]|(?!^)[A-Z](?=[a-z]))/g; return name.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
return name.replace(toSnakeCaseRegex, `_$1`).toLowerCase();
} }
export function cssEscape(s: string): string { export function cssEscape(s: string): string {

View file

@ -610,6 +610,7 @@ export interface DebugControllerChannel extends DebugControllerEventTarget, Chan
} }
export type DebugControllerInspectRequestedEvent = { export type DebugControllerInspectRequestedEvent = {
selector: string, selector: string,
locators: NameValue[],
}; };
export type DebugControllerBrowsersChangedEvent = { export type DebugControllerBrowsersChangedEvent = {
browsers: { browsers: {

View file

@ -693,6 +693,9 @@ DebugController:
inspectRequested: inspectRequested:
parameters: parameters:
selector: string selector: string
locators:
type: array
items: NameValue
browsersChanged: browsersChanged:
parameters: parameters:

View file

@ -132,6 +132,7 @@ export const Recorder: React.FC<RecorderProps> = ({
<div>Target:</div> <div>Target:</div>
<select className='recorder-chooser' hidden={!sources.length} value={fileId} onChange={event => { <select className='recorder-chooser' hidden={!sources.length} value={fileId} onChange={event => {
setFileId(event.target.selectedOptions[0].value); setFileId(event.target.selectedOptions[0].value);
window.dispatch({ event: 'fileChanged', params: { file: event.target.selectedOptions[0].value } });
}}>{renderSourceOptions(sources)}</select> }}>{renderSourceOptions(sources)}</select>
<ToolbarButton icon='clear-all' title='Clear' disabled={!source || !source.text} onClick={() => { <ToolbarButton icon='clear-all' title='Clear' disabled={!source || !source.text} onClick={() => {
window.dispatch({ event: 'clear' }); window.dispatch({ event: 'clear' });

View file

@ -19,7 +19,7 @@ export type Point = { x: number, y: number };
export type Mode = 'inspecting' | 'recording' | 'none'; export type Mode = 'inspecting' | 'recording' | 'none';
export type EventData = { export type EventData = {
event: 'clear' | 'resume' | 'step' | 'pause' | 'setMode' | 'selectorUpdated'; event: 'clear' | 'resume' | 'step' | 'pause' | 'setMode' | 'selectorUpdated' | 'fileChanged';
params: any; params: any;
}; };
@ -27,6 +27,7 @@ export type UIState = {
mode: Mode; mode: Mode;
actionPoint?: Point; actionPoint?: Point;
actionSelector?: string; actionSelector?: string;
language: 'javascript' | 'python' | 'java' | 'csharp';
}; };
export type CallLogStatus = 'in-progress' | 'done' | 'error' | 'paused'; export type CallLogStatus = 'in-progress' | 'done' | 'error' | 'paused';

View file

@ -27,7 +27,7 @@ it('should highlight locator', async ({ page, isAndroid }) => {
const textPromise = waitForTestLog<string>(page, 'Highlight text for test: '); const textPromise = waitForTestLog<string>(page, 'Highlight text for test: ');
const boxPromise = waitForTestLog<{ x: number, y: number, width: number, height: number }>(page, 'Highlight box for test: '); const boxPromise = waitForTestLog<{ x: number, y: number, width: number, height: number }>(page, 'Highlight box for test: ');
await page.locator('input').highlight(); await page.locator('input').highlight();
expect(await textPromise).toBe('input'); expect(await textPromise).toBe('locator(\'input\')');
let box1 = await page.locator('input').boundingBox(); let box1 = await page.locator('input').boundingBox();
let box2 = await boxPromise; let box2 = await boxPromise;