chore: delete recorder in traceviewer experiment (#34347)

This commit is contained in:
Yury Semikhatsky 2025-01-15 17:37:33 -08:00 committed by GitHub
parent 8d39c44b69
commit 00bb17751b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 17 additions and 843 deletions

View File

@ -595,7 +595,6 @@ async function codegen(options: Options & { target: string, output?: string, tes
device: options.device,
saveStorage: options.saveStorage,
mode: 'recording',
codegenMode: process.env.PW_RECORDER_IS_TRACE_VIEWER ? 'trace-events' : 'actions',
testIdAttributeName,
outputFile: outputFile ? path.resolve(outputFile) : undefined,
handleSIGINT: false,

View File

@ -970,7 +970,6 @@ scheme.BrowserContextPauseResult = tOptional(tObject({}));
scheme.BrowserContextEnableRecorderParams = tObject({
language: tOptional(tString),
mode: tOptional(tEnum(['inspecting', 'recording'])),
codegenMode: tOptional(tEnum(['actions', 'trace-events'])),
pauseOnNextStatement: tOptional(tBoolean),
testIdAttributeName: tOptional(tString),
launchOptions: tOptional(tAny),

View File

@ -130,7 +130,7 @@ export abstract class BrowserContext extends SdkObject {
// When PWDEBUG=1, show inspector for each context.
if (debugMode() === 'inspector')
await Recorder.show('actions', this, RecorderApp.factory(this), { pauseOnNextStatement: true });
await Recorder.show(this, RecorderApp.factory(this), { pauseOnNextStatement: true });
// When paused, show inspector.
if (this._debugger.isPaused())

View File

@ -39,7 +39,6 @@ import type { Dialog } from '../dialog';
import type { ConsoleMessage } from '../console';
import { serializeError } from '../errors';
import { ElementHandleDispatcher } from './elementHandlerDispatcher';
import { RecorderInTraceViewer } from '../recorder/recorderInTraceViewer';
import { RecorderApp } from '../recorder/recorderApp';
import { WebSocketRouteDispatcher } from './webSocketRouteDispatcher';
@ -301,17 +300,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
}
async enableRecorder(params: channels.BrowserContextEnableRecorderParams): Promise<void> {
if (params.codegenMode === 'trace-events') {
await this._context.tracing.start({
name: 'trace',
snapshots: true,
screenshots: true,
live: true,
});
await Recorder.show('trace-events', this._context, RecorderInTraceViewer.factory(this._context), params);
} else {
await Recorder.show('actions', this._context, RecorderApp.factory(this._context), params);
}
await Recorder.show(this._context, RecorderApp.factory(this._context), params);
}
async pause(params: channels.BrowserContextPauseParams, metadata: CallMetadata) {

View File

@ -54,33 +54,33 @@ export class Recorder implements InstrumentationListener, IRecorder {
static async showInspector(context: BrowserContext, params: channels.BrowserContextEnableRecorderParams, recorderAppFactory: IRecorderAppFactory) {
if (isUnderTest())
params.language = process.env.TEST_INSPECTOR_LANGUAGE;
return await Recorder.show('actions', context, recorderAppFactory, params);
return await Recorder.show(context, recorderAppFactory, params);
}
static showInspectorNoReply(context: BrowserContext, recorderAppFactory: IRecorderAppFactory) {
Recorder.showInspector(context, {}, recorderAppFactory).catch(() => {});
}
static show(codegenMode: 'actions' | 'trace-events', context: BrowserContext, recorderAppFactory: IRecorderAppFactory, params: channels.BrowserContextEnableRecorderParams): Promise<Recorder> {
static show(context: BrowserContext, recorderAppFactory: IRecorderAppFactory, params: channels.BrowserContextEnableRecorderParams): Promise<Recorder> {
let recorderPromise = (context as any)[recorderSymbol] as Promise<Recorder>;
if (!recorderPromise) {
recorderPromise = Recorder._create(codegenMode, context, recorderAppFactory, params);
recorderPromise = Recorder._create(context, recorderAppFactory, params);
(context as any)[recorderSymbol] = recorderPromise;
}
return recorderPromise;
}
private static async _create(codegenMode: 'actions' | 'trace-events', context: BrowserContext, recorderAppFactory: IRecorderAppFactory, params: channels.BrowserContextEnableRecorderParams = {}): Promise<Recorder> {
const recorder = new Recorder(codegenMode, context, params);
private static async _create(context: BrowserContext, recorderAppFactory: IRecorderAppFactory, params: channels.BrowserContextEnableRecorderParams = {}): Promise<Recorder> {
const recorder = new Recorder(context, params);
const recorderApp = await recorderAppFactory(recorder);
await recorder._install(recorderApp);
return recorder;
}
constructor(codegenMode: 'actions' | 'trace-events', context: BrowserContext, params: channels.BrowserContextEnableRecorderParams) {
constructor(context: BrowserContext, params: channels.BrowserContextEnableRecorderParams) {
this._mode = params.mode || 'none';
this.handleSIGINT = params.handleSIGINT;
this._contextRecorder = new ContextRecorder(codegenMode, context, params, {});
this._contextRecorder = new ContextRecorder(context, params, {});
this._context = context;
this._omitCallTracking = !!params.omitCallTracking;
this._debugger = context.debugger();

View File

@ -10,6 +10,3 @@
../../utils/**
../../utilsBundle.ts
../../zipBundle.ts
[recorderInTraceViewer.ts]
../trace/viewer/traceViewer.ts

View File

@ -54,11 +54,9 @@ export class ContextRecorder extends EventEmitter {
private _throttledOutputFile: ThrottledFile | null = null;
private _orderedLanguages: LanguageGenerator[] = [];
private _listeners: RegisteredListener[] = [];
private _codegenMode: 'actions' | 'trace-events';
constructor(codegenMode: 'actions' | 'trace-events', context: BrowserContext, params: channels.BrowserContextEnableRecorderParams, delegate: ContextRecorderDelegate) {
constructor(context: BrowserContext, params: channels.BrowserContextEnableRecorderParams, delegate: ContextRecorderDelegate) {
super();
this._codegenMode = codegenMode;
this._context = context;
this._params = params;
this._delegate = delegate;
@ -150,12 +148,6 @@ export class ContextRecorder extends EventEmitter {
setEnabled(enabled: boolean) {
this._collection.setEnabled(enabled);
if (this._codegenMode === 'trace-events') {
if (enabled)
this._context.tracing.startChunk({ name: 'trace', title: 'trace' }).catch(() => {});
else
this._context.tracing.stopChunk({ mode: 'discard' }).catch(() => {});
}
}
dispose() {

View File

@ -1,126 +0,0 @@
/**
* 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 path from 'path';
import type { CallLog, ElementInfo, Mode, Source } from '@recorder/recorderTypes';
import { EventEmitter } from 'events';
import type { IRecorder, IRecorderApp, IRecorderAppFactory } from './recorderFrontend';
import { installRootRedirect, openTraceViewerApp, startTraceViewerServer } from '../trace/viewer/traceViewer';
import type { TraceViewerServerOptions } from '../trace/viewer/traceViewer';
import type { BrowserContext } from '../browserContext';
import type { HttpServer, Transport } from '../../utils/httpServer';
import type { Page } from '../page';
import { ManualPromise } from '../../utils/manualPromise';
import type * as actions from '@recorder/actions';
export class RecorderInTraceViewer extends EventEmitter implements IRecorderApp {
readonly wsEndpointForTest: string | undefined;
private _transport: RecorderTransport;
private _tracePage: Page;
private _traceServer: HttpServer;
static factory(context: BrowserContext): IRecorderAppFactory {
return async (recorder: IRecorder) => {
const transport = new RecorderTransport();
const trace = path.join(context._browser.options.tracesDir, 'trace');
const { wsEndpointForTest, tracePage, traceServer } = await openApp(trace, { transport, headless: !context._browser.options.headful });
return new RecorderInTraceViewer(transport, tracePage, traceServer, wsEndpointForTest);
};
}
constructor(transport: RecorderTransport, tracePage: Page, traceServer: HttpServer, wsEndpointForTest: string | undefined) {
super();
this._transport = transport;
this._transport.eventSink.resolve(this);
this._tracePage = tracePage;
this._traceServer = traceServer;
this.wsEndpointForTest = wsEndpointForTest;
this._tracePage.once('close', () => {
this.close();
});
}
async close(): Promise<void> {
await this._tracePage.context().close({ reason: 'Recorder window closed' });
await this._traceServer.stop();
}
async setPaused(paused: boolean): Promise<void> {
this._transport.deliverEvent('setPaused', { paused });
}
async setMode(mode: Mode): Promise<void> {
this._transport.deliverEvent('setMode', { mode });
}
async setRunningFile(file: string | undefined): Promise<void> {
this._transport.deliverEvent('setRunningFile', { file });
}
async elementPicked(elementInfo: ElementInfo, userGesture?: boolean): Promise<void> {
this._transport.deliverEvent('elementPicked', { elementInfo, userGesture });
}
async updateCallLogs(callLogs: CallLog[]): Promise<void> {
this._transport.deliverEvent('updateCallLogs', { callLogs });
}
async setSources(sources: Source[]): Promise<void> {
this._transport.deliverEvent('setSources', { sources });
if (process.env.PWTEST_CLI_IS_UNDER_TEST && sources.length) {
if ((process as any)._didSetSourcesForTest(sources[0].text))
this.close();
}
}
async setActions(actions: actions.ActionInContext[], sources: Source[]): Promise<void> {
this._transport.deliverEvent('setActions', { actions, sources });
}
}
async function openApp(trace: string, options?: TraceViewerServerOptions & { headless?: boolean }): Promise<{ wsEndpointForTest: string | undefined, tracePage: Page, traceServer: HttpServer }> {
const traceServer = await startTraceViewerServer(options);
await installRootRedirect(traceServer, [trace], { ...options, webApp: 'recorder.html' });
const page = await openTraceViewerApp(traceServer.urlPrefix('precise'), 'chromium', options);
return { wsEndpointForTest: page.context()._browser.options.wsEndpoint, tracePage: page, traceServer };
}
class RecorderTransport implements Transport {
private _connected = new ManualPromise<void>();
readonly eventSink = new ManualPromise<EventEmitter>();
constructor() {
}
onconnect() {
this._connected.resolve();
}
async dispatch(method: string, params: any): Promise<any> {
const eventSink = await this.eventSink;
eventSink.emit('event', { event: method, params });
}
onclose() {
}
deliverEvent(method: string, params: any) {
this._connected.then(() => this.sendEvent?.(method, params));
}
sendEvent?: (method: string, params: any) => void;
close?: () => void;
}

View File

@ -1772,7 +1772,6 @@ export type BrowserContextPauseResult = void;
export type BrowserContextEnableRecorderParams = {
language?: string,
mode?: 'inspecting' | 'recording',
codegenMode?: 'actions' | 'trace-events',
pauseOnNextStatement?: boolean,
testIdAttributeName?: string,
launchOptions?: any,
@ -1786,7 +1785,6 @@ export type BrowserContextEnableRecorderParams = {
export type BrowserContextEnableRecorderOptions = {
language?: string,
mode?: 'inspecting' | 'recording',
codegenMode?: 'actions' | 'trace-events',
pauseOnNextStatement?: boolean,
testIdAttributeName?: string,
launchOptions?: any,

View File

@ -1198,11 +1198,6 @@ BrowserContext:
literals:
- inspecting
- recording
codegenMode:
type: enum?
literals:
- actions
- trace-events
pauseOnNextStatement: boolean?
testIdAttributeName: string?
launchOptions: json?

View File

@ -1,28 +0,0 @@
<!--
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.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="/playwright-logo.svg" type="image/svg+xml">
<title>Playwright Recorder</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/recorder.tsx"></script>
</body>
</html>

View File

@ -6,7 +6,3 @@ ui/
[sw-main.ts]
sw/**
[recorder.tsx]
ui/recorder/**

View File

@ -1,41 +0,0 @@
/**
* 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 '@web/common.css';
import { applyTheme } from '@web/theme';
import '@web/third_party/vscode/codicon.css';
import * as ReactDOM from 'react-dom/client';
import { RecorderView } from './ui/recorder/recorderView';
(async () => {
applyTheme();
if (window.location.protocol !== 'file:') {
if (!navigator.serviceWorker)
throw new Error(`Service workers are not supported.\nMake sure to serve the Recorder (${window.location}) via HTTPS or localhost.`);
navigator.serviceWorker.register('sw.bundle.js');
if (!navigator.serviceWorker.controller) {
await new Promise<void>(f => {
navigator.serviceWorker.oncontrollerchange = () => f();
});
}
// Keep SW running.
setInterval(function() { fetch('ping'); }, 10000);
}
ReactDOM.createRoot(document.querySelector('#root')!).render(<RecorderView />);
})();

View File

@ -1,5 +0,0 @@
[*]
@isomorphic/**
@trace/**
@web/**
../**

View File

@ -1,62 +0,0 @@
/*
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 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={render} />
</div>;
};
export const renderAction = (sdkLanguage: Language, action: actionTypes.ActionInContext) => {
const { method, apiName, params } = traceParamsForAction(action);
const locator = params.selector ? asLocator(sdkLanguage || 'javascript', params.selector) : undefined;
return <>
<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

@ -1,118 +0,0 @@
/*
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 actionTypes from '@recorder/actions';
import type { Mode, Source } from '@recorder/recorderTypes';
import * as React from 'react';
export const BackendContext = React.createContext<Backend | undefined>(undefined);
export const BackendProvider: React.FunctionComponent<React.PropsWithChildren<{
guid: string,
}>> = ({ guid, children }) => {
const [connection, setConnection] = React.useState<Connection | undefined>(undefined);
const [mode, setMode] = React.useState<Mode>('none');
const [actions, setActions] = React.useState<{ actions: actionTypes.ActionInContext[], sources: Source[] }>({ actions: [], sources: [] });
const callbacks = React.useRef({ setMode, setActions });
React.useEffect(() => {
const wsURL = new URL(`../${guid}`, window.location.toString());
wsURL.protocol = (window.location.protocol === 'https:' ? 'wss:' : 'ws:');
const webSocket = new WebSocket(wsURL.toString());
setConnection(new Connection(webSocket, callbacks.current));
return () => {
webSocket.close();
};
}, [guid]);
const backend = React.useMemo(() => {
return connection ? { mode, actions: actions.actions, sources: actions.sources, connection } : undefined;
}, [actions, mode, connection]);
return <BackendContext.Provider value={backend}>
{children}
</BackendContext.Provider>;
};
export type Backend = {
actions: actionTypes.ActionInContext[],
sources: Source[],
connection: Connection,
};
type ConnectionCallbacks = {
setMode: (mode: Mode) => void;
setActions: (data: { actions: actionTypes.ActionInContext[], sources: Source[] }) => void;
};
class Connection {
private _lastId = 0;
private _webSocket: WebSocket;
private _callbacks = new Map<number, { resolve: (arg: any) => void, reject: (arg: Error) => void }>();
private _options: ConnectionCallbacks;
constructor(webSocket: WebSocket, options: ConnectionCallbacks) {
this._webSocket = webSocket;
this._callbacks = new Map();
this._options = options;
this._webSocket.addEventListener('message', event => {
const message = JSON.parse(event.data);
const { id, result, error, method, params } = message;
if (id) {
const callback = this._callbacks.get(id);
if (!callback)
return;
this._callbacks.delete(id);
if (error)
callback.reject(new Error(error));
else
callback.resolve(result);
} else {
this._dispatchEvent(method, params);
}
});
}
setMode(mode: Mode) {
this._sendMessageNoReply('setMode', { mode });
}
private async _sendMessage(method: string, params?: any): Promise<any> {
const id = ++this._lastId;
const message = { id, method, params };
this._webSocket.send(JSON.stringify(message));
return new Promise((resolve, reject) => {
this._callbacks.set(id, { resolve, reject });
});
}
private _sendMessageNoReply(method: string, params?: any) {
this._sendMessage(method, params).catch(() => { });
}
private _dispatchEvent(method: string, params?: any) {
if (method === 'setMode') {
const { mode } = params as { mode: Mode };
this._options.setMode(mode);
}
if (method === 'setActions') {
const { actions, sources } = params as { actions: actionTypes.ActionInContext[], sources: Source[] };
this._options.setActions({ actions: actions.filter(a => a.action.name !== 'openPage' && a.action.name !== 'closePage'), sources });
(window as any).playwrightSourcesEchoForTest = sources;
}
}
}

View File

@ -1,71 +0,0 @@
/*
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 { sha1 } from '@web/uiUtils';
import * as React from 'react';
import type { ContextEntry } from '../../types/entries';
import { MultiTraceModel } from '../modelUtil';
export const ModelContext = React.createContext<MultiTraceModel | undefined>(undefined);
export const ModelProvider: React.FunctionComponent<React.PropsWithChildren<{
trace: string,
}>> = ({ trace, children }) => {
const [model, setModel] = React.useState<{ model: MultiTraceModel, sha1: string } | undefined>();
const [counter, setCounter] = React.useState(0);
const pollTimer = React.useRef<NodeJS.Timeout | null>(null);
React.useEffect(() => {
if (pollTimer.current)
clearTimeout(pollTimer.current);
// Start polling running test.
pollTimer.current = setTimeout(async () => {
try {
const result = await loadSingleTraceFile(trace);
if (result.sha1 !== model?.sha1)
setModel(result);
} catch {
setModel(undefined);
} finally {
setCounter(counter + 1);
}
}, 500);
return () => {
if (pollTimer.current)
clearTimeout(pollTimer.current);
};
}, [counter, model, trace]);
return <ModelContext.Provider value={model?.model}>
{children}
</ModelContext.Provider>;
};
async function loadSingleTraceFile(url: string): Promise<{ model: MultiTraceModel, sha1: string }> {
const params = new URLSearchParams();
params.set('trace', url);
params.set('limit', '1');
const response = await fetch(`contexts?${params.toString()}`);
const contextEntries = await response.json() as ContextEntry[];
const tokens: string[] = [];
for (const entry of contextEntries) {
entry.actions.forEach(a => tokens.push(a.type + '@' + a.startTime + '-' + a.endTime));
entry.events.forEach(e => tokens.push(e.type + '@' + e.time));
}
return { model: new MultiTraceModel(contextEntries), sha1: await sha1(tokens.join('|')) };
}

View File

@ -1,15 +0,0 @@
/*
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.
*/

View File

@ -1,299 +0,0 @@
/*
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 actionTypes from '@recorder/actions';
import { SourceChooser } from '@web/components/sourceChooser';
import { SplitView } from '@web/components/splitView';
import type { TabbedPaneTabModel } from '@web/components/tabbedPane';
import { TabbedPane } from '@web/components/tabbedPane';
import { Toolbar } from '@web/components/toolbar';
import { ToolbarButton, ToolbarSeparator } from '@web/components/toolbarButton';
import { copy, useSetting } from '@web/uiUtils';
import * as React from 'react';
import { ConsoleTab, useConsoleTabModel } from '../consoleTab';
import type { Boundaries } from '../geometry';
import { InspectorTab } from '../inspectorTab';
import type * as modelUtil from '../modelUtil';
import type { SourceLocation } from '../modelUtil';
import { NetworkTab, useNetworkTabModel } from '../networkTab';
import { collectSnapshots, extendSnapshot, SnapshotView } from '../snapshotTab';
import { SourceTab } from '../sourceTab';
import { ModelContext, ModelProvider } from './modelContext';
import './recorderView.css';
import { ActionListView } from './actionListView';
import { BackendContext, BackendProvider } from './backendContext';
import type { Language } from '@isomorphic/locatorGenerators';
import { SettingsToolbarButton } from '../settingsToolbarButton';
import type { HighlightedElement } from '../snapshotTab';
export const RecorderView: React.FunctionComponent = () => {
const searchParams = new URLSearchParams(window.location.search);
const guid = searchParams.get('ws')!;
const trace = searchParams.get('trace') + '.json';
return <BackendProvider guid={guid}>
<ModelProvider trace={trace}>
<Workbench />
</ModelProvider>
</BackendProvider>;
};
export const Workbench: React.FunctionComponent = () => {
const backend = React.useContext(BackendContext);
const model = React.useContext(ModelContext);
const [fileId, setFileId] = React.useState<string | undefined>();
const [selectedStartTime, setSelectedStartTime] = React.useState<number | undefined>(undefined);
const [isInspecting, setIsInspecting] = React.useState(false);
const [highlightedElementInProperties, setHighlightedElementInProperties] = React.useState<HighlightedElement>({ lastEdited: 'none' });
const [highlightedElementInTrace, setHighlightedElementInTrace] = React.useState<HighlightedElement>({ lastEdited: 'none' });
const [traceCallId, setTraceCallId] = React.useState<string | undefined>();
const setSelectedAction = React.useCallback((action: actionTypes.ActionInContext | undefined) => {
setSelectedStartTime(action?.startTime);
}, []);
const selectedAction = React.useMemo(() => {
return backend?.actions.find(a => a.startTime === selectedStartTime);
}, [backend?.actions, selectedStartTime]);
React.useEffect(() => {
const callId = model?.actions.find(a => a.endTime && a.endTime === selectedAction?.endTime)?.callId;
if (callId)
setTraceCallId(callId);
}, [model, selectedAction]);
const source = React.useMemo(() => backend?.sources.find(s => s.id === fileId) || backend?.sources[0], [backend?.sources, fileId]);
const sourceLocation = React.useMemo(() => {
if (!source)
return undefined;
const sourceLocation: SourceLocation = {
file: '',
line: 0,
column: 0,
source: {
errors: [],
content: source.text
}
};
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) {
boundaries.minimum = 0;
boundaries.maximum = 30000;
}
// Leave some nice free space on the right hand side.
boundaries.maximum += (boundaries.maximum - boundaries.minimum) / 20;
return { boundaries };
}, [model]);
const elementPickedInTrace = React.useCallback((element: HighlightedElement) => {
setHighlightedElementInProperties(element);
setHighlightedElementInTrace({ lastEdited: 'none' });
setIsInspecting(false);
}, []);
const elementTypedInProperties = React.useCallback((element: HighlightedElement) => {
setHighlightedElementInTrace(element);
setHighlightedElementInProperties(element);
}, []);
const actionList = <ActionListView
sdkLanguage={sdkLanguage}
actions={backend?.actions || []}
selectedAction={selectedAction}
onSelectedAction={setSelectedAction}
/>;
const actionsTab: TabbedPaneTabModel = {
id: 'actions',
title: 'Actions',
component: actionList,
};
const toolbar = <Toolbar sidebarBackground>
<div style={{ width: 4 }}></div>
<ToolbarButton icon='inspect' title='Pick locator' toggled={isInspecting} onClick={() => {
setIsInspecting(!isInspecting);
}} />
<ToolbarButton icon='eye' title='Assert visibility' onClick={() => {
}} />
<ToolbarButton icon='whole-word' title='Assert text' onClick={() => {
}} />
<ToolbarButton icon='symbol-constant' title='Assert value' onClick={() => {
}} />
<ToolbarSeparator />
<ToolbarButton icon='files' title='Copy' disabled={!source || !source.text} onClick={() => {
if (source?.text)
copy(source.text);
}}></ToolbarButton>
<div style={{ flex: 'auto' }}></div>
<div>Target:</div>
<SourceChooser fileId={fileId} sources={backend?.sources || []} setFileId={fileId => {
setFileId(fileId);
}} />
<SettingsToolbarButton />
</Toolbar>;
const sidebarTabbedPane = <TabbedPane tabs={[actionsTab]} />;
const traceView = <TraceView
sdkLanguage={sdkLanguage}
callId={traceCallId}
isInspecting={isInspecting}
setIsInspecting={setIsInspecting}
highlightedElement={highlightedElementInTrace}
setHighlightedElement={elementPickedInTrace} />;
const propertiesView = <PropertiesView
sdkLanguage={sdkLanguage}
boundaries={boundaries}
setIsInspecting={setIsInspecting}
highlightedElement={highlightedElementInProperties}
setHighlightedElement={elementTypedInProperties}
sourceLocation={sourceLocation} />;
return <div className='vbox workbench'>
<SplitView
sidebarSize={250}
orientation={'horizontal'}
settingName='recorderActionListSidebar'
sidebarIsFirst
main={<SplitView
sidebarSize={250}
orientation='vertical'
settingName='recorderPropertiesSidebar'
main={<div className='vbox'>
{toolbar}
{traceView}
</div>}
sidebar={propertiesView}
/>}
sidebar={sidebarTabbedPane}
/>
</div>;
};
const PropertiesView: React.FunctionComponent<{
sdkLanguage: Language,
boundaries: Boundaries,
setIsInspecting: (value: boolean) => void,
highlightedElement: HighlightedElement,
setHighlightedElement: (element: HighlightedElement) => void,
sourceLocation: modelUtil.SourceLocation | undefined,
}> = ({
sdkLanguage,
boundaries,
setIsInspecting,
highlightedElement,
setHighlightedElement,
sourceLocation,
}) => {
const model = React.useContext(ModelContext);
const consoleModel = useConsoleTabModel(model, boundaries);
const networkModel = useNetworkTabModel(model, boundaries);
const sourceModel = React.useRef(new Map<string, modelUtil.SourceModel>());
const [selectedPropertiesTab, setSelectedPropertiesTab] = useSetting<string>('recorderPropertiesTab', 'source');
const inspectorTab: TabbedPaneTabModel = {
id: 'inspector',
title: 'Locator',
render: () => <InspectorTab
sdkLanguage={sdkLanguage}
setIsInspecting={setIsInspecting}
highlightedElement={highlightedElement}
setHighlightedElement={setHighlightedElement} />,
};
const sourceTab: TabbedPaneTabModel = {
id: 'source',
title: 'Source',
render: () => <SourceTab
sources={sourceModel.current}
stackFrameLocation={'right'}
fallbackLocation={sourceLocation}
/>
};
const consoleTab: TabbedPaneTabModel = {
id: 'console',
title: 'Console',
count: consoleModel.entries.length,
render: () => <ConsoleTab boundaries={boundaries} consoleModel={consoleModel} />
};
const networkTab: TabbedPaneTabModel = {
id: 'network',
title: 'Network',
count: networkModel.resources.length,
render: () => <NetworkTab boundaries={boundaries} networkModel={networkModel} sdkLanguage={sdkLanguage} />
};
const tabs: TabbedPaneTabModel[] = [
sourceTab,
inspectorTab,
consoleTab,
networkTab,
];
return <TabbedPane
tabs={tabs}
selectedTab={selectedPropertiesTab}
setSelectedTab={setSelectedPropertiesTab}
/>;
};
const TraceView: React.FunctionComponent<{
sdkLanguage: Language,
callId: string | undefined,
isInspecting: boolean;
setIsInspecting: (value: boolean) => void;
highlightedElement: HighlightedElement;
setHighlightedElement: (element: HighlightedElement) => void;
}> = ({
sdkLanguage,
callId,
isInspecting,
setIsInspecting,
highlightedElement,
setHighlightedElement,
}) => {
const model = React.useContext(ModelContext);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [shouldPopulateCanvasFromScreenshot, _] = useSetting('shouldPopulateCanvasFromScreenshot', false);
const action = React.useMemo(() => {
return model?.actions.find(a => a.callId === callId);
}, [model, callId]);
const snapshot = React.useMemo(() => {
const snapshot = collectSnapshots(action);
return snapshot.action || snapshot.after || snapshot.before;
}, [action]);
const snapshotUrls = React.useMemo(() => {
return snapshot ? extendSnapshot(snapshot, shouldPopulateCanvasFromScreenshot) : undefined;
}, [snapshot, shouldPopulateCanvasFromScreenshot]);
return <SnapshotView
sdkLanguage={sdkLanguage}
testIdAttributeName='data-testid'
isInspecting={isInspecting}
setIsInspecting={setIsInspecting}
highlightedElement={highlightedElement}
setHighlightedElement={setHighlightedElement}
snapshotUrls={snapshotUrls} />;
};

View File

@ -46,7 +46,6 @@ export default defineConfig({
input: {
index: path.resolve(__dirname, 'index.html'),
uiMode: path.resolve(__dirname, 'uiMode.html'),
recorder: path.resolve(__dirname, 'recorder.html'),
snapshot: path.resolve(__dirname, 'snapshot.html'),
},
output: {

View File

@ -21,7 +21,6 @@ import * as playwrightLibrary from 'playwright-core';
export type TestModeWorkerOptions = {
mode: TestModeName;
codegenMode: 'trace-events' | 'actions';
};
export type TestModeTestFixtures = {
@ -49,7 +48,6 @@ export const testModeTest = test.extend<TestModeTestFixtures, TestModeWorkerOpti
await run(playwright);
await testMode.teardown();
}, { scope: 'worker' }],
codegenMode: ['actions', { scope: 'worker', option: true }],
toImplInWorkerScope: [async ({ playwright }, use) => {
await use((playwright as any)._toImpl);

View File

@ -19,7 +19,6 @@ import type { ConsoleMessage } from 'playwright';
test.describe('cli codegen', () => {
test.skip(({ mode }) => mode !== 'default');
test.skip(({ trace, codegenMode }) => trace === 'on' && codegenMode === 'trace-events');
test('should click', async ({ openRecorder }) => {
const { page, recorder } = await openRecorder();
@ -413,7 +412,7 @@ await page.GetByRole(AriaRole.Textbox).PressAsync("Shift+Enter");`);
expect(messages[0].text()).toBe('press');
});
test('should update selected element after pressing Tab', async ({ openRecorder, browserName, codegenMode }) => {
test('should update selected element after pressing Tab', async ({ openRecorder }) => {
const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`

View File

@ -20,7 +20,6 @@ import fs from 'fs';
test.describe('cli codegen', () => {
test.skip(({ mode }) => mode !== 'default');
test.skip(({ trace, codegenMode }) => trace === 'on' && codegenMode === 'trace-events');
test('should contain open page', async ({ openRecorder }) => {
const { recorder } = await openRecorder();
@ -310,8 +309,7 @@ await page.GetByRole(AriaRole.Button, new() { Name = "click me" }).ClickAsync();
}
});
test('should record open in a new tab with url', async ({ openRecorder, browserName, codegenMode }) => {
test.skip(codegenMode === 'trace-events');
test('should record open in a new tab with url', async ({ openRecorder, browserName }) => {
const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`<a href="about:blank?foo">link</a>`);
@ -453,8 +451,7 @@ await page1.GotoAsync("about:blank?foo");`);
await recorder.waitForOutput('JavaScript', `await page.goto('${server.PREFIX}/page2.html');`);
});
test('should --save-trace', async ({ runCLI, codegenMode }, testInfo) => {
test.skip(codegenMode === 'trace-events');
test('should --save-trace', async ({ runCLI }, testInfo) => {
const traceFileName = testInfo.outputPath('trace.zip');
const cli = runCLI([`--save-trace=${traceFileName}`], {
autoExitWhen: ' ',
@ -463,8 +460,7 @@ await page1.GotoAsync("about:blank?foo");`);
expect(fs.existsSync(traceFileName)).toBeTruthy();
});
test('should save assets via SIGINT', async ({ runCLI, platform, codegenMode }, testInfo) => {
test.skip(codegenMode === 'trace-events');
test('should save assets via SIGINT', async ({ runCLI, platform }, testInfo) => {
test.skip(platform === 'win32', 'SIGINT not supported on Windows');
const traceFileName = testInfo.outputPath('trace.zip');

View File

@ -21,7 +21,6 @@ import type { Page } from '@playwright/test';
test.describe('cli codegen', () => {
test.skip(({ mode }) => mode !== 'default');
test.skip(({ trace, codegenMode }) => trace === 'on' && codegenMode === 'trace-events');
test('should click locator.first', async ({ openRecorder }) => {
const { page, recorder } = await openRecorder();

View File

@ -19,7 +19,6 @@ import { roundBox } from '../../page/pageTest';
test.describe(() => {
test.skip(({ mode }) => mode !== 'default');
test.skip(({ trace, codegenMode }) => trace === 'on' && codegenMode === 'trace-events');
test('should generate aria snapshot', async ({ openRecorder }) => {
const { recorder } = await openRecorder();

View File

@ -19,7 +19,6 @@ import { roundBox } from '../../page/pageTest';
test.describe(() => {
test.skip(({ mode }) => mode !== 'default');
test.skip(({ trace, codegenMode }) => trace === 'on' && codegenMode === 'trace-events');
test('should inspect locator', async ({ openRecorder }) => {
const { recorder } = await openRecorder();

View File

@ -67,7 +67,7 @@ export const test = contextTest.extend<CLITestArgs>({
});
},
runCLI: async ({ childProcess, browserName, channel, headless, mode, launchOptions, codegenMode }, run, testInfo) => {
runCLI: async ({ childProcess, browserName, channel, headless, mode, launchOptions }, run, testInfo) => {
testInfo.skip(mode.startsWith('service'));
await run((cliArgs, { autoExitWhen } = {}) => {
@ -78,17 +78,15 @@ export const test = contextTest.extend<CLITestArgs>({
args: cliArgs,
executablePath: launchOptions.executablePath,
autoExitWhen,
codegenMode
});
});
},
openRecorder: async ({ context, recorderPageGetter, codegenMode }, run) => {
openRecorder: async ({ context, recorderPageGetter }, run) => {
await run(async (options?: { testIdAttributeName?: string }) => {
await (context as any)._enableRecorder({
language: 'javascript',
mode: 'recording',
codegenMode,
...options
});
const page = await context.newPage();
@ -235,7 +233,7 @@ export class Recorder {
class CLIMock {
process: TestChildProcess;
constructor(childProcess: CommonFixtures['childProcess'], options: { browserName: string, channel: string | undefined, headless: boolean | undefined, args: string[], executablePath: string | undefined, autoExitWhen: string | undefined, codegenMode?: 'trace-events' | 'actions'}) {
constructor(childProcess: CommonFixtures['childProcess'], options: { browserName: string, channel: string | undefined, headless: boolean | undefined, args: string[], executablePath: string | undefined, autoExitWhen: string | undefined}) {
const nodeArgs = [
'node',
path.join(__dirname, '..', '..', '..', 'packages', 'playwright-core', 'cli.js'),
@ -248,7 +246,6 @@ class CLIMock {
this.process = childProcess({
command: nodeArgs,
env: {
PW_RECORDER_IS_TRACE_VIEWER: options.codegenMode === 'trace-events' ? '1' : undefined,
PWTEST_CLI_AUTO_EXIT_WHEN: options.autoExitWhen,
PWTEST_CLI_IS_UNDER_TEST: '1',
PWTEST_CLI_HEADLESS: options.headless ? '1' : undefined,

View File

@ -147,18 +147,6 @@ for (const browserName of browserNames) {
testDir: path.join(testDir, 'page'),
...projectTemplate,
});
// TODO: figure out reporting to flakiness dashboard (Problem: they get merged, we want to keep them separate)
// config.projects.push({
// name: `${browserName}-codegen-mode-trace`,
// testDir: path.join(testDir, 'library'),
// testMatch: '**/cli-codegen-*.spec.ts',
// ...projectTemplate,
// use: {
// ...projectTemplate.use,
// codegenMode: 'trace-events',
// }
// });
}
export default config;