mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: delete recorder in traceviewer experiment (#34347)
This commit is contained in:
parent
8d39c44b69
commit
00bb17751b
@ -595,7 +595,6 @@ async function codegen(options: Options & { target: string, output?: string, tes
|
|||||||
device: options.device,
|
device: options.device,
|
||||||
saveStorage: options.saveStorage,
|
saveStorage: options.saveStorage,
|
||||||
mode: 'recording',
|
mode: 'recording',
|
||||||
codegenMode: process.env.PW_RECORDER_IS_TRACE_VIEWER ? 'trace-events' : 'actions',
|
|
||||||
testIdAttributeName,
|
testIdAttributeName,
|
||||||
outputFile: outputFile ? path.resolve(outputFile) : undefined,
|
outputFile: outputFile ? path.resolve(outputFile) : undefined,
|
||||||
handleSIGINT: false,
|
handleSIGINT: false,
|
||||||
|
@ -970,7 +970,6 @@ scheme.BrowserContextPauseResult = tOptional(tObject({}));
|
|||||||
scheme.BrowserContextEnableRecorderParams = tObject({
|
scheme.BrowserContextEnableRecorderParams = tObject({
|
||||||
language: tOptional(tString),
|
language: tOptional(tString),
|
||||||
mode: tOptional(tEnum(['inspecting', 'recording'])),
|
mode: tOptional(tEnum(['inspecting', 'recording'])),
|
||||||
codegenMode: tOptional(tEnum(['actions', 'trace-events'])),
|
|
||||||
pauseOnNextStatement: tOptional(tBoolean),
|
pauseOnNextStatement: tOptional(tBoolean),
|
||||||
testIdAttributeName: tOptional(tString),
|
testIdAttributeName: tOptional(tString),
|
||||||
launchOptions: tOptional(tAny),
|
launchOptions: tOptional(tAny),
|
||||||
|
@ -130,7 +130,7 @@ export abstract class BrowserContext extends SdkObject {
|
|||||||
|
|
||||||
// When PWDEBUG=1, show inspector for each context.
|
// When PWDEBUG=1, show inspector for each context.
|
||||||
if (debugMode() === 'inspector')
|
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.
|
// When paused, show inspector.
|
||||||
if (this._debugger.isPaused())
|
if (this._debugger.isPaused())
|
||||||
|
@ -39,7 +39,6 @@ import type { Dialog } from '../dialog';
|
|||||||
import type { ConsoleMessage } from '../console';
|
import type { ConsoleMessage } from '../console';
|
||||||
import { serializeError } from '../errors';
|
import { serializeError } from '../errors';
|
||||||
import { ElementHandleDispatcher } from './elementHandlerDispatcher';
|
import { ElementHandleDispatcher } from './elementHandlerDispatcher';
|
||||||
import { RecorderInTraceViewer } from '../recorder/recorderInTraceViewer';
|
|
||||||
import { RecorderApp } from '../recorder/recorderApp';
|
import { RecorderApp } from '../recorder/recorderApp';
|
||||||
import { WebSocketRouteDispatcher } from './webSocketRouteDispatcher';
|
import { WebSocketRouteDispatcher } from './webSocketRouteDispatcher';
|
||||||
|
|
||||||
@ -301,17 +300,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
|
|||||||
}
|
}
|
||||||
|
|
||||||
async enableRecorder(params: channels.BrowserContextEnableRecorderParams): Promise<void> {
|
async enableRecorder(params: channels.BrowserContextEnableRecorderParams): Promise<void> {
|
||||||
if (params.codegenMode === 'trace-events') {
|
await Recorder.show(this._context, RecorderApp.factory(this._context), params);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async pause(params: channels.BrowserContextPauseParams, metadata: CallMetadata) {
|
async pause(params: channels.BrowserContextPauseParams, metadata: CallMetadata) {
|
||||||
|
@ -54,33 +54,33 @@ export class Recorder implements InstrumentationListener, IRecorder {
|
|||||||
static async showInspector(context: BrowserContext, params: channels.BrowserContextEnableRecorderParams, recorderAppFactory: IRecorderAppFactory) {
|
static async showInspector(context: BrowserContext, params: channels.BrowserContextEnableRecorderParams, recorderAppFactory: IRecorderAppFactory) {
|
||||||
if (isUnderTest())
|
if (isUnderTest())
|
||||||
params.language = process.env.TEST_INSPECTOR_LANGUAGE;
|
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) {
|
static showInspectorNoReply(context: BrowserContext, recorderAppFactory: IRecorderAppFactory) {
|
||||||
Recorder.showInspector(context, {}, recorderAppFactory).catch(() => {});
|
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>;
|
let recorderPromise = (context as any)[recorderSymbol] as Promise<Recorder>;
|
||||||
if (!recorderPromise) {
|
if (!recorderPromise) {
|
||||||
recorderPromise = Recorder._create(codegenMode, context, recorderAppFactory, params);
|
recorderPromise = Recorder._create(context, recorderAppFactory, params);
|
||||||
(context as any)[recorderSymbol] = recorderPromise;
|
(context as any)[recorderSymbol] = recorderPromise;
|
||||||
}
|
}
|
||||||
return recorderPromise;
|
return recorderPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async _create(codegenMode: 'actions' | 'trace-events', context: BrowserContext, recorderAppFactory: IRecorderAppFactory, params: channels.BrowserContextEnableRecorderParams = {}): Promise<Recorder> {
|
private static async _create(context: BrowserContext, recorderAppFactory: IRecorderAppFactory, params: channels.BrowserContextEnableRecorderParams = {}): Promise<Recorder> {
|
||||||
const recorder = new Recorder(codegenMode, context, params);
|
const recorder = new Recorder(context, params);
|
||||||
const recorderApp = await recorderAppFactory(recorder);
|
const recorderApp = await recorderAppFactory(recorder);
|
||||||
await recorder._install(recorderApp);
|
await recorder._install(recorderApp);
|
||||||
return recorder;
|
return recorder;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(codegenMode: 'actions' | 'trace-events', context: BrowserContext, params: channels.BrowserContextEnableRecorderParams) {
|
constructor(context: BrowserContext, params: channels.BrowserContextEnableRecorderParams) {
|
||||||
this._mode = params.mode || 'none';
|
this._mode = params.mode || 'none';
|
||||||
this.handleSIGINT = params.handleSIGINT;
|
this.handleSIGINT = params.handleSIGINT;
|
||||||
this._contextRecorder = new ContextRecorder(codegenMode, context, params, {});
|
this._contextRecorder = new ContextRecorder(context, params, {});
|
||||||
this._context = context;
|
this._context = context;
|
||||||
this._omitCallTracking = !!params.omitCallTracking;
|
this._omitCallTracking = !!params.omitCallTracking;
|
||||||
this._debugger = context.debugger();
|
this._debugger = context.debugger();
|
||||||
|
@ -10,6 +10,3 @@
|
|||||||
../../utils/**
|
../../utils/**
|
||||||
../../utilsBundle.ts
|
../../utilsBundle.ts
|
||||||
../../zipBundle.ts
|
../../zipBundle.ts
|
||||||
|
|
||||||
[recorderInTraceViewer.ts]
|
|
||||||
../trace/viewer/traceViewer.ts
|
|
||||||
|
@ -54,11 +54,9 @@ export class ContextRecorder extends EventEmitter {
|
|||||||
private _throttledOutputFile: ThrottledFile | null = null;
|
private _throttledOutputFile: ThrottledFile | null = null;
|
||||||
private _orderedLanguages: LanguageGenerator[] = [];
|
private _orderedLanguages: LanguageGenerator[] = [];
|
||||||
private _listeners: RegisteredListener[] = [];
|
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();
|
super();
|
||||||
this._codegenMode = codegenMode;
|
|
||||||
this._context = context;
|
this._context = context;
|
||||||
this._params = params;
|
this._params = params;
|
||||||
this._delegate = delegate;
|
this._delegate = delegate;
|
||||||
@ -150,12 +148,6 @@ export class ContextRecorder extends EventEmitter {
|
|||||||
|
|
||||||
setEnabled(enabled: boolean) {
|
setEnabled(enabled: boolean) {
|
||||||
this._collection.setEnabled(enabled);
|
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() {
|
dispose() {
|
||||||
|
@ -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;
|
|
||||||
}
|
|
2
packages/protocol/src/channels.d.ts
vendored
2
packages/protocol/src/channels.d.ts
vendored
@ -1772,7 +1772,6 @@ export type BrowserContextPauseResult = void;
|
|||||||
export type BrowserContextEnableRecorderParams = {
|
export type BrowserContextEnableRecorderParams = {
|
||||||
language?: string,
|
language?: string,
|
||||||
mode?: 'inspecting' | 'recording',
|
mode?: 'inspecting' | 'recording',
|
||||||
codegenMode?: 'actions' | 'trace-events',
|
|
||||||
pauseOnNextStatement?: boolean,
|
pauseOnNextStatement?: boolean,
|
||||||
testIdAttributeName?: string,
|
testIdAttributeName?: string,
|
||||||
launchOptions?: any,
|
launchOptions?: any,
|
||||||
@ -1786,7 +1785,6 @@ export type BrowserContextEnableRecorderParams = {
|
|||||||
export type BrowserContextEnableRecorderOptions = {
|
export type BrowserContextEnableRecorderOptions = {
|
||||||
language?: string,
|
language?: string,
|
||||||
mode?: 'inspecting' | 'recording',
|
mode?: 'inspecting' | 'recording',
|
||||||
codegenMode?: 'actions' | 'trace-events',
|
|
||||||
pauseOnNextStatement?: boolean,
|
pauseOnNextStatement?: boolean,
|
||||||
testIdAttributeName?: string,
|
testIdAttributeName?: string,
|
||||||
launchOptions?: any,
|
launchOptions?: any,
|
||||||
|
@ -1198,11 +1198,6 @@ BrowserContext:
|
|||||||
literals:
|
literals:
|
||||||
- inspecting
|
- inspecting
|
||||||
- recording
|
- recording
|
||||||
codegenMode:
|
|
||||||
type: enum?
|
|
||||||
literals:
|
|
||||||
- actions
|
|
||||||
- trace-events
|
|
||||||
pauseOnNextStatement: boolean?
|
pauseOnNextStatement: boolean?
|
||||||
testIdAttributeName: string?
|
testIdAttributeName: string?
|
||||||
launchOptions: json?
|
launchOptions: json?
|
||||||
|
@ -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>
|
|
@ -6,7 +6,3 @@ ui/
|
|||||||
|
|
||||||
[sw-main.ts]
|
[sw-main.ts]
|
||||||
sw/**
|
sw/**
|
||||||
|
|
||||||
|
|
||||||
[recorder.tsx]
|
|
||||||
ui/recorder/**
|
|
||||||
|
@ -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 />);
|
|
||||||
})();
|
|
@ -1,5 +0,0 @@
|
|||||||
[*]
|
|
||||||
@isomorphic/**
|
|
||||||
@trace/**
|
|
||||||
@web/**
|
|
||||||
../**
|
|
@ -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>
|
|
||||||
</>;
|
|
||||||
};
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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('|')) };
|
|
||||||
}
|
|
@ -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.
|
|
||||||
*/
|
|
@ -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} />;
|
|
||||||
};
|
|
@ -46,7 +46,6 @@ export default defineConfig({
|
|||||||
input: {
|
input: {
|
||||||
index: path.resolve(__dirname, 'index.html'),
|
index: path.resolve(__dirname, 'index.html'),
|
||||||
uiMode: path.resolve(__dirname, 'uiMode.html'),
|
uiMode: path.resolve(__dirname, 'uiMode.html'),
|
||||||
recorder: path.resolve(__dirname, 'recorder.html'),
|
|
||||||
snapshot: path.resolve(__dirname, 'snapshot.html'),
|
snapshot: path.resolve(__dirname, 'snapshot.html'),
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
|
@ -21,7 +21,6 @@ import * as playwrightLibrary from 'playwright-core';
|
|||||||
|
|
||||||
export type TestModeWorkerOptions = {
|
export type TestModeWorkerOptions = {
|
||||||
mode: TestModeName;
|
mode: TestModeName;
|
||||||
codegenMode: 'trace-events' | 'actions';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TestModeTestFixtures = {
|
export type TestModeTestFixtures = {
|
||||||
@ -49,7 +48,6 @@ export const testModeTest = test.extend<TestModeTestFixtures, TestModeWorkerOpti
|
|||||||
await run(playwright);
|
await run(playwright);
|
||||||
await testMode.teardown();
|
await testMode.teardown();
|
||||||
}, { scope: 'worker' }],
|
}, { scope: 'worker' }],
|
||||||
codegenMode: ['actions', { scope: 'worker', option: true }],
|
|
||||||
|
|
||||||
toImplInWorkerScope: [async ({ playwright }, use) => {
|
toImplInWorkerScope: [async ({ playwright }, use) => {
|
||||||
await use((playwright as any)._toImpl);
|
await use((playwright as any)._toImpl);
|
||||||
|
@ -19,7 +19,6 @@ import type { ConsoleMessage } from 'playwright';
|
|||||||
|
|
||||||
test.describe('cli codegen', () => {
|
test.describe('cli codegen', () => {
|
||||||
test.skip(({ mode }) => mode !== 'default');
|
test.skip(({ mode }) => mode !== 'default');
|
||||||
test.skip(({ trace, codegenMode }) => trace === 'on' && codegenMode === 'trace-events');
|
|
||||||
|
|
||||||
test('should click', async ({ openRecorder }) => {
|
test('should click', async ({ openRecorder }) => {
|
||||||
const { page, recorder } = await openRecorder();
|
const { page, recorder } = await openRecorder();
|
||||||
@ -413,7 +412,7 @@ await page.GetByRole(AriaRole.Textbox).PressAsync("Shift+Enter");`);
|
|||||||
expect(messages[0].text()).toBe('press');
|
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();
|
const { page, recorder } = await openRecorder();
|
||||||
|
|
||||||
await recorder.setContentAndWait(`
|
await recorder.setContentAndWait(`
|
||||||
|
@ -20,7 +20,6 @@ import fs from 'fs';
|
|||||||
|
|
||||||
test.describe('cli codegen', () => {
|
test.describe('cli codegen', () => {
|
||||||
test.skip(({ mode }) => mode !== 'default');
|
test.skip(({ mode }) => mode !== 'default');
|
||||||
test.skip(({ trace, codegenMode }) => trace === 'on' && codegenMode === 'trace-events');
|
|
||||||
|
|
||||||
test('should contain open page', async ({ openRecorder }) => {
|
test('should contain open page', async ({ openRecorder }) => {
|
||||||
const { recorder } = await 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('should record open in a new tab with url', async ({ openRecorder, browserName }) => {
|
||||||
test.skip(codegenMode === 'trace-events');
|
|
||||||
const { page, recorder } = await openRecorder();
|
const { page, recorder } = await openRecorder();
|
||||||
await recorder.setContentAndWait(`<a href="about:blank?foo">link</a>`);
|
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');`);
|
await recorder.waitForOutput('JavaScript', `await page.goto('${server.PREFIX}/page2.html');`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should --save-trace', async ({ runCLI, codegenMode }, testInfo) => {
|
test('should --save-trace', async ({ runCLI }, testInfo) => {
|
||||||
test.skip(codegenMode === 'trace-events');
|
|
||||||
const traceFileName = testInfo.outputPath('trace.zip');
|
const traceFileName = testInfo.outputPath('trace.zip');
|
||||||
const cli = runCLI([`--save-trace=${traceFileName}`], {
|
const cli = runCLI([`--save-trace=${traceFileName}`], {
|
||||||
autoExitWhen: ' ',
|
autoExitWhen: ' ',
|
||||||
@ -463,8 +460,7 @@ await page1.GotoAsync("about:blank?foo");`);
|
|||||||
expect(fs.existsSync(traceFileName)).toBeTruthy();
|
expect(fs.existsSync(traceFileName)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should save assets via SIGINT', async ({ runCLI, platform, codegenMode }, testInfo) => {
|
test('should save assets via SIGINT', async ({ runCLI, platform }, testInfo) => {
|
||||||
test.skip(codegenMode === 'trace-events');
|
|
||||||
test.skip(platform === 'win32', 'SIGINT not supported on Windows');
|
test.skip(platform === 'win32', 'SIGINT not supported on Windows');
|
||||||
|
|
||||||
const traceFileName = testInfo.outputPath('trace.zip');
|
const traceFileName = testInfo.outputPath('trace.zip');
|
||||||
|
@ -21,7 +21,6 @@ import type { Page } from '@playwright/test';
|
|||||||
|
|
||||||
test.describe('cli codegen', () => {
|
test.describe('cli codegen', () => {
|
||||||
test.skip(({ mode }) => mode !== 'default');
|
test.skip(({ mode }) => mode !== 'default');
|
||||||
test.skip(({ trace, codegenMode }) => trace === 'on' && codegenMode === 'trace-events');
|
|
||||||
|
|
||||||
test('should click locator.first', async ({ openRecorder }) => {
|
test('should click locator.first', async ({ openRecorder }) => {
|
||||||
const { page, recorder } = await openRecorder();
|
const { page, recorder } = await openRecorder();
|
||||||
|
@ -19,7 +19,6 @@ import { roundBox } from '../../page/pageTest';
|
|||||||
|
|
||||||
test.describe(() => {
|
test.describe(() => {
|
||||||
test.skip(({ mode }) => mode !== 'default');
|
test.skip(({ mode }) => mode !== 'default');
|
||||||
test.skip(({ trace, codegenMode }) => trace === 'on' && codegenMode === 'trace-events');
|
|
||||||
|
|
||||||
test('should generate aria snapshot', async ({ openRecorder }) => {
|
test('should generate aria snapshot', async ({ openRecorder }) => {
|
||||||
const { recorder } = await openRecorder();
|
const { recorder } = await openRecorder();
|
||||||
|
@ -19,7 +19,6 @@ import { roundBox } from '../../page/pageTest';
|
|||||||
|
|
||||||
test.describe(() => {
|
test.describe(() => {
|
||||||
test.skip(({ mode }) => mode !== 'default');
|
test.skip(({ mode }) => mode !== 'default');
|
||||||
test.skip(({ trace, codegenMode }) => trace === 'on' && codegenMode === 'trace-events');
|
|
||||||
|
|
||||||
test('should inspect locator', async ({ openRecorder }) => {
|
test('should inspect locator', async ({ openRecorder }) => {
|
||||||
const { recorder } = await openRecorder();
|
const { recorder } = await openRecorder();
|
||||||
|
@ -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'));
|
testInfo.skip(mode.startsWith('service'));
|
||||||
|
|
||||||
await run((cliArgs, { autoExitWhen } = {}) => {
|
await run((cliArgs, { autoExitWhen } = {}) => {
|
||||||
@ -78,17 +78,15 @@ export const test = contextTest.extend<CLITestArgs>({
|
|||||||
args: cliArgs,
|
args: cliArgs,
|
||||||
executablePath: launchOptions.executablePath,
|
executablePath: launchOptions.executablePath,
|
||||||
autoExitWhen,
|
autoExitWhen,
|
||||||
codegenMode
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
openRecorder: async ({ context, recorderPageGetter, codegenMode }, run) => {
|
openRecorder: async ({ context, recorderPageGetter }, run) => {
|
||||||
await run(async (options?: { testIdAttributeName?: string }) => {
|
await run(async (options?: { testIdAttributeName?: string }) => {
|
||||||
await (context as any)._enableRecorder({
|
await (context as any)._enableRecorder({
|
||||||
language: 'javascript',
|
language: 'javascript',
|
||||||
mode: 'recording',
|
mode: 'recording',
|
||||||
codegenMode,
|
|
||||||
...options
|
...options
|
||||||
});
|
});
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
@ -235,7 +233,7 @@ export class Recorder {
|
|||||||
class CLIMock {
|
class CLIMock {
|
||||||
process: TestChildProcess;
|
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 = [
|
const nodeArgs = [
|
||||||
'node',
|
'node',
|
||||||
path.join(__dirname, '..', '..', '..', 'packages', 'playwright-core', 'cli.js'),
|
path.join(__dirname, '..', '..', '..', 'packages', 'playwright-core', 'cli.js'),
|
||||||
@ -248,7 +246,6 @@ class CLIMock {
|
|||||||
this.process = childProcess({
|
this.process = childProcess({
|
||||||
command: nodeArgs,
|
command: nodeArgs,
|
||||||
env: {
|
env: {
|
||||||
PW_RECORDER_IS_TRACE_VIEWER: options.codegenMode === 'trace-events' ? '1' : undefined,
|
|
||||||
PWTEST_CLI_AUTO_EXIT_WHEN: options.autoExitWhen,
|
PWTEST_CLI_AUTO_EXIT_WHEN: options.autoExitWhen,
|
||||||
PWTEST_CLI_IS_UNDER_TEST: '1',
|
PWTEST_CLI_IS_UNDER_TEST: '1',
|
||||||
PWTEST_CLI_HEADLESS: options.headless ? '1' : undefined,
|
PWTEST_CLI_HEADLESS: options.headless ? '1' : undefined,
|
||||||
|
@ -147,18 +147,6 @@ for (const browserName of browserNames) {
|
|||||||
testDir: path.join(testDir, 'page'),
|
testDir: path.join(testDir, 'page'),
|
||||||
...projectTemplate,
|
...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;
|
export default config;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user