init commit

This commit is contained in:
iFlip721
2025-04-16 09:55:57 -04:00
commit a03209dc9e
935 changed files with 177492 additions and 0 deletions

View File

@@ -0,0 +1,168 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.Snapshotter = void 0;
var _browserContext = require("../../browserContext");
var _page = require("../../page");
var _eventsHelper = require("../../../utils/eventsHelper");
var _debugLogger = require("../../../common/debugLogger");
var _snapshotterInjected = require("./snapshotterInjected");
var _utils = require("../../../utils");
var _utilsBundle = require("../../../utilsBundle");
/**
* 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.
*/
class Snapshotter {
constructor(context, delegate) {
this._context = void 0;
this._delegate = void 0;
this._eventListeners = [];
this._snapshotStreamer = void 0;
this._initialized = false;
this._started = false;
this._context = context;
this._delegate = delegate;
const guid = (0, _utils.createGuid)();
this._snapshotStreamer = '__playwright_snapshot_streamer_' + guid;
}
started() {
return this._started;
}
async start() {
this._started = true;
if (!this._initialized) {
this._initialized = true;
await this._initialize();
}
await this.reset();
}
async reset() {
if (this._started) await this._runInAllFrames(`window["${this._snapshotStreamer}"].reset()`);
}
async stop() {
this._started = false;
}
resetForReuse() {
// Next time we start recording, we will call addInitScript again.
this._initialized = false;
}
async _initialize() {
for (const page of this._context.pages()) this._onPage(page);
this._eventListeners = [_eventsHelper.eventsHelper.addEventListener(this._context, _browserContext.BrowserContext.Events.Page, this._onPage.bind(this))];
const initScript = `(${_snapshotterInjected.frameSnapshotStreamer})("${this._snapshotStreamer}")`;
await this._context.addInitScript(initScript);
await this._runInAllFrames(initScript);
}
async _runInAllFrames(expression) {
const frames = [];
for (const page of this._context.pages()) frames.push(...page.frames());
await Promise.all(frames.map(frame => {
return frame.nonStallingRawEvaluateInExistingMainContext(expression).catch(e => _debugLogger.debugLogger.log('error', e));
}));
}
dispose() {
_eventsHelper.eventsHelper.removeEventListeners(this._eventListeners);
}
async captureSnapshot(page, callId, snapshotName, element) {
// Prepare expression synchronously.
const expression = `window["${this._snapshotStreamer}"].captureSnapshot(${JSON.stringify(snapshotName)})`;
// In a best-effort manner, without waiting for it, mark target element.
element === null || element === void 0 ? void 0 : element.callFunctionNoReply((element, callId) => {
const customEvent = new CustomEvent('__playwright_target__', {
bubbles: true,
cancelable: true,
detail: callId,
composed: false
});
element.dispatchEvent(customEvent);
}, callId);
// In each frame, in a non-stalling manner, capture the snapshots.
const snapshots = page.frames().map(async frame => {
const data = await frame.nonStallingRawEvaluateInExistingMainContext(expression).catch(e => _debugLogger.debugLogger.log('error', e));
// Something went wrong -> bail out, our snapshots are best-efforty.
if (!data || !this._started) return;
const snapshot = {
callId,
snapshotName,
pageId: page.guid,
frameId: frame.guid,
frameUrl: data.url,
doctype: data.doctype,
html: data.html,
viewport: data.viewport,
timestamp: (0, _utils.monotonicTime)(),
collectionTime: data.collectionTime,
resourceOverrides: [],
isMainFrame: page.mainFrame() === frame
};
for (const {
url,
content,
contentType
} of data.resourceOverrides) {
if (typeof content === 'string') {
const buffer = Buffer.from(content);
const sha1 = (0, _utils.calculateSha1)(buffer) + '.' + (_utilsBundle.mime.getExtension(contentType) || 'dat');
this._delegate.onSnapshotterBlob({
sha1,
buffer
});
snapshot.resourceOverrides.push({
url,
sha1
});
} else {
snapshot.resourceOverrides.push({
url,
ref: content
});
}
}
this._delegate.onFrameSnapshot(snapshot);
});
await Promise.all(snapshots);
}
_onPage(page) {
// Annotate frame hierarchy so that snapshots could include frame ids.
for (const frame of page.frames()) this._annotateFrameHierarchy(frame);
this._eventListeners.push(_eventsHelper.eventsHelper.addEventListener(page, _page.Page.Events.FrameAttached, frame => this._annotateFrameHierarchy(frame)));
}
async _annotateFrameHierarchy(frame) {
try {
const frameElement = await frame.frameElement();
const parent = frame.parentFrame();
if (!parent) return;
const context = await parent._mainContext();
await (context === null || context === void 0 ? void 0 : context.evaluate(({
snapshotStreamer,
frameElement,
frameId
}) => {
window[snapshotStreamer].markIframe(frameElement, frameId);
}, {
snapshotStreamer: this._snapshotStreamer,
frameElement,
frameId: frame.guid
}));
frameElement.dispose();
} catch (e) {}
}
}
exports.Snapshotter = Snapshotter;

View File

@@ -0,0 +1,480 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.frameSnapshotStreamer = frameSnapshotStreamer;
/**
* 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.
*/
function frameSnapshotStreamer(snapshotStreamer) {
// Communication with Playwright.
if (window[snapshotStreamer]) return;
// Attributes present in the snapshot.
const kShadowAttribute = '__playwright_shadow_root_';
const kValueAttribute = '__playwright_value_';
const kCheckedAttribute = '__playwright_checked_';
const kSelectedAttribute = '__playwright_selected_';
const kScrollTopAttribute = '__playwright_scroll_top_';
const kScrollLeftAttribute = '__playwright_scroll_left_';
const kStyleSheetAttribute = '__playwright_style_sheet_';
const kTargetAttribute = '__playwright_target__';
const kCustomElementsAttribute = '__playwright_custom_elements__';
// Symbols for our own info on Nodes/StyleSheets.
const kSnapshotFrameId = Symbol('__playwright_snapshot_frameid_');
const kCachedData = Symbol('__playwright_snapshot_cache_');
const kEndOfList = Symbol('__playwright_end_of_list_');
function resetCachedData(obj) {
delete obj[kCachedData];
}
function ensureCachedData(obj) {
if (!obj[kCachedData]) obj[kCachedData] = {};
return obj[kCachedData];
}
function removeHash(url) {
try {
const u = new URL(url);
u.hash = '';
return u.toString();
} catch (e) {
return url;
}
}
class Streamer {
// To avoid invalidating due to our own reads.
constructor() {
this._removeNoScript = true;
this._lastSnapshotNumber = 0;
this._staleStyleSheets = new Set();
this._readingStyleSheet = false;
this._fakeBase = void 0;
this._observer = void 0;
this._interceptNativeMethod(window.CSSStyleSheet.prototype, 'insertRule', sheet => this._invalidateStyleSheet(sheet));
this._interceptNativeMethod(window.CSSStyleSheet.prototype, 'deleteRule', sheet => this._invalidateStyleSheet(sheet));
this._interceptNativeMethod(window.CSSStyleSheet.prototype, 'addRule', sheet => this._invalidateStyleSheet(sheet));
this._interceptNativeMethod(window.CSSStyleSheet.prototype, 'removeRule', sheet => this._invalidateStyleSheet(sheet));
this._interceptNativeGetter(window.CSSStyleSheet.prototype, 'rules', sheet => this._invalidateStyleSheet(sheet));
this._interceptNativeGetter(window.CSSStyleSheet.prototype, 'cssRules', sheet => this._invalidateStyleSheet(sheet));
this._interceptNativeMethod(window.CSSStyleSheet.prototype, 'replaceSync', sheet => this._invalidateStyleSheet(sheet));
this._interceptNativeAsyncMethod(window.CSSStyleSheet.prototype, 'replace', sheet => this._invalidateStyleSheet(sheet));
this._fakeBase = document.createElement('base');
this._observer = new MutationObserver(list => this._handleMutations(list));
const observerConfig = {
attributes: true,
subtree: true
};
this._observer.observe(document, observerConfig);
this._refreshListenersWhenNeeded();
}
_refreshListenersWhenNeeded() {
this._refreshListeners();
const customEventName = '__playwright_snapshotter_global_listeners_check__';
let seenEvent = false;
const handleCustomEvent = () => seenEvent = true;
window.addEventListener(customEventName, handleCustomEvent);
const observer = new MutationObserver(entries => {
// Check for new documentElement in case we need to reinstall document listeners.
const newDocumentElement = entries.some(entry => Array.from(entry.addedNodes).includes(document.documentElement));
if (newDocumentElement) {
// New documentElement - let's check whether listeners are still here.
seenEvent = false;
window.dispatchEvent(new CustomEvent(customEventName));
if (!seenEvent) {
// Listener did not fire. Reattach the listener and notify.
window.addEventListener(customEventName, handleCustomEvent);
this._refreshListeners();
}
}
});
observer.observe(document, {
childList: true
});
}
_refreshListeners() {
document.addEventListener('__playwright_target__', event => {
if (!event.detail) return;
const callId = event.detail;
event.target.__playwright_target__ = callId;
});
}
_interceptNativeMethod(obj, method, cb) {
const native = obj[method];
if (!native) return;
obj[method] = function (...args) {
const result = native.call(this, ...args);
cb(this, result);
return result;
};
}
_interceptNativeAsyncMethod(obj, method, cb) {
const native = obj[method];
if (!native) return;
obj[method] = async function (...args) {
const result = await native.call(this, ...args);
cb(this, result);
return result;
};
}
_interceptNativeGetter(obj, prop, cb) {
const descriptor = Object.getOwnPropertyDescriptor(obj, prop);
Object.defineProperty(obj, prop, {
...descriptor,
get: function () {
const result = descriptor.get.call(this);
cb(this, result);
return result;
}
});
}
_handleMutations(list) {
for (const mutation of list) ensureCachedData(mutation.target).attributesCached = undefined;
}
_invalidateStyleSheet(sheet) {
if (this._readingStyleSheet) return;
this._staleStyleSheets.add(sheet);
}
_updateStyleElementStyleSheetTextIfNeeded(sheet, forceText) {
const data = ensureCachedData(sheet);
if (this._staleStyleSheets.has(sheet) || forceText && data.cssText === undefined) {
this._staleStyleSheets.delete(sheet);
try {
data.cssText = this._getSheetText(sheet);
} catch (e) {
// Sometimes we cannot access cross-origin stylesheets.
data.cssText = '';
}
}
return data.cssText;
}
// Returns either content, ref, or no override.
_updateLinkStyleSheetTextIfNeeded(sheet, snapshotNumber) {
const data = ensureCachedData(sheet);
if (this._staleStyleSheets.has(sheet)) {
this._staleStyleSheets.delete(sheet);
try {
data.cssText = this._getSheetText(sheet);
data.cssRef = snapshotNumber;
return data.cssText;
} catch (e) {
// Sometimes we cannot access cross-origin stylesheets.
}
}
return data.cssRef === undefined ? undefined : snapshotNumber - data.cssRef;
}
markIframe(iframeElement, frameId) {
iframeElement[kSnapshotFrameId] = frameId;
}
reset() {
this._staleStyleSheets.clear();
const visitNode = node => {
resetCachedData(node);
if (node.nodeType === Node.ELEMENT_NODE) {
const element = node;
if (element.shadowRoot) visitNode(element.shadowRoot);
}
for (let child = node.firstChild; child; child = child.nextSibling) visitNode(child);
};
visitNode(document.documentElement);
visitNode(this._fakeBase);
}
__sanitizeMetaAttribute(name, value, httpEquiv) {
if (name === 'charset') return 'utf-8';
if (httpEquiv.toLowerCase() !== 'content-type' || name !== 'content') return value;
const [type, ...params] = value.split(';');
if (type !== 'text/html' || params.length <= 0) return value;
const charsetParamIdx = params.findIndex(param => param.trim().startsWith('charset='));
if (charsetParamIdx > -1) params[charsetParamIdx] = 'charset=utf-8';
return `${type}; ${params.join('; ')}`;
}
_sanitizeUrl(url) {
if (url.startsWith('javascript:') || url.startsWith('vbscript:')) return '';
return url;
}
_sanitizeSrcSet(srcset) {
return srcset.split(',').map(src => {
src = src.trim();
const spaceIndex = src.lastIndexOf(' ');
if (spaceIndex === -1) return this._sanitizeUrl(src);
return this._sanitizeUrl(src.substring(0, spaceIndex).trim()) + src.substring(spaceIndex);
}).join(', ');
}
_resolveUrl(base, url) {
if (url === '') return '';
try {
return new URL(url, base).href;
} catch (e) {
return url;
}
}
_getSheetBase(sheet) {
let rootSheet = sheet;
while (rootSheet.parentStyleSheet) rootSheet = rootSheet.parentStyleSheet;
if (rootSheet.ownerNode) return rootSheet.ownerNode.baseURI;
return document.baseURI;
}
_getSheetText(sheet) {
this._readingStyleSheet = true;
try {
const rules = [];
for (const rule of sheet.cssRules) rules.push(rule.cssText);
return rules.join('\n');
} finally {
this._readingStyleSheet = false;
}
}
captureSnapshot() {
const timestamp = performance.now();
const snapshotNumber = ++this._lastSnapshotNumber;
let nodeCounter = 0;
let shadowDomNesting = 0;
let headNesting = 0;
// Ensure we are up to date.
this._handleMutations(this._observer.takeRecords());
const definedCustomElements = new Set();
const visitNode = node => {
const nodeType = node.nodeType;
const nodeName = nodeType === Node.DOCUMENT_FRAGMENT_NODE ? 'template' : node.nodeName;
if (nodeType !== Node.ELEMENT_NODE && nodeType !== Node.DOCUMENT_FRAGMENT_NODE && nodeType !== Node.TEXT_NODE) return;
if (nodeName === 'SCRIPT') return;
// Don't preload resources.
if (nodeName === 'LINK' && nodeType === Node.ELEMENT_NODE) {
var _getAttribute;
const rel = (_getAttribute = node.getAttribute('rel')) === null || _getAttribute === void 0 ? void 0 : _getAttribute.toLowerCase();
if (rel === 'preload' || rel === 'prefetch') return;
}
if (this._removeNoScript && nodeName === 'NOSCRIPT') return;
if (nodeName === 'META' && node.httpEquiv.toLowerCase() === 'content-security-policy') return;
// Skip iframes which are inside document's head as they are not visisble.
// See https://github.com/microsoft/playwright/issues/12005.
if ((nodeName === 'IFRAME' || nodeName === 'FRAME') && headNesting) return;
const data = ensureCachedData(node);
const values = [];
let equals = !!data.cached;
let extraNodes = 0;
const expectValue = value => {
equals = equals && data.cached[values.length] === value;
values.push(value);
};
const checkAndReturn = n => {
data.attributesCached = true;
if (equals) return {
equals: true,
n: [[snapshotNumber - data.ref[0], data.ref[1]]]
};
nodeCounter += extraNodes;
data.ref = [snapshotNumber, nodeCounter++];
data.cached = values;
return {
equals: false,
n
};
};
if (nodeType === Node.TEXT_NODE) {
const value = node.nodeValue || '';
expectValue(value);
return checkAndReturn(value);
}
if (nodeName === 'STYLE') {
const sheet = node.sheet;
let cssText;
if (sheet) cssText = this._updateStyleElementStyleSheetTextIfNeeded(sheet);
cssText = cssText || node.textContent || '';
expectValue(cssText);
// Compensate for the extra 'cssText' text node.
extraNodes++;
return checkAndReturn([nodeName, {}, cssText]);
}
const attrs = {};
const result = [nodeName, attrs];
const visitChild = child => {
const snapshot = visitNode(child);
if (snapshot) {
result.push(snapshot.n);
expectValue(child);
equals = equals && snapshot.equals;
}
};
const visitChildStyleSheet = child => {
const snapshot = visitStyleSheet(child);
if (snapshot) {
result.push(snapshot.n);
expectValue(child);
equals = equals && snapshot.equals;
}
};
if (nodeType === Node.DOCUMENT_FRAGMENT_NODE) attrs[kShadowAttribute] = 'open';
if (nodeType === Node.ELEMENT_NODE) {
var _window$customElement;
const element = node;
if (element.localName.includes('-') && (_window$customElement = window.customElements) !== null && _window$customElement !== void 0 && _window$customElement.get(element.localName)) definedCustomElements.add(element.localName);
if (nodeName === 'INPUT' || nodeName === 'TEXTAREA') {
const value = element.value;
expectValue(kValueAttribute);
expectValue(value);
attrs[kValueAttribute] = value;
}
if (nodeName === 'INPUT' && ['checkbox', 'radio'].includes(element.type)) {
const value = element.checked ? 'true' : 'false';
expectValue(kCheckedAttribute);
expectValue(value);
attrs[kCheckedAttribute] = value;
}
if (nodeName === 'OPTION') {
const value = element.selected ? 'true' : 'false';
expectValue(kSelectedAttribute);
expectValue(value);
attrs[kSelectedAttribute] = value;
}
if (element.scrollTop) {
expectValue(kScrollTopAttribute);
expectValue(element.scrollTop);
attrs[kScrollTopAttribute] = '' + element.scrollTop;
}
if (element.scrollLeft) {
expectValue(kScrollLeftAttribute);
expectValue(element.scrollLeft);
attrs[kScrollLeftAttribute] = '' + element.scrollLeft;
}
if (element.shadowRoot) {
++shadowDomNesting;
visitChild(element.shadowRoot);
--shadowDomNesting;
}
if ('__playwright_target__' in element) {
expectValue(kTargetAttribute);
expectValue(element['__playwright_target__']);
attrs[kTargetAttribute] = element['__playwright_target__'];
}
}
if (nodeName === 'HEAD') {
++headNesting;
// Insert fake <base> first, to ensure all <link> elements use the proper base uri.
this._fakeBase.setAttribute('href', document.baseURI);
visitChild(this._fakeBase);
}
for (let child = node.firstChild; child; child = child.nextSibling) visitChild(child);
if (nodeName === 'HEAD') --headNesting;
expectValue(kEndOfList);
let documentOrShadowRoot = null;
if (node.ownerDocument.documentElement === node) documentOrShadowRoot = node.ownerDocument;else if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) documentOrShadowRoot = node;
if (documentOrShadowRoot) {
for (const sheet of documentOrShadowRoot.adoptedStyleSheets || []) visitChildStyleSheet(sheet);
expectValue(kEndOfList);
}
// Process iframe src attribute before bailing out since it depends on a symbol, not the DOM.
if (nodeName === 'IFRAME' || nodeName === 'FRAME') {
const element = node;
const frameId = element[kSnapshotFrameId];
const name = 'src';
const value = frameId ? `/snapshot/${frameId}` : '';
expectValue(name);
expectValue(value);
attrs[name] = value;
}
// Process custom elements before bailing out since they depend on JS, not the DOM.
if (nodeName === 'BODY' && definedCustomElements.size) {
const value = [...definedCustomElements].join(',');
expectValue(kCustomElementsAttribute);
expectValue(value);
attrs[kCustomElementsAttribute] = value;
}
// We can skip attributes comparison because nothing else has changed,
// and mutation observer didn't tell us about the attributes.
if (equals && data.attributesCached && !shadowDomNesting) return checkAndReturn(result);
if (nodeType === Node.ELEMENT_NODE) {
const element = node;
for (let i = 0; i < element.attributes.length; i++) {
const name = element.attributes[i].name;
if (nodeName === 'LINK' && name === 'integrity') continue;
if (nodeName === 'IFRAME' && (name === 'src' || name === 'srcdoc' || name === 'sandbox')) continue;
if (nodeName === 'FRAME' && name === 'src') continue;
let value = element.attributes[i].value;
if (nodeName === 'META') value = this.__sanitizeMetaAttribute(name, value, node.httpEquiv);else if (name === 'src' && nodeName === 'IMG') value = this._sanitizeUrl(value);else if (name === 'srcset' && nodeName === 'IMG') value = this._sanitizeSrcSet(value);else if (name === 'srcset' && nodeName === 'SOURCE') value = this._sanitizeSrcSet(value);else if (name === 'href' && nodeName === 'LINK') value = this._sanitizeUrl(value);else if (name.startsWith('on')) value = '';
expectValue(name);
expectValue(value);
attrs[name] = value;
}
expectValue(kEndOfList);
}
if (result.length === 2 && !Object.keys(attrs).length) result.pop(); // Remove empty attrs when there are no children.
return checkAndReturn(result);
};
const visitStyleSheet = sheet => {
const data = ensureCachedData(sheet);
const oldCSSText = data.cssText;
const cssText = this._updateStyleElementStyleSheetTextIfNeeded(sheet, true /* forceText */);
if (cssText === oldCSSText) return {
equals: true,
n: [[snapshotNumber - data.ref[0], data.ref[1]]]
};
data.ref = [snapshotNumber, nodeCounter++];
return {
equals: false,
n: ['template', {
[kStyleSheetAttribute]: cssText
}]
};
};
let html;
if (document.documentElement) {
const {
n
} = visitNode(document.documentElement);
html = n;
} else {
html = ['html'];
}
const result = {
html,
doctype: document.doctype ? document.doctype.name : undefined,
resourceOverrides: [],
viewport: {
width: window.innerWidth,
height: window.innerHeight
},
url: location.href,
timestamp,
collectionTime: 0
};
for (const sheet of this._staleStyleSheets) {
if (sheet.href === null) continue;
const content = this._updateLinkStyleSheetTextIfNeeded(sheet, snapshotNumber);
if (content === undefined) {
// Unable to capture stylesheet contents.
continue;
}
const base = this._getSheetBase(sheet);
const url = removeHash(this._resolveUrl(base, sheet.href));
result.resourceOverrides.push({
url,
content,
contentType: 'text/css'
});
}
result.collectionTime = performance.now() - result.timestamp;
return result;
}
}
window[snapshotStreamer] = new Streamer();
}

View File

@@ -0,0 +1,506 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.Tracing = void 0;
exports.shouldCaptureSnapshot = shouldCaptureSnapshot;
var _fs = _interopRequireDefault(require("fs"));
var _os = _interopRequireDefault(require("os"));
var _path = _interopRequireDefault(require("path"));
var _debug = require("../../../protocol/debug");
var _manualPromise = require("../../../utils/manualPromise");
var _eventsHelper = require("../../../utils/eventsHelper");
var _utils = require("../../../utils");
var _fileUtils = require("../../../utils/fileUtils");
var _artifact = require("../../artifact");
var _browserContext = require("../../browserContext");
var _dom = require("../../dom");
var _instrumentation = require("../../instrumentation");
var _page = require("../../page");
var _harTracer = require("../../har/harTracer");
var _snapshotter = require("./snapshotter");
var _zipBundle = require("../../../zipBundle");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/**
* 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 version = 4;
const kScreencastOptions = {
width: 800,
height: 600,
quality: 90
};
class Tracing extends _instrumentation.SdkObject {
constructor(context, tracesDir) {
super(context, 'tracing');
this._writeChain = Promise.resolve();
this._snapshotter = void 0;
this._harTracer = void 0;
this._screencastListeners = [];
this._eventListeners = [];
this._context = void 0;
this._state = void 0;
this._isStopping = false;
this._precreatedTracesDir = void 0;
this._tracesTmpDir = void 0;
this._allResources = new Set();
this._contextCreatedEvent = void 0;
this._context = context;
this._precreatedTracesDir = tracesDir;
this._harTracer = new _harTracer.HarTracer(context, null, this, {
content: 'attach',
includeTraceInfo: true,
recordRequestOverrides: false,
waitForContentOnStop: false,
skipScripts: true
});
const testIdAttributeName = 'selectors' in context ? context.selectors().testIdAttributeName() : undefined;
this._contextCreatedEvent = {
version,
type: 'context-options',
browserName: '',
options: {},
platform: process.platform,
wallTime: 0,
sdkLanguage: context.attribution.playwright.options.sdkLanguage,
testIdAttributeName
};
if (context instanceof _browserContext.BrowserContext) {
this._snapshotter = new _snapshotter.Snapshotter(context, this);
(0, _utils.assert)(tracesDir, 'tracesDir must be specified for BrowserContext');
this._contextCreatedEvent.browserName = context._browser.options.name;
this._contextCreatedEvent.options = context._options;
}
}
resetForReuse() {
var _this$_snapshotter;
(_this$_snapshotter = this._snapshotter) === null || _this$_snapshotter === void 0 ? void 0 : _this$_snapshotter.resetForReuse();
}
async start(options) {
if (this._isStopping) throw new Error('Cannot start tracing while stopping');
// Re-write for testing.
this._contextCreatedEvent.sdkLanguage = this._context.attribution.playwright.options.sdkLanguage;
if (this._state) {
const o = this._state.options;
if (!o.screenshots !== !options.screenshots || !o.snapshots !== !options.snapshots) throw new Error('Tracing has been already started with different options');
if (options.name && options.name !== this._state.traceName) await this._changeTraceName(this._state, options.name);
return;
}
// TODO: passing the same name for two contexts makes them write into a single file
// and conflict.
const traceName = options.name || (0, _utils.createGuid)();
const tracesDir = this._createTracesDirIfNeeded();
// Init the state synchronously.
this._state = {
options,
traceName,
tracesDir,
traceFile: {
file: _path.default.join(tracesDir, traceName + '.trace'),
buffer: []
},
networkFile: {
file: _path.default.join(tracesDir, traceName + '.network'),
buffer: []
},
resourcesDir: _path.default.join(tracesDir, 'resources'),
chunkOrdinal: 0,
traceSha1s: new Set(),
networkSha1s: new Set(),
recording: false
};
const state = this._state;
this._writeChain = _fs.default.promises.mkdir(state.resourcesDir, {
recursive: true
}).then(() => _fs.default.promises.writeFile(state.networkFile.file, ''));
if (options.snapshots) this._harTracer.start();
}
async startChunk(options = {}) {
var _this$_snapshotter2;
if (this._state && this._state.recording) await this.stopChunk({
mode: 'discard'
});
if (!this._state) throw new Error('Must start tracing before starting a new chunk');
if (this._isStopping) throw new Error('Cannot start a trace chunk while stopping');
const state = this._state;
const suffix = state.chunkOrdinal ? `-${state.chunkOrdinal}` : ``;
state.chunkOrdinal++;
state.traceFile = {
file: _path.default.join(state.tracesDir, `${state.traceName}${suffix}.trace`),
buffer: []
};
state.recording = true;
if (options.name && options.name !== this._state.traceName) this._changeTraceName(this._state, options.name);
this._appendTraceOperation(async () => {
await (0, _fileUtils.mkdirIfNeeded)(state.traceFile.file);
const event = {
...this._contextCreatedEvent,
title: options.title,
wallTime: Date.now()
};
await appendEventAndFlushIfNeeded(state.traceFile, event);
});
this._context.instrumentation.addListener(this, this._context);
this._eventListeners.push(_eventsHelper.eventsHelper.addEventListener(this._context, _browserContext.BrowserContext.Events.Console, this._onConsoleMessage.bind(this)));
if (state.options.screenshots) this._startScreencast();
if (state.options.snapshots) await ((_this$_snapshotter2 = this._snapshotter) === null || _this$_snapshotter2 === void 0 ? void 0 : _this$_snapshotter2.start());
return {
traceName: state.traceName
};
}
_startScreencast() {
if (!(this._context instanceof _browserContext.BrowserContext)) return;
for (const page of this._context.pages()) this._startScreencastInPage(page);
this._screencastListeners.push(_eventsHelper.eventsHelper.addEventListener(this._context, _browserContext.BrowserContext.Events.Page, this._startScreencastInPage.bind(this)));
}
_stopScreencast() {
_eventsHelper.eventsHelper.removeEventListeners(this._screencastListeners);
if (!(this._context instanceof _browserContext.BrowserContext)) return;
for (const page of this._context.pages()) page.setScreencastOptions(null);
}
async _changeTraceName(state, name) {
await this._appendTraceOperation(async () => {
await flushTraceFile(state.traceFile);
await flushTraceFile(state.networkFile);
const oldNetworkFile = state.networkFile;
state.traceName = name;
state.traceFile = {
file: _path.default.join(state.tracesDir, name + '.trace'),
buffer: []
};
state.networkFile = {
file: _path.default.join(state.tracesDir, name + '.network'),
buffer: []
};
// Network file survives across chunks, so make a copy with the new name.
await _fs.default.promises.copyFile(oldNetworkFile.file, state.networkFile.file);
});
}
async stop() {
if (!this._state) return;
if (this._isStopping) throw new Error(`Tracing is already stopping`);
if (this._state.recording) throw new Error(`Must stop trace file before stopping tracing`);
this._harTracer.stop();
await this._writeChain;
this._state = undefined;
}
async deleteTmpTracesDir() {
if (this._tracesTmpDir) await (0, _fileUtils.removeFolders)([this._tracesTmpDir]);
}
_createTracesDirIfNeeded() {
if (this._precreatedTracesDir) return this._precreatedTracesDir;
this._tracesTmpDir = _fs.default.mkdtempSync(_path.default.join(_os.default.tmpdir(), 'playwright-tracing-'));
return this._tracesTmpDir;
}
async dispose() {
var _this$_snapshotter3;
(_this$_snapshotter3 = this._snapshotter) === null || _this$_snapshotter3 === void 0 ? void 0 : _this$_snapshotter3.dispose();
this._harTracer.stop();
await this._writeChain;
}
async stopChunk(params) {
var _this$_state, _this$_snapshotter4;
if (this._isStopping) throw new Error(`Tracing is already stopping`);
this._isStopping = true;
if (!this._state || !this._state.recording) {
this._isStopping = false;
if (params.mode !== 'discard') throw new Error(`Must start tracing before stopping`);
return {};
}
const state = this._state;
this._context.instrumentation.removeListener(this);
_eventsHelper.eventsHelper.removeEventListeners(this._eventListeners);
if ((_this$_state = this._state) !== null && _this$_state !== void 0 && _this$_state.options.screenshots) this._stopScreencast();
if (state.options.snapshots) await ((_this$_snapshotter4 = this._snapshotter) === null || _this$_snapshotter4 === void 0 ? void 0 : _this$_snapshotter4.stop());
// Chain the export operation against write operations,
// so that neither trace files nor sha1s change during the export.
return (await this._appendTraceOperation(async () => {
if (params.mode === 'discard') return {};
await flushTraceFile(state.traceFile);
await flushTraceFile(state.networkFile);
// Network file survives across chunks, make a snapshot before returning the resulting entries.
// We should pick a name starting with "traceName" and ending with .network.
// Something like <traceName>someSuffixHere.network.
// However, this name must not clash with any other "traceName".network in the same tracesDir.
// We can use <traceName>-<guid>.network, but "-pwnetcopy-0" suffix is more readable
// and makes it easier to debug future issues.
const networkFile = _path.default.join(state.tracesDir, state.traceName + `-pwnetcopy-${state.chunkOrdinal}.network`);
await _fs.default.promises.copyFile(state.networkFile.file, networkFile);
const entries = [];
entries.push({
name: 'trace.trace',
value: state.traceFile.file
});
entries.push({
name: 'trace.network',
value: networkFile
});
for (const sha1 of new Set([...state.traceSha1s, ...state.networkSha1s])) entries.push({
name: _path.default.join('resources', sha1),
value: _path.default.join(state.resourcesDir, sha1)
});
if (params.mode === 'entries') return {
entries
};
const artifact = await this._exportZip(entries, state).catch(() => undefined);
return {
artifact
};
}).finally(() => {
// Only reset trace sha1s, network resources are preserved between chunks.
state.traceSha1s = new Set();
this._isStopping = false;
state.recording = false;
})) || {};
}
_exportZip(entries, state) {
const zipFile = new _zipBundle.yazl.ZipFile();
const result = new _manualPromise.ManualPromise();
zipFile.on('error', error => result.reject(error));
for (const entry of entries) zipFile.addFile(entry.value, entry.name);
zipFile.end();
const zipFileName = state.traceFile.file + '.zip';
zipFile.outputStream.pipe(_fs.default.createWriteStream(zipFileName)).on('close', () => {
const artifact = new _artifact.Artifact(this._context, zipFileName);
artifact.reportFinished();
result.resolve(artifact);
});
return result;
}
async _captureSnapshot(snapshotName, sdkObject, metadata, element) {
if (!this._snapshotter) return;
if (!sdkObject.attribution.page) return;
if (!this._snapshotter.started()) return;
if (!shouldCaptureSnapshot(metadata)) return;
// We have |element| for input actions (page.click and handle.click)
// and |sdkObject| element for accessors like handle.textContent.
if (!element && sdkObject instanceof _dom.ElementHandle) element = sdkObject;
await this._snapshotter.captureSnapshot(sdkObject.attribution.page, metadata.id, snapshotName, element).catch(() => {});
}
onBeforeCall(sdkObject, metadata) {
var _sdkObject$attributio;
// IMPORTANT: no awaits before this._appendTraceEvent in this method.
const event = createBeforeActionTraceEvent(metadata);
if (!event) return Promise.resolve();
(_sdkObject$attributio = sdkObject.attribution.page) === null || _sdkObject$attributio === void 0 ? void 0 : _sdkObject$attributio.temporarlyDisableTracingScreencastThrottling();
event.beforeSnapshot = `before@${metadata.id}`;
this._appendTraceEvent(event);
return this._captureSnapshot(event.beforeSnapshot, sdkObject, metadata);
}
onBeforeInputAction(sdkObject, metadata, element) {
var _sdkObject$attributio2;
// IMPORTANT: no awaits before this._appendTraceEvent in this method.
const event = createInputActionTraceEvent(metadata);
if (!event) return Promise.resolve();
(_sdkObject$attributio2 = sdkObject.attribution.page) === null || _sdkObject$attributio2 === void 0 ? void 0 : _sdkObject$attributio2.temporarlyDisableTracingScreencastThrottling();
event.inputSnapshot = `input@${metadata.id}`;
this._appendTraceEvent(event);
return this._captureSnapshot(event.inputSnapshot, sdkObject, metadata, element);
}
async onAfterCall(sdkObject, metadata) {
var _sdkObject$attributio3;
const event = createAfterActionTraceEvent(metadata);
if (!event) return Promise.resolve();
(_sdkObject$attributio3 = sdkObject.attribution.page) === null || _sdkObject$attributio3 === void 0 ? void 0 : _sdkObject$attributio3.temporarlyDisableTracingScreencastThrottling();
event.afterSnapshot = `after@${metadata.id}`;
this._appendTraceEvent(event);
return this._captureSnapshot(event.afterSnapshot, sdkObject, metadata);
}
onEvent(sdkObject, event) {
if (!sdkObject.attribution.context) return;
if (event.method === 'console' || event.method === '__create__' && event.class === 'ConsoleMessage') {
// Console messages are handled separately.
return;
}
this._appendTraceEvent(event);
}
onEntryStarted(entry) {}
onEntryFinished(entry) {
const event = {
type: 'resource-snapshot',
snapshot: entry
};
this._appendTraceOperation(async () => {
const visited = visitTraceEvent(event, this._state.networkSha1s);
await appendEventAndFlushIfNeeded(this._state.networkFile, visited);
});
}
onContentBlob(sha1, buffer) {
this._appendResource(sha1, buffer);
}
onSnapshotterBlob(blob) {
this._appendResource(blob.sha1, blob.buffer);
}
onFrameSnapshot(snapshot) {
this._appendTraceEvent({
type: 'frame-snapshot',
snapshot
});
}
_onConsoleMessage(message) {
const object = {
type: 'object',
class: 'ConsoleMessage',
guid: message.guid,
initializer: {
type: message.type(),
text: message.text(),
location: message.location()
}
};
this._appendTraceEvent(object);
const event = {
type: 'event',
class: 'BrowserContext',
method: 'console',
params: {
message: {
guid: message.guid
}
},
time: (0, _utils.monotonicTime)(),
pageId: message.page().guid
};
this._appendTraceEvent(event);
}
_startScreencastInPage(page) {
page.setScreencastOptions(kScreencastOptions);
const prefix = page.guid;
this._screencastListeners.push(_eventsHelper.eventsHelper.addEventListener(page, _page.Page.Events.ScreencastFrame, params => {
const suffix = params.timestamp || Date.now();
const sha1 = `${prefix}-${suffix}.jpeg`;
const event = {
type: 'screencast-frame',
pageId: page.guid,
sha1,
width: params.width,
height: params.height,
timestamp: (0, _utils.monotonicTime)()
};
// Make sure to write the screencast frame before adding a reference to it.
this._appendResource(sha1, params.buffer);
this._appendTraceEvent(event);
}));
}
_appendTraceEvent(event) {
this._appendTraceOperation(async () => {
const visited = visitTraceEvent(event, this._state.traceSha1s);
await appendEventAndFlushIfNeeded(this._state.traceFile, visited);
});
}
_appendResource(sha1, buffer) {
if (this._allResources.has(sha1)) return;
this._allResources.add(sha1);
const resourcePath = _path.default.join(this._state.resourcesDir, sha1);
this._appendTraceOperation(async () => {
// Note: 'wx' flag only writes when the file does not exist.
// See https://nodejs.org/api/fs.html#file-system-flags.
// This way tracing never have to write the same resource twice.
await _fs.default.promises.writeFile(resourcePath, buffer, {
flag: 'wx'
}).catch(() => {});
});
}
async _appendTraceOperation(cb) {
// This method serializes all writes to the trace.
let error;
let result;
this._writeChain = this._writeChain.then(async () => {
// This check is here because closing the browser removes the tracesDir and tracing
// dies trying to archive.
if (this._context instanceof _browserContext.BrowserContext && !this._context._browser.isConnected()) return;
try {
result = await cb();
} catch (e) {
error = e;
}
});
await this._writeChain;
if (error) throw error;
return result;
}
}
exports.Tracing = Tracing;
function visitTraceEvent(object, sha1s) {
if (Array.isArray(object)) return object.map(o => visitTraceEvent(o, sha1s));
if (object instanceof Buffer) return undefined;
if (typeof object === 'object') {
const result = {};
for (const key in object) {
if (key === 'sha1' || key === '_sha1' || key.endsWith('Sha1')) {
const sha1 = object[key];
if (sha1) sha1s.add(sha1);
}
result[key] = visitTraceEvent(object[key], sha1s);
}
return result;
}
return object;
}
function shouldCaptureSnapshot(metadata) {
return _debug.commandsWithTracingSnapshots.has(metadata.type + '.' + metadata.method);
}
function createBeforeActionTraceEvent(metadata) {
if (metadata.internal || metadata.method.startsWith('tracing')) return null;
return {
type: 'before',
callId: metadata.id,
startTime: metadata.startTime,
apiName: metadata.apiName || metadata.type + '.' + metadata.method,
class: metadata.type,
method: metadata.method,
params: metadata.params,
wallTime: metadata.wallTime,
pageId: metadata.pageId
};
}
function createInputActionTraceEvent(metadata) {
if (metadata.internal || metadata.method.startsWith('tracing')) return null;
return {
type: 'input',
callId: metadata.id,
point: metadata.point
};
}
function createAfterActionTraceEvent(metadata) {
var _metadata$error;
if (metadata.internal || metadata.method.startsWith('tracing')) return null;
return {
type: 'after',
callId: metadata.id,
endTime: metadata.endTime,
log: metadata.log,
error: (_metadata$error = metadata.error) === null || _metadata$error === void 0 ? void 0 : _metadata$error.error,
result: metadata.result
};
}
async function appendEventAndFlushIfNeeded(file, event) {
file.buffer.push(event);
// Do not flush events, they are too noisy.
if (event.type === 'event' || event.type === 'object') return;
await flushTraceFile(file);
}
async function flushTraceFile(file) {
const data = file.buffer.map(e => Buffer.from(JSON.stringify(e) + '\n'));
await _fs.default.promises.appendFile(file.file, Buffer.concat(data));
file.buffer = [];
}

View File

@@ -0,0 +1,90 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.InMemorySnapshotter = void 0;
var _snapshotStorage = require("../../../../../trace-viewer/src/snapshotStorage");
var _snapshotter = require("../recorder/snapshotter");
var _harTracer = require("../../har/harTracer");
var _utils = require("../../../utils");
/**
* 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.
*/
class InMemorySnapshotter {
constructor(context) {
this._blobs = new Map();
this._snapshotter = void 0;
this._harTracer = void 0;
this._snapshotReadyPromises = new Map();
this._storage = void 0;
this._snapshotCount = 0;
this._snapshotter = new _snapshotter.Snapshotter(context, this);
this._harTracer = new _harTracer.HarTracer(context, null, this, {
content: 'attach',
includeTraceInfo: true,
recordRequestOverrides: false,
waitForContentOnStop: false,
skipScripts: true
});
this._storage = new _snapshotStorage.SnapshotStorage();
}
async initialize() {
await this._snapshotter.start();
this._harTracer.start();
}
async reset() {
await this._snapshotter.reset();
await this._harTracer.flush();
this._harTracer.stop();
this._harTracer.start();
}
async dispose() {
this._snapshotter.dispose();
await this._harTracer.flush();
this._harTracer.stop();
}
async captureSnapshot(page, callId, snapshotName, element) {
if (this._snapshotReadyPromises.has(snapshotName)) throw new Error('Duplicate snapshot name: ' + snapshotName);
this._snapshotter.captureSnapshot(page, callId, snapshotName, element).catch(() => {});
const promise = new _utils.ManualPromise();
this._snapshotReadyPromises.set(snapshotName, promise);
return promise;
}
onEntryStarted(entry) {}
onEntryFinished(entry) {
this._storage.addResource(entry);
}
onContentBlob(sha1, buffer) {
this._blobs.set(sha1, buffer);
}
onSnapshotterBlob(blob) {
this._blobs.set(blob.sha1, blob.buffer);
}
onFrameSnapshot(snapshot) {
var _this$_snapshotReadyP;
++this._snapshotCount;
const renderer = this._storage.addFrameSnapshot(snapshot);
(_this$_snapshotReadyP = this._snapshotReadyPromises.get(snapshot.snapshotName || '')) === null || _this$_snapshotReadyP === void 0 ? void 0 : _this$_snapshotReadyP.resolve(renderer);
}
async resourceContentForTest(sha1) {
return this._blobs.get(sha1);
}
snapshotCount() {
return this._snapshotCount;
}
}
exports.InMemorySnapshotter = InMemorySnapshotter;

View File

@@ -0,0 +1,242 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.openTraceViewerApp = openTraceViewerApp;
exports.showTraceViewer = showTraceViewer;
var _path = _interopRequireDefault(require("path"));
var _fs = _interopRequireDefault(require("fs"));
var _httpServer = require("../../../utils/httpServer");
var _registry = require("../../registry");
var _utils = require("../../../utils");
var _crApp = require("../../chromium/crApp");
var _instrumentation = require("../../instrumentation");
var _playwright = require("../../playwright");
var _progress = require("../../progress");
var _utilsBundle = require("playwright-core/lib/utilsBundle");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/**
* 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.
*/
async function showTraceViewer(traceUrls, browserName, options) {
if (options !== null && options !== void 0 && options.openInBrowser) {
await openTraceInBrowser(traceUrls, options);
return;
}
await openTraceViewerApp(traceUrls, browserName, options);
}
async function startTraceViewerServer(traceUrls, options) {
for (const traceUrl of traceUrls) {
let traceFile = traceUrl;
// If .json is requested, we'll synthesize it.
if (traceUrl.endsWith('.json')) traceFile = traceUrl.substring(0, traceUrl.length - '.json'.length);
if (!traceUrl.startsWith('http://') && !traceUrl.startsWith('https://') && !_fs.default.existsSync(traceFile) && !_fs.default.existsSync(traceFile + '.trace')) {
// eslint-disable-next-line no-console
console.error(`Trace file ${traceUrl} does not exist!`);
process.exit(1);
}
}
const server = new _httpServer.HttpServer();
server.routePrefix('/trace', (request, response) => {
const url = new URL('http://localhost' + request.url);
const relativePath = url.pathname.slice('/trace'.length);
if (relativePath.endsWith('/stall.js')) return true;
if (relativePath.startsWith('/file')) {
try {
const filePath = url.searchParams.get('path');
if (_fs.default.existsSync(filePath)) return server.serveFile(request, response, url.searchParams.get('path'));
// If .json is requested, we'll synthesize it for zip-less operation.
if (filePath.endsWith('.json')) {
const traceName = filePath.substring(0, filePath.length - '.json'.length);
response.statusCode = 200;
response.setHeader('Content-Type', 'application/json');
response.end(JSON.stringify(traceDescriptor(traceName)));
return true;
}
} catch (e) {
return false;
}
}
const absolutePath = _path.default.join(__dirname, '..', '..', '..', 'webpack', 'traceViewer', ...relativePath.split('/'));
return server.serveFile(request, response, absolutePath);
});
const params = traceUrls.map(t => `trace=${t}`);
const transport = (options === null || options === void 0 ? void 0 : options.transport) || (options !== null && options !== void 0 && options.isServer ? new StdinServer() : undefined);
if (transport) {
const guid = (0, _utils.createGuid)();
params.push('ws=' + guid);
const wss = new _utilsBundle.wsServer({
server: server.server(),
path: '/' + guid
});
wss.on('connection', ws => {
transport.sendEvent = (method, params) => ws.send(JSON.stringify({
method,
params
}));
transport.close = () => ws.close();
ws.on('message', async message => {
const {
id,
method,
params
} = JSON.parse(message);
const result = await transport.dispatch(method, params);
ws.send(JSON.stringify({
id,
result
}));
});
ws.on('close', () => transport.onclose());
ws.on('error', () => transport.onclose());
});
}
if (options !== null && options !== void 0 && options.isServer) params.push('isServer');
if ((0, _utils.isUnderTest)()) params.push('isUnderTest=true');
const {
host,
port
} = options || {};
const url = await server.start({
preferredPort: port,
host
});
const {
app
} = options || {};
const searchQuery = params.length ? '?' + params.join('&') : '';
const urlPath = `/trace/${app || 'index.html'}${searchQuery}`;
server.routePath('/', (_, response) => {
response.statusCode = 301;
response.setHeader('Location', urlPath);
response.end();
return true;
});
return {
server,
url
};
}
async function openTraceViewerApp(traceUrls, browserName, options) {
const {
url
} = await startTraceViewerServer(traceUrls, options);
const traceViewerPlaywright = (0, _playwright.createPlaywright)({
sdkLanguage: 'javascript',
isInternalPlaywright: true
});
const traceViewerBrowser = (0, _utils.isUnderTest)() ? 'chromium' : browserName;
const args = traceViewerBrowser === 'chromium' ? ['--app=data:text/html,', '--window-size=1280,800', '--test-type='] : [];
const context = await traceViewerPlaywright[traceViewerBrowser].launchPersistentContext((0, _instrumentation.serverSideCallMetadata)(), '', {
// TODO: store language in the trace.
channel: (0, _registry.findChromiumChannel)(traceViewerPlaywright.options.sdkLanguage),
args,
noDefaultViewport: true,
headless: options === null || options === void 0 ? void 0 : options.headless,
ignoreDefaultArgs: ['--enable-automation'],
colorScheme: 'no-override',
useWebSocket: (0, _utils.isUnderTest)()
});
const controller = new _progress.ProgressController((0, _instrumentation.serverSideCallMetadata)(), context._browser);
await controller.run(async progress => {
await context._browser._defaultContext._loadDefaultContextAsIs(progress);
});
const [page] = context.pages();
if (process.env.PWTEST_PRINT_WS_ENDPOINT) process.stderr.write('DevTools listening on: ' + context._browser.options.wsEndpoint + '\n');
if (traceViewerBrowser === 'chromium') await (0, _crApp.installAppIcon)(page);
if (!(0, _utils.isUnderTest)()) await (0, _crApp.syncLocalStorageWithSettings)(page, 'traceviewer');
if ((0, _utils.isUnderTest)()) page.on('close', () => context.close((0, _instrumentation.serverSideCallMetadata)()).catch(() => {}));else page.on('close', () => process.exit());
await page.mainFrame().goto((0, _instrumentation.serverSideCallMetadata)(), url);
return page;
}
async function openTraceInBrowser(traceUrls, options) {
const {
url
} = await startTraceViewerServer(traceUrls, options);
// eslint-disable-next-line no-console
console.log('\nListening on ' + url);
await (0, _utilsBundle.open)(url, {
wait: true
}).catch(() => {});
}
class StdinServer {
constructor() {
this._pollTimer = void 0;
this._traceUrl = void 0;
this._page = void 0;
this.sendEvent = void 0;
this.close = void 0;
process.stdin.on('data', data => {
const url = data.toString().trim();
if (url === this._traceUrl) return;
if (url.endsWith('.json')) this._pollLoadTrace(url);else this._loadTrace(url);
});
process.stdin.on('close', () => this._selfDestruct());
}
async dispatch(method, params) {
if (method === 'ready') {
if (this._traceUrl) this._loadTrace(this._traceUrl);
}
}
onclose() {
this._selfDestruct();
}
_loadTrace(url) {
var _this$sendEvent;
this._traceUrl = url;
clearTimeout(this._pollTimer);
(_this$sendEvent = this.sendEvent) === null || _this$sendEvent === void 0 ? void 0 : _this$sendEvent.call(this, 'loadTrace', {
url
});
}
_pollLoadTrace(url) {
this._loadTrace(url);
this._pollTimer = setTimeout(() => {
this._pollLoadTrace(url);
}, 500);
}
_selfDestruct() {
// Force exit after 30 seconds.
setTimeout(() => process.exit(0), 30000);
// Meanwhile, try to gracefully close all browsers.
(0, _utils.gracefullyCloseAll)().then(() => {
process.exit(0);
});
}
}
function traceDescriptor(traceName) {
const result = {
entries: []
};
const traceDir = _path.default.dirname(traceName);
const traceFile = _path.default.basename(traceName);
for (const name of _fs.default.readdirSync(traceDir)) {
if (name.startsWith(traceFile)) result.entries.push({
name,
path: _path.default.join(traceDir, name)
});
}
const resourcesDir = _path.default.join(traceDir, 'resources');
if (_fs.default.existsSync(resourcesDir)) {
for (const name of _fs.default.readdirSync(resourcesDir)) result.entries.push({
name: 'resources/' + name,
path: _path.default.join(resourcesDir, name)
});
}
return result;
}