chore: style action list in tv mode (#32845)

This commit is contained in:
Pavel Feldman 2024-09-27 17:52:03 -07:00 committed by GitHub
parent 6721cc1746
commit 908b0de5d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 183 additions and 140 deletions

View File

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

View File

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

View File

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

View File

@ -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');
@ -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;
@ -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) {

View File

@ -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;
}

View File

@ -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>
</>;
};

View File

@ -118,7 +118,7 @@ class Connection {
}
if (method === 'setActions') {
const { actions } = params as { actions: actionTypes.ActionInContext[] };
this._options.setActions(actions);
this._options.setActions(actions.filter(a => a.action.name !== 'openPage' && a.action.name !== 'closePage'));
}
}
}

View File

@ -36,6 +36,7 @@ import { ModelContext, ModelProvider } from './modelContext';
import './recorderView.css';
import { ActionListView } from './actionListView';
import { BackendContext, BackendProvider } from './backendContext';
import type { Language } from '@isomorphic/locatorGenerators';
export const RecorderView: React.FunctionComponent = () => {
const searchParams = new URLSearchParams(window.location.search);
@ -81,6 +82,8 @@ export const Workbench: React.FunctionComponent = () => {
return sourceLocation;
}, [source]);
const sdkLanguage: Language = source?.language || 'javascript';
const { boundaries } = React.useMemo(() => {
const boundaries = { minimum: model?.startTime || 0, maximum: model?.endTime || 30000 };
if (boundaries.minimum > boundaries.maximum) {
@ -93,6 +96,7 @@ export const Workbench: React.FunctionComponent = () => {
}, [model]);
const actionList = <ActionListView
sdkLanguage={sdkLanguage}
actions={backend?.actions || []}
selectedAction={selectedAction}
onSelectedAction={setSelectedAction}
@ -132,12 +136,14 @@ export const Workbench: React.FunctionComponent = () => {
const sidebarTabbedPane = <TabbedPane tabs={[actionsTab]} />;
const traceView = <TraceView
sdkLanguage={sdkLanguage}
callTime={selectedCallTime || 0}
isInspecting={isInspecting}
setIsInspecting={setIsInspecting}
highlightedLocator={highlightedLocator}
setHighlightedLocator={setHighlightedLocator} />;
const propertiesView = <PropertiesView
sdkLanguage={sdkLanguage}
boundaries={boundaries}
setIsInspecting={setIsInspecting}
highlightedLocator={highlightedLocator}
@ -166,12 +172,14 @@ export const Workbench: React.FunctionComponent = () => {
};
const PropertiesView: React.FunctionComponent<{
sdkLanguage: Language,
boundaries: Boundaries,
setIsInspecting: (value: boolean) => void,
highlightedLocator: string,
setHighlightedLocator: (locator: string) => void,
sourceLocation: modelUtil.SourceLocation | undefined,
}> = ({
sdkLanguage,
boundaries,
setIsInspecting,
highlightedLocator,
@ -184,8 +192,6 @@ const PropertiesView: React.FunctionComponent<{
const sourceModel = React.useRef(new Map<string, modelUtil.SourceModel>());
const [selectedPropertiesTab, setSelectedPropertiesTab] = useSetting<string>('recorderPropertiesTab', 'source');
const sdkLanguage = model?.sdkLanguage || 'javascript';
const inspectorTab: TabbedPaneTabModel = {
id: 'inspector',
title: 'Locator',
@ -233,12 +239,14 @@ const PropertiesView: React.FunctionComponent<{
};
const TraceView: React.FunctionComponent<{
sdkLanguage: Language,
callTime: number;
isInspecting: boolean;
setIsInspecting: (value: boolean) => void;
highlightedLocator: string;
setHighlightedLocator: (locator: string) => void;
}> = ({
sdkLanguage,
callTime,
isInspecting,
setIsInspecting,
@ -259,7 +267,7 @@ const TraceView: React.FunctionComponent<{
}, [snapshot]);
return <SnapshotView
sdkLanguage='javascript'
sdkLanguage={sdkLanguage}
testIdAttributeName='data-testid'
isInspecting={isInspecting}
setIsInspecting={setIsInspecting}