rebrowser-playwright-core
Version:
A drop-in replacement for playwright-core patched with rebrowser-patches. It allows to pass modern automation detection tests.
364 lines (358 loc) • 15.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.Recorder = void 0;
var fs = _interopRequireWildcard(require("fs"));
var consoleApiSource = _interopRequireWildcard(require("../generated/consoleApiSource"));
var _utils = require("../utils");
var _locatorParser = require("../utils/isomorphic/locatorParser");
var _browserContext = require("./browserContext");
var _debugger = require("./debugger");
var _contextRecorder = require("./recorder/contextRecorder");
var _recorderUtils = require("./recorder/recorderUtils");
var _recorderUtils2 = require("../utils/isomorphic/recorderUtils");
var _selectorParser = require("../utils/isomorphic/selectorParser");
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
/**
* 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.
*/
const recorderSymbol = Symbol('recorderSymbol');
class Recorder {
static async showInspector(context, params, recorderAppFactory) {
if ((0, _utils.isUnderTest)()) params.language = process.env.TEST_INSPECTOR_LANGUAGE;
return await Recorder.show('actions', context, recorderAppFactory, params);
}
static showInspectorNoReply(context, recorderAppFactory) {
Recorder.showInspector(context, {}, recorderAppFactory).catch(() => {});
}
static show(codegenMode, context, recorderAppFactory, params) {
let recorderPromise = context[recorderSymbol];
if (!recorderPromise) {
recorderPromise = Recorder._create(codegenMode, context, recorderAppFactory, params);
context[recorderSymbol] = recorderPromise;
}
return recorderPromise;
}
static async _create(codegenMode, context, recorderAppFactory, params = {}) {
const recorder = new Recorder(codegenMode, context, params);
const recorderApp = await recorderAppFactory(recorder);
await recorder._install(recorderApp);
return recorder;
}
constructor(codegenMode, context, params) {
this.handleSIGINT = void 0;
this._context = void 0;
this._mode = void 0;
this._highlightedElement = {};
this._overlayState = {
offsetX: 0
};
this._recorderApp = null;
this._currentCallsMetadata = new Map();
this._recorderSources = [];
this._userSources = new Map();
this._debugger = void 0;
this._contextRecorder = void 0;
this._omitCallTracking = false;
this._currentLanguage = void 0;
this._mode = params.mode || 'none';
this.handleSIGINT = params.handleSIGINT;
this._contextRecorder = new _contextRecorder.ContextRecorder(codegenMode, context, params, {});
this._context = context;
this._omitCallTracking = !!params.omitCallTracking;
this._debugger = context.debugger();
context.instrumentation.addListener(this, context);
this._currentLanguage = this._contextRecorder.languageName();
if ((0, _utils.isUnderTest)()) {
// Most of our tests put elements at the top left, so get out of the way.
this._overlayState.offsetX = 200;
}
}
async _install(recorderApp) {
this._recorderApp = recorderApp;
recorderApp.once('close', () => {
this._debugger.resume(false);
this._recorderApp = null;
});
recorderApp.on('event', data => {
if (data.event === 'setMode') {
this.setMode(data.params.mode);
return;
}
if (data.event === 'highlightRequested') {
if (data.params.selector) this.setHighlightedSelector(this._currentLanguage, data.params.selector);
if (data.params.ariaTemplate) this.setHighlightedAriaTemplate(data.params.ariaTemplate);
return;
}
if (data.event === 'step') {
this._debugger.resume(true);
return;
}
if (data.event === 'fileChanged') {
this._currentLanguage = this._contextRecorder.languageName(data.params.file);
this._refreshOverlay();
return;
}
if (data.event === 'resume') {
this._debugger.resume(false);
return;
}
if (data.event === 'pause') {
this._debugger.pauseOnNextStatement();
return;
}
if (data.event === 'clear') {
this._contextRecorder.clearScript();
return;
}
});
await Promise.all([recorderApp.setMode(this._mode), recorderApp.setPaused(this._debugger.isPaused()), this._pushAllSources()]);
this._context.once(_browserContext.BrowserContext.Events.Close, () => {
var _this$_recorderApp;
this._contextRecorder.dispose();
this._context.instrumentation.removeListener(this);
(_this$_recorderApp = this._recorderApp) === null || _this$_recorderApp === void 0 || _this$_recorderApp.close().catch(() => {});
});
this._contextRecorder.on(_contextRecorder.ContextRecorder.Events.Change, data => {
this._recorderSources = data.sources;
recorderApp.setActions(data.actions, data.sources);
recorderApp.setRunningFile(undefined);
this._pushAllSources();
});
await this._context.exposeBinding('__pw_recorderState', false, async source => {
let actionSelector;
let actionPoint;
const hasActiveScreenshotCommand = [...this._currentCallsMetadata.keys()].some(isScreenshotCommand);
if (!hasActiveScreenshotCommand) {
actionSelector = await this._scopeHighlightedSelectorToFrame(source.frame);
for (const [metadata, sdkObject] of this._currentCallsMetadata) {
if (source.page === sdkObject.attribution.page) {
actionPoint = metadata.point || actionPoint;
actionSelector = actionSelector || metadata.params.selector;
}
}
}
const uiState = {
mode: this._mode,
actionPoint,
actionSelector,
ariaTemplate: this._highlightedElement.ariaTemplate,
language: this._currentLanguage,
testIdAttributeName: this._contextRecorder.testIdAttributeName(),
overlay: this._overlayState
};
return uiState;
});
await this._context.exposeBinding('__pw_recorderElementPicked', false, async ({
frame
}, elementInfo) => {
var _this$_recorderApp2;
const selectorChain = await (0, _contextRecorder.generateFrameSelector)(frame);
await ((_this$_recorderApp2 = this._recorderApp) === null || _this$_recorderApp2 === void 0 ? void 0 : _this$_recorderApp2.elementPicked({
selector: (0, _recorderUtils2.buildFullSelector)(selectorChain, elementInfo.selector),
ariaSnapshot: elementInfo.ariaSnapshot
}, true));
});
await this._context.exposeBinding('__pw_recorderSetMode', false, async ({
frame
}, mode) => {
if (frame.parentFrame()) return;
this.setMode(mode);
});
await this._context.exposeBinding('__pw_recorderSetOverlayState', false, async ({
frame
}, state) => {
if (frame.parentFrame()) return;
this._overlayState = state;
});
await this._context.exposeBinding('__pw_resume', false, () => {
this._debugger.resume(false);
});
await this._context.extendInjectedScript(consoleApiSource.source);
await this._contextRecorder.install();
if (this._debugger.isPaused()) this._pausedStateChanged();
this._debugger.on(_debugger.Debugger.Events.PausedStateChanged, () => this._pausedStateChanged());
this._context.recorderAppForTest = this._recorderApp;
}
_pausedStateChanged() {
var _this$_recorderApp3;
// If we are called upon page.pause, we don't have metadatas, populate them.
for (const {
metadata,
sdkObject
} of this._debugger.pausedDetails()) {
if (!this._currentCallsMetadata.has(metadata)) this.onBeforeCall(sdkObject, metadata);
}
(_this$_recorderApp3 = this._recorderApp) === null || _this$_recorderApp3 === void 0 || _this$_recorderApp3.setPaused(this._debugger.isPaused());
this._updateUserSources();
this.updateCallLog([...this._currentCallsMetadata.keys()]);
}
setMode(mode) {
var _this$_recorderApp4;
if (this._mode === mode) return;
this._highlightedElement = {};
this._mode = mode;
(_this$_recorderApp4 = this._recorderApp) === null || _this$_recorderApp4 === void 0 || _this$_recorderApp4.setMode(this._mode);
this._contextRecorder.setEnabled(this._isRecording());
this._debugger.setMuted(this._isRecording());
if (this._mode !== 'none' && this._mode !== 'standby' && this._context.pages().length === 1) this._context.pages()[0].bringToFront().catch(() => {});
this._refreshOverlay();
}
resume() {
this._debugger.resume(false);
}
mode() {
return this._mode;
}
setHighlightedSelector(language, selector) {
this._highlightedElement = {
selector: (0, _locatorParser.locatorOrSelectorAsSelector)(language, selector, this._context.selectors().testIdAttributeName())
};
this._refreshOverlay();
}
setHighlightedAriaTemplate(ariaTemplate) {
this._highlightedElement = {
ariaTemplate
};
this._refreshOverlay();
}
hideHighlightedSelector() {
this._highlightedElement = {};
this._refreshOverlay();
}
async _scopeHighlightedSelectorToFrame(frame) {
if (!this._highlightedElement.selector) return;
try {
const mainFrame = frame._page.mainFrame();
const resolved = await mainFrame.selectors.resolveFrameForSelector(this._highlightedElement.selector);
// selector couldn't be found, don't highlight anything
if (!resolved) return '';
// selector points to no specific frame, highlight in all frames
if ((resolved === null || resolved === void 0 ? void 0 : resolved.frame) === mainFrame) return (0, _selectorParser.stringifySelector)(resolved.info.parsed);
// selector points to this frame, highlight it
if ((resolved === null || resolved === void 0 ? void 0 : resolved.frame) === frame) return (0, _selectorParser.stringifySelector)(resolved.info.parsed);
// selector points to a different frame, highlight nothing
return '';
} catch {
return '';
}
}
setOutput(codegenId, outputFile) {
this._contextRecorder.setOutput(codegenId, outputFile);
}
_refreshOverlay() {
for (const page of this._context.pages()) {
for (const frame of page.frames()) frame.evaluateExpression('window.__pw_refreshOverlay()').catch(() => {});
}
}
async onBeforeCall(sdkObject, metadata) {
if (this._omitCallTracking || this._isRecording()) return;
this._currentCallsMetadata.set(metadata, sdkObject);
this._updateUserSources();
this.updateCallLog([metadata]);
if (isScreenshotCommand(metadata)) this.hideHighlightedSelector();else if (metadata.params && metadata.params.selector) this._highlightedElement = {
selector: metadata.params.selector
};
}
async onAfterCall(sdkObject, metadata) {
if (this._omitCallTracking || this._isRecording()) return;
if (!metadata.error) this._currentCallsMetadata.delete(metadata);
this._updateUserSources();
this.updateCallLog([metadata]);
}
_updateUserSources() {
var _this$_recorderApp5;
// Remove old decorations.
for (const source of this._userSources.values()) {
source.highlight = [];
source.revealLine = undefined;
}
// Apply new decorations.
let fileToSelect = undefined;
for (const metadata of this._currentCallsMetadata.keys()) {
if (!metadata.location) continue;
const {
file,
line
} = metadata.location;
let source = this._userSources.get(file);
if (!source) {
source = {
isRecorded: false,
label: file,
id: file,
text: this._readSource(file),
highlight: [],
language: languageForFile(file)
};
this._userSources.set(file, source);
}
if (line) {
const paused = this._debugger.isPaused(metadata);
source.highlight.push({
line,
type: metadata.error ? 'error' : paused ? 'paused' : 'running'
});
source.revealLine = line;
fileToSelect = source.id;
}
}
this._pushAllSources();
if (fileToSelect) (_this$_recorderApp5 = this._recorderApp) === null || _this$_recorderApp5 === void 0 || _this$_recorderApp5.setRunningFile(fileToSelect);
}
_pushAllSources() {
var _this$_recorderApp6;
(_this$_recorderApp6 = this._recorderApp) === null || _this$_recorderApp6 === void 0 || _this$_recorderApp6.setSources([...this._recorderSources, ...this._userSources.values()]);
}
async onBeforeInputAction(sdkObject, metadata) {}
async onCallLog(sdkObject, metadata, logName, message) {
this.updateCallLog([metadata]);
}
updateCallLog(metadatas) {
var _this$_recorderApp7;
if (this._isRecording()) return;
const logs = [];
for (const metadata of metadatas) {
if (!metadata.method || metadata.internal) continue;
let status = 'done';
if (this._currentCallsMetadata.has(metadata)) status = 'in-progress';
if (this._debugger.isPaused(metadata)) status = 'paused';
logs.push((0, _recorderUtils.metadataToCallLog)(metadata, status));
}
(_this$_recorderApp7 = this._recorderApp) === null || _this$_recorderApp7 === void 0 || _this$_recorderApp7.updateCallLogs(logs);
}
_isRecording() {
return ['recording', 'assertingText', 'assertingVisibility', 'assertingValue', 'assertingSnapshot'].includes(this._mode);
}
_readSource(fileName) {
try {
return fs.readFileSync(fileName, 'utf-8');
} catch (e) {
return '// No source available';
}
}
}
exports.Recorder = Recorder;
function isScreenshotCommand(metadata) {
return metadata.method.toLowerCase().includes('screenshot');
}
function languageForFile(file) {
if (file.endsWith('.py')) return 'python';
if (file.endsWith('.java')) return 'java';
if (file.endsWith('.cs')) return 'csharp';
return 'javascript';
}