mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: style action list in tv mode (#32845)
This commit is contained in:
parent
6721cc1746
commit
908b0de5d4
@ -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');
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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) {
|
||||
|
||||
147
packages/playwright-core/src/utils/isomorphic/recorderUtils.ts
Normal file
147
packages/playwright-core/src/utils/isomorphic/recorderUtils.ts
Normal 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;
|
||||
}
|
||||
@ -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>
|
||||
</>;
|
||||
};
|
||||
|
||||
@ -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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user