Skip to content

Commit 7fd8ec2

Browse files
authored
chore: work towards streaming recorder backend (microsoft#36484)
1 parent 3a02f89 commit 7fd8ec2

3 files changed

Lines changed: 138 additions & 163 deletions

File tree

packages/playwright-core/src/server/recorder/contextRecorder.ts

Lines changed: 70 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import { EventEmitter } from 'events';
1818

19-
import { RecorderCollection } from './recorderCollection';
19+
import { RecorderSignalProcessor } from './recorderSignalProcessor';
2020
import * as rawRecorderSource from '../../generated/pollingRecorderSource';
2121
import { eventsHelper, monotonicTime, quoteCSSAttributeValue } from '../../utils';
2222
import { raceAgainstDeadline } from '../../utils/isomorphic/timeoutRunner';
@@ -26,13 +26,16 @@ import { Frame } from '../frames';
2626
import { Page } from '../page';
2727
import { ThrottledFile } from './throttledFile';
2828
import { generateCode } from '../codegen/language';
29+
import { performAction } from './recorderRunner';
30+
import { collapseActions } from './recorderUtils';
2931

3032
import type { RegisteredListener } from '../../utils';
3133
import type { Language, LanguageGenerator, LanguageGeneratorOptions } from '../codegen/types';
3234
import type * as channels from '@protocol/channels';
3335
import type * as actions from '@recorder/actions';
3436
import type { Source } from '@recorder/recorderTypes';
3537
import type { Progress } from '@protocol/progress';
38+
import type { Signal } from '../../../../recorder/src/actions';
3639

3740
type BindingSource = { frame: Frame, page: Page };
3841

@@ -45,7 +48,7 @@ export class ContextRecorder extends EventEmitter {
4548
Change: 'change'
4649
};
4750

48-
private _collection: RecorderCollection;
51+
private _collection: RecorderSignalProcessor;
4952
private _pageAliases = new Map<Page, string>();
5053
private _lastPopupOrdinal = 0;
5154
private _lastDialogOrdinal = -1;
@@ -57,6 +60,9 @@ export class ContextRecorder extends EventEmitter {
5760
private _throttledOutputFile: ThrottledFile | null = null;
5861
private _orderedLanguages: LanguageGenerator[] = [];
5962
private _listeners: RegisteredListener[] = [];
63+
private _actions: actions.ActionInContext[] = [];
64+
private _languageGeneratorOptions: LanguageGeneratorOptions;
65+
private _enabled: boolean = false;
6066

6167
constructor(context: BrowserContext, params: channels.BrowserContextEnableRecorderParams, delegate: ContextRecorderDelegate) {
6268
super();
@@ -65,53 +71,76 @@ export class ContextRecorder extends EventEmitter {
6571
this._delegate = delegate;
6672
this._recorderSources = [];
6773
const language = params.language || context._browser.sdkLanguage();
68-
this.setOutput(language, params.outputFile);
6974

7075
// Make a copy of options to modify them later.
71-
const languageGeneratorOptions: LanguageGeneratorOptions = {
76+
this._languageGeneratorOptions = {
7277
browserName: context._browser.options.name,
7378
launchOptions: { headless: false, ...params.launchOptions, tracesDir: undefined },
7479
contextOptions: { ...params.contextOptions },
7580
deviceName: params.device,
7681
saveStorage: params.saveStorage,
7782
};
7883

79-
this._collection = new RecorderCollection(this._pageAliases);
80-
this._collection.on('change', (actions: actions.ActionInContext[]) => {
81-
this._recorderSources = [];
82-
for (const languageGenerator of this._orderedLanguages) {
83-
const { header, footer, actionTexts, text } = generateCode(actions, languageGenerator, languageGeneratorOptions);
84-
const source: Source = {
85-
isRecorded: true,
86-
label: languageGenerator.name,
87-
group: languageGenerator.groupName,
88-
id: languageGenerator.id,
89-
text,
90-
header,
91-
footer,
92-
actions: actionTexts,
93-
language: languageGenerator.highlighter,
94-
highlight: []
95-
};
96-
source.revealLine = text.split('\n').length - 1;
97-
this._recorderSources.push(source);
98-
if (languageGenerator === this._orderedLanguages[0])
99-
this._throttledOutputFile?.setContent(source.text);
100-
}
101-
this.emit(ContextRecorder.Events.Change, {
102-
sources: this._recorderSources,
103-
actions
104-
});
84+
this._collection = new RecorderSignalProcessor();
85+
this._collection.on('action', (actionInContext: actions.ActionInContext) => {
86+
if (!this._enabled)
87+
return;
88+
this._actions.push(actionInContext);
89+
this._updateActions();
90+
});
91+
this._collection.on('signal', (signal: Signal) => {
92+
if (!this._enabled)
93+
return;
94+
const lastAction = this._actions[this._actions.length - 1];
95+
if (lastAction)
96+
lastAction.action.signals.push(signal);
97+
this._updateActions();
10598
});
99+
106100
context.on(BrowserContext.Events.BeforeClose, () => {
107101
this._throttledOutputFile?.flush();
108102
});
109103
this._listeners.push(eventsHelper.addEventListener(process, 'exit', () => {
110104
this._throttledOutputFile?.flush();
111105
}));
106+
107+
this.setOutput(language, params.outputFile);
112108
this.setEnabled(params.mode === 'recording');
113109
}
114110

111+
private _resetActions() {
112+
this._actions = [];
113+
this._updateActions();
114+
}
115+
116+
private _updateActions() {
117+
const actions = collapseActions(this._actions);
118+
this._recorderSources = [];
119+
for (const languageGenerator of this._orderedLanguages) {
120+
const { header, footer, actionTexts, text } = generateCode(actions, languageGenerator, this._languageGeneratorOptions);
121+
const source: Source = {
122+
isRecorded: true,
123+
label: languageGenerator.name,
124+
group: languageGenerator.groupName,
125+
id: languageGenerator.id,
126+
text,
127+
header,
128+
footer,
129+
actions: actionTexts,
130+
language: languageGenerator.highlighter,
131+
highlight: []
132+
};
133+
source.revealLine = text.split('\n').length - 1;
134+
this._recorderSources.push(source);
135+
if (languageGenerator === this._orderedLanguages[0])
136+
this._throttledOutputFile?.setContent(source.text);
137+
}
138+
this.emit(ContextRecorder.Events.Change, {
139+
sources: this._recorderSources,
140+
actions
141+
});
142+
}
143+
115144
setOutput(codegenId: string, outputFile?: string) {
116145
const languages = languageSet();
117146
const primaryLanguage = [...languages].find(l => l.id === codegenId);
@@ -120,7 +149,7 @@ export class ContextRecorder extends EventEmitter {
120149
languages.delete(primaryLanguage);
121150
this._orderedLanguages = [primaryLanguage, ...languages];
122151
this._throttledOutputFile = outputFile ? new ThrottledFile(outputFile) : null;
123-
this._collection?.restart();
152+
this._resetActions();
124153
}
125154

126155
languageName(id?: string): Language {
@@ -154,7 +183,7 @@ export class ContextRecorder extends EventEmitter {
154183
}
155184

156185
setEnabled(enabled: boolean) {
157-
this._collection.setEnabled(enabled);
186+
this._enabled = enabled;
158187
}
159188

160189
dispose() {
@@ -165,7 +194,7 @@ export class ContextRecorder extends EventEmitter {
165194
// First page is called page, others are called popup1, popup2, etc.
166195
const frame = page.mainFrame();
167196
page.on('close', () => {
168-
this._collection.addRecordedAction({
197+
this._collection.addAction({
169198
frame: this._describeMainFrame(page),
170199
action: {
171200
name: 'closePage',
@@ -187,7 +216,7 @@ export class ContextRecorder extends EventEmitter {
187216
if (page.opener()) {
188217
this._onPopup(page.opener()!, page);
189218
} else {
190-
this._collection.addRecordedAction({
219+
this._collection.addAction({
191220
frame: this._describeMainFrame(page),
192221
action: {
193222
name: 'openPage',
@@ -200,7 +229,7 @@ export class ContextRecorder extends EventEmitter {
200229
}
201230

202231
clearScript(): void {
203-
this._collection.restart();
232+
this._resetActions();
204233
if (this._params.mode === 'recording') {
205234
for (const page of this._context.pages())
206235
this._onFrameNavigated(page.mainFrame(), page);
@@ -242,11 +271,15 @@ export class ContextRecorder extends EventEmitter {
242271
}
243272

244273
private async _performAction(frame: Frame, action: actions.PerformOnRecordAction) {
245-
await this._collection.performAction(await this._createActionInContext(frame, action));
274+
const actionInContext = await this._createActionInContext(frame, action);
275+
this._collection.addAction(actionInContext);
276+
if (actionInContext.action.name !== 'openPage' && actionInContext.action.name !== 'closePage')
277+
await performAction(this._pageAliases, actionInContext);
278+
actionInContext.endTime = monotonicTime();
246279
}
247280

248281
private async _recordAction(frame: Frame, action: actions.Action) {
249-
this._collection.addRecordedAction(await this._createActionInContext(frame, action));
282+
this._collection.addAction(await this._createActionInContext(frame, action));
250283
}
251284

252285
private _onFrameNavigated(frame: Frame, page: Page) {

packages/playwright-core/src/server/recorder/recorderCollection.ts

Lines changed: 0 additions & 126 deletions
This file was deleted.

0 commit comments

Comments
 (0)