Skip to content

Commit

Permalink
chore: style action list in tv mode (#32845)
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelfeldman authored Sep 28, 2024
1 parent 6721cc1 commit 908b0de
Show file tree
Hide file tree
Showing 8 changed files with 183 additions and 140 deletions.
3 changes: 2 additions & 1 deletion packages/playwright-core/src/server/recorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ import { Debugger } from './debugger';
import type { CallMetadata, InstrumentationListener, SdkObject } from './instrumentation';
import { ContextRecorder, generateFrameSelector } from './recorder/contextRecorder';
import type { IRecorderAppFactory, IRecorderApp, IRecorder } from './recorder/recorderFrontend';
import { buildFullSelector, metadataToCallLog } from './recorder/recorderUtils';
import { metadataToCallLog } from './recorder/recorderUtils';
import type * as actions from '@recorder/actions';
import { buildFullSelector } from '../utils/isomorphic/recorderUtils';

const recorderSymbol = Symbol('recorderSymbol');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import type { CallMetadata } from '../instrumentation';
import type { Page } from '../page';
import type * as actions from '@recorder/actions';
import type * as types from '../types';
import { buildFullSelector, mainFrameForAction } from './recorderUtils';
import { mainFrameForAction } from './recorderUtils';
import { buildFullSelector } from '../../utils/isomorphic/recorderUtils';

export async function performAction(callMetadata: CallMetadata, pageAliases: Map<Page, string>, actionInContext: actions.ActionInContext) {
const mainFrame = mainFrameForAction(pageAliases, actionInContext);
Expand Down
120 changes: 1 addition & 119 deletions packages/playwright-core/src/server/recorder/recorderUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,8 @@ import type { CallLog, CallLogStatus } from '@recorder/recorderTypes';
import type { Page } from '../page';
import type { Frame } from '../frames';
import type * as actions from '@recorder/actions';
import type * as channels from '@protocol/channels';
import { toKeyboardModifiers } from '../codegen/language';
import { serializeExpectedTextValues } from '../../utils/expectUtils';
import { createGuid } from '../../utils';
import { serializeValue } from '../../protocol/serializers';
import { buildFullSelector, traceParamsForAction } from '../../utils/isomorphic/recorderUtils';

export function metadataToCallLog(metadata: CallMetadata, status: CallLogStatus): CallLog {
let title = metadata.apiName || metadata.method;
Expand Down Expand Up @@ -53,10 +50,6 @@ export function metadataToCallLog(metadata: CallMetadata, status: CallLogStatus)
return callLog;
}

export function buildFullSelector(framePath: string[], selector: string) {
return [...framePath, selector].join(' >> internal:control=enter-frame >> ');
}

export function mainFrameForAction(pageAliases: Map<Page, string>, actionInContext: actions.ActionInContext): Frame {
const pageAlias = actionInContext.frame.pageAlias;
const page = [...pageAliases.entries()].find(([, alias]) => pageAlias === alias)?.[0];
Expand All @@ -77,117 +70,6 @@ export async function frameForAction(pageAliases: Map<Page, string>, actionInCon
return result.frame;
}

export function traceParamsForAction(actionInContext: actions.ActionInContext): { method: string, params: any } {
const { action } = actionInContext;

switch (action.name) {
case 'navigate': {
const params: channels.FrameGotoParams = {
url: action.url,
};
return { method: 'goto', params };
}
case 'openPage':
case 'closePage':
throw new Error('Not reached');
}

const selector = buildFullSelector(actionInContext.frame.framePath, action.selector);
switch (action.name) {
case 'click': {
const params: channels.FrameClickParams = {
selector,
strict: true,
modifiers: toKeyboardModifiers(action.modifiers),
button: action.button,
clickCount: action.clickCount,
position: action.position,
};
return { method: 'click', params };
}
case 'press': {
const params: channels.FramePressParams = {
selector,
strict: true,
key: [...toKeyboardModifiers(action.modifiers), action.key].join('+'),
};
return { method: 'press', params };
}
case 'fill': {
const params: channels.FrameFillParams = {
selector,
strict: true,
value: action.text,
};
return { method: 'fill', params };
}
case 'setInputFiles': {
const params: channels.FrameSetInputFilesParams = {
selector,
strict: true,
localPaths: action.files,
};
return { method: 'setInputFiles', params };
}
case 'check': {
const params: channels.FrameCheckParams = {
selector,
strict: true,
};
return { method: 'check', params };
}
case 'uncheck': {
const params: channels.FrameUncheckParams = {
selector,
strict: true,
};
return { method: 'uncheck', params };
}
case 'select': {
const params: channels.FrameSelectOptionParams = {
selector,
strict: true,
options: action.options.map(option => ({ value: option })),
};
return { method: 'selectOption', params };
}
case 'assertChecked': {
const params: channels.FrameExpectParams = {
selector: action.selector,
expression: 'to.be.checked',
isNot: !action.checked,
};
return { method: 'expect', params };
}
case 'assertText': {
const params: channels.FrameExpectParams = {
selector,
expression: 'to.have.text',
expectedText: serializeExpectedTextValues([action.text], { matchSubstring: action.substring, normalizeWhiteSpace: true }),
isNot: false,
};
return { method: 'expect', params };
}
case 'assertValue': {
const params: channels.FrameExpectParams = {
selector,
expression: 'to.have.value',
expectedValue: { value: serializeValue(action.value, value => ({ fallThrough: value })), handles: [] },
isNot: false,
};
return { method: 'expect', params };
}
case 'assertVisible': {
const params: channels.FrameExpectParams = {
selector,
expression: 'to.be.visible',
isNot: false,
};
return { method: 'expect', params };
}
}
}

export function callMetadataForAction(pageAliases: Map<Page, string>, actionInContext: actions.ActionInContext): { callMetadata: CallMetadata, mainFrame: Frame } {
const mainFrame = mainFrameForAction(pageAliases, actionInContext);
const { method, params } = traceParamsForAction(actionInContext);
Expand Down
11 changes: 0 additions & 11 deletions packages/playwright-core/src/server/trace/recorder/tracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,6 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
private _allResources = new Set<string>();
private _contextCreatedEvent: trace.ContextCreatedTraceEvent;
private _pendingHarEntries = new Set<har.Entry>();
private _inMemoryEvents: trace.TraceEvent[] | undefined;
private _inMemoryEventsCallback: ((events: trace.TraceEvent[]) => void) | undefined;

constructor(context: BrowserContext | APIRequestContext, tracesDir: string | undefined) {
super(context, 'tracing');
Expand Down Expand Up @@ -197,11 +195,6 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
return { traceName: this._state.traceName };
}

onMemoryEvents(callback: (events: trace.TraceEvent[]) => void) {
this._inMemoryEventsCallback = callback;
this._inMemoryEvents = [];
}

private _startScreencast() {
if (!(this._context instanceof BrowserContext))
return;
Expand Down Expand Up @@ -540,10 +533,6 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
// Do not flush (console) events, they are too noisy, unless we are in ui mode (live).
const flush = this._state!.options.live || (event.type !== 'event' && event.type !== 'console' && event.type !== 'log');
this._fs.appendFile(this._state!.traceFile, JSON.stringify(visited) + '\n', flush);
if (this._inMemoryEvents) {
this._inMemoryEvents.push(event);
this._inMemoryEventsCallback?.(this._inMemoryEvents);
}
}

private _appendResource(sha1: string, buffer: Buffer) {
Expand Down
147 changes: 147 additions & 0 deletions packages/playwright-core/src/utils/isomorphic/recorderUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/**
* 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 type * as recorderActions from '@recorder/actions';
import type * as channels from '@protocol/channels';
import type * as types from '../../server/types';

export function buildFullSelector(framePath: string[], selector: string) {
return [...framePath, selector].join(' >> internal:control=enter-frame >> ');
}

export function traceParamsForAction(actionInContext: recorderActions.ActionInContext): { method: string, params: any } {
const { action } = actionInContext;

switch (action.name) {
case 'navigate': {
const params: channels.FrameGotoParams = {
url: action.url,
};
return { method: 'goto', params };
}
case 'openPage':
case 'closePage':
throw new Error('Not reached');
}

const selector = buildFullSelector(actionInContext.frame.framePath, action.selector);
switch (action.name) {
case 'click': {
const params: channels.FrameClickParams = {
selector,
strict: true,
modifiers: toKeyboardModifiers(action.modifiers),
button: action.button,
clickCount: action.clickCount,
position: action.position,
};
return { method: 'click', params };
}
case 'press': {
const params: channels.FramePressParams = {
selector,
strict: true,
key: [...toKeyboardModifiers(action.modifiers), action.key].join('+'),
};
return { method: 'press', params };
}
case 'fill': {
const params: channels.FrameFillParams = {
selector,
strict: true,
value: action.text,
};
return { method: 'fill', params };
}
case 'setInputFiles': {
const params: channels.FrameSetInputFilesParams = {
selector,
strict: true,
localPaths: action.files,
};
return { method: 'setInputFiles', params };
}
case 'check': {
const params: channels.FrameCheckParams = {
selector,
strict: true,
};
return { method: 'check', params };
}
case 'uncheck': {
const params: channels.FrameUncheckParams = {
selector,
strict: true,
};
return { method: 'uncheck', params };
}
case 'select': {
const params: channels.FrameSelectOptionParams = {
selector,
strict: true,
options: action.options.map(option => ({ value: option })),
};
return { method: 'selectOption', params };
}
case 'assertChecked': {
const params: channels.FrameExpectParams = {
selector: action.selector,
expression: 'to.be.checked',
isNot: !action.checked,
};
return { method: 'expect', params };
}
case 'assertText': {
const params: channels.FrameExpectParams = {
selector,
expression: 'to.have.text',
expectedText: [],
isNot: false,
};
return { method: 'expect', params };
}
case 'assertValue': {
const params: channels.FrameExpectParams = {
selector,
expression: 'to.have.value',
expectedValue: undefined,
isNot: false,
};
return { method: 'expect', params };
}
case 'assertVisible': {
const params: channels.FrameExpectParams = {
selector,
expression: 'to.be.visible',
isNot: false,
};
return { method: 'expect', params };
}
}
}

export function toKeyboardModifiers(modifiers: number): types.SmartKeyboardModifier[] {
const result: types.SmartKeyboardModifier[] = [];
if (modifiers & 1)
result.push('Alt');
if (modifiers & 2)
result.push('ControlOrMeta');
if (modifiers & 4)
result.push('ControlOrMeta');
if (modifiers & 8)
result.push('Shift');
return result;
}
23 changes: 19 additions & 4 deletions packages/trace-viewer/src/ui/recorder/actionListView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,32 +17,47 @@
import type * as actionTypes from '@recorder/actions';
import { ListView } from '@web/components/listView';
import * as React from 'react';
import '../actionList.css';
import { traceParamsForAction } from '@isomorphic/recorderUtils';
import { asLocator } from '@isomorphic/locatorGenerators';
import type { Language } from '@isomorphic/locatorGenerators';

const ActionList = ListView<actionTypes.ActionInContext>;

export const ActionListView: React.FC<{
sdkLanguage: Language,
actions: actionTypes.ActionInContext[],
selectedAction: actionTypes.ActionInContext | undefined,
onSelectedAction: (action: actionTypes.ActionInContext | undefined) => void,
}> = ({
sdkLanguage,
actions,
selectedAction,
onSelectedAction,
}) => {
const render = React.useCallback((action: actionTypes.ActionInContext) => {
return renderAction(sdkLanguage, action);
}, [sdkLanguage]);
return <div className='vbox'>
<ActionList
name='actions'
items={actions}
selectedItem={selectedAction}
onSelected={onSelectedAction}
render={renderAction} />
render={render} />
</div>;
};

export const renderAction = (action: actionTypes.ActionInContext) => {
export const renderAction = (sdkLanguage: Language, action: actionTypes.ActionInContext) => {
const { method, params } = traceParamsForAction(action);
const locator = params.selector ? asLocator(sdkLanguage || 'javascript', params.selector) : undefined;

const apiName = `page.${method}`;
return <>
<div title={action.action.name}>
<span>{action.action.name}</span>
<div className='action-title' title={apiName}>
<span>{apiName}</span>
{locator && <div className='action-selector' title={locator}>{locator}</div>}
{method === 'goto' && params.url && <div className='action-url' title={params.url}>{params.url}</div>}
</div>
</>;
};
Loading

0 comments on commit 908b0de

Please sign in to comment.