diff --git a/packages/playwright-core/src/server/recorder.ts b/packages/playwright-core/src/server/recorder.ts index d6bc0331f995a..79efd4a46ed3f 100644 --- a/packages/playwright-core/src/server/recorder.ts +++ b/packages/playwright-core/src/server/recorder.ts @@ -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'); diff --git a/packages/playwright-core/src/server/recorder/recorderRunner.ts b/packages/playwright-core/src/server/recorder/recorderRunner.ts index 381d9b6fc5f55..b0f476ffb1eef 100644 --- a/packages/playwright-core/src/server/recorder/recorderRunner.ts +++ b/packages/playwright-core/src/server/recorder/recorderRunner.ts @@ -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, actionInContext: actions.ActionInContext) { const mainFrame = mainFrameForAction(pageAliases, actionInContext); diff --git a/packages/playwright-core/src/server/recorder/recorderUtils.ts b/packages/playwright-core/src/server/recorder/recorderUtils.ts index be8a04a9c34f1..fafeaee434cbc 100644 --- a/packages/playwright-core/src/server/recorder/recorderUtils.ts +++ b/packages/playwright-core/src/server/recorder/recorderUtils.ts @@ -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, 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, 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, actionInContext: actions.ActionInContext): { callMetadata: CallMetadata, mainFrame: Frame } { const mainFrame = mainFrameForAction(pageAliases, actionInContext); const { method, params } = traceParamsForAction(actionInContext); diff --git a/packages/playwright-core/src/server/trace/recorder/tracing.ts b/packages/playwright-core/src/server/trace/recorder/tracing.ts index 83b7fe612005e..c3317fcab4269 100644 --- a/packages/playwright-core/src/server/trace/recorder/tracing.ts +++ b/packages/playwright-core/src/server/trace/recorder/tracing.ts @@ -81,8 +81,6 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps private _allResources = new Set(); private _contextCreatedEvent: trace.ContextCreatedTraceEvent; private _pendingHarEntries = new Set(); - 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) { diff --git a/packages/playwright-core/src/utils/isomorphic/recorderUtils.ts b/packages/playwright-core/src/utils/isomorphic/recorderUtils.ts new file mode 100644 index 0000000000000..40ce3acd44c47 --- /dev/null +++ b/packages/playwright-core/src/utils/isomorphic/recorderUtils.ts @@ -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; +} diff --git a/packages/trace-viewer/src/ui/recorder/actionListView.tsx b/packages/trace-viewer/src/ui/recorder/actionListView.tsx index 71a7c05c7ec42..a68df255ea4b8 100644 --- a/packages/trace-viewer/src/ui/recorder/actionListView.tsx +++ b/packages/trace-viewer/src/ui/recorder/actionListView.tsx @@ -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; 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
+ render={render} />
; }; -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 <> -
- {action.action.name} +
+ {apiName} + {locator &&
{locator}
} + {method === 'goto' && params.url &&
{params.url}
}
; }; diff --git a/packages/trace-viewer/src/ui/recorder/backendContext.tsx b/packages/trace-viewer/src/ui/recorder/backendContext.tsx index b65dfa26863b9..29c8a9b19cc2f 100644 --- a/packages/trace-viewer/src/ui/recorder/backendContext.tsx +++ b/packages/trace-viewer/src/ui/recorder/backendContext.tsx @@ -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')); } } } diff --git a/packages/trace-viewer/src/ui/recorder/recorderView.tsx b/packages/trace-viewer/src/ui/recorder/recorderView.tsx index 3df4c0e7baf6e..67dae2c23929a 100644 --- a/packages/trace-viewer/src/ui/recorder/recorderView.tsx +++ b/packages/trace-viewer/src/ui/recorder/recorderView.tsx @@ -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 = { const sidebarTabbedPane = ; const traceView = ; const propertiesView = { }; 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()); const [selectedPropertiesTab, setSelectedPropertiesTab] = useSetting('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