mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat(ui): extract recorder sidebar into a window (#5223)
This commit is contained in:
parent
82bb92f103
commit
bf8c30a88b
@ -27,7 +27,7 @@ export const parameters = {
|
|||||||
|
|
||||||
addDecorator(storyFn => {
|
addDecorator(storyFn => {
|
||||||
applyTheme();
|
applyTheme();
|
||||||
return <div style={{backgroundColor: 'var(--background)', display: 'flex'}}>
|
return <div style={{backgroundColor: 'var(--background)', display: 'flex', flex: 'auto'}}>
|
||||||
{storyFn()}
|
{storyFn()}
|
||||||
</div>
|
</div>
|
||||||
});
|
});
|
||||||
|
@ -31,7 +31,8 @@ export interface BrowserProcess {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type PlaywrightOptions = {
|
export type PlaywrightOptions = {
|
||||||
contextListeners: ContextListener[]
|
contextListeners: ContextListener[],
|
||||||
|
isInternal: boolean
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BrowserOptions = PlaywrightOptions & {
|
export type BrowserOptions = PlaywrightOptions & {
|
||||||
|
@ -244,8 +244,6 @@ export abstract class BrowserContext extends EventEmitter {
|
|||||||
|
|
||||||
async _loadDefaultContext(progress: Progress) {
|
async _loadDefaultContext(progress: Progress) {
|
||||||
const pages = await this._loadDefaultContextAsIs(progress);
|
const pages = await this._loadDefaultContextAsIs(progress);
|
||||||
if (pages.length !== 1 || pages[0].mainFrame().url() !== 'about:blank')
|
|
||||||
throw new Error(`Arguments can not specify page to be opened (first url is ${pages[0].mainFrame().url()})`);
|
|
||||||
if (this._options.isMobile || this._options.locale) {
|
if (this._options.isMobile || this._options.locale) {
|
||||||
// Workaround for:
|
// Workaround for:
|
||||||
// - chromium fails to change isMobile for existing page;
|
// - chromium fails to change isMobile for existing page;
|
||||||
|
@ -122,7 +122,7 @@ export class Chromium extends BrowserType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_ARGS = [
|
export const DEFAULT_ARGS = [
|
||||||
'--disable-background-networking',
|
'--disable-background-networking',
|
||||||
'--enable-features=NetworkService,NetworkServiceInProcess',
|
'--enable-features=NetworkService,NetworkServiceInProcess',
|
||||||
'--disable-background-timer-throttling',
|
'--disable-background-timer-throttling',
|
||||||
|
@ -41,6 +41,7 @@ import { VideoRecorder } from './videoRecorder';
|
|||||||
|
|
||||||
|
|
||||||
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
|
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
|
||||||
|
export type WindowBounds = { top?: number, left?: number, width?: number, height?: number };
|
||||||
|
|
||||||
export class CRPage implements PageDelegate {
|
export class CRPage implements PageDelegate {
|
||||||
readonly _mainFrameSession: FrameSession;
|
readonly _mainFrameSession: FrameSession;
|
||||||
@ -64,6 +65,11 @@ export class CRPage implements PageDelegate {
|
|||||||
// of new popup targets.
|
// of new popup targets.
|
||||||
readonly _nextWindowOpenPopupFeatures: string[][] = [];
|
readonly _nextWindowOpenPopupFeatures: string[][] = [];
|
||||||
|
|
||||||
|
static mainFrameSession(page: Page): FrameSession {
|
||||||
|
const crPage = page._delegate as CRPage;
|
||||||
|
return crPage._mainFrameSession;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(client: CRSession, targetId: string, browserContext: CRBrowserContext, opener: CRPage | null, hasUIWindow: boolean) {
|
constructor(client: CRSession, targetId: string, browserContext: CRBrowserContext, opener: CRPage | null, hasUIWindow: boolean) {
|
||||||
this._targetId = targetId;
|
this._targetId = targetId;
|
||||||
this._opener = opener;
|
this._opener = opener;
|
||||||
@ -380,8 +386,8 @@ class FrameSession {
|
|||||||
|
|
||||||
async _initialize(hasUIWindow: boolean) {
|
async _initialize(hasUIWindow: boolean) {
|
||||||
if (hasUIWindow &&
|
if (hasUIWindow &&
|
||||||
!this._crPage._browserContext._browser.isClank() &&
|
!this._crPage._browserContext._browser.isClank() &&
|
||||||
!this._crPage._browserContext._options.noDefaultViewport) {
|
!this._crPage._browserContext._options.noDefaultViewport) {
|
||||||
const { windowId } = await this._client.send('Browser.getWindowForTarget');
|
const { windowId } = await this._client.send('Browser.getWindowForTarget');
|
||||||
this._windowId = windowId;
|
this._windowId = windowId;
|
||||||
}
|
}
|
||||||
@ -855,14 +861,28 @@ class FrameSession {
|
|||||||
else if (process.platform === 'darwin')
|
else if (process.platform === 'darwin')
|
||||||
insets = { width: 2, height: 80 };
|
insets = { width: 2, height: 80 };
|
||||||
}
|
}
|
||||||
promises.push(this._client.send('Browser.setWindowBounds', {
|
promises.push(this.setWindowBounds({
|
||||||
windowId: this._windowId,
|
width: viewportSize.width + insets.width,
|
||||||
bounds: { width: viewportSize.width + insets.width, height: viewportSize.height + insets.height }
|
height: viewportSize.height + insets.height
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async windowBounds(): Promise<WindowBounds> {
|
||||||
|
const { bounds } = await this._client.send('Browser.getWindowBounds', {
|
||||||
|
windowId: this._windowId!
|
||||||
|
});
|
||||||
|
return bounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setWindowBounds(bounds: WindowBounds) {
|
||||||
|
return await this._client.send('Browser.setWindowBounds', {
|
||||||
|
windowId: this._windowId!,
|
||||||
|
bounds
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async _updateEmulateMedia(initial: boolean): Promise<void> {
|
async _updateEmulateMedia(initial: boolean): Promise<void> {
|
||||||
if (this._crPage._browserContext._browser.isClank())
|
if (this._crPage._browserContext._browser.isClank())
|
||||||
return;
|
return;
|
||||||
|
@ -19,6 +19,7 @@ import { Tracer } from '../trace/tracer';
|
|||||||
import * as browserPaths from '../utils/browserPaths';
|
import * as browserPaths from '../utils/browserPaths';
|
||||||
import { Android } from './android/android';
|
import { Android } from './android/android';
|
||||||
import { AdbBackend } from './android/backendAdb';
|
import { AdbBackend } from './android/backendAdb';
|
||||||
|
import { PlaywrightOptions } from './browser';
|
||||||
import { Chromium } from './chromium/chromium';
|
import { Chromium } from './chromium/chromium';
|
||||||
import { Electron } from './electron/electron';
|
import { Electron } from './electron/electron';
|
||||||
import { Firefox } from './firefox/firefox';
|
import { Firefox } from './firefox/firefox';
|
||||||
@ -34,15 +35,17 @@ export class Playwright {
|
|||||||
readonly electron: Electron;
|
readonly electron: Electron;
|
||||||
readonly firefox: Firefox;
|
readonly firefox: Firefox;
|
||||||
readonly webkit: WebKit;
|
readonly webkit: WebKit;
|
||||||
readonly options = {
|
readonly options: PlaywrightOptions;
|
||||||
contextListeners: [
|
|
||||||
new InspectorController(),
|
|
||||||
new Tracer(),
|
|
||||||
new HarTracer()
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(packagePath: string, browsers: browserPaths.BrowserDescriptor[]) {
|
constructor(isInternal: boolean, packagePath: string, browsers: browserPaths.BrowserDescriptor[]) {
|
||||||
|
this.options = {
|
||||||
|
isInternal,
|
||||||
|
contextListeners: isInternal ? [] : [
|
||||||
|
new InspectorController(),
|
||||||
|
new Tracer(),
|
||||||
|
new HarTracer()
|
||||||
|
]
|
||||||
|
};
|
||||||
const chromium = browsers.find(browser => browser.name === 'chromium');
|
const chromium = browsers.find(browser => browser.name === 'chromium');
|
||||||
this.chromium = new Chromium(packagePath, chromium!, this.options);
|
this.chromium = new Chromium(packagePath, chromium!, this.options);
|
||||||
|
|
||||||
@ -57,6 +60,6 @@ export class Playwright {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createPlaywright() {
|
export function createPlaywright(isInternal = false) {
|
||||||
return new Playwright(path.join(__dirname, '..', '..'), require('../../browsers.json')['browsers']);
|
return new Playwright(isInternal, path.join(__dirname, '..', '..'), require('../../browsers.json')['browsers']);
|
||||||
}
|
}
|
||||||
|
@ -28,13 +28,11 @@ declare global {
|
|||||||
playwrightRecorderState: () => Promise<State>;
|
playwrightRecorderState: () => Promise<State>;
|
||||||
playwrightRecorderSetUIState: (state: SetUIState) => Promise<void>;
|
playwrightRecorderSetUIState: (state: SetUIState) => Promise<void>;
|
||||||
playwrightRecorderResume: () => Promise<boolean>;
|
playwrightRecorderResume: () => Promise<boolean>;
|
||||||
playwrightRecorderClearScript: () => Promise<void>;
|
playwrightRecorderShowRecorderPage: () => Promise<void>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const scriptSymbol = Symbol('scriptSymbol');
|
const scriptSymbol = Symbol('scriptSymbol');
|
||||||
const pressRecordMessageElement = html`<span>Press <span id="pw-button-record">⬤</span> to start recording</span>`;
|
|
||||||
const performActionsMessageElement = html`<span>Perform actions to record</span>`;
|
|
||||||
|
|
||||||
export class Recorder {
|
export class Recorder {
|
||||||
private _injectedScript: InjectedScript;
|
private _injectedScript: InjectedScript;
|
||||||
@ -51,18 +49,12 @@ export class Recorder {
|
|||||||
private _expectProgrammaticKeyUp = false;
|
private _expectProgrammaticKeyUp = false;
|
||||||
private _pollRecorderModeTimer: NodeJS.Timeout | undefined;
|
private _pollRecorderModeTimer: NodeJS.Timeout | undefined;
|
||||||
private _outerToolbarElement: HTMLElement;
|
private _outerToolbarElement: HTMLElement;
|
||||||
private _outerDrawerElement: HTMLElement;
|
|
||||||
private _toolbar: Element$;
|
private _toolbar: Element$;
|
||||||
private _drawer: Element$;
|
|
||||||
private _drawerTimeout: NodeJS.Timeout | undefined;
|
|
||||||
private _state: State = {
|
private _state: State = {
|
||||||
codegenScript: '',
|
|
||||||
canResume: false,
|
canResume: false,
|
||||||
uiState: {
|
uiState: {
|
||||||
mode: 'none',
|
mode: 'none',
|
||||||
drawerVisible: false
|
|
||||||
},
|
},
|
||||||
isController: true,
|
|
||||||
isPaused: false
|
isPaused: false
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -122,10 +114,14 @@ export class Recorder {
|
|||||||
this._toolbar = html`
|
this._toolbar = html`
|
||||||
<x-pw-toolbar class="vertical">
|
<x-pw-toolbar class="vertical">
|
||||||
${commonStyles()}
|
${commonStyles()}
|
||||||
<x-pw-button-group class="vertical">
|
<x-pw-button-group>
|
||||||
<x-pw-icon>
|
<x-pw-button id="pw-button-playwright" tabIndex=0 title="Playwright">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="400" fill="none"><path d="M136 222c-12 3-21 10-26 16 5-5 12-9 22-12 10-2 18-2 25-1v-6c-6 0-13 0-21 3zm-27-46l-48 12 3 3 40-10s0 7-5 14c9-7 10-19 10-19zm40 112C82 306 46 228 35 188a227 227 0 01-7-45c-4 1-6 2-5 8 0 9 2 23 7 42 11 40 47 118 114 100 15-4 26-11 34-20-7 7-17 12-29 15zm13-160v5h26l-2-5h-24z" fill="#2D4552"/><path d="M194 168c12 3 18 11 21 19l14 3s-2-25-25-32c-22-6-36 12-37 14 6-4 15-8 27-4zm105 19c-21-6-35 12-36 14 6-4 15-8 27-5 12 4 18 12 21 19l14 4s-2-26-26-32zm-13 68l-110-31s1 6 6 14l93 26 11-9zm-76 66c-87-23-77-134-63-187 6-22 12-38 17-49-3 0-5 1-8 6-5 11-12 28-18 52-14 53-25 164 62 188 41 11 73-6 97-32a90 90 0 01-87 22z" fill="#2D4552"/><path d="M162 262v-22l-63 18s5-27 37-36c10-3 19-3 26-2v-92h31l-10-24c-4-9-9-3-19 6-8 6-27 19-55 27-29 8-52 6-61 4-14-2-21-5-20 5 0 9 2 23 7 42 11 40 47 118 114 100 18-4 30-14 39-26h-26zM61 188l48-12s-1 18-19 23-29-11-29-11z" fill="#E2574C"/><path d="M342 129c-13 2-43 5-79-5-37-10-62-27-71-35-14-12-20-20-26-8-5 11-12 29-19 53-14 53-24 164 63 187s134-78 148-131c6-24 9-42 10-54 1-14-9-10-26-7zm-176 44s14-22 38-15c23 7 25 32 25 32l-63-17zm57 96c-41-12-47-45-47-45l110 31s-22 26-63 14zm39-68s14-21 37-14c24 6 26 32 26 32l-63-18z" fill="#2EAD33"/><path d="M140 246l-41 12s5-26 35-36l-23-86-2 1c-29 8-52 6-61 4-14-2-21-5-20 5 0 9 2 23 7 42 11 40 47 118 114 100h2l-11-42zm-79-58l48-12s-1 18-19 23-29-11-29-11z" fill="#D65348"/><path d="M225 269h-2c-41-12-47-45-47-45l57 16 30-116c-37-10-62-27-71-35-14-12-20-20-26-8-5 11-12 29-19 53-14 53-24 164 63 187l2 1 13-53zm-59-96s14-22 38-15c23 7 25 32 25 32l-63-17z" fill="#1D8D22"/><path d="M142 245l-11 4c3 14 7 28 14 40l4-1 9-3c-8-12-13-25-16-40zm-4-102c-6 21-11 51-10 81l8-2 2-1a273 273 0 0114-103l-8 5-6 20z" fill="#C04B41"/></svg>
|
<x-pw-icon>
|
||||||
</x-pw-icon>
|
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="400" fill="none"><path d="M136 222c-12 3-21 10-26 16 5-5 12-9 22-12 10-2 18-2 25-1v-6c-6 0-13 0-21 3zm-27-46l-48 12 3 3 40-10s0 7-5 14c9-7 10-19 10-19zm40 112C82 306 46 228 35 188a227 227 0 01-7-45c-4 1-6 2-5 8 0 9 2 23 7 42 11 40 47 118 114 100 15-4 26-11 34-20-7 7-17 12-29 15zm13-160v5h26l-2-5h-24z" fill="#2D4552"/><path d="M194 168c12 3 18 11 21 19l14 3s-2-25-25-32c-22-6-36 12-37 14 6-4 15-8 27-4zm105 19c-21-6-35 12-36 14 6-4 15-8 27-5 12 4 18 12 21 19l14 4s-2-26-26-32zm-13 68l-110-31s1 6 6 14l93 26 11-9zm-76 66c-87-23-77-134-63-187 6-22 12-38 17-49-3 0-5 1-8 6-5 11-12 28-18 52-14 53-25 164 62 188 41 11 73-6 97-32a90 90 0 01-87 22z" fill="#2D4552"/><path d="M162 262v-22l-63 18s5-27 37-36c10-3 19-3 26-2v-92h31l-10-24c-4-9-9-3-19 6-8 6-27 19-55 27-29 8-52 6-61 4-14-2-21-5-20 5 0 9 2 23 7 42 11 40 47 118 114 100 18-4 30-14 39-26h-26zM61 188l48-12s-1 18-19 23-29-11-29-11z" fill="#E2574C"/><path d="M342 129c-13 2-43 5-79-5-37-10-62-27-71-35-14-12-20-20-26-8-5 11-12 29-19 53-14 53-24 164 63 187s134-78 148-131c6-24 9-42 10-54 1-14-9-10-26-7zm-176 44s14-22 38-15c23 7 25 32 25 32l-63-17zm57 96c-41-12-47-45-47-45l110 31s-22 26-63 14zm39-68s14-21 37-14c24 6 26 32 26 32l-63-18z" fill="#2EAD33"/><path d="M140 246l-41 12s5-26 35-36l-23-86-2 1c-29 8-52 6-61 4-14-2-21-5-20 5 0 9 2 23 7 42 11 40 47 118 114 100h2l-11-42zm-79-58l48-12s-1 18-19 23-29-11-29-11z" fill="#D65348"/><path d="M225 269h-2c-41-12-47-45-47-45l57 16 30-116c-37-10-62-27-71-35-14-12-20-20-26-8-5 11-12 29-19 53-14 53-24 164 63 187l2 1 13-53zm-59-96s14-22 38-15c23 7 25 32 25 32l-63-17z" fill="#1D8D22"/><path d="M142 245l-11 4c3 14 7 28 14 40l4-1 9-3c-8-12-13-25-16-40zm-4-102c-6 21-11 51-10 81l8-2 2-1a273 273 0 0114-103l-8 5-6 20z" fill="#C04B41"/></svg>
|
||||||
|
</x-pw-icon>
|
||||||
|
</x-pw-button>
|
||||||
|
</x-pw-button-group>
|
||||||
|
<x-pw-button-group>
|
||||||
<x-pw-button id="pw-button-inspect" tabIndex=0 title="Inspect selectors">
|
<x-pw-button id="pw-button-inspect" tabIndex=0 title="Inspect selectors">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 8c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm8.94 3c-.46-4.17-3.77-7.48-7.94-7.94V1h-2v2.06C6.83 3.52 3.52 6.83 3.06 11H1v2h2.06c.46 4.17 3.77 7.48 7.94 7.94V23h2v-2.06c4.17-.46 7.48-3.77 7.94-7.94H23v-2h-2.06zM12 19c-3.87 0-7-3.13-7-7s3.13-7 7-7 7 3.13 7 7-3.13 7-7 7z"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 8c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm8.94 3c-.46-4.17-3.77-7.48-7.94-7.94V1h-2v2.06C6.83 3.52 3.52 6.83 3.06 11H1v2h2.06c.46 4.17 3.77 7.48 7.94 7.94V23h2v-2.06c4.17-.46 7.48-3.77 7.94-7.94H23v-2h-2.06zM12 19c-3.87 0-7-3.13-7-7s3.13-7 7-7 7 3.13 7 7-3.13 7-7 7z"/></svg>
|
||||||
</x-pw-button>
|
</x-pw-button>
|
||||||
@ -140,77 +136,12 @@ export class Recorder {
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M8 5v14l11-7z"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M8 5v14l11-7z"/></svg>
|
||||||
</x-pw-button>
|
</x-pw-button>
|
||||||
</x-pw-button-group>
|
</x-pw-button-group>
|
||||||
<x-pw-button-group id="pw-button-drawer-group">
|
|
||||||
<x-pw-button id="pw-button-drawer" tabIndex=0 title="Display script">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M4 15h16v-2H4v2zm0 4h16v-2H4v2zm0-8h16V9H4v2zm0-6v2h16V5H4z"/></svg>
|
|
||||||
</x-pw-button>
|
|
||||||
</x-pw-button-group>
|
|
||||||
</x-pw-toolbar>`;
|
</x-pw-toolbar>`;
|
||||||
|
|
||||||
this._outerToolbarElement = html`<x-pw-div style="position: fixed; top: 100px; left: 10px; flex-direction: column; z-index: 2147483647;"></x-pw-div>`;
|
this._outerToolbarElement = html`<x-pw-div style="position: fixed; top: 100px; left: 10px; flex-direction: column; z-index: 2147483647;"></x-pw-div>`;
|
||||||
const toolbarShadow = this._outerToolbarElement.attachShadow({ mode: 'open' });
|
const toolbarShadow = this._outerToolbarElement.attachShadow({ mode: 'open' });
|
||||||
toolbarShadow.appendChild(this._toolbar);
|
toolbarShadow.appendChild(this._toolbar);
|
||||||
|
|
||||||
this._drawer = html`
|
|
||||||
<x-pw-drawer>
|
|
||||||
${commonStyles()}
|
|
||||||
${highlighterStyles()}
|
|
||||||
<style>
|
|
||||||
x-pw-drawer {
|
|
||||||
position: relative;
|
|
||||||
background: white;
|
|
||||||
background-color: #1d1f21;
|
|
||||||
border-left: 1px solid gray;
|
|
||||||
flex: auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
x-pw-toolbar {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
background-color: #00000060;
|
|
||||||
}
|
|
||||||
x-pw-code {
|
|
||||||
flex: auto;
|
|
||||||
color: #9cdcfe;
|
|
||||||
font-family: "SF Mono", Monaco, Menlo, Consolas, "Droid Sans Mono", Inconsolata, "Courier New", monospace;
|
|
||||||
font-size: 14px;
|
|
||||||
white-space: pre;
|
|
||||||
overflow: auto;
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
#pw-button-record {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<x-pw-code>${pressRecordMessageElement}</x-pw-code>
|
|
||||||
<x-pw-toolbar class="dark">
|
|
||||||
<x-pw-button id="pw-button-copy" tabIndex=0 title="Copy into clipboard">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>
|
|
||||||
</x-pw-button>
|
|
||||||
<x-pw-button id="pw-button-clear" tabIndex=0 title="Clear script">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
|
|
||||||
</x-pw-button>
|
|
||||||
<x-pw-button id="pw-button-close" tabIndex=0 title="Hide sidebar">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>
|
|
||||||
</x-pw-button>
|
|
||||||
</x-pw-toolbar>
|
|
||||||
</x-pw-drawer>`;
|
|
||||||
this._outerDrawerElement = html`<x-pw-div style="
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
width: 400px;
|
|
||||||
display: none;
|
|
||||||
z-index: 2147483647;
|
|
||||||
transition: transform 0.2s;
|
|
||||||
transform: translateX(400px);
|
|
||||||
"></x-pw-div>`;
|
|
||||||
const drawerShadow = this._outerDrawerElement.attachShadow({ mode: 'open' });
|
|
||||||
drawerShadow.appendChild(this._drawer);
|
|
||||||
|
|
||||||
this._hydrate();
|
this._hydrate();
|
||||||
this._refreshListenersIfNeeded();
|
this._refreshListenersIfNeeded();
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
@ -218,7 +149,7 @@ export class Recorder {
|
|||||||
if ((window as any)._recorderScriptReadyForTest)
|
if ((window as any)._recorderScriptReadyForTest)
|
||||||
(window as any)._recorderScriptReadyForTest();
|
(window as any)._recorderScriptReadyForTest();
|
||||||
}, 500);
|
}, 500);
|
||||||
this._pollRecorderMode(true).catch(e => {});
|
this._pollRecorderMode(true).catch(e => console.log(e)); // eslint-disable-line no-console
|
||||||
}
|
}
|
||||||
|
|
||||||
private _hydrate() {
|
private _hydrate() {
|
||||||
@ -237,25 +168,12 @@ export class Recorder {
|
|||||||
this._updateUIState({ mode: 'none' });
|
this._updateUIState({ mode: 'none' });
|
||||||
window.playwrightRecorderResume().catch(() => {});
|
window.playwrightRecorderResume().catch(() => {});
|
||||||
});
|
});
|
||||||
this._toolbar.$('#pw-button-drawer').addEventListener('click', () => {
|
this._toolbar.$('#pw-button-playwright').addEventListener('click', () => {
|
||||||
if (this._toolbar.$('#pw-button-drawer').classList.contains('disabled'))
|
if (this._toolbar.$('#pw-button-playwright').classList.contains('disabled'))
|
||||||
return;
|
return;
|
||||||
this._toolbar.$('#pw-button-drawer').classList.toggle('toggled');
|
this._toolbar.$('#pw-button-playwright').classList.toggle('toggled');
|
||||||
this._updateUIState({ drawerVisible: this._toolbar.$('#pw-button-drawer').classList.contains('toggled') });
|
window.playwrightRecorderShowRecorderPage().catch(() => {});
|
||||||
});
|
});
|
||||||
this._drawer.$('#pw-button-copy').addEventListener('click', () => {
|
|
||||||
if (this._drawer.$('#pw-button-copy').classList.contains('disabled'))
|
|
||||||
return;
|
|
||||||
copy(this._drawer.$('x-pw-code').textContent || '');
|
|
||||||
});
|
|
||||||
this._drawer.$('#pw-button-clear').addEventListener('click', () => {
|
|
||||||
window.playwrightRecorderClearScript().catch(() => {});
|
|
||||||
});
|
|
||||||
this._drawer.$('#pw-button-close').addEventListener('click', () => {
|
|
||||||
this._toolbar.$('#pw-button-drawer').classList.toggle('toggled', false);
|
|
||||||
this._updateUIState({ drawerVisible: false });
|
|
||||||
});
|
|
||||||
this._drawer.$('x-pw-code span').addEventListener('click', () => this._toggleRecording());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _refreshListenersIfNeeded() {
|
private _refreshListenersIfNeeded() {
|
||||||
@ -280,7 +198,6 @@ export class Recorder {
|
|||||||
];
|
];
|
||||||
document.documentElement.appendChild(this._outerGlassPaneElement);
|
document.documentElement.appendChild(this._outerGlassPaneElement);
|
||||||
document.documentElement.appendChild(this._outerToolbarElement);
|
document.documentElement.appendChild(this._outerToolbarElement);
|
||||||
document.documentElement.appendChild(this._outerDrawerElement);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _toggleRecording() {
|
private _toggleRecording() {
|
||||||
@ -304,40 +221,15 @@ export class Recorder {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { canResume, isController, isPaused, uiState, codegenScript } = state;
|
const { canResume, isPaused, uiState } = state;
|
||||||
if (uiState.mode !== this._state.uiState.mode) {
|
if (uiState.mode !== this._state.uiState.mode) {
|
||||||
this._state.uiState.mode = uiState.mode;
|
this._state.uiState.mode = uiState.mode;
|
||||||
this._toolbar.$('#pw-button-inspect').classList.toggle('toggled', uiState.mode === 'inspecting');
|
this._toolbar.$('#pw-button-inspect').classList.toggle('toggled', uiState.mode === 'inspecting');
|
||||||
this._toolbar.$('#pw-button-record').classList.toggle('toggled', uiState.mode === 'recording');
|
this._toolbar.$('#pw-button-record').classList.toggle('toggled', uiState.mode === 'recording');
|
||||||
this._toolbar.$('#pw-button-resume').classList.toggle('disabled', uiState.mode === 'recording');
|
this._toolbar.$('#pw-button-resume').classList.toggle('disabled', uiState.mode === 'recording');
|
||||||
this._updateDrawerMessage();
|
|
||||||
this._clearHighlight();
|
this._clearHighlight();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isController !== this._state.isController)
|
|
||||||
this._toolbar.$('#pw-button-drawer-group').classList.toggle('hidden', !isController);
|
|
||||||
|
|
||||||
if (isController && uiState.drawerVisible !== this._state.uiState.drawerVisible) {
|
|
||||||
this._state.uiState.drawerVisible = uiState.drawerVisible;
|
|
||||||
this._toolbar.$('#pw-button-drawer').classList.toggle('toggled', uiState.drawerVisible);
|
|
||||||
if (this._drawerTimeout)
|
|
||||||
clearTimeout(this._drawerTimeout);
|
|
||||||
if (uiState.drawerVisible) {
|
|
||||||
this._outerDrawerElement.style.display = 'flex';
|
|
||||||
const show = () => this._outerDrawerElement.style.transform = 'translateX(0)';
|
|
||||||
if (skipAnimations)
|
|
||||||
show();
|
|
||||||
else
|
|
||||||
window.requestAnimationFrame(show);
|
|
||||||
} else {
|
|
||||||
this._outerDrawerElement.style.transform = 'translateX(400px)';
|
|
||||||
if (!skipAnimations) {
|
|
||||||
this._drawerTimeout = setTimeout(() => {
|
|
||||||
this._outerDrawerElement.style.display = 'none';
|
|
||||||
}, 300);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isPaused !== this._state.isPaused) {
|
if (isPaused !== this._state.isPaused) {
|
||||||
this._state.isPaused = isPaused;
|
this._state.isPaused = isPaused;
|
||||||
this._toolbar.$('#pw-button-resume-group').classList.toggle('hidden', false);
|
this._toolbar.$('#pw-button-resume-group').classList.toggle('hidden', false);
|
||||||
@ -349,26 +241,10 @@ export class Recorder {
|
|||||||
this._toolbar.$('#pw-button-resume-group').classList.toggle('hidden', !canResume);
|
this._toolbar.$('#pw-button-resume-group').classList.toggle('hidden', !canResume);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (codegenScript !== this._state.codegenScript) {
|
|
||||||
this._state.codegenScript = codegenScript;
|
|
||||||
this._updateDrawerMessage();
|
|
||||||
}
|
|
||||||
this._state = state;
|
this._state = state;
|
||||||
this._pollRecorderModeTimer = setTimeout(() => this._pollRecorderMode(), 250);
|
this._pollRecorderModeTimer = setTimeout(() => this._pollRecorderMode(), 250);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _updateDrawerMessage() {
|
|
||||||
if (!this._state.codegenScript) {
|
|
||||||
this._drawer.$('x-pw-code').textContent = '';
|
|
||||||
if (this._state.uiState.mode === 'recording')
|
|
||||||
this._drawer.$('x-pw-code').appendChild(performActionsMessageElement);
|
|
||||||
else
|
|
||||||
this._drawer.$('x-pw-code').appendChild(pressRecordMessageElement);
|
|
||||||
} else {
|
|
||||||
this._drawer.$('x-pw-code').innerHTML = this._state.codegenScript;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _clearHighlight() {
|
private _clearHighlight() {
|
||||||
this._hoveredModel = null;
|
this._hoveredModel = null;
|
||||||
this._activeModel = null;
|
this._activeModel = null;
|
||||||
@ -712,7 +588,7 @@ export class Recorder {
|
|||||||
|
|
||||||
private async _performAction(action: actions.Action) {
|
private async _performAction(action: actions.Action) {
|
||||||
this._performingAction = true;
|
this._performingAction = true;
|
||||||
await window.playwrightRecorderPerformAction(action).catch(e => {});
|
await window.playwrightRecorderPerformAction(action).catch(() => {});
|
||||||
this._performingAction = false;
|
this._performingAction = false;
|
||||||
|
|
||||||
// Action could have changed DOM, update hovered model selectors.
|
// Action could have changed DOM, update hovered model selectors.
|
||||||
@ -820,7 +696,7 @@ x-pw-button-group {
|
|||||||
box-shadow: rgba(0, 0, 0, 0.1) 0px 0.25em 0.5em;
|
box-shadow: rgba(0, 0, 0, 0.1) 0px 0.25em 0.5em;
|
||||||
margin: 4px 0px;
|
margin: 4px 0px;
|
||||||
}
|
}
|
||||||
x-pw-button-group.vertical {
|
x-pw-toolbar.vertical x-pw-button-group {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
x-pw-button {
|
x-pw-button {
|
||||||
@ -899,51 +775,4 @@ x-pw-icon svg {
|
|||||||
</style>`;
|
</style>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function highlighterStyles() {
|
|
||||||
return html`
|
|
||||||
<style>
|
|
||||||
.hljs-comment,
|
|
||||||
.hljs-quote {
|
|
||||||
color: #6a9955;
|
|
||||||
}
|
|
||||||
.hljs-variable,
|
|
||||||
.hljs-template-variable,
|
|
||||||
.hljs-tag,
|
|
||||||
.hljs-name,
|
|
||||||
.hljs-selector-id,
|
|
||||||
.hljs-selector-class,
|
|
||||||
.hljs-regexp,
|
|
||||||
.hljs-deletion {
|
|
||||||
color: #4fc1ff;
|
|
||||||
}
|
|
||||||
.hljs-number,
|
|
||||||
.hljs-built_in,
|
|
||||||
.hljs-builtin-name,
|
|
||||||
.hljs-literal,
|
|
||||||
.hljs-type,
|
|
||||||
.hljs-params,
|
|
||||||
.hljs-meta,
|
|
||||||
.hljs-link {
|
|
||||||
color: #de935f;
|
|
||||||
}
|
|
||||||
.hljs-attribute {
|
|
||||||
color: #cc6666;
|
|
||||||
}
|
|
||||||
.hljs-string,
|
|
||||||
.hljs-symbol,
|
|
||||||
.hljs-bullet,
|
|
||||||
.hljs-addition {
|
|
||||||
color: #ce9178;
|
|
||||||
}
|
|
||||||
.hljs-title,
|
|
||||||
.hljs-section {
|
|
||||||
color: #4271ae;
|
|
||||||
}
|
|
||||||
.hljs-keyword,
|
|
||||||
.hljs-selector-tag {
|
|
||||||
color: #c586c0;
|
|
||||||
}
|
|
||||||
</style>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Recorder;
|
export default Recorder;
|
||||||
|
@ -18,8 +18,6 @@ import { BrowserContext, ContextListener } from '../browserContext';
|
|||||||
import { isDebugMode } from '../../utils/utils';
|
import { isDebugMode } from '../../utils/utils';
|
||||||
import { ConsoleApiSupplement } from './consoleApiSupplement';
|
import { ConsoleApiSupplement } from './consoleApiSupplement';
|
||||||
import { RecorderSupplement } from './recorderSupplement';
|
import { RecorderSupplement } from './recorderSupplement';
|
||||||
import { Page } from '../page';
|
|
||||||
import { ConsoleMessage } from '../console';
|
|
||||||
|
|
||||||
export class InspectorController implements ContextListener {
|
export class InspectorController implements ContextListener {
|
||||||
async onContextCreated(context: BrowserContext): Promise<void> {
|
async onContextCreated(context: BrowserContext): Promise<void> {
|
||||||
@ -30,9 +28,6 @@ export class InspectorController implements ContextListener {
|
|||||||
language: 'javascript',
|
language: 'javascript',
|
||||||
terminal: true,
|
terminal: true,
|
||||||
});
|
});
|
||||||
context.on(BrowserContext.Events.Page, (page: Page) => {
|
|
||||||
page.on(Page.Events.Console, (message: ConsoleMessage) => context.emit(BrowserContext.Events.StdOut, message.text() + '\n'));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async onContextWillDestroy(context: BrowserContext): Promise<void> {}
|
async onContextWillDestroy(context: BrowserContext): Promise<void> {}
|
||||||
|
@ -64,15 +64,16 @@ export class OutputMultiplexer implements RecorderOutput {
|
|||||||
export class BufferedOutput implements RecorderOutput {
|
export class BufferedOutput implements RecorderOutput {
|
||||||
private _lines: string[] = [];
|
private _lines: string[] = [];
|
||||||
private _buffer: string | null = null;
|
private _buffer: string | null = null;
|
||||||
private _language: string | null = null;
|
private _onUpdate: ((text: string) => void);
|
||||||
|
|
||||||
constructor(language?: string) {
|
constructor(onUpdate: (text: string) => void = () => {}) {
|
||||||
this._language = language || null;
|
this._onUpdate = onUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
printLn(text: string) {
|
printLn(text: string) {
|
||||||
this._buffer = null;
|
this._buffer = null;
|
||||||
this._lines.push(...text.trimEnd().split('\n'));
|
this._lines.push(...text.trimEnd().split('\n'));
|
||||||
|
this._onUpdate(this.buffer());
|
||||||
}
|
}
|
||||||
|
|
||||||
popLn(text: string) {
|
popLn(text: string) {
|
||||||
@ -81,17 +82,15 @@ export class BufferedOutput implements RecorderOutput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buffer(): string {
|
buffer(): string {
|
||||||
if (this._buffer === null) {
|
if (this._buffer === null)
|
||||||
this._buffer = this._lines.join('\n');
|
this._buffer = this._lines.join('\n');
|
||||||
if (this._language)
|
|
||||||
this._buffer = hljs.highlight(this._language, this._buffer).value;
|
|
||||||
}
|
|
||||||
return this._buffer;
|
return this._buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
this._lines = [];
|
this._lines = [];
|
||||||
this._buffer = null;
|
this._buffer = null;
|
||||||
|
this._onUpdate(this.buffer());
|
||||||
}
|
}
|
||||||
|
|
||||||
flush() {
|
flush() {
|
||||||
|
126
src/server/supplements/recorder/recorderApp.ts
Normal file
126
src/server/supplements/recorder/recorderApp.ts
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
/**
|
||||||
|
* 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 * as os from 'os';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as util from 'util';
|
||||||
|
import { CRPage } from '../../chromium/crPage';
|
||||||
|
import { Page } from '../../page';
|
||||||
|
import { ProgressController } from '../../progress';
|
||||||
|
import { createPlaywright } from '../../playwright';
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
|
import { DEFAULT_ARGS } from '../../chromium/chromium';
|
||||||
|
|
||||||
|
const readFileAsync = util.promisify(fs.readFile);
|
||||||
|
|
||||||
|
export class RecorderApp extends EventEmitter {
|
||||||
|
|
||||||
|
private _page: Page;
|
||||||
|
|
||||||
|
constructor(page: Page) {
|
||||||
|
super();
|
||||||
|
this._page = page;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _init() {
|
||||||
|
const icon = await readFileAsync(require.resolve('../../../../lib/web/recorder/app_icon.png'));
|
||||||
|
const crPopup = this._page._delegate as CRPage;
|
||||||
|
await crPopup._mainFrameSession._client.send('Browser.setDockTile', {
|
||||||
|
image: icon.toString('base64')
|
||||||
|
});
|
||||||
|
|
||||||
|
await this._page._setServerRequestInterceptor(async route => {
|
||||||
|
if (route.request().url().startsWith('https://playwright/')) {
|
||||||
|
const uri = route.request().url().substring('https://playwright/'.length);
|
||||||
|
const file = require.resolve('../../../../lib/web/recorder/' + uri);
|
||||||
|
const buffer = await readFileAsync(file);
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
headers: [
|
||||||
|
{ name: 'Content-Type', value: extensionToMime[path.extname(file)] }
|
||||||
|
],
|
||||||
|
body: buffer.toString('base64'),
|
||||||
|
isBase64: true
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await route.continue();
|
||||||
|
});
|
||||||
|
|
||||||
|
await this._page.exposeBinding('playwrightClear', false, (_, text: string) => {
|
||||||
|
this.emit('clear');
|
||||||
|
});
|
||||||
|
|
||||||
|
this._page.once('close', () => {
|
||||||
|
this.emit('close');
|
||||||
|
this._page.context().close().catch(e => console.error(e));
|
||||||
|
});
|
||||||
|
|
||||||
|
await this._page.mainFrame().goto(new ProgressController(), 'https://playwright/index.html');
|
||||||
|
}
|
||||||
|
|
||||||
|
static async open(inspectedPage: Page): Promise<RecorderApp> {
|
||||||
|
const bounds = await CRPage.mainFrameSession(inspectedPage).windowBounds();
|
||||||
|
const recorderPlaywright = createPlaywright(true);
|
||||||
|
const context = await recorderPlaywright.chromium.launchPersistentContext('', {
|
||||||
|
ignoreAllDefaultArgs: true,
|
||||||
|
args: [
|
||||||
|
...DEFAULT_ARGS,
|
||||||
|
`--user-data-dir=${path.join(os.homedir(),'.playwright-recorder')}`,
|
||||||
|
'--remote-debugging-pipe',
|
||||||
|
'--app=data:text/html,',
|
||||||
|
`--window-size=300,${bounds.height}`,
|
||||||
|
`--window-position=${bounds.left! + bounds.width! + 1},${bounds.top!}`
|
||||||
|
],
|
||||||
|
noDefaultViewport: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const controller = new ProgressController();
|
||||||
|
await controller.run(async progress => {
|
||||||
|
await context._browser._defaultContext!._loadDefaultContextAsIs(progress);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [page] = context.pages();
|
||||||
|
const result = new RecorderApp(page);
|
||||||
|
await result._init();
|
||||||
|
await inspectedPage.bringToFront();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setScript(text: string, language: string): Promise<void> {
|
||||||
|
await this._page.mainFrame()._evaluateExpression(((param: { text: string, language: string }) => {
|
||||||
|
(window as any).playwrightSetSource(param);
|
||||||
|
}).toString(), true, { text, language }, 'main');
|
||||||
|
}
|
||||||
|
|
||||||
|
async bringToFront() {
|
||||||
|
await this._page.bringToFront();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const extensionToMime: { [key: string]: string } = {
|
||||||
|
'.css': 'text/css',
|
||||||
|
'.html': 'text/html',
|
||||||
|
'.jpeg': 'image/jpeg',
|
||||||
|
'.js': 'application/javascript',
|
||||||
|
'.png': 'image/png',
|
||||||
|
'.ttf': 'font/ttf',
|
||||||
|
'.svg': 'image/svg+xml',
|
||||||
|
'.webp': 'image/webp',
|
||||||
|
'.woff': 'font/woff',
|
||||||
|
'.woff2': 'font/woff2',
|
||||||
|
};
|
@ -16,18 +16,14 @@
|
|||||||
|
|
||||||
export type UIState = {
|
export type UIState = {
|
||||||
mode: 'inspecting' | 'recording' | 'none',
|
mode: 'inspecting' | 'recording' | 'none',
|
||||||
drawerVisible: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SetUIState = {
|
export type SetUIState = {
|
||||||
mode?: 'inspecting' | 'recording' | 'none',
|
mode?: 'inspecting' | 'recording' | 'none',
|
||||||
drawerVisible?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type State = {
|
export type State = {
|
||||||
canResume: boolean,
|
canResume: boolean,
|
||||||
isController: boolean,
|
|
||||||
isPaused: boolean,
|
isPaused: boolean,
|
||||||
codegenScript: string,
|
|
||||||
uiState: UIState,
|
uiState: UIState,
|
||||||
}
|
}
|
||||||
|
@ -30,13 +30,13 @@ import * as recorderSource from '../../generated/recorderSource';
|
|||||||
import * as consoleApiSource from '../../generated/consoleApiSource';
|
import * as consoleApiSource from '../../generated/consoleApiSource';
|
||||||
import { BufferedOutput, FileOutput, FlushingTerminalOutput, OutputMultiplexer, RecorderOutput, TerminalOutput, Writable } from './recorder/outputs';
|
import { BufferedOutput, FileOutput, FlushingTerminalOutput, OutputMultiplexer, RecorderOutput, TerminalOutput, Writable } from './recorder/outputs';
|
||||||
import type { State, UIState } from './recorder/state';
|
import type { State, UIState } from './recorder/state';
|
||||||
|
import { RecorderApp } from './recorder/recorderApp';
|
||||||
|
|
||||||
type BindingSource = { frame: Frame, page: Page };
|
type BindingSource = { frame: Frame, page: Page };
|
||||||
type App = 'codegen' | 'debug' | 'pause';
|
type App = 'codegen' | 'debug' | 'pause';
|
||||||
|
|
||||||
const symbol = Symbol('RecorderSupplement');
|
const symbol = Symbol('RecorderSupplement');
|
||||||
|
|
||||||
|
|
||||||
export class RecorderSupplement {
|
export class RecorderSupplement {
|
||||||
private _generator: CodeGenerator;
|
private _generator: CodeGenerator;
|
||||||
private _pageAliases = new Map<Page, string>();
|
private _pageAliases = new Map<Page, string>();
|
||||||
@ -50,6 +50,8 @@ export class RecorderSupplement {
|
|||||||
private _app: App;
|
private _app: App;
|
||||||
private _output: OutputMultiplexer;
|
private _output: OutputMultiplexer;
|
||||||
private _bufferedOutput: BufferedOutput;
|
private _bufferedOutput: BufferedOutput;
|
||||||
|
private _recorderApp: Promise<RecorderApp> | null = null;
|
||||||
|
private _highlighterType: string;
|
||||||
|
|
||||||
static getOrCreate(context: BrowserContext, app: App, params: channels.BrowserContextRecorderSupplementEnableParams): Promise<RecorderSupplement> {
|
static getOrCreate(context: BrowserContext, app: App, params: channels.BrowserContextRecorderSupplementEnableParams): Promise<RecorderSupplement> {
|
||||||
let recorderPromise = (context as any)[symbol] as Promise<RecorderSupplement>;
|
let recorderPromise = (context as any)[symbol] as Promise<RecorderSupplement>;
|
||||||
@ -66,7 +68,6 @@ export class RecorderSupplement {
|
|||||||
this._app = app;
|
this._app = app;
|
||||||
this._recorderUIState = {
|
this._recorderUIState = {
|
||||||
mode: app === 'codegen' ? 'recording' : 'none',
|
mode: app === 'codegen' ? 'recording' : 'none',
|
||||||
drawerVisible: false
|
|
||||||
};
|
};
|
||||||
let languageGenerator: LanguageGenerator;
|
let languageGenerator: LanguageGenerator;
|
||||||
switch (params.language) {
|
switch (params.language) {
|
||||||
@ -84,7 +85,13 @@ export class RecorderSupplement {
|
|||||||
write: (text: string) => context.emit(BrowserContext.Events.StdOut, text)
|
write: (text: string) => context.emit(BrowserContext.Events.StdOut, text)
|
||||||
};
|
};
|
||||||
const outputs: RecorderOutput[] = [params.terminal ? new TerminalOutput(writable, highlighterType) : new FlushingTerminalOutput(writable)];
|
const outputs: RecorderOutput[] = [params.terminal ? new TerminalOutput(writable, highlighterType) : new FlushingTerminalOutput(writable)];
|
||||||
this._bufferedOutput = new BufferedOutput(highlighterType);
|
this._highlighterType = highlighterType;
|
||||||
|
this._bufferedOutput = new BufferedOutput(async text => {
|
||||||
|
if (this._recorderApp) {
|
||||||
|
const app = await this._recorderApp;
|
||||||
|
await app.setScript(text, highlighterType).catch(e => {});
|
||||||
|
}
|
||||||
|
});
|
||||||
outputs.push(this._bufferedOutput);
|
outputs.push(this._bufferedOutput);
|
||||||
if (params.outputFile)
|
if (params.outputFile)
|
||||||
outputs.push(new FileOutput(params.outputFile));
|
outputs.push(new FileOutput(params.outputFile));
|
||||||
@ -120,33 +127,32 @@ export class RecorderSupplement {
|
|||||||
await this._context.exposeBinding('playwrightRecorderCommitAction', false,
|
await this._context.exposeBinding('playwrightRecorderCommitAction', false,
|
||||||
(source: BindingSource, action: actions.Action) => this._generator.commitLastAction());
|
(source: BindingSource, action: actions.Action) => this._generator.commitLastAction());
|
||||||
|
|
||||||
await this._context.exposeBinding('playwrightRecorderClearScript', false,
|
await this._context.exposeBinding('playwrightRecorderShowRecorderPage', false, ({ page }) => {
|
||||||
(source: BindingSource, action: actions.Action) => {
|
if (this._recorderApp) {
|
||||||
this._bufferedOutput.clear();
|
this._recorderApp.then(p => p.bringToFront()).catch(() => {});
|
||||||
this._generator.restart();
|
return;
|
||||||
if (this._app === 'codegen') {
|
}
|
||||||
for (const page of this._context.pages())
|
this._recorderApp = RecorderApp.open(page);
|
||||||
this._onFrameNavigated(page.mainFrame(), page);
|
this._recorderApp.then(app => {
|
||||||
}
|
app.once('close', () => {
|
||||||
|
this._recorderApp = null;
|
||||||
});
|
});
|
||||||
|
app.on('clear', () => this._clearScript());
|
||||||
|
return app.setScript(this._bufferedOutput.buffer(), this._highlighterType);
|
||||||
|
}).catch(e => console.error(e));
|
||||||
|
});
|
||||||
|
|
||||||
await this._context.exposeBinding('playwrightRecorderState', false, ({ page }) => {
|
await this._context.exposeBinding('playwrightRecorderState', false, ({ page }) => {
|
||||||
const state: State = {
|
const state: State = {
|
||||||
isController: page === this._context.pages()[0],
|
|
||||||
uiState: this._recorderUIState,
|
uiState: this._recorderUIState,
|
||||||
canResume: this._app === 'pause',
|
canResume: this._app === 'pause',
|
||||||
isPaused: this._paused,
|
isPaused: this._paused,
|
||||||
codegenScript: this._bufferedOutput.buffer()
|
|
||||||
};
|
};
|
||||||
return state;
|
return state;
|
||||||
});
|
});
|
||||||
|
|
||||||
await this._context.exposeBinding('playwrightRecorderSetUIState', false, (source, state: UIState) => {
|
await this._context.exposeBinding('playwrightRecorderSetUIState', false, (source, state: UIState) => {
|
||||||
const isController = source.page === this._context.pages()[0];
|
this._recorderUIState = { ...this._recorderUIState, ...state };
|
||||||
if (isController)
|
|
||||||
this._recorderUIState = { ...this._recorderUIState, ...state };
|
|
||||||
else
|
|
||||||
this._recorderUIState = { ...this._recorderUIState, mode: state.mode };
|
|
||||||
this._output.setEnabled(state.mode === 'recording');
|
this._output.setEnabled(state.mode === 'recording');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -164,7 +170,7 @@ export class RecorderSupplement {
|
|||||||
|
|
||||||
async pause() {
|
async pause() {
|
||||||
this._paused = true;
|
this._paused = true;
|
||||||
return new Promise(f => this._resumeCallback = f);
|
return new Promise<void>(f => this._resumeCallback = f);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _onPage(page: Page) {
|
private async _onPage(page: Page) {
|
||||||
@ -208,6 +214,15 @@ export class RecorderSupplement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _clearScript(): void {
|
||||||
|
this._bufferedOutput.clear();
|
||||||
|
this._generator.restart();
|
||||||
|
if (this._app === 'codegen') {
|
||||||
|
for (const page of this._context.pages())
|
||||||
|
this._onFrameNavigated(page.mainFrame(), page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async _performAction(frame: Frame, action: actions.Action) {
|
private async _performAction(frame: Frame, action: actions.Action) {
|
||||||
const page = frame._page;
|
const page = frame._page;
|
||||||
const controller = new ProgressController();
|
const controller = new ProgressController();
|
||||||
@ -274,4 +289,3 @@ export class RecorderSupplement {
|
|||||||
this._generator.signal(pageAlias, page.mainFrame(), { name: 'dialog', dialogAlias: String(++this._lastDialogOrdinal) });
|
this._generator.signal(pageAlias, page.mainFrame(), { name: 'dialog', dialogAlias: String(++this._lastDialogOrdinal) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,11 +33,13 @@ const Template: Story<SourceProps> = args => <Source {...args} />;
|
|||||||
|
|
||||||
export const Primary = Template.bind({});
|
export const Primary = Template.bind({});
|
||||||
Primary.args = {
|
Primary.args = {
|
||||||
|
language: 'javascript',
|
||||||
text: exampleText()
|
text: exampleText()
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HighlightLine = Template.bind({});
|
export const HighlightLine = Template.bind({});
|
||||||
HighlightLine.args = {
|
HighlightLine.args = {
|
||||||
|
language: 'javascript',
|
||||||
text: exampleText(),
|
text: exampleText(),
|
||||||
highlightedLine: 11
|
highlightedLine: 11
|
||||||
};
|
};
|
||||||
|
@ -21,18 +21,20 @@ import '../../third_party/highlightjs/highlightjs/tomorrow.css';
|
|||||||
|
|
||||||
export interface SourceProps {
|
export interface SourceProps {
|
||||||
text: string,
|
text: string,
|
||||||
|
language: string,
|
||||||
highlightedLine?: number
|
highlightedLine?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Source: React.FC<SourceProps> = ({
|
export const Source: React.FC<SourceProps> = ({
|
||||||
text = '',
|
text,
|
||||||
|
language,
|
||||||
highlightedLine = -1
|
highlightedLine = -1
|
||||||
}) => {
|
}) => {
|
||||||
const lines = React.useMemo<string[]>(() => {
|
const lines = React.useMemo<string[]>(() => {
|
||||||
const result = [];
|
const result = [];
|
||||||
let continuation: any;
|
let continuation: any;
|
||||||
for (const line of text.split('\n')) {
|
for (const line of text.split('\n')) {
|
||||||
const highlighted = highlightjs.highlight('javascript', line, true, continuation);
|
const highlighted = highlightjs.highlight(language, line, true, continuation);
|
||||||
continuation = highlighted.top;
|
continuation = highlighted.top;
|
||||||
result.push(highlighted.value);
|
result.push(highlighted.value);
|
||||||
}
|
}
|
||||||
|
BIN
src/web/recorder/app_icon.png
Normal file
BIN
src/web/recorder/app_icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
@ -28,5 +28,5 @@ declare global {
|
|||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
applyTheme();
|
applyTheme();
|
||||||
ReactDOM.render(<Recorder text=""/>, document.querySelector('#root'));
|
ReactDOM.render(<Recorder/>, document.querySelector('#root'));
|
||||||
})();
|
})();
|
||||||
|
@ -17,7 +17,6 @@
|
|||||||
import { Story, Meta } from '@storybook/react/types-6-0';
|
import { Story, Meta } from '@storybook/react/types-6-0';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Recorder, RecorderProps } from './recorder';
|
import { Recorder, RecorderProps } from './recorder';
|
||||||
import { exampleText } from '../components/exampleText';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Recorder/Recorder',
|
title: 'Recorder/Recorder',
|
||||||
@ -33,5 +32,4 @@ const Template: Story<RecorderProps> = args => <Recorder {...args} />;
|
|||||||
|
|
||||||
export const Primary = Template.bind({});
|
export const Primary = Template.bind({});
|
||||||
Primary.args = {
|
Primary.args = {
|
||||||
text: exampleText()
|
|
||||||
};
|
};
|
||||||
|
@ -20,20 +20,42 @@ import { Toolbar } from '../components/toolbar';
|
|||||||
import { ToolbarButton } from '../components/toolbarButton';
|
import { ToolbarButton } from '../components/toolbarButton';
|
||||||
import { Source } from '../components/source';
|
import { Source } from '../components/source';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
playwrightClear(): Promise<void>
|
||||||
|
playwrightSetSource: (params: { text: string, language: string }) => void
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface RecorderProps {
|
export interface RecorderProps {
|
||||||
text: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Recorder: React.FC<RecorderProps> = ({
|
export const Recorder: React.FC<RecorderProps> = ({
|
||||||
text
|
|
||||||
}) => {
|
}) => {
|
||||||
|
const [source, setSource] = React.useState({ language: 'javascript', text: '' });
|
||||||
|
window.playwrightSetSource = setSource;
|
||||||
|
|
||||||
return <div className="recorder">
|
return <div className="recorder">
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<ToolbarButton icon="clone" title="Copy" onClick={() => {}}></ToolbarButton>
|
<ToolbarButton icon="clone" title="Copy" onClick={() => {
|
||||||
<ToolbarButton icon="trashcan" title="Erase" onClick={() => {}}></ToolbarButton>
|
copy(source.text);
|
||||||
|
}}></ToolbarButton>
|
||||||
|
<ToolbarButton icon="trashcan" title="Clear" onClick={() => {
|
||||||
|
window.playwrightClear().catch(e => console.error(e));
|
||||||
|
}}></ToolbarButton>
|
||||||
<div style={{flex: "auto"}}></div>
|
<div style={{flex: "auto"}}></div>
|
||||||
<ToolbarButton icon="close" title="Close" onClick={() => {}}></ToolbarButton>
|
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
<Source text={text}></Source>
|
<Source text={source.text} language={source.language}></Source>
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function copy(text: string) {
|
||||||
|
const textArea = document.createElement('textarea');
|
||||||
|
textArea.style.position = 'absolute';
|
||||||
|
textArea.style.zIndex = '-1000';
|
||||||
|
textArea.value = text;
|
||||||
|
document.body.appendChild(textArea);
|
||||||
|
textArea.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
textArea.remove();
|
||||||
|
}
|
||||||
|
@ -122,4 +122,11 @@ onChanges.push({
|
|||||||
script: 'utils/generate_types/index.js',
|
script: 'utils/generate_types/index.js',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Copy images.
|
||||||
|
steps.push({
|
||||||
|
command: process.platform === 'win32' ? 'copy' : 'cp',
|
||||||
|
args: ['src/web/recorder/*.png'.replace(/\//g, path.sep), 'lib/web/recorder/'.replace(/\//g, path.sep)],
|
||||||
|
shell: true,
|
||||||
|
});
|
||||||
|
|
||||||
watchMode ? runWatch() : runBuild();
|
watchMode ? runWatch() : runBuild();
|
||||||
|
@ -154,6 +154,8 @@ DEPS['src/service.ts'] = ['src/remote/'];
|
|||||||
// CLI should only use client-side features.
|
// CLI should only use client-side features.
|
||||||
DEPS['src/cli/'] = ['src/cli/**', 'src/client/**', 'src/install/**', 'src/generated/', 'src/server/injected/', 'src/debug/injected/', 'src/trace/**', 'src/utils/**'];
|
DEPS['src/cli/'] = ['src/cli/**', 'src/client/**', 'src/install/**', 'src/generated/', 'src/server/injected/', 'src/debug/injected/', 'src/trace/**', 'src/utils/**'];
|
||||||
|
|
||||||
|
DEPS['src/server/supplements/recorder/recorderApp.ts'] = ['src/server/', 'src/server/chromium/']
|
||||||
|
|
||||||
checkDeps().catch(e => {
|
checkDeps().catch(e => {
|
||||||
console.error(e && e.stack ? e.stack : e);
|
console.error(e && e.stack ? e.stack : e);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user