Merge branch 'microsoft:main' into cnm_dev

This commit is contained in:
Christopher Tangonan 2025-02-14 16:45:35 -08:00 committed by GitHub
commit 98faa5b0b6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
118 changed files with 1563 additions and 979 deletions

View file

@ -33,7 +33,7 @@ jobs:
- name: Merge reports
run: |
npx playwright merge-reports --config .github/workflows/merge.config.ts ./all-blob-reports
npx playwright merge-reports --reporter=html,packages/playwright/lib/reporters/markdown.js ./all-blob-reports
env:
NODE_OPTIONS: --max-old-space-size=8192

View file

@ -1,4 +0,0 @@
export default {
testDir: '../../tests',
reporter: [[require.resolve('../../packages/playwright/lib/reporters/markdown')], ['html']]
};

View file

@ -1,6 +1,6 @@
# 🎭 Playwright
[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[![Chromium version](https://img.shields.io/badge/chromium-134.0.6998.3-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-135.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[![WebKit version](https://img.shields.io/badge/webkit-18.2-blue.svg?logo=safari)](https://webkit.org/)<!-- GEN:stop --> [![Join Discord](https://img.shields.io/badge/join-discord-infomational)](https://aka.ms/playwright/discord)
[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[![Chromium version](https://img.shields.io/badge/chromium-134.0.6998.15-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-135.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[![WebKit version](https://img.shields.io/badge/webkit-18.2-blue.svg?logo=safari)](https://webkit.org/)<!-- GEN:stop --> [![Join Discord](https://img.shields.io/badge/join-discord-infomational)](https://aka.ms/playwright/discord)
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
@ -8,7 +8,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr
| | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->134.0.6998.3<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Chromium <!-- GEN:chromium-version -->134.0.6998.15<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->18.2<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Firefox <!-- GEN:firefox-version -->135.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |

16
package-lock.json generated
View file

@ -59,6 +59,7 @@
"formidable": "^2.1.1",
"immutable": "^4.3.7",
"license-checker": "^25.0.1",
"markdown-to-jsx": "^7.7.3",
"mime": "^3.0.0",
"node-stream-zip": "^1.15.0",
"react": "^18.1.0",
@ -5897,7 +5898,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dev": true,
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
@ -5953,6 +5953,18 @@
"semver": "bin/semver"
}
},
"node_modules/markdown-to-jsx": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.7.3.tgz",
"integrity": "sha512-o35IhJDFP6Fv60zPy+hbvZSQMmgvSGdK5j8NRZ7FeZMY+Bgqw+dSg7SC1ZEzC26++CiOUCqkbq96/c3j/FfTEQ==",
"license": "MIT",
"engines": {
"node": ">= 10"
},
"peerDependencies": {
"react": ">= 0.14.0"
}
},
"node_modules/matcher": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
@ -6784,7 +6796,6 @@
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
"dev": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@ -8975,6 +8986,7 @@
"packages/trace-viewer": {
"version": "0.0.0",
"dependencies": {
"markdown-to-jsx": "^7.7.3",
"yaml": "^2.6.0"
}
},

View file

@ -98,6 +98,7 @@
"formidable": "^2.1.1",
"immutable": "^4.3.7",
"license-checker": "^25.0.1",
"markdown-to-jsx": "^7.7.3",
"mime": "^3.0.0",
"node-stream-zip": "^1.15.0",
"react": "^18.1.0",

View file

@ -107,14 +107,24 @@ const InnerMetadataView = () => {
const GitCommitInfoView: React.FC<{ info: GitCommitInfo }> = ({ info }) => {
const email = info['revision.email'] ? ` <${info['revision.email']}>` : '';
const author = `${info['revision.author'] || ''}${email}`;
const subject = info['revision.subject'] || '';
let subject = info['revision.subject'] || '';
let link = info['revision.link'];
let shortSubject = info['revision.id']?.slice(0, 7) || 'unknown';
if (info['pull.link'] && info['pull.title']) {
subject = info['pull.title'];
link = info['pull.link'];
shortSubject = link ? 'Pull Request' : '';
}
const shortTimestamp = Intl.DateTimeFormat(undefined, { dateStyle: 'medium' }).format(info['revision.timestamp']);
const longTimestamp = Intl.DateTimeFormat(undefined, { dateStyle: 'full', timeStyle: 'long' }).format(info['revision.timestamp']);
return <div className='hbox git-commit-info metadata-section'>
<div className='vbox metadata-properties'>
<div>
{info['revision.link'] ? (
<a href={info['revision.link']} target='_blank' rel='noopener noreferrer' title={subject}>
{link ? (
<a href={link} target='_blank' rel='noopener noreferrer' title={subject}>
{subject}
</a>
) : <span title={subject}>
@ -130,18 +140,12 @@ const GitCommitInfoView: React.FC<{ info: GitCommitInfo }> = ({ info }) => {
<a href={info['ci.link']} target='_blank' rel='noopener noreferrer' title='CI/CD logs'>Logs</a>
</>
)}
{info['pull.link'] && (
<>
<span className='mx-2'>·</span>
<a href={info['pull.link']} target='_blank' rel='noopener noreferrer'>Pull Request</a>
</>
)}
</div>
</div>
{!!info['revision.link'] ? (
<a href={info['revision.link']} target='_blank' rel='noopener noreferrer' title='View commit details'>
{info['revision.id']?.slice(0, 7) || 'unknown'}
{link ? (
<a href={link} target='_blank' rel='noopener noreferrer' title='View commit details'>
{shortSubject}
</a>
) : !!info['revision.id'] && <span>{info['revision.id'].slice(0, 7)}</span>}
) : !!shortSubject && <span>{shortSubject}</span>}
</div>;
};

View file

@ -3,27 +3,27 @@
"browsers": [
{
"name": "chromium",
"revision": "1158",
"revision": "1159",
"installByDefault": true,
"browserVersion": "134.0.6998.3"
"browserVersion": "134.0.6998.15"
},
{
"name": "chromium-headless-shell",
"revision": "1158",
"revision": "1159",
"installByDefault": true,
"browserVersion": "134.0.6998.3"
"browserVersion": "134.0.6998.15"
},
{
"name": "chromium-tip-of-tree",
"revision": "1302",
"revision": "1303",
"installByDefault": false,
"browserVersion": "135.0.7011.0"
"browserVersion": "135.0.7015.0"
},
{
"name": "chromium-tip-of-tree-headless-shell",
"revision": "1302",
"revision": "1303",
"installByDefault": false,
"browserVersion": "135.0.7011.0"
"browserVersion": "135.0.7015.0"
},
{
"name": "firefox",
@ -39,7 +39,7 @@
},
{
"name": "webkit",
"revision": "2134",
"revision": "2137",
"installByDefault": true,
"revisionOverrides": {
"debian11-x64": "2105",

View file

@ -1,22 +1,27 @@
[browserServerImpl.ts]
**
remote/
server/
server/utils
utils/isomorphic/
utilsBundle.ts
[androidServerImpl.ts]
**
remote/
server/
server/utils
utils/isomorphic/
utilsBundle.ts
[inProcessFactory.ts]
**
[inprocess.ts]
common/
utils/
server/utils
[outofprocess.ts]
client/
common/
protocol/
utils/
utils/isomorphic
server/utils
common/

View file

@ -14,7 +14,6 @@
* limitations under the License.
*/
import { envObjectToArray } from './client/clientHelper';
import { SocksProxy } from './server/utils/socksProxy';
import { PlaywrightServer } from './remote/playwrightServer';
import { helper } from './server/helper';
@ -25,7 +24,7 @@ import { rewriteErrorMessage } from './utils/isomorphic/stackTrace';
import { ws } from './utilsBundle';
import type { BrowserServer, BrowserServerLauncher } from './client/browserType';
import type { LaunchServerOptions, Logger } from './client/types';
import type { LaunchServerOptions, Logger, Env } from './client/types';
import type { ProtocolLogger } from './server/types';
import type { WebSocketEventEmitter } from './utilsBundle';
@ -85,3 +84,12 @@ function toProtocolLogger(logger: Logger | undefined): ProtocolLogger | undefine
logger.log('protocol', 'verbose', (direction === 'send' ? 'SEND ► ' : '◀ RECV ') + JSON.stringify(message), [], {});
} : undefined;
}
function envObjectToArray(env: Env): { name: string, value: string }[] {
const result: { name: string, value: string }[] = [];
for (const name in env) {
if (!Object.is(env[name], undefined))
result.push({ name, value: String(env[name]) });
}
return result;
}

View file

@ -1,7 +1,5 @@
[*]
../../
../client
../common
../debug/injected
../generated/
../server/

View file

@ -22,7 +22,6 @@ import * as path from 'path';
import * as playwright from '../..';
import { launchBrowserServer, printApiJson, runDriver, runServer } from './driver';
import { isTargetClosedError } from '../client/errors';
import { registry, writeDockerVersion } from '../server';
import { gracefullyProcessExitDoNotHang } from '../utils';
import { runTraceInBrowser, runTraceViewerApp } from '../server/trace/viewer/traceViewer';
@ -553,7 +552,7 @@ async function openPage(context: BrowserContext, url: string | undefined): Promi
else if (!url.startsWith('http') && !url.startsWith('file://') && !url.startsWith('about:') && !url.startsWith('data:'))
url = 'http://' + url;
await page.goto(url).catch(error => {
if (process.env.PWTEST_CLI_AUTO_EXIT_WHEN && isTargetClosedError(error)) {
if (process.env.PWTEST_CLI_AUTO_EXIT_WHEN) {
// Tests with PWTEST_CLI_AUTO_EXIT_WHEN might close page too fast, resulting
// in a stray navigation aborted error. We should ignore it.
} else {

View file

@ -1,4 +1,3 @@
[*]
../common/
../protocol/
../utils/isomorphic

View file

@ -20,7 +20,7 @@ import { ChannelOwner } from './channelOwner';
import { TargetClosedError, isTargetClosedError } from './errors';
import { Events } from './events';
import { Waiter } from './waiter';
import { TimeoutSettings } from '../utils/isomorphic/timeoutSettings';
import { TimeoutSettings } from './timeoutSettings';
import { isRegExp, isString } from '../utils/isomorphic/rtti';
import { monotonicTime } from '../utils/isomorphic/time';
import { raceAgainstDeadline } from '../utils/isomorphic/timeoutRunner';
@ -30,7 +30,7 @@ import type { Page } from './page';
import type * as types from './types';
import type * as api from '../../types/types';
import type { AndroidServerLauncherImpl } from '../androidServerImpl';
import type { Platform } from '../common/platform';
import type { Platform } from './platform';
import type * as channels from '@protocol/channels';
type Direction = 'down' | 'up' | 'left' | 'right';
@ -46,7 +46,7 @@ export class Android extends ChannelOwner<channels.AndroidChannel> implements ap
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.AndroidInitializer) {
super(parent, type, guid, initializer);
this._timeoutSettings = new TimeoutSettings();
this._timeoutSettings = new TimeoutSettings(this._platform);
}
setDefaultTimeout(timeout: number) {
@ -112,7 +112,7 @@ export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel> i
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.AndroidDeviceInitializer) {
super(parent, type, guid, initializer);
this.input = new AndroidInput(this);
this._timeoutSettings = new TimeoutSettings((parent as Android)._timeoutSettings);
this._timeoutSettings = new TimeoutSettings(this._platform, (parent as Android)._timeoutSettings);
this._channel.on('webViewAdded', ({ webView }) => this._onWebViewAdded(webView));
this._channel.on('webViewRemoved', ({ socketName }) => this._onWebViewRemoved(socketName));
this._channel.on('close', () => this._didClose());

View file

@ -34,7 +34,7 @@ import { Tracing } from './tracing';
import { Waiter } from './waiter';
import { WebError } from './webError';
import { Worker } from './worker';
import { TimeoutSettings } from '../utils/isomorphic/timeoutSettings';
import { TimeoutSettings } from './timeoutSettings';
import { mkdirIfNeeded } from './fileUtils';
import { headersObjectToArray } from '../utils/isomorphic/headers';
import { urlMatchesEqual } from '../utils/isomorphic/urlMatch';
@ -46,7 +46,7 @@ import type { BrowserContextOptions, Headers, LaunchOptions, StorageState, WaitF
import type * as structs from '../../types/structs';
import type * as api from '../../types/types';
import type { URLMatch } from '../utils/isomorphic/urlMatch';
import type { Platform } from '../common/platform';
import type { Platform } from './platform';
import type * as channels from '@protocol/channels';
export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel> implements api.BrowserContext {
@ -56,7 +56,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
readonly _browser: Browser | null = null;
_browserType: BrowserType | undefined;
readonly _bindings = new Map<string, (source: structs.BindingSource, ...args: any[]) => any>();
_timeoutSettings = new TimeoutSettings();
_timeoutSettings: TimeoutSettings;
_ownerPage: Page | undefined;
private _closedPromise: Promise<void>;
_options: channels.BrowserNewContextParams = { };
@ -83,6 +83,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.BrowserContextInitializer) {
super(parent, type, guid, initializer);
this._timeoutSettings = new TimeoutSettings(this._platform);
if (parent instanceof Browser)
this._browser = parent;
this._browser?._contexts.add(this);

View file

@ -19,7 +19,7 @@ import { BrowserContext, prepareBrowserContextParams } from './browserContext';
import { ChannelOwner } from './channelOwner';
import { envObjectToArray } from './clientHelper';
import { Events } from './events';
import { assert } from '../utils/isomorphic/debug';
import { assert } from '../utils/isomorphic/assert';
import { headersObjectToArray } from '../utils/isomorphic/headers';
import { monotonicTime } from '../utils/isomorphic/time';
import { raceAgainstDeadline } from '../utils/isomorphic/timeoutRunner';

View file

@ -16,14 +16,14 @@
import { EventEmitter } from './eventEmitter';
import { ValidationError, maybeFindValidator } from '../protocol/validator';
import { isUnderTest } from '../utils/isomorphic/debug';
import { captureLibraryStackTrace, stringifyStackFrames } from '../utils/isomorphic/stackTrace';
import { captureLibraryStackTrace } from './clientStackTrace';
import { stringifyStackFrames } from '../utils/isomorphic/stackTrace';
import type { ClientInstrumentation } from './clientInstrumentation';
import type { Connection } from './connection';
import type { Logger } from './types';
import type { ValidatorContext } from '../protocol/validator';
import type { Platform } from '../common/platform';
import type { Platform } from './platform';
import type * as channels from '@protocol/channels';
type Listener = (...args: any[]) => void;
@ -181,7 +181,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
if (isInternal === undefined)
isInternal = this._isInternalType;
const stackTrace = captureLibraryStackTrace(this._platform.pathSeparator);
const stackTrace = captureLibraryStackTrace(this._platform);
const apiZone: ApiZone = { apiName: stackTrace.apiName, frames: stackTrace.frames, isInternal, reported: false, userData: undefined, stepId: undefined };
try {
@ -192,7 +192,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
}
return result;
} catch (e) {
const innerError = ((process.env.PWDEBUGIMPL || isUnderTest()) && e.stack) ? '\n<inner error>\n' + e.stack : '';
const innerError = ((process.env.PWDEBUGIMPL || this._platform.isUnderTest()) && e.stack) ? '\n<inner error>\n' + e.stack : '';
if (apiZone.apiName && !apiZone.apiName.includes('<anonymous>'))
e.message = apiZone.apiName + ': ' + e.message;
const stackFrames = '\n' + stringifyStackFrames(stackTrace.frames).join('\n') + innerError;

View file

@ -0,0 +1,29 @@
/**
* 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 { Connection } from './connection';
import { setPlatformForSelectors } from './selectors';
import { setPlatformForEventEmitter } from './eventEmitter';
import { setIsUnderTestForValidator } from '../protocol/validatorPrimitives';
import type { Platform } from './platform';
export function createConnectionFactory(platform: Platform): () => Connection {
setPlatformForSelectors(platform);
setPlatformForEventEmitter(platform);
setIsUnderTestForValidator(() => platform.isUnderTest());
return () => new Connection(platform);
}

View file

@ -18,7 +18,7 @@
import { isString } from '../utils/isomorphic/rtti';
import type * as types from './types';
import type { Platform } from '../common/platform';
import type { Platform } from './platform';
export function envObjectToArray(env: types.Env): { name: string, value: string }[] {
const result: { name: string, value: string }[] = [];

View file

@ -0,0 +1,78 @@
/**
* 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 { captureRawStack, parseStackFrame } from '../utils/isomorphic/stackTrace';
import type { Platform } from './platform';
import type { StackFrame } from '@isomorphic/stackTrace';
export function captureLibraryStackTrace(platform: Platform): { frames: StackFrame[], apiName: string } {
const stack = captureRawStack();
type ParsedFrame = {
frame: StackFrame;
frameText: string;
isPlaywrightLibrary: boolean;
};
let parsedFrames = stack.map(line => {
const frame = parseStackFrame(line, platform.pathSeparator);
if (!frame || !frame.file)
return null;
const isPlaywrightLibrary = !!platform.coreDir && frame.file.startsWith(platform.coreDir);
const parsed: ParsedFrame = {
frame,
frameText: line,
isPlaywrightLibrary
};
return parsed;
}).filter(Boolean) as ParsedFrame[];
let apiName = '';
// Deepest transition between non-client code calling into client
// code is the api entry.
for (let i = 0; i < parsedFrames.length - 1; i++) {
const parsedFrame = parsedFrames[i];
if (parsedFrame.isPlaywrightLibrary && !parsedFrames[i + 1].isPlaywrightLibrary) {
apiName = apiName || normalizeAPIName(parsedFrame.frame.function);
break;
}
}
function normalizeAPIName(name?: string): string {
if (!name)
return '';
const match = name.match(/(API|JS|CDP|[A-Z])(.*)/);
if (!match)
return name;
return match[1].toLowerCase() + match[2];
}
// This is for the inspector so that it did not include the test runner stack frames.
const filterPrefixes = platform.coreDir ? [platform.coreDir, ...platform.boxedStackPrefixes()] : platform.boxedStackPrefixes();
parsedFrames = parsedFrames.filter(f => {
if (process.env.PWDEBUGIMPL)
return true;
if (filterPrefixes.some(prefix => f.frame.file.startsWith(prefix)))
return false;
return true;
});
return {
frames: parsedFrames.map(p => p.frame),
apiName
};
}

View file

@ -14,7 +14,6 @@
* limitations under the License.
*/
import { EventEmitter } from './eventEmitter';
import { Android, AndroidDevice, AndroidSocket } from './android';
import { Artifact } from './artifact';
@ -42,12 +41,12 @@ import { Tracing } from './tracing';
import { Worker } from './worker';
import { WritableStream } from './writableStream';
import { ValidationError, findValidator } from '../protocol/validator';
import { formatCallLog, rewriteErrorMessage } from '../utils/isomorphic/stackTrace';
import { rewriteErrorMessage } from '../utils/isomorphic/stackTrace';
import type { ClientInstrumentation } from './clientInstrumentation';
import type { HeadersArray } from './types';
import type { ValidatorContext } from '../protocol/validator';
import type { Platform } from '../common/platform';
import type { Platform } from './platform';
import type * as channels from '@protocol/channels';
class Root extends ChannelOwner<channels.RootChannel> {
@ -83,7 +82,7 @@ export class Connection extends EventEmitter {
// Used from @playwright/test fixtures -> TODO remove?
readonly headers: HeadersArray;
constructor(localUtils: LocalUtils | undefined, platform: Platform, instrumentation: ClientInstrumentation | undefined, headers: HeadersArray) {
constructor(platform: Platform, localUtils?: LocalUtils, instrumentation?: ClientInstrumentation, headers: HeadersArray = []) {
super();
this._instrumentation = instrumentation || createInstrumentation();
this._localUtils = localUtils;
@ -333,3 +332,12 @@ export class Connection extends EventEmitter {
return result;
}
}
function formatCallLog(platform: Platform, log: string[] | undefined): string {
if (!log || !log.some(l => !!l))
return '';
return `
Call log:
${platform.colors.dim(log.join('\n'))}
`;
}

View file

@ -18,7 +18,7 @@ import { JSHandle } from './jsHandle';
import { Page } from './page';
import type * as api from '../../types/types';
import type { Platform } from '../common/platform';
import type { Platform } from './platform';
import type * as channels from '@protocol/channels';
type ConsoleMessageLocation = channels.BrowserContextConsoleEvent['location'];

View file

@ -22,7 +22,7 @@ import { TargetClosedError, isTargetClosedError } from './errors';
import { Events } from './events';
import { JSHandle, parseResult, serializeArgument } from './jsHandle';
import { Waiter } from './waiter';
import { TimeoutSettings } from '../utils/isomorphic/timeoutSettings';
import { TimeoutSettings } from './timeoutSettings';
import type { Page } from './page';
import type { BrowserContextOptions, Env, Headers, WaitForEventOptions } from './types';
@ -66,7 +66,7 @@ export class Electron extends ChannelOwner<channels.ElectronChannel> implements
export class ElectronApplication extends ChannelOwner<channels.ElectronApplicationChannel> implements api.ElectronApplication {
readonly _context: BrowserContext;
private _windows = new Set<Page>();
private _timeoutSettings = new TimeoutSettings();
private _timeoutSettings: TimeoutSettings;
static from(electronApplication: channels.ElectronApplicationChannel): ElectronApplication {
return (electronApplication as any)._object;
@ -74,6 +74,8 @@ export class ElectronApplication extends ChannelOwner<channels.ElectronApplicati
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.ElectronApplicationInitializer) {
super(parent, type, guid, initializer);
this._timeoutSettings = new TimeoutSettings(this._platform);
this._context = BrowserContext.from(initializer.context);
for (const page of this._context._pages)
this._onPage(page);

View file

@ -14,12 +14,9 @@
* limitations under the License.
*/
import { pipeline } from 'stream';
import { promisify } from 'util';
import { Frame } from './frame';
import { JSHandle, parseResult, serializeArgument } from './jsHandle';
import { assert } from '../utils/isomorphic/debug';
import { assert } from '../utils/isomorphic/assert';
import { fileUploadSizeLimit, mkdirIfNeeded } from './fileUtils';
import { isString } from '../utils/isomorphic/rtti';
import { WritableStream } from './writableStream';
@ -31,11 +28,9 @@ import type { Locator } from './locator';
import type { FilePayload, Rect, SelectOption, SelectOptionOptions } from './types';
import type * as structs from '../../types/structs';
import type * as api from '../../types/types';
import type { Platform } from '../common/platform';
import type { Platform } from './platform';
import type * as channels from '@protocol/channels';
const pipelineAsync = promisify(pipeline);
export class ElementHandle<T extends Node = Node> extends JSHandle<T> implements api.ElementHandle {
readonly _elementChannel: channels.ElementHandleChannel;
@ -306,7 +301,7 @@ export async function convertInputFiles(platform: Platform, files: string | File
}), true);
for (let i = 0; i < files.length; i++) {
const writable = WritableStream.from(writableStreams[i]);
await pipelineAsync(platform.fs().createReadStream(files[i]), writable.stream());
await platform.streamFile(files[i], writable.stream());
}
return {
directoryStream: rootDir,

View file

@ -22,18 +22,19 @@
* USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { isUnderTest } from '../utils/isomorphic/debug';
import { emptyPlatform } from './platform';
import type { EventEmitter as EventEmitterType } from 'events';
import type { Platform } from './platform';
type EventType = string | symbol;
type Listener = (...args: any[]) => any;
type EventMap = Record<EventType, Listener | Listener[]>;
let defaultMaxListenersProvider = () => 10;
let platform = emptyPlatform;
export function setDefaultMaxListenersProvider(provider: () => number) {
defaultMaxListenersProvider = provider;
export function setPlatformForEventEmitter(p: Platform) {
platform = p;
}
export class EventEmitter implements EventEmitterType {
@ -62,7 +63,7 @@ export class EventEmitter implements EventEmitterType {
}
getMaxListeners(): number {
return this._maxListeners === undefined ? defaultMaxListenersProvider() : this._maxListeners;
return this._maxListeners === undefined ? platform.defaultMaxListeners() : this._maxListeners;
}
emit(type: EventType, ...args: any[]): boolean {
@ -160,7 +161,7 @@ export class EventEmitter implements EventEmitterType {
w.emitter = this;
w.type = type;
w.count = existing.length;
if (!isUnderTest()) {
if (!platform.isUnderTest()) {
// eslint-disable-next-line no-console
console.warn(w);
}

View file

@ -19,7 +19,7 @@ import { ChannelOwner } from './channelOwner';
import { TargetClosedError, isTargetClosedError } from './errors';
import { RawHeaders } from './network';
import { Tracing } from './tracing';
import { assert } from '../utils/isomorphic/debug';
import { assert } from '../utils/isomorphic/assert';
import { mkdirIfNeeded } from './fileUtils';
import { headersObjectToArray } from '../utils/isomorphic/headers';
import { isString } from '../utils/isomorphic/rtti';
@ -28,8 +28,8 @@ import type { Playwright } from './playwright';
import type { ClientCertificate, FilePayload, Headers, SetStorageState, StorageState } from './types';
import type { Serializable } from '../../types/structs';
import type * as api from '../../types/types';
import type { HeadersArray, NameValue } from '../common/types';
import type { Platform } from '../common/platform';
import type { HeadersArray, NameValue } from '../utils/isomorphic/types';
import type { Platform } from './platform';
import type * as channels from '@protocol/channels';
import type * as fs from 'fs';

View file

@ -14,7 +14,7 @@
* limitations under the License.
*/
import type { Platform } from '../common/platform';
import type { Platform } from './platform';
// Keep in sync with the server.
export const fileUploadSizeLimit = 50 * 1024 * 1024;

View file

@ -25,7 +25,7 @@ import { FrameLocator, Locator, testIdAttributeName } from './locator';
import * as network from './network';
import { kLifecycleEvents } from './types';
import { Waiter } from './waiter';
import { assert } from '../utils/isomorphic/debug';
import { assert } from '../utils/isomorphic/assert';
import { getByAltTextSelector, getByLabelSelector, getByPlaceholderSelector, getByRoleSelector, getByTestIdSelector, getByTextSelector, getByTitleSelector } from '../utils/isomorphic/locatorUtils';
import { urlMatches } from '../utils/isomorphic/urlMatch';

View file

@ -21,7 +21,7 @@ import { APIResponse } from './fetch';
import { Frame } from './frame';
import { Waiter } from './waiter';
import { Worker } from './worker';
import { assert } from '../utils/isomorphic/debug';
import { assert } from '../utils/isomorphic/assert';
import { headersObjectToArray } from '../utils/isomorphic/headers';
import { urlMatches } from '../utils/isomorphic/urlMatch';
import { LongStandingScope, ManualPromise } from '../utils/isomorphic/manualPromise';
@ -35,10 +35,10 @@ import type { Page } from './page';
import type { Headers, RemoteAddr, SecurityDetails, WaitForEventOptions } from './types';
import type { Serializable } from '../../types/structs';
import type * as api from '../../types/types';
import type { HeadersArray } from '../common/types';
import type { HeadersArray } from '../utils/isomorphic/types';
import type { URLMatch } from '../utils/isomorphic/urlMatch';
import type * as channels from '@protocol/channels';
import type { Platform, Zone } from '../common/platform';
import type { Platform, Zone } from './platform';
export type NetworkCookie = {
name: string,

View file

@ -33,8 +33,8 @@ import { Response, Route, RouteHandler, WebSocket, WebSocketRoute, WebSocketRou
import { Video } from './video';
import { Waiter } from './waiter';
import { Worker } from './worker';
import { TimeoutSettings } from '../utils/isomorphic/timeoutSettings';
import { assert } from '../utils/isomorphic/debug';
import { TimeoutSettings } from './timeoutSettings';
import { assert } from '../utils/isomorphic/assert';
import { mkdirIfNeeded } from './fileUtils';
import { headersObjectToArray } from '../utils/isomorphic/headers';
import { trimStringWithEllipsis } from '../utils/isomorphic/stringUtils';
@ -118,7 +118,7 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.PageInitializer) {
super(parent, type, guid, initializer);
this._browserContext = parent as unknown as BrowserContext;
this._timeoutSettings = new TimeoutSettings(this._browserContext._timeoutSettings);
this._timeoutSettings = new TimeoutSettings(this._platform, this._browserContext._timeoutSettings);
this.accessibility = new Accessibility(this._channel);
this.keyboard = new Keyboard(this);
@ -799,7 +799,7 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
}
async pause(_options?: { __testHookKeepTestTimeout: boolean }) {
if (this._platform.isDebuggerAttached())
if (this._platform.isJSDebuggerAttached())
return;
const defaultNavigationTimeout = this._browserContext._timeoutSettings.defaultNavigationTimeout();
const defaultTimeout = this._browserContext._timeoutSettings.defaultTimeout();

View file

@ -18,7 +18,9 @@ import { webColors, noColors } from '../utils/isomorphic/colors';
import type * as fs from 'fs';
import type * as path from 'path';
import type { Colors } from '../utils/isomorphic/colors';
import type { Readable, Writable } from 'stream';
import type { Colors } from '@isomorphic/colors';
import type * as channels from '@protocol/channels';
export type Zone = {
push(data: unknown): Zone;
@ -37,22 +39,90 @@ const noopZone: Zone = {
export type Platform = {
name: 'node' | 'web' | 'empty';
calculateSha1(text: string): Promise<string>;
boxedStackPrefixes: () => string[];
calculateSha1: (text: string) => Promise<string>;
colors: Colors;
coreDir?: string;
createGuid: () => string;
defaultMaxListeners: () => number;
fs: () => typeof fs;
inspectCustom: symbol | undefined;
isDebuggerAttached(): boolean;
isLogEnabled(name: 'api' | 'channel'): boolean;
log(name: 'api' | 'channel', message: string | Error | object): void;
isDebugMode: () => boolean;
isJSDebuggerAttached: () => boolean;
isLogEnabled: (name: 'api' | 'channel') => boolean;
isUnderTest: () => boolean,
log: (name: 'api' | 'channel', message: string | Error | object) => void;
path: () => typeof path;
pathSeparator: string;
streamFile: (path: string, writable: Writable) => Promise<void>,
streamReadable: (channel: channels.StreamChannel) => Readable,
streamWritable: (channel: channels.WritableStreamChannel) => Writable,
zones: { empty: Zone, current: () => Zone; };
};
export const emptyPlatform: Platform = {
name: 'empty',
boxedStackPrefixes: () => [],
calculateSha1: async () => {
throw new Error('Not implemented');
},
colors: noColors,
createGuid: () => {
throw new Error('Not implemented');
},
defaultMaxListeners: () => 10,
fs: () => {
throw new Error('Not implemented');
},
inspectCustom: undefined,
isDebugMode: () => false,
isJSDebuggerAttached: () => false,
isLogEnabled(name: 'api' | 'channel') {
return false;
},
isUnderTest: () => false,
log(name: 'api' | 'channel', message: string | Error | object) { },
path: () => {
throw new Error('Function not implemented.');
},
pathSeparator: '/',
streamFile(path: string, writable: Writable): Promise<void> {
throw new Error('Streams are not available');
},
streamReadable: (channel: channels.StreamChannel) => {
throw new Error('Streams are not available');
},
streamWritable: (channel: channels.WritableStreamChannel) => {
throw new Error('Streams are not available');
},
zones: { empty: noopZone, current: () => noopZone },
};
export const webPlatform: Platform = {
...emptyPlatform,
name: 'web',
boxedStackPrefixes: () => [],
calculateSha1: async (text: string) => {
const bytes = new TextEncoder().encode(text);
const hashBuffer = await window.crypto.subtle.digest('SHA-1', bytes);
@ -64,62 +134,4 @@ export const webPlatform: Platform = {
createGuid: () => {
return Array.from(window.crypto.getRandomValues(new Uint8Array(16)), b => b.toString(16).padStart(2, '0')).join('');
},
fs: () => {
throw new Error('File system is not available');
},
inspectCustom: undefined,
isDebuggerAttached: () => false,
isLogEnabled(name: 'api' | 'channel') {
return false;
},
log(name: 'api' | 'channel', message: string | Error | object) {},
path: () => {
throw new Error('Path module is not available');
},
pathSeparator: '/',
zones: { empty: noopZone, current: () => noopZone },
};
export const emptyPlatform: Platform = {
name: 'empty',
calculateSha1: async () => {
throw new Error('Not implemented');
},
colors: noColors,
createGuid: () => {
throw new Error('Not implemented');
},
fs: () => {
throw new Error('Not implemented');
},
inspectCustom: undefined,
isDebuggerAttached: () => false,
isLogEnabled(name: 'api' | 'channel') {
return false;
},
log(name: 'api' | 'channel', message: string | Error | object) { },
path: () => {
throw new Error('Function not implemented.');
},
pathSeparator: '/',
zones: { empty: noopZone, current: () => noopZone },
};

View file

@ -17,12 +17,12 @@
import { ChannelOwner } from './channelOwner';
import { evaluationScript } from './clientHelper';
import { setTestIdAttribute, testIdAttributeName } from './locator';
import { emptyPlatform } from '../common/platform';
import { emptyPlatform } from './platform';
import type { SelectorEngine } from './types';
import type * as api from '../../types/types';
import type * as channels from '@protocol/channels';
import type { Platform } from '../common/platform';
import type { Platform } from './platform';
let platform = emptyPlatform;

View file

@ -30,29 +30,6 @@ export class Stream extends ChannelOwner<channels.StreamChannel> {
}
stream(): Readable {
return new StreamImpl(this._channel);
}
}
class StreamImpl extends Readable {
private _channel: channels.StreamChannel;
constructor(channel: channels.StreamChannel) {
super();
this._channel = channel;
}
override async _read() {
const result = await this._channel.read({ size: 1024 * 1024 });
if (result.binary.byteLength)
this.push(result.binary);
else
this.push(null);
}
override _destroy(error: Error | null, callback: (error: Error | null | undefined) => void): void {
// Stream might be destroyed after the connection was closed.
this._channel.close().catch(e => null);
super._destroy(error, callback);
return this._platform.streamReadable(this._channel);
}
}

View file

@ -0,0 +1,76 @@
/**
* Copyright 2019 Google Inc. All rights reserved.
* Modifications copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Platform } from './platform';
// Keep in sync with server.
export const DEFAULT_TIMEOUT = 30000;
export const DEFAULT_LAUNCH_TIMEOUT = 3 * 60 * 1000; // 3 minutes
export class TimeoutSettings {
private _parent: TimeoutSettings | undefined;
private _defaultTimeout: number | undefined;
private _defaultNavigationTimeout: number | undefined;
private _platform: Platform;
constructor(platform: Platform, parent?: TimeoutSettings) {
this._parent = parent;
this._platform = platform;
}
setDefaultTimeout(timeout: number | undefined) {
this._defaultTimeout = timeout;
}
setDefaultNavigationTimeout(timeout: number | undefined) {
this._defaultNavigationTimeout = timeout;
}
defaultNavigationTimeout() {
return this._defaultNavigationTimeout;
}
defaultTimeout() {
return this._defaultTimeout;
}
navigationTimeout(options: { timeout?: number }): number {
if (typeof options.timeout === 'number')
return options.timeout;
if (this._defaultNavigationTimeout !== undefined)
return this._defaultNavigationTimeout;
if (this._platform.isDebugMode())
return 0;
if (this._defaultTimeout !== undefined)
return this._defaultTimeout;
if (this._parent)
return this._parent.navigationTimeout(options);
return DEFAULT_TIMEOUT;
}
timeout(options: { timeout?: number }): number {
if (typeof options.timeout === 'number')
return options.timeout;
if (this._platform.isDebugMode())
return 0;
if (this._defaultTimeout !== undefined)
return this._defaultTimeout;
if (this._parent)
return this._parent.timeout(options);
return DEFAULT_TIMEOUT;
}
}

View file

@ -15,9 +15,9 @@
* limitations under the License.
*/
import type { Size } from '../common/types';
import type { Size } from '../utils/isomorphic/types';
import type * as channels from '@protocol/channels';
export type { HeadersArray, Point, Quad, Rect, Size, TimeoutOptions } from '../common/types';
export type { HeadersArray, Point, Quad, Rect, Size, TimeoutOptions } from '../utils/isomorphic/types';
type LoggerSeverity = 'verbose' | 'info' | 'warning' | 'error';
export interface Logger {

View file

@ -20,7 +20,7 @@ import { rewriteErrorMessage } from '../utils/isomorphic/stackTrace';
import type { ChannelOwner } from './channelOwner';
import type * as channels from '@protocol/channels';
import type { EventEmitter } from 'events';
import type { Zone } from '../common/platform';
import type { Zone } from './platform';
export class Waiter {
private _dispose: (() => void)[];

View file

@ -24,7 +24,7 @@ export async function connectOverWebSocket(parentConnection: Connection, params:
const localUtils = parentConnection.localUtils();
const transport = localUtils ? new JsonPipeTransport(localUtils) : new WebSocketTransport();
const connectHeaders = await transport.connect(params);
const connection = new Connection(localUtils, parentConnection.platform, parentConnection._instrumentation, connectHeaders);
const connection = new Connection(parentConnection.platform, localUtils, parentConnection._instrumentation, connectHeaders);
connection.markAsRemote();
connection.on('close', () => transport.close());

View file

@ -14,11 +14,10 @@
* limitations under the License.
*/
import { Writable } from 'stream';
import { ChannelOwner } from './channelOwner';
import type * as channels from '@protocol/channels';
import type { Writable } from 'stream';
export class WritableStream extends ChannelOwner<channels.WritableStreamChannel> {
static from(Stream: channels.WritableStreamChannel): WritableStream {
@ -30,26 +29,6 @@ export class WritableStream extends ChannelOwner<channels.WritableStreamChannel>
}
stream(): Writable {
return new WritableStreamImpl(this._channel);
}
}
class WritableStreamImpl extends Writable {
private _channel: channels.WritableStreamChannel;
constructor(channel: channels.WritableStreamChannel) {
super();
this._channel = channel;
}
override async _write(chunk: Buffer | string, encoding: BufferEncoding, callback: (error?: Error | null) => void) {
const error = await this._channel.write({ binary: typeof chunk === 'string' ? Buffer.from(chunk) : chunk }).catch(e => e);
callback(error || null);
}
override async _final(callback: (error?: Error | null) => void) {
// Stream might be destroyed after the connection was closed.
const error = await this._channel.close().catch(e => e);
callback(error || null);
return this._platform.streamWritable(this._channel);
}
}

View file

@ -14,34 +14,21 @@
* limitations under the License.
*/
import * as path from 'path';
import { EventEmitter } from 'events';
import { AndroidServerLauncherImpl } from './androidServerImpl';
import { BrowserServerLauncherImpl } from './browserServerImpl';
import { Connection } from './client/connection';
import { createConnectionFactory } from './client/clientBundle';
import { DispatcherConnection, PlaywrightDispatcher, RootDispatcher, createPlaywright } from './server';
import { setLibraryStackPrefix } from './utils/isomorphic/stackTrace';
import { setDebugMode } from './utils/isomorphic/debug';
import { getFromENV } from './server/utils/env';
import { nodePlatform } from './server/utils/nodePlatform';
import { setPlatformForSelectors } from './client/selectors';
import { setDefaultMaxListenersProvider } from './client/eventEmitter';
import type { Playwright as PlaywrightAPI } from './client/playwright';
import type { Language } from './utils';
import type { Platform } from './common/platform';
const connectionFactory = createConnectionFactory(nodePlatform);
export function createInProcessPlaywright(platform: Platform): PlaywrightAPI {
export function createInProcessPlaywright(): PlaywrightAPI {
const playwright = createPlaywright({ sdkLanguage: (process.env.PW_LANG_NAME as Language | undefined) || 'javascript' });
setDebugMode(getFromENV('PWDEBUG') || '');
setPlatformForSelectors(nodePlatform);
setDefaultMaxListenersProvider(() => EventEmitter.defaultMaxListeners);
setLibraryStackPrefix(path.join(__dirname, '..'));
const clientConnection = new Connection(undefined, platform, undefined, []);
const clientConnection = connectionFactory();
clientConnection.useRawBuffers();
const dispatcherConnection = new DispatcherConnection(true /* local */);

View file

@ -15,6 +15,5 @@
*/
import { createInProcessPlaywright } from './inProcessFactory';
import { nodePlatform } from './server/utils/nodePlatform';
module.exports = createInProcessPlaywright(nodePlatform);
module.exports = createInProcessPlaywright();

View file

@ -17,13 +17,15 @@
import * as childProcess from 'child_process';
import * as path from 'path';
import { Connection } from './client/connection';
import { createConnectionFactory } from './client/clientBundle';
import { PipeTransport } from './server/utils/pipeTransport';
import { ManualPromise } from './utils/isomorphic/manualPromise';
import { nodePlatform } from './server/utils/nodePlatform';
import type { Playwright } from './client/playwright';
const connectionFactory = createConnectionFactory(nodePlatform);
export async function start(env: any = {}): Promise<{ playwright: Playwright, stop: () => Promise<void> }> {
const client = new PlaywrightClient(env);
const playwright = await client._playwright;
@ -48,7 +50,7 @@ class PlaywrightClient {
this._driverProcess.unref();
this._driverProcess.stderr!.on('data', data => process.stderr.write(data));
const connection = new Connection(undefined, nodePlatform, undefined, []);
const connection = connectionFactory();
const transport = new PipeTransport(this._driverProcess.stdin!, this._driverProcess.stdout!);
connection.onmessage = message => transport.send(JSON.stringify(message));
transport.onmessage = message => connection.dispatch(JSON.parse(message));

View file

@ -1,3 +1,2 @@
[*]
../common/
../utils/isomorphic

View file

@ -14,7 +14,11 @@
* limitations under the License.
*/
import { isUnderTest } from '../utils/isomorphic/debug';
let isUnderTest = () => false;
export function setIsUnderTestForValidator(getter: () => boolean) {
isUnderTest = getter;
}
export class ValidationError extends Error {}
export type Validator = (arg: any, path: string, context: ValidatorContext) => any;

View file

@ -1,6 +1,4 @@
[*]
../client/
../common/
../server/
../server/android/
../server/dispatchers/

View file

@ -20,7 +20,8 @@ import { AndroidDevice } from '../server/android/android';
import { Browser } from '../server/browser';
import { DebugControllerDispatcher } from '../server/dispatchers/debugControllerDispatcher';
import { serverSideCallMetadata } from '../server/instrumentation';
import { assert, isUnderTest } from '../utils';
import { assert } from '../utils/isomorphic/assert';
import { isUnderTest } from '../server/utils/debug';
import { startProfiling, stopProfiling } from '../server/utils/profiler';
import { monotonicTime } from '../utils';
import { debugLogger } from '../server/utils/debugLogger';

View file

@ -1,8 +1,7 @@
[*]
../common/
../generated/
../protocol/
../utils/
../utils
../utils/isomorphic/
../utilsBundle.ts
../zipBundle.ts

View file

@ -1,8 +1,6 @@
[*]
../
../../common/
../../protocol/
../../utils/
../../utils/isomorphic/
../../utilsBundle.ts
../chromium/

View file

@ -19,10 +19,10 @@ import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { TimeoutSettings } from '../../utils/isomorphic/timeoutSettings';
import { TimeoutSettings } from '../timeoutSettings';
import { PipeTransport } from '../utils/pipeTransport';
import { createGuid } from '../utils/crypto';
import { isUnderTest } from '../../utils/isomorphic/debug';
import { isUnderTest } from '../utils/debug';
import { getPackageManagerExecCommand } from '../utils/env';
import { makeWaitForNextTask } from '../utils/task';
import { RecentLogsCollector } from '../utils/debugLogger';

View file

@ -17,7 +17,7 @@
import { EventEmitter } from 'events';
import * as net from 'net';
import { assert } from '../../utils/isomorphic/debug';
import { assert } from '../../utils/isomorphic/assert';
import { createGuid } from '../utils/crypto';
import { debug } from '../../utilsBundle';

View file

@ -519,11 +519,6 @@ export class BidiPage implements PageDelegate {
return quads as types.Quad[];
}
async setInputFiles(handle: dom.ElementHandle<HTMLInputElement>, files: types.FilePayload[]): Promise<void> {
await handle.evaluateInUtility(([injected, node, files]) =>
injected.setInputFiles(node, files), files);
}
async setInputFilePaths(handle: dom.ElementHandle<HTMLInputElement>, paths: string[]): Promise<void> {
const fromContext = toBidiExecutionContext(handle._context);
await this._session.send('input.setFiles', {

View file

@ -18,9 +18,9 @@
import * as fs from 'fs';
import * as path from 'path';
import { TimeoutSettings } from '../utils/isomorphic/timeoutSettings';
import { TimeoutSettings } from './timeoutSettings';
import { createGuid } from './utils/crypto';
import { debugMode } from '../utils/isomorphic/debug';
import { debugMode } from './utils/debug';
import { Clock } from './clock';
import { Debugger } from './debugger';
import { BrowserContextAPIRequestContext } from './fetch';

View file

@ -19,8 +19,10 @@ import * as os from 'os';
import * as path from 'path';
import { normalizeProxySettings, validateBrowserContextOptions } from './browserContext';
import { DEFAULT_TIMEOUT, TimeoutSettings } from '../utils/isomorphic/timeoutSettings';
import { ManualPromise, assert, debugMode } from '../utils';
import { DEFAULT_TIMEOUT, TimeoutSettings } from './timeoutSettings';
import { debugMode } from './utils/debug';
import { assert } from '../utils/isomorphic/assert';
import { ManualPromise } from '../utils/isomorphic/manualPromise';
import { existsAsync } from './utils/fileUtils';
import { helper } from './helper';
import { SdkObject } from './instrumentation';

View file

@ -1,7 +1,7 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* 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
*
@ -14,7 +14,27 @@
* limitations under the License.
*/
export function findRepeatedSubsequences(s: string[]): { sequence: string[]; count: number }[] {
export function compressCallLog(log: string[]): string[] {
const lines: string[] = [];
for (const block of findRepeatedSubsequences(log)) {
for (let i = 0; i < block.sequence.length; i++) {
const line = block.sequence[i];
const leadingWhitespace = line.match(/^\s*/);
const whitespacePrefix = ' ' + leadingWhitespace?.[0] || '';
const countPrefix = `${block.count} × `;
if (block.count > 1 && i === 0)
lines.push(whitespacePrefix + countPrefix + line.trim());
else if (block.count > 1)
lines.push(whitespacePrefix + ' '.repeat(countPrefix.length - 2) + '- ' + line.trim());
else
lines.push(whitespacePrefix + '- ' + line.trim());
}
}
return lines;
}
function findRepeatedSubsequences(s: string[]): { sequence: string[]; count: number }[] {
const n = s.length;
const result = [];
let i = 0;
@ -64,3 +84,5 @@ export function findRepeatedSubsequences(s: string[]): { sequence: string[]; cou
return result;
}
export const findRepeatedSubsequencesForTest = findRepeatedSubsequences;

View file

@ -22,7 +22,7 @@ import * as path from 'path';
import { chromiumSwitches } from './chromiumSwitches';
import { CRBrowser } from './crBrowser';
import { kBrowserCloseMessageId } from './crConnection';
import { TimeoutSettings } from '../../utils/isomorphic/timeoutSettings';
import { TimeoutSettings } from '../timeoutSettings';
import { debugMode, headersArrayToObject, headersObjectToArray, } from '../../utils';
import { wrapInASCIIBox } from '../utils/ascii';
import { RecentLogsCollector } from '../utils/debugLogger';

View file

@ -17,7 +17,7 @@
import * as path from 'path';
import { assert } from '../../utils/isomorphic/debug';
import { assert } from '../../utils/isomorphic/assert';
import { createGuid } from '../utils/crypto';
import { Artifact } from '../artifact';
import { Browser } from '../browser';

View file

@ -17,7 +17,7 @@
import * as path from 'path';
import { assert } from '../../utils/isomorphic/debug';
import { assert } from '../../utils/isomorphic/assert';
import { createGuid } from '../utils/crypto';
import { eventsHelper } from '../utils/eventsHelper';
import { rewriteErrorMessage } from '../../utils/isomorphic/stackTrace';
@ -314,11 +314,6 @@ export class CRPage implements PageDelegate {
return this._sessionForHandle(handle)._getContentQuads(handle);
}
async setInputFiles(handle: dom.ElementHandle<HTMLInputElement>, files: types.FilePayload[]): Promise<void> {
await handle.evaluateInUtility(([injected, node, files]) =>
injected.setInputFiles(node, files), files);
}
async setInputFilePaths(handle: dom.ElementHandle<HTMLInputElement>, files: string[]): Promise<void> {
const frame = await handle.ownerFrame();
if (!frame)

View file

@ -110,7 +110,7 @@
"defaultBrowserType": "webkit"
},
"Galaxy S5": {
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36",
"viewport": {
"width": 360,
"height": 640
@ -121,7 +121,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S5 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 360
@ -132,7 +132,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S8": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36",
"viewport": {
"width": 360,
"height": 740
@ -143,7 +143,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S8 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36",
"viewport": {
"width": 740,
"height": 360
@ -154,7 +154,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S9+": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36",
"viewport": {
"width": 320,
"height": 658
@ -165,7 +165,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S9+ landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36",
"viewport": {
"width": 658,
"height": 320
@ -176,7 +176,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy Tab S4": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Safari/537.36",
"viewport": {
"width": 712,
"height": 1138
@ -187,7 +187,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy Tab S4 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Safari/537.36",
"viewport": {
"width": 1138,
"height": 712
@ -1098,7 +1098,7 @@
"defaultBrowserType": "webkit"
},
"LG Optimus L70": {
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/134.0.6998.3 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/134.0.6998.15 Mobile Safari/537.36",
"viewport": {
"width": 384,
"height": 640
@ -1109,7 +1109,7 @@
"defaultBrowserType": "chromium"
},
"LG Optimus L70 landscape": {
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/134.0.6998.3 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/134.0.6998.15 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 384
@ -1120,7 +1120,7 @@
"defaultBrowserType": "chromium"
},
"Microsoft Lumia 550": {
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36 Edge/14.14263",
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36 Edge/14.14263",
"viewport": {
"width": 360,
"height": 640
@ -1131,7 +1131,7 @@
"defaultBrowserType": "chromium"
},
"Microsoft Lumia 550 landscape": {
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36 Edge/14.14263",
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36 Edge/14.14263",
"viewport": {
"width": 640,
"height": 360
@ -1142,7 +1142,7 @@
"defaultBrowserType": "chromium"
},
"Microsoft Lumia 950": {
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36 Edge/14.14263",
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36 Edge/14.14263",
"viewport": {
"width": 360,
"height": 640
@ -1153,7 +1153,7 @@
"defaultBrowserType": "chromium"
},
"Microsoft Lumia 950 landscape": {
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36 Edge/14.14263",
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36 Edge/14.14263",
"viewport": {
"width": 640,
"height": 360
@ -1164,7 +1164,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 10": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Safari/537.36",
"viewport": {
"width": 800,
"height": 1280
@ -1175,7 +1175,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 10 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Safari/537.36",
"viewport": {
"width": 1280,
"height": 800
@ -1186,7 +1186,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 4": {
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36",
"viewport": {
"width": 384,
"height": 640
@ -1197,7 +1197,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 4 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 384
@ -1208,7 +1208,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 5": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36",
"viewport": {
"width": 360,
"height": 640
@ -1219,7 +1219,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 5 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 360
@ -1230,7 +1230,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 5X": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36",
"viewport": {
"width": 412,
"height": 732
@ -1241,7 +1241,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 5X landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36",
"viewport": {
"width": 732,
"height": 412
@ -1252,7 +1252,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 6": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36",
"viewport": {
"width": 412,
"height": 732
@ -1263,7 +1263,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 6 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36",
"viewport": {
"width": 732,
"height": 412
@ -1274,7 +1274,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 6P": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36",
"viewport": {
"width": 412,
"height": 732
@ -1285,7 +1285,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 6P landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36",
"viewport": {
"width": 732,
"height": 412
@ -1296,7 +1296,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 7": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Safari/537.36",
"viewport": {
"width": 600,
"height": 960
@ -1307,7 +1307,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 7 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Safari/537.36",
"viewport": {
"width": 960,
"height": 600
@ -1362,7 +1362,7 @@
"defaultBrowserType": "webkit"
},
"Pixel 2": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36",
"viewport": {
"width": 411,
"height": 731
@ -1373,7 +1373,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 2 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36",
"viewport": {
"width": 731,
"height": 411
@ -1384,7 +1384,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 2 XL": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36",
"viewport": {
"width": 411,
"height": 823
@ -1395,7 +1395,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 2 XL landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36",
"viewport": {
"width": 823,
"height": 411
@ -1406,7 +1406,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 3": {
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36",
"viewport": {
"width": 393,
"height": 786
@ -1417,7 +1417,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 3 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36",
"viewport": {
"width": 786,
"height": 393
@ -1428,7 +1428,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 4": {
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36",
"viewport": {
"width": 353,
"height": 745
@ -1439,7 +1439,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 4 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36",
"viewport": {
"width": 745,
"height": 353
@ -1450,7 +1450,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 4a (5G)": {
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36",
"screen": {
"width": 412,
"height": 892
@ -1465,7 +1465,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 4a (5G) landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36",
"screen": {
"height": 892,
"width": 412
@ -1480,7 +1480,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 5": {
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36",
"screen": {
"width": 393,
"height": 851
@ -1495,7 +1495,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 5 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36",
"screen": {
"width": 851,
"height": 393
@ -1510,7 +1510,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 7": {
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36",
"screen": {
"width": 412,
"height": 915
@ -1525,7 +1525,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 7 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36",
"screen": {
"width": 915,
"height": 412
@ -1540,7 +1540,7 @@
"defaultBrowserType": "chromium"
},
"Moto G4": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36",
"viewport": {
"width": 360,
"height": 640
@ -1551,7 +1551,7 @@
"defaultBrowserType": "chromium"
},
"Moto G4 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 360
@ -1562,7 +1562,7 @@
"defaultBrowserType": "chromium"
},
"Desktop Chrome HiDPI": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Safari/537.36",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Safari/537.36",
"screen": {
"width": 1792,
"height": 1120
@ -1577,7 +1577,7 @@
"defaultBrowserType": "chromium"
},
"Desktop Edge HiDPI": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Safari/537.36 Edg/134.0.6998.3",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Safari/537.36 Edg/134.0.6998.15",
"screen": {
"width": 1792,
"height": 1120
@ -1622,7 +1622,7 @@
"defaultBrowserType": "webkit"
},
"Desktop Chrome": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Safari/537.36",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Safari/537.36",
"screen": {
"width": 1920,
"height": 1080
@ -1637,7 +1637,7 @@
"defaultBrowserType": "chromium"
},
"Desktop Edge": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Safari/537.36 Edg/134.0.6998.3",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Safari/537.36 Edg/134.0.6998.15",
"screen": {
"width": 1920,
"height": 1080

View file

@ -1,5 +1,4 @@
[*]
../../common/
../../generated/
../../protocol/
../../utils/

View file

@ -18,10 +18,12 @@ import { EventEmitter } from 'events';
import { eventsHelper } from '../utils/eventsHelper';
import { ValidationError, createMetadataValidator, findValidator } from '../../protocol/validator';
import { LongStandingScope, assert, compressCallLog, isUnderTest, monotonicTime, rewriteErrorMessage } from '../../utils';
import { LongStandingScope, assert, monotonicTime, rewriteErrorMessage } from '../../utils';
import { isUnderTest } from '../utils/debug';
import { TargetClosedError, isTargetClosedError, serializeError } from '../errors';
import { SdkObject } from '../instrumentation';
import { isProtocolError } from '../protocolError';
import { compressCallLog } from '../callLog';
import type { CallMetadata } from '../instrumentation';
import type { PlaywrightDispatcher } from './playwrightDispatcher';

View file

@ -20,7 +20,6 @@ import { ElementHandleDispatcher } from './elementHandlerDispatcher';
import { parseArgument, serializeResult } from './jsHandleDispatcher';
import { ResponseDispatcher } from './networkDispatchers';
import { RequestDispatcher } from './networkDispatchers';
import { debugAssert } from '../../utils';
import { parseAriaSnapshotUnsafe } from '../../utils/isomorphic/ariaSnapshot';
import { yaml } from '../../utilsBundle';
@ -50,7 +49,6 @@ export class FrameDispatcher extends Dispatcher<Frame, channels.FrameChannel, Br
// Main frames are gc'ed separately from any other frames, so that
// methods on Page that redirect to the main frame remain operational.
// Note: we cannot check parentFrame() here because it may be null after the frame has been detached.
debugAssert(frame._page.mainFrame(), 'Cannot determine whether the frame is a main frame');
const gcBucket = frame._page.mainFrame() === frame ? 'MainFrame' : 'Frame';
const pageDispatcher = existingDispatcher<PageDispatcher>(frame._page);
super(pageDispatcher || scope, frame, 'Frame', {

View file

@ -30,7 +30,7 @@ import type { Page } from './page';
import type { Progress } from './progress';
import type { ScreenshotOptions } from './screenshotter';
import type * as types from './types';
import type { TimeoutOptions } from '../common/types';
import type { TimeoutOptions } from '../utils/isomorphic/types';
import type * as channels from '@protocol/channels';
@ -705,7 +705,8 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
await this._page._delegate.setInputFilePaths(retargeted, localPathsOrDirectory);
await waitForInputEvent;
} else {
await this._page._delegate.setInputFiles(retargeted, filePayloads!);
await retargeted.evaluateInUtility(([injected, node, files]) =>
injected.setInputFiles(node, files), filePayloads!);
}
return 'done';
}

View file

@ -1,6 +1,5 @@
[*]
../
../../common/
../../utils/
../../utils/isomorphic/
../chromium/

View file

@ -19,7 +19,7 @@ import * as os from 'os';
import * as path from 'path';
import * as readline from 'readline';
import { TimeoutSettings } from '../../utils/isomorphic/timeoutSettings';
import { TimeoutSettings } from '../timeoutSettings';
import { ManualPromise } from '../../utils';
import { wrapInASCIIBox } from '../utils/ascii';
import { RecentLogsCollector } from '../utils/debugLogger';

View file

@ -21,7 +21,7 @@ import { TLSSocket } from 'tls';
import * as url from 'url';
import * as zlib from 'zlib';
import { TimeoutSettings } from '../utils/isomorphic/timeoutSettings';
import { TimeoutSettings } from './timeoutSettings';
import { assert, constructURLBasedOnBaseURL, eventsHelper, monotonicTime } from '../utils';
import { createGuid } from './utils/crypto';
import { getUserAgent } from './utils/userAgent';

View file

@ -17,7 +17,7 @@
import * as fs from 'fs';
import * as path from 'path';
import { assert } from '../utils/isomorphic/debug';
import { assert } from '../utils/isomorphic/assert';
import { mime } from '../utilsBundle';
import type { WritableStreamDispatcher } from './dispatchers/writableStreamDispatcher';

View file

@ -519,11 +519,6 @@ export class FFPage implements PageDelegate {
return result.quads.map(quad => [quad.p1, quad.p2, quad.p3, quad.p4]);
}
async setInputFiles(handle: dom.ElementHandle<HTMLInputElement>, files: types.FilePayload[]): Promise<void> {
await handle.evaluateInUtility(([injected, node, files]) =>
injected.setInputFiles(node, files), files);
}
async setInputFilePaths(handle: dom.ElementHandle<HTMLInputElement>, files: string[]): Promise<void> {
await this._session.send('Page.setFileInputFiles', {
frameId: handle._context.frame._id,

View file

@ -27,12 +27,13 @@ import * as network from './network';
import { Page } from './page';
import { ProgressController } from './progress';
import * as types from './types';
import { LongStandingScope, asLocator, assert, compressCallLog, constructURLBasedOnBaseURL, makeWaitForNextTask, monotonicTime } from '../utils';
import { LongStandingScope, asLocator, assert, constructURLBasedOnBaseURL, makeWaitForNextTask, monotonicTime } from '../utils';
import { isSessionClosedError } from './protocolError';
import { debugLogger } from './utils/debugLogger';
import { eventsHelper } from './utils/eventsHelper';
import { isInvalidSelectorError } from '../utils/isomorphic/selectorParser';
import { ManualPromise } from '../utils/isomorphic/manualPromise';
import { compressCallLog } from './callLog';
import type { ConsoleMessage } from './console';
import type { Dialog } from './dialog';

View file

@ -20,7 +20,7 @@ import * as path from 'path';
import { createGuid } from './utils/crypto';
import { ZipFile } from './utils/zipFile';
import type { HeadersArray } from '../common/types';
import type { HeadersArray } from '../utils/isomorphic/types';
import type * as har from '@trace/har';
const redirectStatus = [301, 302, 303, 307, 308];

View file

@ -16,7 +16,7 @@
import clipPaths from './clipPaths';
import type { Point } from '../../../common/types';
import type { Point } from '../../../utils/isomorphic/types';
import type { Highlight, HighlightOptions } from '../highlight';
import type { InjectedScript } from '../injectedScript';
import type { ElementText } from '../selectorUtils';

View file

@ -24,7 +24,7 @@ import { ManualPromise } from '../utils/isomorphic/manualPromise';
import { ZipFile } from './utils/zipFile';
import { yauzl, yazl } from '../zipBundle';
import { serializeClientSideCallMetadata } from '../utils/isomorphic/traceUtils';
import { assert } from '../utils/isomorphic/debug';
import { assert } from '../utils/isomorphic/assert';
import { removeFolders } from './utils/fileUtils';
import type * as channels from '@protocol/channels';

View file

@ -25,7 +25,7 @@ import type * as frames from './frames';
import type * as pages from './page';
import type * as types from './types';
import type { NormalizedContinueOverrides } from './types';
import type { HeadersArray, NameValue } from '../common/types';
import type { HeadersArray, NameValue } from '../utils/isomorphic/types';
import type * as channels from '@protocol/channels';

View file

@ -28,14 +28,15 @@ import { parseEvaluationResultValue, source } from './isomorphic/utilityScriptSe
import * as js from './javascript';
import { ProgressController } from './progress';
import { Screenshotter, validateScreenshotOptions } from './screenshotter';
import { TimeoutSettings } from '../utils/isomorphic/timeoutSettings';
import { LongStandingScope, assert, compressCallLog, trimStringWithEllipsis } from '../utils';
import { TimeoutSettings } from './timeoutSettings';
import { LongStandingScope, assert, trimStringWithEllipsis } from '../utils';
import { createGuid } from './utils/crypto';
import { asLocator } from '../utils';
import { getComparator } from './utils/comparators';
import { debugLogger } from './utils/debugLogger';
import { isInvalidSelectorError } from '../utils/isomorphic/selectorParser';
import { ManualPromise } from '../utils/isomorphic/manualPromise';
import { compressCallLog } from './callLog';
import type { Artifact } from './artifact';
import type * as dom from './dom';
@ -45,7 +46,7 @@ import type * as network from './network';
import type { Progress } from './progress';
import type { ScreenshotOptions } from './screenshotter';
import type * as types from './types';
import type { TimeoutOptions } from '../common/types';
import type { TimeoutOptions } from '../utils/isomorphic/types';
import type { ImageComparatorOptions } from './utils/comparators';
import type * as channels from '@protocol/channels';
@ -79,7 +80,6 @@ export interface PageDelegate {
getContentFrame(handle: dom.ElementHandle): Promise<frames.Frame | null>; // Only called for frame owner elements.
getOwnerFrame(handle: dom.ElementHandle): Promise<string | null>; // Returns frameId.
getContentQuads(handle: dom.ElementHandle): Promise<types.Quad[] | null | 'error:notconnected'>;
setInputFiles(handle: dom.ElementHandle<HTMLInputElement>, files: types.FilePayload[]): Promise<void>;
setInputFilePaths(handle: dom.ElementHandle<HTMLInputElement>, files: string[]): Promise<void>;
getBoundingBox(handle: dom.ElementHandle): Promise<types.Rect | null>;
getFrameElement(frame: frames.Frame): Promise<dom.ElementHandle>;

View file

@ -30,7 +30,7 @@ import type { Frame } from './frames';
import type { CallMetadata, InstrumentationListener, SdkObject } from './instrumentation';
import type { Page } from './page';
import type { IRecorder, IRecorderApp, IRecorderAppFactory } from './recorder/recorderFrontend';
import type { Point } from '../common/types';
import type { Point } from '../utils/isomorphic/types';
import type { AriaTemplateNode } from '@isomorphic/ariaSnapshot';
import type * as channels from '@protocol/channels';
import type * as actions from '@recorder/actions';

View file

@ -4,7 +4,7 @@
../codegen/languages.ts
../isomorphic/**
../registry/**
../../common/
../utils/**
../../generated/pollingRecorderSource.ts
../../protocol/
../../utils/**

View file

@ -18,7 +18,7 @@ import { EventEmitter } from 'events';
import * as fs from 'fs';
import * as path from 'path';
import { isUnderTest } from '../../utils';
import { isUnderTest } from '../utils/debug';
import { mime } from '../../utilsBundle';
import { serverSideCallMetadata } from '../instrumentation';
import { syncLocalStorageWithSettings } from '../launchApp';

View file

@ -18,7 +18,7 @@ import { EventEmitter } from 'events';
import { performAction } from './recorderRunner';
import { collapseActions } from './recorderUtils';
import { isUnderTest } from '../../utils/isomorphic/debug';
import { isUnderTest } from '../utils/debug';
import { monotonicTime } from '../../utils/isomorphic/time';
import type { Signal } from '../../../../recorder/src/actions';

View file

@ -24,7 +24,7 @@ import type { Frame } from './frames';
import type { Page } from './page';
import type { Progress } from './progress';
import type * as types from './types';
import type { Rect } from '../common/types';
import type { Rect } from '../utils/isomorphic/types';
import type { ParsedSelector } from '../utils/isomorphic/selectorParser';

View file

@ -15,8 +15,9 @@
* limitations under the License.
*/
import { debugMode } from './debug';
import { debugMode } from './utils/debug';
// Keep in sync with client.
export const DEFAULT_TIMEOUT = 30000;
export const DEFAULT_LAUNCH_TIMEOUT = 3 * 60 * 1000; // 3 minutes

View file

@ -1,9 +1,7 @@
[*]
../../
../../har/
../../../common/
../../../protocol/
../../../utils/
../../../utilsBundle.ts
../../../utils/isomorphic/
../../../zipBundle.ts

View file

@ -20,7 +20,7 @@ import * as path from 'path';
import { Snapshotter } from './snapshotter';
import { commandsWithTracingSnapshots } from '../../../protocol/debug';
import { assert } from '../../../utils/isomorphic/debug';
import { assert } from '../../../utils/isomorphic/assert';
import { monotonicTime } from '../../../utils/isomorphic/time';
import { eventsHelper } from '../../utils/eventsHelper';
import { createGuid } from '../../utils/crypto';
@ -34,7 +34,7 @@ import { SdkObject } from '../../instrumentation';
import { Page } from '../../page';
import type { SnapshotterBlob, SnapshotterDelegate } from './snapshotter';
import type { NameValue } from '../../../common/types';
import type { NameValue } from '../../../utils/isomorphic/types';
import type { RegisteredListener } from '../../../utils';
import type { ConsoleMessage } from '../../console';
import type { Dialog } from '../../dialog';

View file

@ -142,6 +142,16 @@ export async function installRootRedirect(server: HttpServer, traceUrls: string[
server.routePath('/', (_, response) => {
response.statusCode = 302;
response.setHeader('Location', urlPath);
if (process.env.OPENAI_API_KEY)
response.appendHeader('Set-Cookie', `openai_api_key=${process.env.OPENAI_API_KEY}`);
if (process.env.OPENAI_BASE_URL)
response.appendHeader('Set-Cookie', `openai_base_url=${process.env.OPENAI_BASE_URL}`);
if (process.env.ANTHROPIC_API_KEY)
response.appendHeader('Set-Cookie', `anthropic_api_key=${process.env.ANTHROPIC_API_KEY}`);
if (process.env.ANTHROPIC_BASE_URL)
response.appendHeader('Set-Cookie', `anthropic_base_url=${process.env.ANTHROPIC_BASE_URL}`);
response.end();
return true;
});

View file

@ -15,8 +15,8 @@
* limitations under the License.
*/
import type { HeadersArray, Point, Size, TimeoutOptions } from '../common/types';
export type { HeadersArray, Point, Quad, Rect, Size, TimeoutOptions } from '../common/types';
import type { HeadersArray, Point, Size, TimeoutOptions } from '../utils/isomorphic/types';
export type { HeadersArray, Point, Quad, Rect, Size, TimeoutOptions } from '../utils/isomorphic/types';
import type * as channels from '@protocol/channels';
export type StrictOptions = {

View file

@ -1,5 +1,4 @@
[*]
../../common
../../utils
../../utils/isomorphic
../../utilsBundle.ts

View file

@ -16,7 +16,7 @@
import * as crypto from 'crypto';
import { assert } from '../../utils/isomorphic/debug';
import { assert } from '../../utils/isomorphic/assert';
export function createGuid(): string {
return crypto.randomBytes(16).toString('hex');

View file

@ -14,21 +14,9 @@
* limitations under the License.
*/
export function assert(value: any, message?: string): asserts value {
if (!value)
throw new Error(message || 'Assertion error');
}
import { getFromENV } from './env';
export function debugAssert(value: any, message?: string): asserts value {
if (isUnderTest() && !value)
throw new Error(message);
}
let _debugMode: string | undefined;
export function setDebugMode(mode: string) {
_debugMode = mode;
}
const _debugMode = getFromENV('PWDEBUG') || '';
export function debugMode() {
if (_debugMode === 'console')

View file

@ -20,7 +20,7 @@ import * as https from 'https';
import * as net from 'net';
import * as tls from 'tls';
import { assert } from '../../utils/isomorphic/debug';
import { assert } from '../../utils/isomorphic/assert';
import { ManualPromise } from '../../utils/isomorphic/manualPromise';
import { monotonicTime } from '../../utils/isomorphic/time';

View file

@ -19,7 +19,7 @@ import * as path from 'path';
import { mime, wsServer } from '../../utilsBundle';
import { createGuid } from './crypto';
import { assert } from '../../utils/isomorphic/debug';
import { assert } from '../../utils/isomorphic/assert';
import { ManualPromise } from '../../utils/isomorphic/manualPromise';
import { createHttpServer } from './network';

View file

@ -18,13 +18,19 @@ import * as crypto from 'crypto';
import * as fs from 'fs';
import * as path from 'path';
import * as util from 'util';
import { Readable, Writable, pipeline } from 'stream';
import { EventEmitter } from 'events';
import { colors } from '../../utilsBundle';
import { debugLogger } from './debugLogger';
import { currentZone, emptyZone } from './zones';
import { debugMode, isUnderTest } from './debug';
import type { Platform, Zone } from '../../common/platform';
import type { Platform, Zone } from '../../client/platform';
import type { Zone as ZoneImpl } from './zones';
import type * as channels from '@protocol/channels';
const pipelineAsync = util.promisify(pipeline);
class NodeZone implements Zone {
private _zone: ZoneImpl;
@ -50,9 +56,16 @@ class NodeZone implements Zone {
}
}
let boxedStackPrefixes: string[] = [];
export function setBoxedStackPrefixes(prefixes: string[]) {
boxedStackPrefixes = prefixes;
}
export const nodePlatform: Platform = {
name: 'node',
boxedStackPrefixes: () => boxedStackPrefixes,
calculateSha1: (text: string) => {
const sha1 = crypto.createHash('sha1');
sha1.update(text);
@ -61,18 +74,25 @@ export const nodePlatform: Platform = {
colors,
coreDir: path.dirname(require.resolve('../../../package.json')),
createGuid: () => crypto.randomBytes(16).toString('hex'),
defaultMaxListeners: () => EventEmitter.defaultMaxListeners,
fs: () => fs,
inspectCustom: util.inspect.custom,
isDebuggerAttached: () => !!require('inspector').url(),
isDebugMode: () => !!debugMode(),
isJSDebuggerAttached: () => !!require('inspector').url(),
isLogEnabled(name: 'api' | 'channel') {
return debugLogger.isEnabled(name);
},
isUnderTest: () => isUnderTest(),
log(name: 'api' | 'channel', message: string | Error | object) {
debugLogger.log(name, message);
},
@ -81,8 +101,63 @@ export const nodePlatform: Platform = {
pathSeparator: path.sep,
async streamFile(path: string, stream: Writable): Promise<void> {
await pipelineAsync(fs.createReadStream(path), stream);
},
streamReadable: (channel: channels.StreamChannel) => {
return new ReadableStreamImpl(channel);
},
streamWritable: (channel: channels.WritableStreamChannel) => {
return new WritableStreamImpl(channel);
},
zones: {
current: () => new NodeZone(currentZone()),
empty: new NodeZone(emptyZone),
}
};
class ReadableStreamImpl extends Readable {
private _channel: channels.StreamChannel;
constructor(channel: channels.StreamChannel) {
super();
this._channel = channel;
}
override async _read() {
const result = await this._channel.read({ size: 1024 * 1024 });
if (result.binary.byteLength)
this.push(result.binary);
else
this.push(null);
}
override _destroy(error: Error | null, callback: (error: Error | null | undefined) => void): void {
// Stream might be destroyed after the connection was closed.
this._channel.close().catch(e => null);
super._destroy(error, callback);
}
}
class WritableStreamImpl extends Writable {
private _channel: channels.WritableStreamChannel;
constructor(channel: channels.WritableStreamChannel) {
super();
this._channel = channel;
}
override async _write(chunk: Buffer | string, encoding: BufferEncoding, callback: (error?: Error | null) => void) {
const error = await this._channel.write({ binary: typeof chunk === 'string' ? Buffer.from(chunk) : chunk }).catch(e => e);
callback(error || null);
}
override async _final(callback: (error?: Error | null) => void) {
// Stream might be destroyed after the connection was closed.
const error = await this._channel.close().catch(e => e);
callback(error || null);
}
}

View file

@ -17,7 +17,7 @@
import EventEmitter from 'events';
import * as net from 'net';
import { assert } from '../../utils/isomorphic/debug';
import { assert } from '../../utils/isomorphic/assert';
import { createGuid } from './crypto';
import { debugLogger } from './debugLogger';
import { createSocket } from './happyEyeballs';

View file

@ -193,142 +193,6 @@ export module Protocol {
}
}
export module ApplicationCache {
/**
* Detailed application cache resource information.
*/
export interface ApplicationCacheResource {
/**
* Resource url.
*/
url: string;
/**
* Resource size.
*/
size: number;
/**
* Resource type.
*/
type: string;
}
/**
* Detailed application cache information.
*/
export interface ApplicationCache {
/**
* Manifest URL.
*/
manifestURL: string;
/**
* Application cache size.
*/
size: number;
/**
* Application cache creation time.
*/
creationTime: number;
/**
* Application cache update time.
*/
updateTime: number;
/**
* Application cache resources.
*/
resources: ApplicationCacheResource[];
}
/**
* Frame identifier - manifest URL pair.
*/
export interface FrameWithManifest {
/**
* Frame identifier.
*/
frameId: Network.FrameId;
/**
* Manifest URL.
*/
manifestURL: string;
/**
* Application cache status.
*/
status: number;
}
export type applicationCacheStatusUpdatedPayload = {
/**
* Identifier of the frame containing document whose application cache updated status.
*/
frameId: Network.FrameId;
/**
* Manifest URL.
*/
manifestURL: string;
/**
* Updated application cache status.
*/
status: number;
}
export type networkStateUpdatedPayload = {
isNowOnline: boolean;
}
/**
* Returns array of frame identifiers with manifest urls for each frame containing a document associated with some application cache.
*/
export type getFramesWithManifestsParameters = {
}
export type getFramesWithManifestsReturnValue = {
/**
* Array of frame identifiers with manifest urls for each frame containing a document associated with some application cache.
*/
frameIds: FrameWithManifest[];
}
/**
* Enables application cache domain notifications.
*/
export type enableParameters = {
}
export type enableReturnValue = {
}
/**
* Disable application cache domain notifications.
*/
export type disableParameters = {
}
export type disableReturnValue = {
}
/**
* Returns manifest URL for document in the given frame.
*/
export type getManifestForFrameParameters = {
/**
* Identifier of the frame containing document whose manifest is retrieved.
*/
frameId: Network.FrameId;
}
export type getManifestForFrameReturnValue = {
/**
* Manifest URL for document in the given frame.
*/
manifestURL: string;
}
/**
* Returns relevant application cache data for the document in given frame.
*/
export type getApplicationCacheForFrameParameters = {
/**
* Identifier of the frame containing document whose application cache is retrieved.
*/
frameId: Network.FrameId;
}
export type getApplicationCacheForFrameReturnValue = {
/**
* Relevant application cache data for the document in given frame.
*/
applicationCache: ApplicationCache;
}
}
export module Audit {
@ -3644,81 +3508,6 @@ might return multiple quads for inline nodes.
}
}
export module Database {
/**
* Unique identifier of Database object.
*/
export type DatabaseId = string;
/**
* Database object.
*/
export interface Database {
/**
* Database ID.
*/
id: DatabaseId;
/**
* Database domain.
*/
domain: string;
/**
* Database name.
*/
name: string;
/**
* Database version.
*/
version: string;
}
/**
* Database error.
*/
export interface Error {
/**
* Error message.
*/
message: string;
/**
* Error code.
*/
code: number;
}
export type addDatabasePayload = {
database: Database;
}
/**
* Enables database tracking, database events will now be delivered to the client.
*/
export type enableParameters = {
}
export type enableReturnValue = {
}
/**
* Disables database tracking, prevents database events from being sent to the client.
*/
export type disableParameters = {
}
export type disableReturnValue = {
}
export type getDatabaseTableNamesParameters = {
databaseId: DatabaseId;
}
export type getDatabaseTableNamesReturnValue = {
tableNames: string[];
}
export type executeSQLParameters = {
databaseId: DatabaseId;
query: string;
}
export type executeSQLReturnValue = {
columnNames?: string[];
values?: any[];
sqlError?: Error;
}
}
/**
* Debugger domain exposes JavaScript debugging capabilities. It allows setting and removing breakpoints, stepping through execution, exploring stack traces, etc.
*/
@ -9285,8 +9074,6 @@ the top of the viewport and Y increases as it proceeds towards the bottom of the
"Animation.trackingStart": Animation.trackingStartPayload;
"Animation.trackingUpdate": Animation.trackingUpdatePayload;
"Animation.trackingComplete": Animation.trackingCompletePayload;
"ApplicationCache.applicationCacheStatusUpdated": ApplicationCache.applicationCacheStatusUpdatedPayload;
"ApplicationCache.networkStateUpdated": ApplicationCache.networkStateUpdatedPayload;
"Browser.extensionsEnabled": Browser.extensionsEnabledPayload;
"Browser.extensionsDisabled": Browser.extensionsDisabledPayload;
"CPUProfiler.trackingStart": CPUProfiler.trackingStartPayload;
@ -9336,7 +9123,6 @@ the top of the viewport and Y increases as it proceeds towards the bottom of the
"DOMStorage.domStorageItemRemoved": DOMStorage.domStorageItemRemovedPayload;
"DOMStorage.domStorageItemAdded": DOMStorage.domStorageItemAddedPayload;
"DOMStorage.domStorageItemUpdated": DOMStorage.domStorageItemUpdatedPayload;
"Database.addDatabase": Database.addDatabasePayload;
"Debugger.globalObjectCleared": Debugger.globalObjectClearedPayload;
"Debugger.scriptParsed": Debugger.scriptParsedPayload;
"Debugger.scriptFailedToParse": Debugger.scriptFailedToParsePayload;
@ -9418,11 +9204,6 @@ the top of the viewport and Y increases as it proceeds towards the bottom of the
"Animation.resolveAnimation": Animation.resolveAnimationParameters;
"Animation.startTracking": Animation.startTrackingParameters;
"Animation.stopTracking": Animation.stopTrackingParameters;
"ApplicationCache.getFramesWithManifests": ApplicationCache.getFramesWithManifestsParameters;
"ApplicationCache.enable": ApplicationCache.enableParameters;
"ApplicationCache.disable": ApplicationCache.disableParameters;
"ApplicationCache.getManifestForFrame": ApplicationCache.getManifestForFrameParameters;
"ApplicationCache.getApplicationCacheForFrame": ApplicationCache.getApplicationCacheForFrameParameters;
"Audit.setup": Audit.setupParameters;
"Audit.run": Audit.runParameters;
"Audit.teardown": Audit.teardownParameters;
@ -9532,10 +9313,6 @@ the top of the viewport and Y increases as it proceeds towards the bottom of the
"DOMStorage.setDOMStorageItem": DOMStorage.setDOMStorageItemParameters;
"DOMStorage.removeDOMStorageItem": DOMStorage.removeDOMStorageItemParameters;
"DOMStorage.clearDOMStorageItems": DOMStorage.clearDOMStorageItemsParameters;
"Database.enable": Database.enableParameters;
"Database.disable": Database.disableParameters;
"Database.getDatabaseTableNames": Database.getDatabaseTableNamesParameters;
"Database.executeSQL": Database.executeSQLParameters;
"Debugger.enable": Debugger.enableParameters;
"Debugger.disable": Debugger.disableParameters;
"Debugger.setAsyncStackTraceDepth": Debugger.setAsyncStackTraceDepthParameters;
@ -9731,11 +9508,6 @@ the top of the viewport and Y increases as it proceeds towards the bottom of the
"Animation.resolveAnimation": Animation.resolveAnimationReturnValue;
"Animation.startTracking": Animation.startTrackingReturnValue;
"Animation.stopTracking": Animation.stopTrackingReturnValue;
"ApplicationCache.getFramesWithManifests": ApplicationCache.getFramesWithManifestsReturnValue;
"ApplicationCache.enable": ApplicationCache.enableReturnValue;
"ApplicationCache.disable": ApplicationCache.disableReturnValue;
"ApplicationCache.getManifestForFrame": ApplicationCache.getManifestForFrameReturnValue;
"ApplicationCache.getApplicationCacheForFrame": ApplicationCache.getApplicationCacheForFrameReturnValue;
"Audit.setup": Audit.setupReturnValue;
"Audit.run": Audit.runReturnValue;
"Audit.teardown": Audit.teardownReturnValue;
@ -9845,10 +9617,6 @@ the top of the viewport and Y increases as it proceeds towards the bottom of the
"DOMStorage.setDOMStorageItem": DOMStorage.setDOMStorageItemReturnValue;
"DOMStorage.removeDOMStorageItem": DOMStorage.removeDOMStorageItemReturnValue;
"DOMStorage.clearDOMStorageItems": DOMStorage.clearDOMStorageItemsReturnValue;
"Database.enable": Database.enableReturnValue;
"Database.disable": Database.disableReturnValue;
"Database.getDatabaseTableNames": Database.getDatabaseTableNamesReturnValue;
"Database.executeSQL": Database.executeSQLReturnValue;
"Debugger.enable": Debugger.enableReturnValue;
"Debugger.disable": Debugger.disableReturnValue;
"Debugger.setAsyncStackTraceDepth": Debugger.setAsyncStackTraceDepthReturnValue;

View file

@ -17,7 +17,7 @@
import * as path from 'path';
import { assert, debugAssert } from '../../utils';
import { assert } from '../../utils';
import { headersArrayToObject } from '../../utils/isomorphic/headers';
import { createGuid } from '../utils/crypto';
import { eventsHelper } from '../utils/eventsHelper';
@ -294,7 +294,6 @@ export class WKPage implements PageDelegate {
}
handleWindowOpen(event: Protocol.Playwright.windowOpenPayload) {
debugAssert(!this._nextWindowOpenPopupFeatures);
this._nextWindowOpenPopupFeatures = event.windowFeatures;
}
@ -947,16 +946,6 @@ export class WKPage implements PageDelegate {
]);
}
async setInputFiles(handle: dom.ElementHandle<HTMLInputElement>, files: types.FilePayload[]): Promise<void> {
const objectId = handle._objectId;
const protocolFiles = files.map(file => ({
name: file.name,
type: file.mimeType,
data: file.buffer,
}));
await this._session.send('DOM.setInputFiles', { objectId, files: protocolFiles });
}
async setInputFilePaths(handle: dom.ElementHandle<HTMLInputElement>, paths: string[]): Promise<void> {
const pageProxyId = this._pageProxySession.sessionId;
const objectId = handle._objectId;

View file

@ -15,24 +15,24 @@
*/
export * from './utils/isomorphic/colors';
export * from './utils/isomorphic/debug';
export * from './utils/isomorphic/assert';
export * from './utils/isomorphic/headers';
export * from './utils/isomorphic/locatorGenerators';
export * from './utils/isomorphic/manualPromise';
export * from './utils/isomorphic/mimeType';
export * from './utils/isomorphic/multimap';
export * from './utils/isomorphic/rtti';
export * from './utils/isomorphic/semaphore';
export * from './utils/isomorphic/stackTrace';
export * from './utils/isomorphic/stringUtils';
export * from './utils/isomorphic/time';
export * from './utils/isomorphic/timeoutRunner';
export * from './utils/isomorphic/urlMatch';
export * from './utils/isomorphic/headers';
export * from './utils/isomorphic/semaphore';
export * from './utils/isomorphic/stackTrace';
export * from './server/utils/ascii';
export * from './server/utils/comparators';
export * from './server/utils/crypto';
export * from './server/utils/debug';
export * from './server/utils/debugLogger';
export * from './server/utils/env';
export * from './server/utils/eventsHelper';

View file

@ -0,0 +1,20 @@
/**
* 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.
*/
export function assert(value: any, message?: string): asserts value {
if (!value)
throw new Error(message || 'Assertion error');
}

View file

@ -14,6 +14,8 @@
* limitations under the License.
*/
import { captureRawStack } from './stackTrace';
export class ManualPromise<T = void> extends Promise<T> {
private _resolve!: (t: T) => void;
private _reject!: (e: Error) => void;
@ -116,12 +118,3 @@ function cloneError(error: Error, frames: string[]) {
clone.stack = [error.name + ':' + error.message, ...frames].join('\n');
return clone;
}
function captureRawStack(): string[] {
const stackTraceLimit = Error.stackTraceLimit;
Error.stackTraceLimit = 50;
const error = new Error();
const stack = error.stack || '';
Error.stackTraceLimit = stackTraceLimit;
return stack.split('\n');
}

View file

@ -1,62 +1,33 @@
/**
* Copyright (c) Microsoft Corporation.
* The MIT License (MIT)
* Modifications 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
* Copyright (c) 2016-2023 Isaac Z. Schlueter i@izs.me, James Talmage james@talmage.io (github.com/jamestalmage), and
* Contributors
*
* http://www.apache.org/licenses/LICENSE-2.0
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* 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.
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
* Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
* WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { findRepeatedSubsequences } from './sequence';
import { parseStackFrame } from './stackUtils';
import type { StackFrame } from '@protocol/channels';
import type { Platform } from '../../common/platform';
export function parseStackTraceLine(line: string, pathSeparator: string): StackFrame | null {
const frame = parseStackFrame(line, pathSeparator);
if (!frame)
return null;
if (!process.env.PWDEBUGIMPL && (frame.file?.startsWith('internal') || frame.file?.startsWith('node:')))
return null;
if (!frame.file)
return null;
return {
file: frame.file,
line: frame.line || 0,
column: frame.column || 0,
function: frame.function,
};
}
export function rewriteErrorMessage<E extends Error>(e: E, newMessage: string): E {
const lines: string[] = (e.stack?.split('\n') || []).filter(l => l.startsWith(' at '));
e.message = newMessage;
const errorTitle = `${e.name}: ${e.message}`;
if (lines.length)
e.stack = `${errorTitle}\n${lines.join('\n')}`;
return e;
}
let coreDir: string | undefined;
const playwrightStackPrefixes: string[] = [];
export const addInternalStackPrefix = (prefix: string) => playwrightStackPrefixes.push(prefix);
export const setLibraryStackPrefix = (prefix: string) => {
coreDir = prefix;
playwrightStackPrefixes.push(prefix);
};
export type RawStack = string[];
export type StackFrame = {
file: string,
line: number,
column: number,
function?: string,
};
export function captureRawStack(): RawStack {
const stackTraceLimit = Error.stackTraceLimit;
Error.stackTraceLimit = 50;
@ -66,61 +37,82 @@ export function captureRawStack(): RawStack {
return stack.split('\n');
}
export function captureLibraryStackTrace(pathSeparator: string): { frames: StackFrame[], apiName: string } {
const stack = captureRawStack();
export function parseStackFrame(text: string, pathSeparator: string): StackFrame | null {
const match = text && text.match(re);
if (!match)
return null;
type ParsedFrame = {
frame: StackFrame;
frameText: string;
isPlaywrightLibrary: boolean;
let fname = match[2];
let file = match[7];
if (!file)
return null;
if (!process.env.PWDEBUGIMPL && (file.startsWith('internal') || file.startsWith('node:')))
return null;
const line = match[8];
const column = match[9];
const closeParen = match[11] === ')';
const frame: StackFrame = {
file: '',
line: 0,
column: 0,
};
let parsedFrames = stack.map(line => {
const frame = parseStackTraceLine(line, pathSeparator);
if (!frame || !frame.file)
return null;
const isPlaywrightLibrary = !!coreDir && frame.file.startsWith(coreDir);
const parsed: ParsedFrame = {
frame,
frameText: line,
isPlaywrightLibrary
};
return parsed;
}).filter(Boolean) as ParsedFrame[];
let apiName = '';
if (line)
frame.line = Number(line);
// Deepest transition between non-client code calling into client
// code is the api entry.
for (let i = 0; i < parsedFrames.length - 1; i++) {
const parsedFrame = parsedFrames[i];
if (parsedFrame.isPlaywrightLibrary && !parsedFrames[i + 1].isPlaywrightLibrary) {
apiName = apiName || normalizeAPIName(parsedFrame.frame.function);
break;
if (column)
frame.column = Number(column);
if (closeParen && file) {
// make sure parens are balanced
// if we have a file like "asdf) [as foo] (xyz.js", then odds are
// that the fname should be += " (asdf) [as foo]" and the file
// should be just "xyz.js"
// walk backwards from the end to find the last unbalanced (
let closes = 0;
for (let i = file.length - 1; i > 0; i--) {
if (file.charAt(i) === ')') {
closes++;
} else if (file.charAt(i) === '(' && file.charAt(i - 1) === ' ') {
closes--;
if (closes === -1 && file.charAt(i - 1) === ' ') {
const before = file.slice(0, i - 1);
const after = file.slice(i + 1);
file = after;
fname += ` (${before}`;
break;
}
}
}
}
function normalizeAPIName(name?: string): string {
if (!name)
return '';
const match = name.match(/(API|JS|CDP|[A-Z])(.*)/);
if (!match)
return name;
return match[1].toLowerCase() + match[2];
if (fname) {
const methodMatch = fname.match(methodRe);
if (methodMatch)
fname = methodMatch[1];
}
// This is for the inspector so that it did not include the test runner stack frames.
parsedFrames = parsedFrames.filter(f => {
if (process.env.PWDEBUGIMPL)
return true;
if (playwrightStackPrefixes.some(prefix => f.frame.file.startsWith(prefix)))
return false;
return true;
});
if (file) {
if (file.startsWith('file://'))
file = fileURLToPath(file, pathSeparator);
frame.file = file;
}
return {
frames: parsedFrames.map(p => p.frame),
apiName
};
if (fname)
frame.function = fname;
return frame;
}
export function rewriteErrorMessage<E extends Error>(e: E, newMessage: string): E {
const lines: string[] = (e.stack?.split('\n') || []).filter(l => l.startsWith(' at '));
e.message = newMessage;
const errorTitle = `${e.name}: ${e.message}`;
if (lines.length)
e.stack = `${errorTitle}\n${lines.join('\n')}`;
return e;
}
export function stringifyStackFrames(frames: StackFrame[]): string[] {
@ -142,31 +134,40 @@ export function splitErrorMessage(message: string): { name: string, message: str
};
}
export function formatCallLog(platform: Platform, log: string[] | undefined): string {
if (!log || !log.some(l => !!l))
return '';
return `
Call log:
${platform.colors.dim(log.join('\n'))}
`;
}
const re = new RegExp('^' +
// Sometimes we strip out the ' at' because it's noisy
'(?:\\s*at )?' +
// $1 = ctor if 'new'
'(?:(new) )?' +
// $2 = function name (can be literally anything)
// May contain method at the end as [as xyz]
'(?:(.*?) \\()?' +
// (eval at <anonymous> (file.js:1:1),
// $3 = eval origin
// $4:$5:$6 are eval file/line/col, but not normally reported
'(?:eval at ([^ ]+) \\((.+?):(\\d+):(\\d+)\\), )?' +
// file:line:col
// $7:$8:$9
// $10 = 'native' if native
'(?:(.+?):(\\d+):(\\d+)|(native))' +
// maybe close the paren, then end
// if $11 is ), then we only allow balanced parens in the filename
// any imbalance is placed on the fname. This is a heuristic, and
// bound to be incorrect in some edge cases. The bet is that
// having weird characters in method names is more common than
// having weird characters in filenames, which seems reasonable.
'(\\)?)$'
);
export function compressCallLog(log: string[]): string[] {
const lines: string[] = [];
const methodRe = /^(.*?) \[as (.*?)\]$/;
for (const block of findRepeatedSubsequences(log)) {
for (let i = 0; i < block.sequence.length; i++) {
const line = block.sequence[i];
const leadingWhitespace = line.match(/^\s*/);
const whitespacePrefix = ' ' + leadingWhitespace?.[0] || '';
const countPrefix = `${block.count} × `;
if (block.count > 1 && i === 0)
lines.push(whitespacePrefix + countPrefix + line.trim());
else if (block.count > 1)
lines.push(whitespacePrefix + ' '.repeat(countPrefix.length - 2) + '- ' + line.trim());
else
lines.push(whitespacePrefix + '- ' + line.trim());
}
}
return lines;
function fileURLToPath(fileUrl: string, pathSeparator: string): string {
if (!fileUrl.startsWith('file://'))
return fileUrl;
let path = decodeURIComponent(fileUrl.slice(7));
if (path.startsWith('/') && /^[a-zA-Z]:/.test(path.slice(1)))
path = path.slice(1);
return path.replace(/\//g, pathSeparator);
}

View file

@ -1,158 +0,0 @@
/**
* The MIT License (MIT)
* Modifications copyright (c) Microsoft Corporation.
*
* Copyright (c) 2016-2023 Isaac Z. Schlueter i@izs.me, James Talmage james@talmage.io (github.com/jamestalmage), and
* Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
* Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
* WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
type StackData = {
line?: number;
column?: number;
file?: string;
isConstructor?: boolean;
evalOrigin?: string;
native?: boolean;
function?: string;
method?: string;
evalLine?: number | undefined;
evalColumn?: number | undefined;
evalFile?: string | undefined;
};
export function parseStackFrame(line: string, pathSeparator: string): StackData | null {
const match = line && line.match(re);
if (!match)
return null;
const ctor = match[1] === 'new';
let fname = match[2];
const evalOrigin = match[3];
const evalFile = match[4];
const evalLine = Number(match[5]);
const evalCol = Number(match[6]);
let file = match[7];
const lnum = match[8];
const col = match[9];
const native = match[10] === 'native';
const closeParen = match[11] === ')';
let method;
const res: StackData = {};
if (lnum)
res.line = Number(lnum);
if (col)
res.column = Number(col);
if (closeParen && file) {
// make sure parens are balanced
// if we have a file like "asdf) [as foo] (xyz.js", then odds are
// that the fname should be += " (asdf) [as foo]" and the file
// should be just "xyz.js"
// walk backwards from the end to find the last unbalanced (
let closes = 0;
for (let i = file.length - 1; i > 0; i--) {
if (file.charAt(i) === ')') {
closes++;
} else if (file.charAt(i) === '(' && file.charAt(i - 1) === ' ') {
closes--;
if (closes === -1 && file.charAt(i - 1) === ' ') {
const before = file.slice(0, i - 1);
const after = file.slice(i + 1);
file = after;
fname += ` (${before}`;
break;
}
}
}
}
if (fname) {
const methodMatch = fname.match(methodRe);
if (methodMatch) {
fname = methodMatch[1];
method = methodMatch[2];
}
}
setFile(res, file, pathSeparator);
if (ctor)
res.isConstructor = true;
if (evalOrigin) {
res.evalOrigin = evalOrigin;
res.evalLine = evalLine;
res.evalColumn = evalCol;
res.evalFile = evalFile && evalFile.replace(/\\/g, '/');
}
if (native)
res.native = true;
if (fname)
res.function = fname;
if (method && fname !== method)
res.method = method;
return res;
}
function setFile(result: StackData, filename: string, pathSeparator: string) {
if (filename) {
if (filename.startsWith('file://'))
filename = fileURLToPath(filename, pathSeparator);
result.file = filename;
}
}
const re = new RegExp('^' +
// Sometimes we strip out the ' at' because it's noisy
'(?:\\s*at )?' +
// $1 = ctor if 'new'
'(?:(new) )?' +
// $2 = function name (can be literally anything)
// May contain method at the end as [as xyz]
'(?:(.*?) \\()?' +
// (eval at <anonymous> (file.js:1:1),
// $3 = eval origin
// $4:$5:$6 are eval file/line/col, but not normally reported
'(?:eval at ([^ ]+) \\((.+?):(\\d+):(\\d+)\\), )?' +
// file:line:col
// $7:$8:$9
// $10 = 'native' if native
'(?:(.+?):(\\d+):(\\d+)|(native))' +
// maybe close the paren, then end
// if $11 is ), then we only allow balanced parens in the filename
// any imbalance is placed on the fname. This is a heuristic, and
// bound to be incorrect in some edge cases. The bet is that
// having weird characters in method names is more common than
// having weird characters in filenames, which seems reasonable.
'(\\)?)$'
);
const methodRe = /^(.*?) \[as (.*?)\]$/;
function fileURLToPath(fileUrl: string, pathSeparator: string): string {
if (!fileUrl.startsWith('file://'))
return fileUrl;
let path = decodeURIComponent(fileUrl.slice(7));
if (path.startsWith('/') && /^[a-zA-Z]:/.test(path.slice(1)))
path = path.slice(1);
return path.replace(/\//g, pathSeparator);
}

View file

@ -18,7 +18,7 @@ import * as fs from 'fs';
import * as path from 'path';
import * as playwrightLibrary from 'playwright-core';
import { addInternalStackPrefix, asLocator, createGuid, currentZone, debugMode, isString, jsonStringifyForceASCII } from 'playwright-core/lib/utils';
import { setBoxedStackPrefixes, asLocator, createGuid, currentZone, debugMode, isString, jsonStringifyForceASCII } from 'playwright-core/lib/utils';
import { currentTestInfo } from './common/globals';
import { rootTestType } from './common/testType';
@ -32,7 +32,7 @@ import type { APIRequestContext, Browser, BrowserContext, BrowserContextOptions,
export { expect } from './matchers/expect';
export const _baseTest: TestType<{}, {}> = rootTestType.test;
addInternalStackPrefix(path.dirname(require.resolve('../package.json')));
setBoxedStackPrefixes([path.dirname(require.resolve('../package.json'))]);
if ((process as any)['__pw_initiator__']) {
const originalStackTraceLimit = Error.stackTraceLimit;

View file

@ -25,5 +25,6 @@ export interface GitCommitInfo {
'pull.link'?: string;
'pull.diff'?: string;
'pull.base'?: string;
'pull.title'?: string;
'ci.link'?: string;
}

View file

@ -14,6 +14,8 @@
* limitations under the License.
*/
import fs from 'fs';
import { createGuid, spawnAsync } from 'playwright-core/lib/utils';
import type { TestRunnerPlugin } from './';
@ -33,7 +35,7 @@ export const gitCommitInfo = (options?: GitCommitInfoPluginOptions): TestRunnerP
name: 'playwright:git-commit-info',
setup: async (config: FullConfig, configDir: string) => {
const fromEnv = linksFromEnv();
const fromEnv = await linksFromEnv();
const fromCLI = await gitStatusFromCLI(options?.directory || configDir, fromEnv);
config.metadata = config.metadata || {};
config.metadata['git.commit.info'] = { ...fromEnv, ...fromCLI };
@ -45,7 +47,7 @@ interface GitCommitInfoPluginOptions {
directory?: string;
}
function linksFromEnv() {
async function linksFromEnv() {
const out: Partial<GitCommitInfo> = {};
// Jenkins: https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#using-environment-variables
if (process.env.BUILD_URL)
@ -55,15 +57,21 @@ function linksFromEnv() {
out['revision.link'] = `${process.env.CI_PROJECT_URL}/-/commit/${process.env.CI_COMMIT_SHA}`;
if (process.env.CI_JOB_URL)
out['ci.link'] = process.env.CI_JOB_URL;
// GitHub: https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables
// GitHub: https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables
if (process.env.GITHUB_SERVER_URL && process.env.GITHUB_REPOSITORY && process.env.GITHUB_SHA)
out['revision.link'] = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/commit/${process.env.GITHUB_SHA}`;
if (process.env.GITHUB_SERVER_URL && process.env.GITHUB_REPOSITORY && process.env.GITHUB_RUN_ID)
out['ci.link'] = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
if (process.env.GITHUB_REF_NAME && process.env.GITHUB_REF_NAME.endsWith('/merge')) {
const pullId = process.env.GITHUB_REF_NAME.substring(0, process.env.GITHUB_REF_NAME.indexOf('/merge'));
out['pull.link'] = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/pull/${pullId}`;
out['pull.base'] = process.env.GITHUB_BASE_REF;
if (process.env.GITHUB_EVENT_PATH) {
try {
const json = JSON.parse(await fs.promises.readFile(process.env.GITHUB_EVENT_PATH, 'utf8'));
if (json.pull_request) {
out['pull.title'] = json.pull_request.title;
out['pull.link'] = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/pull/${json.pull_request.number}`;
out['pull.base'] = json.pull_request.base.ref;
}
} catch {
}
}
return out;
}

View file

@ -17,7 +17,7 @@
import * as path from 'path';
import { getPackageManagerExecCommand } from 'playwright-core/lib/utils';
import { parseStackTraceLine } from 'playwright-core/lib/utils';
import { parseStackFrame } from 'playwright-core/lib/utils';
import { ms as milliseconds } from 'playwright-core/lib/utilsBundle';
import { colors as realColors, noColors } from 'playwright-core/lib/utils';
@ -522,7 +522,7 @@ export function prepareErrorStack(stack: string): {
const stackLines = lines.slice(firstStackLine);
let location: Location | undefined;
for (const line of stackLines) {
const frame = parseStackTraceLine(line, path.sep);
const frame = parseStackFrame(line, path.sep);
if (!frame || !frame.file)
continue;
if (belongsToNodeModules(frame.file))

Some files were not shown because too many files have changed in this diff Show more