If braces changes

This commit is contained in:
Adam Gastineau 2024-12-17 06:17:35 -08:00
parent ea592d32aa
commit b77146632b
538 changed files with 12363 additions and 6451 deletions

View file

@ -27,16 +27,19 @@ export function bundle(): Plugin {
},
transformIndexHtml: {
handler(html, ctx) {
if (!ctx || !ctx.bundle)
if (!ctx || !ctx.bundle) {
return html;
}
html = html.replace(/(?=<!--)([\s\S]*?)-->/, '');
for (const [name, value] of Object.entries(ctx.bundle)) {
if (name.endsWith('.map'))
if (name.endsWith('.map')) {
continue;
if ('code' in value)
}
if ('code' in value) {
html = html.replace(/<script type="module".*<\/script>/, () => `<script type="module">${value.code}</script>`);
else
} else {
html = html.replace(/<link rel="stylesheet"[^>]*>/, () => `<style type='text/css'>${value.source}</style>`);
}
}
return html;
},

View file

@ -98,8 +98,9 @@ export class Filter {
}
token.push(c);
}
if (token.length)
if (token.length) {
result.push(token.join('').toLowerCase());
}
return result;
}
@ -107,37 +108,44 @@ export class Filter {
const searchValues = cacheSearchValues(test);
if (this.project.length) {
const matches = !!this.project.find(p => searchValues.project.includes(p));
if (!matches)
if (!matches) {
return false;
}
}
if (this.status.length) {
const matches = !!this.status.find(s => searchValues.status.includes(s));
if (!matches)
if (!matches) {
return false;
}
} else {
if (searchValues.status === 'skipped')
if (searchValues.status === 'skipped') {
return false;
}
}
if (this.text.length) {
for (const text of this.text) {
if (searchValues.text.includes(text))
if (searchValues.text.includes(text)) {
continue;
}
const [fileName, line, column] = text.split(':');
if (searchValues.file.includes(fileName) && searchValues.line === line && (column === undefined || searchValues.column === column))
if (searchValues.file.includes(fileName) && searchValues.line === line && (column === undefined || searchValues.column === column)) {
continue;
}
return false;
}
}
if (this.labels.length) {
const matches = this.labels.every(l => searchValues.labels.includes(l));
if (!matches)
if (!matches) {
return false;
}
}
if (this.annotations.length) {
const matches = this.annotations.every(annotation =>
searchValues.annotations.some(a => a.includes(annotation)));
if (!matches)
if (!matches) {
return false;
}
}
return true;
}
@ -158,16 +166,20 @@ const searchValuesSymbol = Symbol('searchValues');
function cacheSearchValues(test: TestCaseSummary & { [searchValuesSymbol]?: SearchValues }): SearchValues {
const cached = test[searchValuesSymbol];
if (cached)
if (cached) {
return cached;
}
let status: SearchValues['status'] = 'passed';
if (test.outcome === 'unexpected')
if (test.outcome === 'unexpected') {
status = 'failed';
if (test.outcome === 'flaky')
}
if (test.outcome === 'flaky') {
status = 'flaky';
if (test.outcome === 'skipped')
}
if (test.outcome === 'skipped') {
status = 'skipped';
}
const searchValues: SearchValues = {
text: (status + ' ' + test.projectName + ' ' + test.tags.join(' ') + ' ' + test.location.file + ' ' + test.path.join(' ') + ' ' + test.title).toLowerCase(),
project: test.projectName.toLowerCase(),
@ -184,19 +196,23 @@ function cacheSearchValues(test: TestCaseSummary & { [searchValuesSymbol]?: Sear
export function filterWithToken(tokens: string[], token: string, append: boolean): string {
if (append) {
if (!tokens.includes(token))
if (!tokens.includes(token)) {
return '#?q=' + [...tokens, token].join(' ').trim();
}
return '#?q=' + tokens.filter(t => t !== token).join(' ').trim();
}
// if metaKey or ctrlKey is not pressed, replace existing token with new token
let prefix: 's:' | 'p:' | '@';
if (token.startsWith('s:'))
if (token.startsWith('s:')) {
prefix = 's:';
if (token.startsWith('p:'))
}
if (token.startsWith('p:')) {
prefix = 'p:';
if (token.startsWith('@'))
}
if (token.startsWith('@')) {
prefix = '@';
}
const newTokens = tokens.filter(t => !t.startsWith(prefix));
newTokens.push(token);

View file

@ -36,8 +36,9 @@ document.head.appendChild(link);
const ReportLoader: React.FC = () => {
const [report, setReport] = React.useState<LoadedReport | undefined>();
React.useEffect(() => {
if (report)
if (report) {
return;
}
const zipReport = new ZipReport();
zipReport.load().then(() => setReport(zipReport));
}, [report]);
@ -58,8 +59,9 @@ class ZipReport implements LoadedReport {
async load() {
const zipURI = await new Promise<string>(resolve => {
if (window.playwrightReportBase64)
if (window.playwrightReportBase64) {
return resolve(window.playwrightReportBase64);
}
if (window.opener) {
window.addEventListener('message', event => {
if (event.source === window.opener) {
@ -70,15 +72,17 @@ class ZipReport implements LoadedReport {
window.opener.postMessage('ready', '*');
} else {
const oldReport = localStorage.getItem(kPlaywrightReportStorageForHMR);
if (oldReport)
if (oldReport) {
return resolve(oldReport);
}
alert('couldnt find report, something with HMR is broken');
}
});
const zipReader = new zipjs.ZipReader(new zipjs.Data64URIReader(zipURI), { useWebWorkers: false });
for (const entry of await zipReader.getEntries())
for (const entry of await zipReader.getEntries()) {
this._entries.set(entry.filename, entry);
}
this._json = await this.entry('report.json') as HTMLReport;
}

View file

@ -101,11 +101,13 @@ export const SearchParamsProvider: React.FunctionComponent<React.PropsWithChildr
};
function downloadFileNameForAttachment(attachment: TestAttachment): string {
if (attachment.name.includes('.') || !attachment.path)
if (attachment.name.includes('.') || !attachment.path) {
return attachment.name;
}
const firstDotIndex = attachment.path.indexOf('.');
if (firstDotIndex === -1)
if (firstDotIndex === -1) {
return attachment.name;
}
return attachment.name + attachment.path.slice(firstDotIndex, attachment.path.length);
}
@ -121,22 +123,27 @@ export function useAnchor(id: AnchorID, onReveal: () => void) {
const searchParams = React.useContext(SearchParamsContext);
const isAnchored = useIsAnchored(id);
React.useEffect(() => {
if (isAnchored)
if (isAnchored) {
onReveal();
}
}, [isAnchored, onReveal, searchParams]);
}
export function useIsAnchored(id: AnchorID) {
const searchParams = React.useContext(SearchParamsContext);
const anchor = searchParams.get('anchor');
if (anchor === null)
if (anchor === null) {
return false;
if (typeof id === 'undefined')
}
if (typeof id === 'undefined') {
return false;
if (typeof id === 'string')
}
if (typeof id === 'string') {
return id === anchor;
if (Array.isArray(id))
}
if (Array.isArray(id)) {
return id.includes(anchor);
}
return id(anchor);
}
@ -152,11 +159,14 @@ export function Anchor({ id, children }: React.PropsWithChildren<{ id: AnchorID
export function testResultHref({ test, result, anchor }: { test?: TestCase | TestCaseSummary, result?: TestResult | TestResultSummary, anchor?: string }) {
const params = new URLSearchParams();
if (test)
if (test) {
params.set('testId', test.testId);
if (test && result)
}
if (test && result) {
params.set('run', '' + test.results.indexOf(result as any));
if (anchor)
}
if (anchor) {
params.set('anchor', anchor);
}
return `#?` + params;
}

View file

@ -62,8 +62,9 @@ class ErrorBoundary extends React.Component<React.PropsWithChildren<{}>, { error
export const MetadataView: React.FC<Metainfo> = metadata => <ErrorBoundary><InnerMetadataView {...metadata} /></ErrorBoundary>;
const InnerMetadataView: React.FC<Metainfo> = metadata => {
if (!Object.keys(metadata).find(k => k.startsWith('revision.') || k.startsWith('ci.')))
if (!Object.keys(metadata).find(k => k.startsWith('revision.') || k.startsWith('ci.'))) {
return null;
}
return (
<AutoChip header={

View file

@ -54,8 +54,9 @@ export const ReportView: React.FC<{
const testIdToFileIdMap = React.useMemo(() => {
const map = new Map<string, string>();
for (const file of report?.json().files || []) {
for (const test of file.tests)
for (const test of file.tests) {
map.set(test.testId, file.fileId);
}
}
return map;
}, [report]);
@ -66,8 +67,9 @@ export const ReportView: React.FC<{
const result: TestModelSummary = { files: [], tests: [] };
for (const file of report?.json().files || []) {
const tests = file.tests.filter(t => filter.matches(t));
if (tests.length)
if (tests.length) {
result.files.push({ ...file, tests });
}
result.tests.push(...tests);
}
return result;
@ -112,11 +114,13 @@ const TestCaseViewLoader: React.FC<{
React.useEffect(() => {
(async () => {
if (!testId || testId === test?.testId)
if (!testId || testId === test?.testId) {
return;
}
const fileId = testIdToFileIdMap.get(testId);
if (!fileId)
if (!fileId) {
return;
}
const file = await report.entry(`${fileId}.json`) as TestFile;
for (const t of file.tests) {
if (t.testId === testId) {
@ -144,8 +148,9 @@ function computeStats(files: TestFileSummary[], filter: Filter): FilteredStats {
for (const file of files) {
const tests = file.tests.filter(t => filter.matches(t));
stats.total += tests.length;
for (const test of tests)
for (const test of tests) {
stats.duration += test.duration;
}
}
return stats;
}

View file

@ -45,8 +45,9 @@ export const TabbedPane: React.FunctionComponent<{
</div>
{
tabs.map(tab => {
if (selectedTab === tab.id)
if (selectedTab === tab.id) {
return <div key={tab.id} className='tab-content'>{tab.render()}</div>;
}
})
}
</div>

View file

@ -40,8 +40,9 @@ export const TestCaseView: React.FC<{
const filterParam = searchParams.has('q') ? '&q=' + searchParams.get('q') : '';
const labels = React.useMemo(() => {
if (!test)
if (!test) {
return undefined;
}
return test.tags;
}, [test]);
@ -93,8 +94,9 @@ function TestCaseAnnotationView({ annotation: { type, description } }: { annotat
}
function retryLabel(index: number) {
if (!index)
if (!index) {
return 'Run';
}
return `Retry #${index}`;
}

View file

@ -74,8 +74,9 @@ export const TestFileView: React.FC<React.PropsWithChildren<{
function imageDiffBadge(test: TestCaseSummary): JSX.Element | undefined {
for (const result of test.results) {
for (const attachment of result.attachments) {
if (attachment.contentType.startsWith('image/') && !!attachment.name.match(/-(expected|actual|diff)/))
if (attachment.contentType.startsWith('image/') && !!attachment.name.match(/-(expected|actual|diff)/)) {
return <Link href={testResultHref({ test, result, anchor: `attachment-${attachment.name}` })} title='View images' className='test-file-badge'>{image()}</Link>;
}
}
}
}

View file

@ -45,8 +45,9 @@ export const TestFilesView: React.FC<{
projectNames={projectNames}
isFileExpanded={fileId => {
const value = expandedFiles.get(fileId);
if (value === undefined)
if (value === undefined) {
return defaultExpanded;
}
return !!value;
}}
setFileExpanded={(fileId, expanded) => {
@ -63,8 +64,9 @@ export const TestFilesHeader: React.FC<{
report: HTMLReport | undefined,
filteredStats?: FilteredStats,
}> = ({ report, filteredStats }) => {
if (!report)
if (!report) {
return;
}
return <>
<div className='mt-2 mx-1' style={{ display: 'flex' }}>
{report.projectNames.length === 1 && !!report.projectNames[0] && <div data-testid='project-name' style={{ color: 'var(--color-fg-subtle)' }}>Project: {report.projectNames[0]}</div>}

View file

@ -36,8 +36,9 @@ function groupImageDiffs(screenshots: Set<TestAttachment>): ImageDiffWithAnchors
const snapshotNameToImageDiff = new Map<string, ImageDiffWithAnchors>();
for (const attachment of screenshots) {
const match = attachment.name.match(/^(.*)-(expected|actual|diff|previous)(\.[^.]+)?$/);
if (!match)
if (!match) {
continue;
}
const [, name, category, extension = ''] = match;
const snapshotName = name + extension;
let imageDiff = snapshotNameToImageDiff.get(snapshotName);
@ -46,14 +47,18 @@ function groupImageDiffs(screenshots: Set<TestAttachment>): ImageDiffWithAnchors
snapshotNameToImageDiff.set(snapshotName, imageDiff);
}
imageDiff.anchors.push(`attachment-${attachment.name}`);
if (category === 'actual')
if (category === 'actual') {
imageDiff.actual = { attachment };
if (category === 'expected')
}
if (category === 'expected') {
imageDiff.expected = { attachment, title: 'Expected' };
if (category === 'previous')
}
if (category === 'previous') {
imageDiff.expected = { attachment, title: 'Previous' };
if (category === 'diff')
}
if (category === 'diff') {
imageDiff.diff = { attachment };
}
}
for (const [name, diff] of snapshotNameToImageDiff) {
if (!diff.actual || !diff.expected) {
@ -88,8 +93,9 @@ export const TestResultView: React.FC<{
return <div className='test-result'>
{!!errors.length && <AutoChip header='Errors'>
{errors.map((error, index) => {
if (error.type === 'screenshot')
if (error.type === 'screenshot') {
return <TestScreenshotErrorView key={'test-result-error-message-' + index} errorPrefix={error.errorPrefix} diff={error.diff!} errorSuffix={error.errorSuffix}></TestScreenshotErrorView>;
}
return <TestErrorView key={'test-result-error-message-' + index} error={error.error!}></TestErrorView>;
})}
</AutoChip>}
@ -177,15 +183,18 @@ const StepTreeItem: React.FC<{
const attachmentName = step.title.match(/^attach "(.*)"$/)?.[1];
return <TreeItem title={<span aria-label={step.title}>
<span style={{ float: 'right' }}>{msToString(step.duration)}</span>
{attachmentName && <a style={{ float: 'right' }} title='link to attachment' href={testResultHref({ test, result, anchor: `attachment-${attachmentName}` })} onClick={evt => { evt.stopPropagation(); }}>{icons.attachment()}</a>}
{attachmentName && <a style={{ float: 'right' }} title='link to attachment' href={testResultHref({ test, result, anchor: `attachment-${attachmentName}` })} onClick={evt => {
evt.stopPropagation();
}}>{icons.attachment()}</a>}
{statusIcon(step.error || step.duration === -1 ? 'failed' : 'passed')}
<span>{step.title}</span>
{step.count > 1 && <> <span className='test-result-counter'>{step.count}</span></>}
{step.location && <span className='test-result-path'> {step.location.file}:{step.location.line}</span>}
</span>} loadChildren={step.steps.length + (step.snippet ? 1 : 0) ? () => {
const children = step.steps.map((s, i) => <StepTreeItem key={i} step={s} depth={depth + 1} result={result} test={test} />);
if (step.snippet)
if (step.snippet) {
children.unshift(<TestErrorView testId='test-snippet' key='line' error={step.snippet}/>);
}
return children;
} : undefined} depth={depth}/>;
};

View file

@ -30,7 +30,9 @@ export const TreeItem: React.FunctionComponent<{
}> = ({ title, loadChildren, onClick, expandByDefault, depth, selected, style }) => {
const [expanded, setExpanded] = React.useState(expandByDefault || false);
return <div className={'tree-item'} style={style}>
<span className={clsx('tree-item-title', selected && 'selected')} style={{ whiteSpace: 'nowrap', paddingLeft: depth * 22 + 4 }} onClick={() => { onClick?.(); setExpanded(!expanded); }} >
<span className={clsx('tree-item-title', selected && 'selected')} style={{ whiteSpace: 'nowrap', paddingLeft: depth * 22 + 4 }} onClick={() => {
onClick?.(); setExpanded(!expanded);
}} >
{loadChildren && !!expanded && icons.downArrow()}
{loadChildren && !expanded && icons.rightArrow()}
{!loadChildren && <span style={{ visibility: 'hidden' }}>{icons.rightArrow()}</span>}

View file

@ -15,26 +15,32 @@
*/
export function msToString(ms: number): string {
if (!isFinite(ms))
if (!isFinite(ms)) {
return '-';
}
if (ms === 0)
if (ms === 0) {
return '0ms';
}
if (ms < 1000)
if (ms < 1000) {
return ms.toFixed(0) + 'ms';
}
const seconds = ms / 1000;
if (seconds < 60)
if (seconds < 60) {
return seconds.toFixed(1) + 's';
}
const minutes = seconds / 60;
if (minutes < 60)
if (minutes < 60) {
return minutes.toFixed(1) + 'm';
}
const hours = minutes / 60;
if (hours < 24)
if (hours < 24) {
return hours.toFixed(1) + 'h';
}
const days = hours / 24;
return days.toFixed(1) + 'd';
@ -43,8 +49,9 @@ export function msToString(ms: number): string {
// hash string to integer in range [0, 6] for color index, to get same color for same tag
export function hashStringToInt(str: string) {
let hash = 0;
for (let i = 0; i < str.length; i++)
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 8) - hash);
}
return Math.abs(hash % 6);
}

View file

@ -32,17 +32,20 @@ export class AndroidServerLauncherImpl {
omitDriverInstall: options.omitDriverInstall,
});
if (devices.length === 0)
if (devices.length === 0) {
throw new Error('No devices found');
}
if (options.deviceSerialNumber) {
devices = devices.filter(d => d.serial === options.deviceSerialNumber);
if (devices.length === 0)
if (devices.length === 0) {
throw new Error(`No device with serial number '${options.deviceSerialNumber}' was found`);
}
}
if (devices.length > 1)
if (devices.length > 1) {
throw new Error(`More than one device found. Please specify deviceSerialNumber`);
}
const device = devices[0];

View file

@ -80,7 +80,8 @@ export class BrowserServerLauncherImpl implements BrowserServerLauncher {
function toProtocolLogger(logger: Logger | undefined): ProtocolLogger | undefined {
return logger ? (direction: 'send' | 'receive', message: object) => {
if (logger.isEnabled('protocol', 'verbose'))
if (logger.isEnabled('protocol', 'verbose')) {
logger.log('protocol', 'verbose', (direction === 'send' ? 'SEND ► ' : '◀ RECV ') + JSON.stringify(message), [], {});
}
} : undefined;
}

View file

@ -41,8 +41,9 @@ export function runDriver() {
// Certain Language Binding JSON parsers (e.g. .NET) do not like strings with lone surrogates.
const isJavaScriptLanguageBinding = !process.env.PW_LANG_NAME || process.env.PW_LANG_NAME === 'javascript';
const replacer = !isJavaScriptLanguageBinding && (String.prototype as any).toWellFormed ? (key: string, value: any): any => {
if (typeof value === 'string')
if (typeof value === 'string') {
return value.toWellFormed();
}
return value;
} : undefined;
dispatcherConnection.onmessage = message => transport.send(JSON.stringify(message, replacer));
@ -85,8 +86,9 @@ export async function runServer(options: RunServerOptions) {
export async function launchBrowserServer(browserName: string, configFile?: string) {
let options: LaunchServerOptions = {};
if (configFile)
if (configFile) {
options = JSON.parse(fs.readFileSync(configFile).toString());
}
const browserType = (playwright as any)[browserName] as BrowserType;
const server = await browserType.launchServer(options);
console.log(server.wsEndpoint());

View file

@ -82,42 +82,50 @@ function suggestedBrowsersToInstall() {
function defaultBrowsersToInstall(options: { noShell?: boolean, onlyShell?: boolean }): Executable[] {
let executables = registry.defaultExecutables();
if (options.noShell)
if (options.noShell) {
executables = executables.filter(e => e.name !== 'chromium-headless-shell');
if (options.onlyShell)
}
if (options.onlyShell) {
executables = executables.filter(e => e.name !== 'chromium');
}
return executables;
}
function checkBrowsersToInstall(args: string[], options: { noShell?: boolean, onlyShell?: boolean }): Executable[] {
if (options.noShell && options.onlyShell)
if (options.noShell && options.onlyShell) {
throw new Error(`Only one of --no-shell and --only-shell can be specified`);
}
const faultyArguments: string[] = [];
const executables: Executable[] = [];
const handleArgument = (arg: string) => {
const executable = registry.findExecutable(arg);
if (!executable || executable.installType === 'none')
if (!executable || executable.installType === 'none') {
faultyArguments.push(arg);
else
} else {
executables.push(executable);
if (executable?.browserName === 'chromium')
}
if (executable?.browserName === 'chromium') {
executables.push(registry.findExecutable('ffmpeg')!);
}
};
for (const arg of args) {
if (arg === 'chromium') {
if (!options.onlyShell)
if (!options.onlyShell) {
handleArgument('chromium');
if (!options.noShell)
}
if (!options.noShell) {
handleArgument('chromium-headless-shell');
}
} else {
handleArgument(arg);
}
}
if (faultyArguments.length)
if (faultyArguments.length) {
throw new Error(`Invalid installation targets: ${faultyArguments.map(name => `'${name}'`).join(', ')}. Expecting one of: ${suggestedBrowsersToInstall()}`);
}
return executables;
}
@ -132,8 +140,9 @@ program
.option('--no-shell', 'do not install chromium headless shell')
.action(async function(args: string[], options: { withDeps?: boolean, force?: boolean, dryRun?: boolean, shell?: boolean, noShell?: boolean, onlyShell?: boolean }) {
// For '--no-shell' option, commander sets `shell: false` instead.
if (options.shell === false)
if (options.shell === false) {
options.noShell = true;
}
if (isLikelyNpxGlobal()) {
console.error(wrapInASCIIBox([
`WARNING: It looks like you are running 'npx playwright install' without first`,
@ -157,8 +166,9 @@ program
try {
const hasNoArguments = !args.length;
const executables = hasNoArguments ? defaultBrowsersToInstall(options) : checkBrowsersToInstall(args, options);
if (options.withDeps)
if (options.withDeps) {
await registry.installDeps(executables, !!options.dryRun);
}
if (options.dryRun) {
for (const executable of executables) {
const version = executable.browserVersion ? `version ` + executable.browserVersion : '';
@ -167,8 +177,9 @@ program
if (executable.downloadURLs?.length) {
const [url, ...fallbacks] = executable.downloadURLs;
console.log(` Download url: ${url}`);
for (let i = 0; i < fallbacks.length; ++i)
for (let i = 0; i < fallbacks.length; ++i) {
console.log(` Download fallback ${i + 1}: ${fallbacks[i]}`);
}
}
console.log(``);
}
@ -213,10 +224,11 @@ program
.option('--dry-run', 'Do not execute installation commands, only print them')
.action(async function(args: string[], options: { dryRun?: boolean }) {
try {
if (!args.length)
if (!args.length) {
await registry.installDeps(defaultBrowsersToInstall({}), !!options.dryRun);
else
} else {
await registry.installDeps(checkBrowsersToInstall(args, {}), !!options.dryRun);
}
} catch (e) {
console.log(`Failed to install browser dependencies\n${e}`);
gracefullyProcessExitDoNotHang(1);
@ -313,12 +325,15 @@ program
.option('--stdin', 'Accept trace URLs over stdin to update the viewer')
.description('show trace viewer')
.action(function(traces, options) {
if (options.browser === 'cr')
if (options.browser === 'cr') {
options.browser = 'chromium';
if (options.browser === 'ff')
}
if (options.browser === 'ff') {
options.browser = 'firefox';
if (options.browser === 'wk')
}
if (options.browser === 'wk') {
options.browser = 'webkit';
}
const openOptions: TraceViewerServerOptions = {
host: options.host,
@ -326,10 +341,11 @@ program
isServer: !!options.stdin,
};
if (options.port !== undefined || options.host !== undefined)
if (options.port !== undefined || options.host !== undefined) {
runTraceInBrowser(traces, openOptions).catch(logErrorAndExit);
else
} else {
runTraceViewerApp(traces, options.browser, openOptions, true).catch(logErrorAndExit);
}
}).addHelpText('afterAll', `
Examples:
@ -367,8 +383,9 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro
validateOptions(options);
const browserType = lookupBrowserType(options);
const launchOptions: LaunchOptions = extraOptions;
if (options.channel)
if (options.channel) {
launchOptions.channel = options.channel as any;
}
launchOptions.handleSIGINT = false;
const contextOptions: BrowserContextOptions =
@ -378,8 +395,9 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro
// In headful mode, use host device scale factor for things to look nice.
// In headless, keep things the way it works in Playwright by default.
// Assume high-dpi on MacOS. TODO: this is not perfect.
if (!extraOptions.headless)
if (!extraOptions.headless) {
contextOptions.deviceScaleFactor = os.platform() === 'darwin' ? 2 : 1;
}
// Work around the WebKit GTK scrolling issue.
if (browserType.name() === 'webkit' && process.platform === 'linux') {
@ -387,11 +405,13 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro
delete contextOptions.isMobile;
}
if (contextOptions.isMobile && browserType.name() === 'firefox')
if (contextOptions.isMobile && browserType.name() === 'firefox') {
contextOptions.isMobile = undefined;
}
if (options.blockServiceWorkers)
if (options.blockServiceWorkers) {
contextOptions.serviceWorkers = 'block';
}
// Proxy
@ -399,8 +419,9 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro
launchOptions.proxy = {
server: options.proxyServer
};
if (options.proxyBypass)
if (options.proxyBypass) {
launchOptions.proxy.bypass = options.proxyBypass;
}
}
const browser = await browserType.launch(launchOptions);
@ -411,8 +432,9 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro
process.stdout.write(text);
process.stdout.write('\n-------------8<-------------\n');
const autoExitCondition = process.env.PWTEST_CLI_AUTO_EXIT_WHEN;
if (autoExitCondition && text.includes(autoExitCondition))
if (autoExitCondition && text.includes(autoExitCondition)) {
closeBrowser();
}
};
// Make sure we exit abnormally when browser crashes.
const logs: string[] = [];
@ -434,8 +456,9 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro
if (options.viewportSize) {
try {
const [width, height] = options.viewportSize.split(',').map(n => +n);
if (isNaN(width) || isNaN(height))
if (isNaN(width) || isNaN(height)) {
throw new Error('bad values');
}
contextOptions.viewport = { width, height };
} catch (e) {
throw new Error('Invalid viewport size format: use "width,height", for example --viewport-size="800,600"');
@ -459,38 +482,45 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro
// User agent
if (options.userAgent)
if (options.userAgent) {
contextOptions.userAgent = options.userAgent;
}
// Lang
if (options.lang)
if (options.lang) {
contextOptions.locale = options.lang;
}
// Color scheme
if (options.colorScheme)
if (options.colorScheme) {
contextOptions.colorScheme = options.colorScheme as 'dark' | 'light';
}
// Timezone
if (options.timezone)
if (options.timezone) {
contextOptions.timezoneId = options.timezone;
}
// Storage
if (options.loadStorage)
if (options.loadStorage) {
contextOptions.storageState = options.loadStorage;
}
if (options.ignoreHttpsErrors)
if (options.ignoreHttpsErrors) {
contextOptions.ignoreHTTPSErrors = true;
}
// HAR
if (options.saveHar) {
contextOptions.recordHar = { path: path.resolve(process.cwd(), options.saveHar), mode: 'minimal' };
if (options.saveHarGlob)
if (options.saveHarGlob) {
contextOptions.recordHar.urlFilter = options.saveHarGlob;
}
contextOptions.serviceWorkers = 'block';
}
@ -502,15 +532,19 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro
async function closeBrowser() {
// We can come here multiple times. For example, saving storage creates
// a temporary page and we call closeBrowser again when that page closes.
if (closingBrowser)
if (closingBrowser) {
return;
}
closingBrowser = true;
if (options.saveTrace)
if (options.saveTrace) {
await context.tracing.stop({ path: options.saveTrace });
if (options.saveStorage)
}
if (options.saveStorage) {
await context.storageState({ path: options.saveStorage }).catch(e => null);
if (options.saveHar)
}
if (options.saveHar) {
await context.close();
}
await browser.close();
}
@ -518,8 +552,9 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro
page.on('dialog', () => {}); // Prevent dialogs from being automatically dismissed.
page.on('close', () => {
const hasPage = browser.contexts().some(context => context.pages().length > 0);
if (hasPage)
if (hasPage) {
return;
}
// Avoid the error when the last page is closed because the browser has been closed.
closeBrowser().catch(() => {});
});
@ -533,8 +568,9 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro
context.setDefaultTimeout(timeout);
context.setDefaultNavigationTimeout(timeout);
if (options.saveTrace)
if (options.saveTrace) {
await context.tracing.start({ screenshots: true, snapshots: true });
}
// Omit options that we add automatically for presentation purpose.
delete launchOptions.headless;
@ -547,10 +583,11 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro
async function openPage(context: BrowserContext, url: string | undefined): Promise<Page> {
const page = await context.newPage();
if (url) {
if (fs.existsSync(url))
if (fs.existsSync(url)) {
url = 'file://' + path.resolve(url);
else if (!url.startsWith('http') && !url.startsWith('file://') && !url.startsWith('about:') && !url.startsWith('data:'))
} 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)) {
// Tests with PWTEST_CLI_AUTO_EXIT_WHEN might close page too fast, resulting
@ -623,8 +660,9 @@ async function screenshot(options: Options, captureOptions: CaptureOptions, url:
}
async function pdf(options: Options, captureOptions: CaptureOptions, url: string, path: string) {
if (options.browser !== 'chromium')
if (options.browser !== 'chromium') {
throw new Error('PDF creation is only working with Chromium');
}
const { context } = await launchContext({ ...options, browser: 'chromium' }, { headless: true });
console.log('Navigating to ' + url);
const page = await openPage(context, url);
@ -650,27 +688,31 @@ function lookupBrowserType(options: Options): BrowserType {
case 'wk': browserType = playwright.webkit; break;
case 'ff': browserType = playwright.firefox; break;
}
if (browserType)
if (browserType) {
return browserType;
}
program.help();
}
function validateOptions(options: Options) {
if (options.device && !(options.device in playwright.devices)) {
const lines = [`Device descriptor not found: '${options.device}', available devices are:`];
for (const name in playwright.devices)
for (const name in playwright.devices) {
lines.push(` "${name}"`);
}
throw new Error(lines.join('\n'));
}
if (options.colorScheme && !['light', 'dark'].includes(options.colorScheme))
if (options.colorScheme && !['light', 'dark'].includes(options.colorScheme)) {
throw new Error('Invalid color scheme, should be one of "light", "dark"');
}
}
function logErrorAndExit(e: Error) {
if (process.env.PWDEBUGIMPL)
if (process.env.PWDEBUGIMPL) {
console.error(e);
else
} else {
console.error(e.name + ': ' + e.message);
}
gracefullyProcessExitDoNotHang(1);
}
@ -680,8 +722,9 @@ function codegenId(): string {
function commandWithOpenOptions(command: string, description: string, options: any[][]): Command {
let result = program.command(command).description(description);
for (const option of options)
for (const option of options) {
result = result.option(option[0], ...option.slice(1));
}
return result
.option('-b, --browser <browserType>', 'browser to use, one of cr, chromium, ff, firefox, wk, webkit', 'chromium')
.option('--block-service-workers', 'block service workers')

View file

@ -29,8 +29,9 @@ function printPlaywrightTestError(command: string) {
} catch (e) {
}
}
if (!packages.length)
if (!packages.length) {
packages.push('playwright');
}
const packageManager = getPackageManager();
if (packageManager === 'yarn') {
console.error(`Please install @playwright/test package before running "yarn playwright ${command}"`);
@ -63,5 +64,6 @@ function addExternalPlaywrightTestCommands() {
}
}
if (!process.env.PW_LANG_NAME)
if (!process.env.PW_LANG_NAME) {
addExternalPlaywrightTestCommands();
}

View file

@ -58,8 +58,9 @@ export class Android extends ChannelOwner<channels.AndroidChannel> implements ap
}
async launchServer(options: types.LaunchServerOptions = {}): Promise<api.BrowserServer> {
if (!this._serverLauncher)
if (!this._serverLauncher) {
throw new Error('Launching server is not supported');
}
return await this._serverLauncher.launchServer(options);
}
@ -78,7 +79,7 @@ export class Android extends ChannelOwner<channels.AndroidChannel> implements ap
let device: AndroidDevice;
let closeError: string | undefined;
const onPipeClosed = () => {
device?._didClose();
device._didClose();
connection.close(closeError);
};
pipe.on('closed', onPipeClosed);
@ -143,8 +144,9 @@ export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel> i
private _onWebViewRemoved(socketName: string) {
const view = this._webViews.get(socketName);
this._webViews.delete(socketName);
if (view)
if (view) {
view.emit(Events.AndroidWebView.Close);
}
}
setDefaultTimeout(timeout: number) {
@ -166,15 +168,18 @@ export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel> i
async webView(selector: { pkg?: string; socketName?: string; }, options?: types.TimeoutOptions): Promise<AndroidWebView> {
const predicate = (v: AndroidWebView) => {
if (selector.pkg)
if (selector.pkg) {
return v.pkg() === selector.pkg;
if (selector.socketName)
}
if (selector.socketName) {
return v._socketName() === selector.socketName;
}
return false;
};
const webView = [...this._webViews.values()].find(predicate);
if (webView)
if (webView) {
return webView;
}
return await this.waitForEvent('webview', { ...options, predicate });
}
@ -229,8 +234,9 @@ export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel> i
async screenshot(options: { path?: string } = {}): Promise<Buffer> {
const { binary } = await this._channel.screenshot();
if (options.path)
if (options.path) {
await fs.promises.writeFile(options.path, binary);
}
return binary;
}
@ -240,13 +246,15 @@ export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel> i
async close() {
try {
if (this._shouldCloseConnectionOnClose)
if (this._shouldCloseConnectionOnClose) {
this._connection.close();
else
} else {
await this._channel.close();
}
} catch (e) {
if (isTargetClosedError(e))
if (isTargetClosedError(e)) {
return;
}
throw e;
}
}
@ -286,8 +294,9 @@ export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel> i
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
const waiter = Waiter.createForEvent(this, event);
waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`);
if (event !== Events.AndroidDevice.Close)
if (event !== Events.AndroidDevice.Close) {
waiter.rejectOnEvent(this, Events.AndroidDevice.Close, () => new TargetClosedError());
}
const result = await waiter.waitForEvent(this, event, predicate as any);
waiter.dispose();
return result;
@ -320,8 +329,9 @@ export class AndroidSocket extends ChannelOwner<channels.AndroidSocketChannel> i
}
async function loadFile(file: string | Buffer): Promise<Buffer> {
if (isString(file))
if (isString(file)) {
return await fs.promises.readFile(file);
}
return file;
}
@ -375,10 +385,12 @@ function toSelectorChannel(selector: api.AndroidSelector): channels.AndroidSelec
} = selector;
const toRegex = (value: RegExp | string | undefined): string | undefined => {
if (value === undefined)
if (value === undefined) {
return undefined;
if (isRegExp(value))
}
if (isRegExp(value)) {
return value.source;
}
return '^' + value.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&').replace(/-/g, '\\x2d') + '$';
};
@ -427,8 +439,9 @@ export class AndroidWebView extends EventEmitter implements api.AndroidWebView {
}
async page(): Promise<Page> {
if (!this._pagePromise)
if (!this._pagePromise) {
this._pagePromise = this._fetchPage();
}
return await this._pagePromise;
}

View file

@ -27,8 +27,9 @@ export class Artifact extends ChannelOwner<channels.ArtifactChannel> {
}
async pathAfterFinished(): Promise<string> {
if (this._connection.isRemote())
if (this._connection.isRemote()) {
throw new Error(`Path is not available when connecting remotely. Use saveAs() to save a local copy.`);
}
return (await this._channel.pathAfterFinished()).value;
}

View file

@ -65,8 +65,9 @@ export class Browser extends ChannelOwner<channels.BrowserChannel> implements ap
return await this._wrapApiCall(async () => {
for (const context of this._contexts) {
await this._browserType._willCloseContext(context);
for (const page of context.pages())
for (const page of context.pages()) {
page._onClose();
}
context._onClose();
}
return await this._innerNewContext(options, true);
@ -138,14 +139,16 @@ export class Browser extends ChannelOwner<channels.BrowserChannel> implements ap
async close(options: { reason?: string } = {}): Promise<void> {
this._closeReason = options.reason;
try {
if (this._shouldCloseConnectionOnClose)
if (this._shouldCloseConnectionOnClose) {
this._connection.close();
else
} else {
await this._channel.close(options);
}
await this._closedPromise;
} catch (e) {
if (isTargetClosedError(e))
if (isTargetClosedError(e)) {
return;
}
throw e;
}
}

View file

@ -79,8 +79,9 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.BrowserContextInitializer) {
super(parent, type, guid, initializer);
if (parent instanceof Browser)
if (parent instanceof Browser) {
this._browser = parent;
}
this._browser?._contexts.add(this);
this._isChromium = this._browser?._name === 'chromium';
this.tracing = Tracing.from(initializer.tracing);
@ -107,31 +108,35 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
const consoleMessage = new ConsoleMessage(event);
this.emit(Events.BrowserContext.Console, consoleMessage);
const page = consoleMessage.page();
if (page)
if (page) {
page.emit(Events.Page.Console, consoleMessage);
}
});
this._channel.on('pageError', ({ error, page }) => {
const pageObject = Page.from(page);
const parsedError = parseError(error);
this.emit(Events.BrowserContext.WebError, new WebError(pageObject, parsedError));
if (pageObject)
if (pageObject) {
pageObject.emit(Events.Page.PageError, parsedError);
}
});
this._channel.on('dialog', ({ dialog }) => {
const dialogObject = Dialog.from(dialog);
let hasListeners = this.emit(Events.BrowserContext.Dialog, dialogObject);
const page = dialogObject.page();
if (page)
if (page) {
hasListeners = page.emit(Events.Page.Dialog, dialogObject) || hasListeners;
}
if (!hasListeners) {
// Although we do similar handling on the server side, we still need this logic
// on the client side due to a possible race condition between two async calls:
// a) removing "dialog" listener subscription (client->server)
// b) actual "dialog" event (server->client)
if (dialogObject.type() === 'beforeunload')
if (dialogObject.type() === 'beforeunload') {
dialog.accept({}).catch(() => {});
else
} else {
dialog.dismiss().catch(() => {});
}
}
});
this._channel.on('request', ({ request, page }) => this._onRequest(network.Request.from(request), Page.fromNullable(page)));
@ -152,36 +157,41 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
_setOptions(contextOptions: channels.BrowserNewContextParams, browserOptions: LaunchOptions) {
this._options = contextOptions;
if (this._options.recordHar)
if (this._options.recordHar) {
this._harRecorders.set('', { path: this._options.recordHar.path, content: this._options.recordHar.content });
}
this.tracing._tracesDir = browserOptions.tracesDir;
}
private _onPage(page: Page): void {
this._pages.add(page);
this.emit(Events.BrowserContext.Page, page);
if (page._opener && !page._opener.isClosed())
if (page._opener && !page._opener.isClosed()) {
page._opener.emit(Events.Page.Popup, page);
}
}
private _onRequest(request: network.Request, page: Page | null) {
this.emit(Events.BrowserContext.Request, request);
if (page)
if (page) {
page.emit(Events.Page.Request, request);
}
}
private _onResponse(response: network.Response, page: Page | null) {
this.emit(Events.BrowserContext.Response, response);
if (page)
if (page) {
page.emit(Events.Page.Response, response);
}
}
private _onRequestFailed(request: network.Request, responseEndTiming: number, failureText: string | undefined, page: Page | null) {
request._failureText = failureText || null;
request._setResponseEndTiming(responseEndTiming);
this.emit(Events.BrowserContext.RequestFailed, request);
if (page)
if (page) {
page.emit(Events.Page.RequestFailed, request);
}
}
private _onRequestFinished(params: channels.BrowserContextRequestFinishedEvent) {
@ -191,10 +201,12 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
const page = Page.fromNullable(params.page);
request._setResponseEndTiming(responseEndTiming);
this.emit(Events.BrowserContext.RequestFinished, request);
if (page)
if (page) {
page.emit(Events.Page.RequestFinished, request);
if (response)
}
if (response) {
response._finishedPromise.resolve(null);
}
}
async _onRoute(route: network.Route) {
@ -203,20 +215,26 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
const routeHandlers = this._routes.slice();
for (const routeHandler of routeHandlers) {
// If the page or the context was closed we stall all requests right away.
if (page?._closeWasCalled || this._closeWasCalled)
if (page?._closeWasCalled || this._closeWasCalled) {
return;
if (!routeHandler.matches(route.request().url()))
}
if (!routeHandler.matches(route.request().url())) {
continue;
}
const index = this._routes.indexOf(routeHandler);
if (index === -1)
if (index === -1) {
continue;
if (routeHandler.willExpire())
}
if (routeHandler.willExpire()) {
this._routes.splice(index, 1);
}
const handled = await routeHandler.handle(route);
if (!this._routes.length)
if (!this._routes.length) {
this._wrapApiCall(() => this._updateInterceptionPatterns(), true).catch(() => {});
if (handled)
}
if (handled) {
return;
}
}
// If the page is closed or unrouteAll() was called without waiting and interception disabled,
// the method will throw an error - silence it.
@ -225,16 +243,18 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
async _onWebSocketRoute(webSocketRoute: network.WebSocketRoute) {
const routeHandler = this._webSocketRoutes.find(route => route.matches(webSocketRoute.url()));
if (routeHandler)
if (routeHandler) {
await routeHandler.handle(webSocketRoute);
else
} else {
webSocketRoute.connectToServer();
}
}
async _onBinding(bindingCall: BindingCall) {
const func = this._bindings.get(bindingCall._initializer.name);
if (!func)
if (!func) {
return;
}
await bindingCall.call(func);
}
@ -261,16 +281,19 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
}
async newPage(): Promise<Page> {
if (this._ownerPage)
if (this._ownerPage) {
throw new Error('Please use browser.newContext()');
}
return Page.from((await this._channel.newPage()).page);
}
async cookies(urls?: string | string[]): Promise<network.NetworkCookie[]> {
if (!urls)
if (!urls) {
urls = [];
if (urls && typeof urls === 'string')
}
if (urls && typeof urls === 'string') {
urls = [urls];
}
return (await this._channel.cookies({ urls: urls as string[] })).cookies;
}
@ -380,10 +403,11 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
const removed = [];
const remaining = [];
for (const route of this._routes) {
if (urlMatchesEqual(route.url, url) && (!handler || route.handler === handler))
if (urlMatchesEqual(route.url, url) && (!handler || route.handler === handler)) {
removed.push(route);
else
} else {
remaining.push(route);
}
}
await this._unrouteInternal(removed, remaining, 'default');
}
@ -391,8 +415,9 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
private async _unrouteInternal(removed: network.RouteHandler[], remaining: network.RouteHandler[], behavior?: 'wait'|'ignoreErrors'|'default'): Promise<void> {
this._routes = remaining;
await this._updateInterceptionPatterns();
if (!behavior || behavior === 'default')
if (!behavior || behavior === 'default') {
return;
}
const promises = removed.map(routeHandler => routeHandler.stop(behavior));
await Promise.all(promises);
}
@ -417,8 +442,9 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
const waiter = Waiter.createForEvent(this, event);
waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`);
if (event !== Events.BrowserContext.Close)
if (event !== Events.BrowserContext.Close) {
waiter.rejectOnEvent(this, Events.BrowserContext.Close, () => new TargetClosedError(this._effectiveCloseReason()));
}
const result = await waiter.waitForEvent(this, event, predicate as any);
waiter.dispose();
return result;
@ -444,16 +470,18 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
async newCDPSession(page: Page | Frame): Promise<api.CDPSession> {
// channelOwner.ts's validation messages don't handle the pseudo-union type, so we're explicit here
if (!(page instanceof Page) && !(page instanceof Frame))
if (!(page instanceof Page) && !(page instanceof Frame)) {
throw new Error('page: expected Page or Frame');
}
const result = await this._channel.newCDPSession(page instanceof Page ? { page: page._channel } : { frame: page._channel });
return CDPSession.from(result.session);
}
_onClose() {
if (this._browser)
if (this._browser) {
this._browser._contexts.delete(this);
this._browserType?._contexts?.delete(this);
}
this._browserType?._contexts.delete(this);
this._disposeHarRouters();
this.tracing._resetStackCounter();
this.emit(Events.BrowserContext.Close, this);
@ -464,8 +492,9 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
}
async close(options: { reason?: string } = {}): Promise<void> {
if (this._closeWasCalled)
if (this._closeWasCalled) {
return;
}
this._closeReason = options.reason;
this._closeWasCalled = true;
await this._wrapApiCall(async () => {
@ -498,8 +527,9 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
}
async function prepareStorageState(options: BrowserContextOptions): Promise<channels.BrowserNewContextParams['storageState']> {
if (typeof options.storageState !== 'string')
if (typeof options.storageState !== 'string') {
return options.storageState;
}
try {
return JSON.parse(await fs.promises.readFile(options.storageState, 'utf8'));
} catch (e) {
@ -509,8 +539,9 @@ async function prepareStorageState(options: BrowserContextOptions): Promise<chan
}
function prepareRecordHarOptions(options: BrowserContextOptions['recordHar']): channels.RecordHarOptions | undefined {
if (!options)
if (!options) {
return;
}
return {
path: options.path,
content: options.content || (options.omitContent ? 'omit' : undefined),
@ -522,10 +553,12 @@ function prepareRecordHarOptions(options: BrowserContextOptions['recordHar']): c
}
export async function prepareBrowserContextParams(options: BrowserContextOptions): Promise<channels.BrowserNewContextParams> {
if (options.videoSize && !options.videosPath)
if (options.videoSize && !options.videosPath) {
throw new Error(`"videoSize" option requires "videosPath" to be specified`);
if (options.extraHTTPHeaders)
}
if (options.extraHTTPHeaders) {
network.validateHeaders(options.extraHTTPHeaders);
}
const contextParams: channels.BrowserNewContextParams = {
...options,
viewport: options.viewport === null ? undefined : options.viewport,
@ -546,28 +579,34 @@ export async function prepareBrowserContextParams(options: BrowserContextOptions
size: options.videoSize
};
}
if (contextParams.recordVideo && contextParams.recordVideo.dir)
if (contextParams.recordVideo && contextParams.recordVideo.dir) {
contextParams.recordVideo.dir = path.resolve(process.cwd(), contextParams.recordVideo.dir);
}
return contextParams;
}
function toAcceptDownloadsProtocol(acceptDownloads?: boolean) {
if (acceptDownloads === undefined)
if (acceptDownloads === undefined) {
return undefined;
if (acceptDownloads)
}
if (acceptDownloads) {
return 'accept';
}
return 'deny';
}
export async function toClientCertificatesProtocol(certs?: BrowserContextOptions['clientCertificates']): Promise<channels.PlaywrightNewRequestParams['clientCertificates']> {
if (!certs)
if (!certs) {
return undefined;
}
const bufferizeContent = async (value?: Buffer, path?: string): Promise<Buffer | undefined> => {
if (value)
if (value) {
return value;
if (path)
}
if (path) {
return await fs.promises.readFile(path);
}
};
return await Promise.all(certs.map(async cert => ({

View file

@ -56,8 +56,9 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
}
executablePath(): string {
if (!this._initializer.executablePath)
if (!this._initializer.executablePath) {
throw new Error('Browser is not supported on current platform');
}
return this._initializer.executablePath;
}
@ -85,8 +86,9 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
}
async launchServer(options: LaunchServerOptions = {}): Promise<api.BrowserServer> {
if (!this._serverLauncher)
if (!this._serverLauncher) {
throw new Error('Launching server is not supported');
}
options = { ...this._defaultLaunchOptions, ...options };
return await this._serverLauncher.launchServer(options);
}
@ -115,8 +117,9 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
connect(options: api.ConnectOptions & { wsEndpoint: string }): Promise<api.Browser>;
connect(wsEndpoint: string, options?: api.ConnectOptions): Promise<api.Browser>;
async connect(optionsOrWsEndpoint: string | (api.ConnectOptions & { wsEndpoint: string }), options?: api.ConnectOptions): Promise<Browser>{
if (typeof optionsOrWsEndpoint === 'string')
if (typeof optionsOrWsEndpoint === 'string') {
return await this._connect({ ...options, wsEndpoint: optionsOrWsEndpoint });
}
assert(optionsOrWsEndpoint.wsEndpoint, 'options.wsEndpoint is required');
return await this._connect(optionsOrWsEndpoint);
}
@ -134,8 +137,9 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
slowMo: params.slowMo,
timeout: params.timeout,
};
if ((params as any).__testHookRedirectPortForwarding)
if ((params as any).__testHookRedirectPortForwarding) {
connectParams.socksProxyRedirectPortForTest = (params as any).__testHookRedirectPortForwarding;
}
const { pipe, headers: connectHeaders } = await localUtils._channel.connect(connectParams);
const closePipe = () => pipe.close().catch(() => {});
const connection = new Connection(localUtils, this._instrumentation);
@ -146,9 +150,10 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
let closeError: string | undefined;
const onPipeClosed = (reason?: string) => {
// Emulate all pages, contexts and the browser closing upon disconnect.
for (const context of browser?.contexts() || []) {
for (const page of context.pages())
for (const context of browser.contexts() || []) {
for (const page of context.pages()) {
page._onClose();
}
context._onClose();
}
connection.close(reason || closeError);
@ -158,7 +163,7 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
// here and promises did not have a chance to reject.
// The order of rejects vs closure is a part of the API contract and our test runner
// relies on it to attribute rejections to the right test.
setTimeout(() => browser?._didClose(), 0);
setTimeout(() => browser._didClose(), 0);
};
pipe.on('closed', params => onPipeClosed(params.reason));
connection.onmessage = message => this._wrapApiCall(() => pipe.send({ message }).catch(() => onPipeClosed()), /* isInternal */ true);
@ -174,8 +179,9 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
const result = await raceAgainstDeadline(async () => {
// For tests.
if ((params as any).__testHookBeforeCreateBrowser)
if ((params as any).__testHookBeforeCreateBrowser) {
await (params as any).__testHookBeforeCreateBrowser();
}
const playwright = await connection!.initializePlaywright();
if (!playwright._initializer.preLaunchedBrowser) {
@ -202,16 +208,18 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
async connectOverCDP(options: api.ConnectOverCDPOptions & { wsEndpoint?: string }): Promise<api.Browser>;
async connectOverCDP(endpointURL: string, options?: api.ConnectOverCDPOptions): Promise<api.Browser>;
async connectOverCDP(endpointURLOrOptions: (api.ConnectOverCDPOptions & { wsEndpoint?: string })|string, options?: api.ConnectOverCDPOptions) {
if (typeof endpointURLOrOptions === 'string')
if (typeof endpointURLOrOptions === 'string') {
return await this._connectOverCDP(endpointURLOrOptions, options);
}
const endpointURL = 'endpointURL' in endpointURLOrOptions ? endpointURLOrOptions.endpointURL : endpointURLOrOptions.wsEndpoint;
assert(endpointURL, 'Cannot connect over CDP without wsEndpoint.');
return await this.connectOverCDP(endpointURL, endpointURLOrOptions);
}
async _connectOverCDP(endpointURL: string, params: api.ConnectOverCDPOptions = {}): Promise<Browser> {
if (this.name() !== 'chromium')
if (this.name() !== 'chromium') {
throw new Error('Connecting over CDP is only supported in Chromium.');
}
const headers = params.headers ? headersObjectToArray(params.headers) : undefined;
const result = await this._channel.connectOverCDP({
endpointURL,
@ -221,8 +229,9 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
});
const browser = Browser.from(result.browser);
this._didLaunchBrowser(browser, {}, params.logger);
if (result.defaultContext)
if (result.defaultContext) {
await this._didCreateContext(BrowserContext.from(result.defaultContext), {}, {}, params.logger);
}
return browser;
}
@ -237,10 +246,12 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
context._browserType = this;
this._contexts.add(context);
context._setOptions(contextOptions, browserOptions);
if (this._defaultContextTimeout !== undefined)
if (this._defaultContextTimeout !== undefined) {
context.setDefaultTimeout(this._defaultContextTimeout);
if (this._defaultContextNavigationTimeout !== undefined)
}
if (this._defaultContextNavigationTimeout !== undefined) {
context.setDefaultNavigationTimeout(this._defaultContextNavigationTimeout);
}
await this._instrumentation.runAfterCreateBrowserContext(context);
}

View file

@ -80,37 +80,42 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
}
override on(event: string | symbol, listener: Listener): this {
if (!this.listenerCount(event))
if (!this.listenerCount(event)) {
this._updateSubscription(event, true);
}
super.on(event, listener);
return this;
}
override addListener(event: string | symbol, listener: Listener): this {
if (!this.listenerCount(event))
if (!this.listenerCount(event)) {
this._updateSubscription(event, true);
}
super.addListener(event, listener);
return this;
}
override prependListener(event: string | symbol, listener: Listener): this {
if (!this.listenerCount(event))
if (!this.listenerCount(event)) {
this._updateSubscription(event, true);
}
super.prependListener(event, listener);
return this;
}
override off(event: string | symbol, listener: Listener): this {
super.off(event, listener);
if (!this.listenerCount(event))
if (!this.listenerCount(event)) {
this._updateSubscription(event, false);
}
return this;
}
override removeListener(event: string | symbol, listener: Listener): this {
super.removeListener(event, listener);
if (!this.listenerCount(event))
if (!this.listenerCount(event)) {
this._updateSubscription(event, false);
}
return this;
}
@ -122,14 +127,16 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
_dispose(reason: 'gc' | undefined) {
// Clean up from parent and connection.
if (this._parent)
if (this._parent) {
this._parent._objects.delete(this._guid);
}
this._connection._objects.delete(this._guid);
this._wasCollected = reason === 'gc';
// Dispose all children.
for (const object of [...this._objects.values()])
for (const object of [...this._objects.values()]) {
object._dispose(reason);
}
this._objects.clear();
}
@ -171,23 +178,27 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
async _wrapApiCall<R>(func: (apiZone: ApiZone) => Promise<R>, isInternal?: boolean): Promise<R> {
const logger = this._logger;
const apiZone = zones.zoneData<ApiZone>('apiZone');
if (apiZone)
if (apiZone) {
return await func(apiZone);
}
const stackTrace = captureLibraryStackTrace();
let apiName: string | undefined = stackTrace.apiName;
const frames: channels.StackFrame[] = stackTrace.frames;
if (isInternal === undefined)
if (isInternal === undefined) {
isInternal = this._isInternalType;
if (isInternal)
}
if (isInternal) {
apiName = undefined;
}
// Enclosing zone could have provided the apiName and wallTime.
const expectZone = zones.zoneData<ExpectZone>('expectZone');
const stepId = expectZone?.stepId;
if (!isInternal && expectZone)
if (!isInternal && expectZone) {
apiName = expectZone.title;
}
// If we are coming from the expectZone, there is no need to generate a new
// step for the API call, since it will be generated by the expect itself.
@ -203,13 +214,15 @@ 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 : '';
if (apiName && !apiName.includes('<anonymous>'))
if (apiName && !apiName.includes('<anonymous>')) {
e.message = apiName + ': ' + e.message;
}
const stackFrames = '\n' + stringifyStackFrames(stackTrace.frames).join('\n') + innerError;
if (stackFrames.trim())
if (stackFrames.trim()) {
e.stack = e.message + stackFrames;
else
} else {
e.stack = '';
}
csi?.onApiCallEnd(callCookie, e);
logApiCall(logger, `<= ${apiName} failed`, isInternal);
throw e;
@ -233,16 +246,19 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
}
function logApiCall(logger: Logger | undefined, message: string, isNested: boolean) {
if (isNested)
if (isNested) {
return;
if (logger && logger.isEnabled('api', 'info'))
}
if (logger && logger.isEnabled('api', 'info')) {
logger.log('api', 'info', message, [], { color: 'cyan' });
}
debugLogger.log('api', message);
}
function tChannelImplToWire(names: '*' | string[], arg: any, path: string, context: ValidatorContext) {
if (arg._object instanceof ChannelOwner && (names === '*' || names.includes(arg._object._type)))
if (arg._object instanceof ChannelOwner && (names === '*' || names.includes(arg._object._type))) {
return { guid: arg._object._guid };
}
throw new ValidationError(`${path}: expected channel ${names.toString()}`);
}

View file

@ -22,8 +22,9 @@ import { isString } from '../utils';
export function envObjectToArray(env: types.Env): { name: string, value: string }[] {
const result: { name: string, value: string }[] = [];
for (const name in env) {
if (!Object.is(env[name], undefined))
if (!Object.is(env[name], undefined)) {
result.push({ name, value: String(env[name]) });
}
}
return result;
}
@ -34,16 +35,20 @@ export async function evaluationScript(fun: Function | string | { path?: string,
const argString = Object.is(arg, undefined) ? 'undefined' : JSON.stringify(arg);
return `(${source})(${argString})`;
}
if (arg !== undefined)
if (arg !== undefined) {
throw new Error('Cannot evaluate a string with arguments');
if (isString(fun))
}
if (isString(fun)) {
return fun;
if (fun.content !== undefined)
}
if (fun.content !== undefined) {
return fun.content;
}
if (fun.path !== undefined) {
let source = await fs.promises.readFile(fun.path, 'utf8');
if (addSourceUrl)
if (addSourceUrl) {
source = addSourceUrlToScript(source, fun.path);
}
return source;
}
throw new Error('Either path or content property must be present');

View file

@ -47,24 +47,30 @@ export function createInstrumentation(): ClientInstrumentation {
const listeners: ClientInstrumentationListener[] = [];
return new Proxy({}, {
get: (obj: any, prop: string | symbol) => {
if (typeof prop !== 'string')
if (typeof prop !== 'string') {
return obj[prop];
if (prop === 'addListener')
}
if (prop === 'addListener') {
return (listener: ClientInstrumentationListener) => listeners.push(listener);
if (prop === 'removeListener')
}
if (prop === 'removeListener') {
return (listener: ClientInstrumentationListener) => listeners.splice(listeners.indexOf(listener), 1);
if (prop === 'removeAllListeners')
}
if (prop === 'removeAllListeners') {
return () => listeners.splice(0, listeners.length);
}
if (prop.startsWith('run')) {
return async (...params: any[]) => {
for (const listener of listeners)
for (const listener of listeners) {
await (listener as any)[prop]?.(...params);
}
};
}
if (prop.startsWith('on')) {
return (...params: any[]) => {
for (const listener of listeners)
for (const listener of listeners) {
(listener as any)[prop]?.(...params);
}
};
}
return obj[prop];

View file

@ -54,12 +54,15 @@ export class Clock implements api.Clock {
}
function parseTime(time: string | number | Date): { timeNumber?: number, timeString?: string } {
if (typeof time === 'number')
if (typeof time === 'number') {
return { timeNumber: time };
if (typeof time === 'string')
}
if (typeof time === 'string') {
return { timeString: time };
if (!isFinite(time.getTime()))
}
if (!isFinite(time.getTime())) {
throw new Error(`Invalid date: ${time}`);
}
return { timeNumber: time.getTime() };
}

View file

@ -112,17 +112,20 @@ export class Connection extends EventEmitter {
}
setIsTracing(isTracing: boolean) {
if (isTracing)
if (isTracing) {
this._tracingCount++;
else
} else {
this._tracingCount--;
}
}
async sendMessageToServer(object: ChannelOwner, method: string, params: any, apiName: string | undefined, frames: channels.StackFrame[], stepId?: string): Promise<any> {
if (this._closedError)
if (this._closedError) {
throw this._closedError;
if (object._wasCollected)
}
if (object._wasCollected) {
throw new Error('The object has been collected to prevent unbounded heap growth.');
}
const guid = object._guid;
const type = object._type;
@ -134,8 +137,9 @@ export class Connection extends EventEmitter {
}
const location = frames[0] ? { file: frames[0].file, line: frames[0].line, column: frames[0].column } : undefined;
const metadata: channels.Metadata = { apiName, location, internal: !apiName, stepId };
if (this._tracingCount && frames && type !== 'LocalUtils')
if (this._tracingCount && frames && type !== 'LocalUtils') {
this._localUtils?._channel.addStackToTracingNoReply({ callData: { stack: frames, id } }).catch(() => {});
}
// We need to exit zones before calling into the server, otherwise
// when we receive events from the server, we would be in an API zone.
zones.exitZones(() => this.onmessage({ ...message, metadata }));
@ -143,16 +147,19 @@ export class Connection extends EventEmitter {
}
dispatch(message: object) {
if (this._closedError)
if (this._closedError) {
return;
}
const { id, guid, method, params, result, error, log } = message as any;
if (id) {
if (debugLogger.isEnabled('channel'))
if (debugLogger.isEnabled('channel')) {
debugLogger.log('channel', '<RECV ' + JSON.stringify(message));
}
const callback = this._callbacks.get(id);
if (!callback)
if (!callback) {
throw new Error(`Cannot find command to respond: ${id}`);
}
this._callbacks.delete(id);
if (error && !result) {
const parsedError = parseError(error);
@ -165,21 +172,24 @@ export class Connection extends EventEmitter {
return;
}
if (debugLogger.isEnabled('channel'))
if (debugLogger.isEnabled('channel')) {
debugLogger.log('channel', '<EVENT ' + JSON.stringify(message));
}
if (method === '__create__') {
this._createRemoteObject(guid, params.type, params.guid, params.initializer);
return;
}
const object = this._objects.get(guid);
if (!object)
if (!object) {
throw new Error(`Cannot find object to "${method}": ${guid}`);
}
if (method === '__adopt__') {
const child = this._objects.get(params.guid);
if (!child)
if (!child) {
throw new Error(`Unknown new child: ${params.guid}`);
}
object._adopt(child);
return;
}
@ -194,11 +204,13 @@ export class Connection extends EventEmitter {
}
close(cause?: string) {
if (this._closedError)
if (this._closedError) {
return;
}
this._closedError = new TargetClosedError(cause);
for (const callback of this._callbacks.values())
for (const callback of this._callbacks.values()) {
callback.reject(this._closedError);
}
this._callbacks.clear();
this.emit('close');
}
@ -206,10 +218,12 @@ export class Connection extends EventEmitter {
private _tChannelImplFromWire(names: '*' | string[], arg: any, path: string, context: ValidatorContext) {
if (arg && typeof arg === 'object' && typeof arg.guid === 'string') {
const object = this._objects.get(arg.guid)!;
if (!object)
if (!object) {
throw new Error(`Object with guid ${arg.guid} was not bound in the connection`);
if (names !== '*' && !names.includes(object._type))
}
if (names !== '*' && !names.includes(object._type)) {
throw new ValidationError(`${path}: expected channel ${names.toString()}`);
}
return object._channel;
}
throw new ValidationError(`${path}: expected channel ${names.toString()}`);
@ -217,8 +231,9 @@ export class Connection extends EventEmitter {
private _createRemoteObject(parentGuid: string, type: string, guid: string, initializer: any): any {
const parent = this._objects.get(parentGuid);
if (!parent)
if (!parent) {
throw new Error(`Cannot find parent object ${parentGuid} to create ${guid}`);
}
let result: ChannelOwner<any>;
const validator = findValidator(type, '', 'Initializer');
initializer = validator(initializer, '', { tChannelImpl: this._tChannelImplFromWire.bind(this), binary: this._rawBuffers ? 'buffer' : 'fromBase64' });
@ -276,8 +291,9 @@ export class Connection extends EventEmitter {
break;
case 'LocalUtils':
result = new LocalUtils(parent, type, guid, initializer);
if (!this._localUtils)
if (!this._localUtils) {
this._localUtils = result as LocalUtils;
}
break;
case 'Page':
result = new Page(parent, type, guid, initializer);

View file

@ -74,8 +74,9 @@ export class ElectronApplication extends ChannelOwner<channels.ElectronApplicati
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.ElectronApplicationInitializer) {
super(parent, type, guid, initializer);
this._context = BrowserContext.from(initializer.context);
for (const page of this._context._pages)
for (const page of this._context._pages) {
this._onPage(page);
}
this._context.on(Events.BrowserContext.Page, page => this._onPage(page));
this._channel.on('close', () => {
this.emit(Events.ElectronApplication.Close);
@ -102,8 +103,9 @@ export class ElectronApplication extends ChannelOwner<channels.ElectronApplicati
}
async firstWindow(options?: { timeout?: number }): Promise<Page> {
if (this._windows.size)
if (this._windows.size) {
return this._windows.values().next().value!;
}
return await this.waitForEvent('window', options);
}
@ -119,8 +121,9 @@ export class ElectronApplication extends ChannelOwner<channels.ElectronApplicati
try {
await this._context.close();
} catch (e) {
if (isTargetClosedError(e))
if (isTargetClosedError(e)) {
return;
}
throw e;
}
}
@ -131,8 +134,9 @@ export class ElectronApplication extends ChannelOwner<channels.ElectronApplicati
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
const waiter = Waiter.createForEvent(this, event);
waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`);
if (event !== Events.ElectronApplication.Close)
if (event !== Events.ElectronApplication.Close) {
waiter.rejectOnEvent(this, Events.ElectronApplication.Close, () => new TargetClosedError());
}
const result = await waiter.waitForEvent(this, event, predicate as any);
waiter.dispose();
return result;

View file

@ -147,8 +147,9 @@ export class ElementHandle<T extends Node = Node> extends JSHandle<T> implements
async setInputFiles(files: string | FilePayload | string[] | FilePayload[], options: channels.ElementHandleSetInputFilesOptions = {}) {
const frame = await this.ownerFrame();
if (!frame)
if (!frame) {
throw new Error('Cannot set input files to detached element');
}
const converted = await convertInputFiles(files, frame.page().context());
await this._elementChannel.setInputFiles({ ...converted, ...options });
}
@ -174,10 +175,11 @@ export class ElementHandle<T extends Node = Node> extends JSHandle<T> implements
}
async setChecked(checked: boolean, options?: channels.ElementHandleCheckOptions) {
if (checked)
if (checked) {
await this.check(options);
else
} else {
await this.uncheck(options);
}
}
async boundingBox(): Promise<Rect | null> {
@ -187,8 +189,9 @@ export class ElementHandle<T extends Node = Node> extends JSHandle<T> implements
async screenshot(options: Omit<channels.ElementHandleScreenshotOptions, 'mask'> & { path?: string, mask?: Locator[] } = {}): Promise<Buffer> {
const copy: channels.ElementHandleScreenshotOptions = { ...options, mask: undefined };
if (!copy.type)
if (!copy.type) {
copy.type = determineScreenshotType(options);
}
if (options.mask) {
copy.mask = options.mask.map(locator => ({
frame: locator._frame._channel,
@ -235,18 +238,24 @@ export class ElementHandle<T extends Node = Node> extends JSHandle<T> implements
}
export function convertSelectOptionValues(values: string | api.ElementHandle | SelectOption | string[] | api.ElementHandle[] | SelectOption[] | null): { elements?: channels.ElementHandleChannel[], options?: SelectOption[] } {
if (values === null)
if (values === null) {
return {};
if (!Array.isArray(values))
}
if (!Array.isArray(values)) {
values = [values as any];
if (!values.length)
}
if (!values.length) {
return {};
for (let i = 0; i < values.length; i++)
}
for (let i = 0; i < values.length; i++) {
assert(values[i] !== null, `options[${i}]: expected object, got null`);
if (values[0] instanceof ElementHandle)
}
if (values[0] instanceof ElementHandle) {
return { elements: (values as ElementHandle[]).map((v: ElementHandle) => v._elementChannel) };
if (isString(values[0]))
}
if (isString(values[0])) {
return { options: (values as string[]).map(valueOrLabel => ({ valueOrLabel })) };
}
return { options: values as SelectOption[] };
}
@ -262,16 +271,18 @@ async function resolvePathsAndDirectoryForInputFiles(items: string[]): Promise<[
for (const item of items) {
const stat = await fs.promises.stat(item as string);
if (stat.isDirectory()) {
if (localDirectory)
if (localDirectory) {
throw new Error('Multiple directories are not supported');
}
localDirectory = path.resolve(item as string);
} else {
localPaths ??= [];
localPaths.push(path.resolve(item as string));
}
}
if (localPaths?.length && localDirectory)
if (localPaths?.length && localDirectory) {
throw new Error('File paths must be all files or a single directory');
}
return [localPaths, localDirectory];
}
@ -279,8 +290,9 @@ export async function convertInputFiles(files: string | FilePayload | string[] |
const items: (string | FilePayload)[] = Array.isArray(files) ? files.slice() : [files];
if (items.some(item => typeof item === 'string')) {
if (!items.every(item => typeof item === 'string'))
if (!items.every(item => typeof item === 'string')) {
throw new Error('File paths cannot be mixed with buffers');
}
const [localPaths, localDirectory] = await resolvePathsAndDirectoryForInputFiles(items);
@ -312,18 +324,20 @@ export async function convertInputFiles(files: string | FilePayload | string[] |
}
const payloads = items as FilePayload[];
if (filePayloadExceedsSizeLimit(payloads))
if (filePayloadExceedsSizeLimit(payloads)) {
throw new Error('Cannot set buffer larger than 50Mb, please write it to a file and pass its path instead.');
}
return { payloads };
}
export function determineScreenshotType(options: { path?: string, type?: 'png' | 'jpeg' }): 'png' | 'jpeg' | undefined {
if (options.path) {
const mimeType = mime.getType(options.path);
if (mimeType === 'image/png')
if (mimeType === 'image/png') {
return 'png';
else if (mimeType === 'image/jpeg')
} else if (mimeType === 'image/jpeg') {
return 'jpeg';
}
throw new Error(`path: unsupported mime type "${mimeType}"`);
}
return options.type;

View file

@ -36,15 +36,17 @@ export function isTargetClosedError(error: Error) {
}
export function serializeError(e: any): SerializedError {
if (isError(e))
if (isError(e)) {
return { error: { message: e.message, stack: e.stack, name: e.name } };
}
return { value: serializeValue(e, value => ({ fallThrough: value })) };
}
export function parseError(error: SerializedError): Error {
if (!error.error) {
if (error.value === undefined)
if (error.value === undefined) {
throw new Error('Serialized error must have either an error or a value');
}
return parseSerializedValue(error.value, undefined);
}
if (error.error.name === 'TimeoutError') {

View file

@ -48,8 +48,9 @@ export class EventEmitter implements EventEmitterType {
}
setMaxListeners(n: number): this {
if (typeof n !== 'number' || n < 0 || Number.isNaN(n))
if (typeof n !== 'number' || n < 0 || Number.isNaN(n)) {
throw new RangeError('The value of "n" is out of range. It must be a non-negative number. Received ' + n + '.');
}
this._maxListeners = n;
return this;
}
@ -60,28 +61,32 @@ export class EventEmitter implements EventEmitterType {
emit(type: EventType, ...args: any[]): boolean {
const events = this._events;
if (events === undefined)
if (events === undefined) {
return false;
}
const handler = events?.[type];
if (handler === undefined)
const handler = events[type];
if (handler === undefined) {
return false;
}
if (typeof handler === 'function') {
this._callHandler(type, handler, args);
} else {
const len = handler.length;
const listeners = handler.slice();
for (let i = 0; i < len; ++i)
for (let i = 0; i < len; ++i) {
this._callHandler(type, listeners[i], args);
}
}
return true;
}
private _callHandler(type: EventType, handler: Listener, args: any[]): void {
const promise = Reflect.apply(handler, this, args);
if (!(promise instanceof Promise))
if (!(promise instanceof Promise)) {
return;
}
let set = this._pendingHandlers.get(type);
if (!set) {
set = new Set();
@ -89,10 +94,11 @@ export class EventEmitter implements EventEmitterType {
}
set.add(promise);
promise.catch(e => {
if (this._rejectionHandler)
if (this._rejectionHandler) {
this._rejectionHandler(e);
else
} else {
throw e;
}
}).finally(() => set.delete(promise));
}
@ -183,20 +189,23 @@ export class EventEmitter implements EventEmitterType {
checkListener(listener);
const events = this._events;
if (events === undefined)
if (events === undefined) {
return this;
}
const list = events[type];
if (list === undefined)
if (list === undefined) {
return this;
}
if (list === listener || (list as any).listener === listener) {
if (--this._eventsCount === 0) {
this._events = Object.create(null);
} else {
delete events[type];
if (events.removeListener)
if (events.removeListener) {
this.emit('removeListener', type, (list as any).listener ?? listener);
}
}
} else if (typeof list !== 'function') {
let position = -1;
@ -210,19 +219,23 @@ export class EventEmitter implements EventEmitterType {
}
}
if (position < 0)
if (position < 0) {
return this;
}
if (position === 0)
if (position === 0) {
list.shift();
else
} else {
list.splice(position, 1);
}
if (list.length === 1)
if (list.length === 1) {
events[type] = list[0];
}
if (events.removeListener !== undefined)
if (events.removeListener !== undefined) {
this.emit('removeListener', type, originalListener || listener);
}
}
return this;
@ -237,21 +250,24 @@ export class EventEmitter implements EventEmitterType {
removeAllListeners(type: EventType | undefined, options: { behavior?: 'wait'|'ignoreErrors'|'default' }): Promise<void>;
removeAllListeners(type?: string, options?: { behavior?: 'wait'|'ignoreErrors'|'default' }): this | Promise<void> {
this._removeAllListeners(type);
if (!options)
if (!options) {
return this;
}
if (options.behavior === 'wait') {
const errors: Error[] = [];
this._rejectionHandler = error => errors.push(error);
// eslint-disable-next-line internal-playwright/await-promise-in-class-returns
return this._waitFor(type).then(() => {
if (errors.length)
if (errors.length) {
throw errors[0];
}
});
}
if (options.behavior === 'ignoreErrors')
if (options.behavior === 'ignoreErrors') {
this._rejectionHandler = () => {};
}
// eslint-disable-next-line internal-playwright/await-promise-in-class-returns
return Promise.resolve();
@ -259,8 +275,9 @@ export class EventEmitter implements EventEmitterType {
private _removeAllListeners(type?: string) {
const events = this._events;
if (!events)
if (!events) {
return;
}
// not listening for removeListener, no need to emit
if (!events.removeListener) {
@ -268,10 +285,11 @@ export class EventEmitter implements EventEmitterType {
this._events = Object.create(null);
this._eventsCount = 0;
} else if (events[type] !== undefined) {
if (--this._eventsCount === 0)
if (--this._eventsCount === 0) {
this._events = Object.create(null);
else
} else {
delete events[type];
}
}
return;
}
@ -282,8 +300,9 @@ export class EventEmitter implements EventEmitterType {
let key;
for (let i = 0; i < keys.length; ++i) {
key = keys[i];
if (key === 'removeListener')
if (key === 'removeListener') {
continue;
}
this._removeAllListeners(key);
}
this._removeAllListeners('removeListener');
@ -298,8 +317,9 @@ export class EventEmitter implements EventEmitterType {
this.removeListener(type, listeners);
} else if (listeners !== undefined) {
// LIFO order
for (let i = listeners.length - 1; i >= 0; i--)
for (let i = listeners.length - 1; i >= 0; i--) {
this.removeListener(type, listeners[i]);
}
}
}
@ -315,10 +335,12 @@ export class EventEmitter implements EventEmitterType {
const events = this._events;
if (events !== undefined) {
const listener = events[type];
if (typeof listener === 'function')
if (typeof listener === 'function') {
return 1;
if (listener !== undefined)
}
if (listener !== undefined) {
return listener.length;
}
}
return 0;
}
@ -333,8 +355,9 @@ export class EventEmitter implements EventEmitterType {
promises = [...(this._pendingHandlers.get(type) || [])];
} else {
promises = [];
for (const [, pending] of this._pendingHandlers)
for (const [, pending] of this._pendingHandlers) {
promises.push(...pending);
}
}
await Promise.all(promises);
}
@ -342,23 +365,27 @@ export class EventEmitter implements EventEmitterType {
private _listeners(target: EventEmitter, type: EventType, unwrap: boolean): Listener[] {
const events = target._events;
if (events === undefined)
if (events === undefined) {
return [];
}
const listener = events[type];
if (listener === undefined)
if (listener === undefined) {
return [];
}
if (typeof listener === 'function')
if (typeof listener === 'function') {
return unwrap ? [unwrapListener(listener)] : [listener];
}
return unwrap ? unwrapListeners(listener) : listener.slice();
}
}
function checkListener(listener: any) {
if (typeof listener !== 'function')
if (typeof listener !== 'function') {
throw new TypeError('The "listener" argument must be of type Function. Received type ' + typeof listener);
}
}
class OnceWrapper {
@ -377,8 +404,9 @@ class OnceWrapper {
}
private _handle(...args: any[]) {
if (this._fired)
if (this._fired) {
return;
}
this._fired = true;
this._eventEmitter.removeListener(this._eventType, this.wrapperFunction);
return this._listener.apply(this._eventEmitter, args);

View file

@ -110,8 +110,9 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
try {
await this._channel.dispose(options);
} catch (e) {
if (isTargetClosedError(e))
if (isTargetClosedError(e)) {
return;
}
throw e;
}
this._tracing._resetStackCounter();
@ -168,8 +169,9 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
async _innerFetch(options: FetchOptions & { url?: string, request?: api.Request } = {}): Promise<APIResponse> {
return await this._wrapApiCall(async () => {
if (this._closeReason)
if (this._closeReason) {
throw new TargetClosedError(this._closeReason);
}
assert(options.request || typeof options.url === 'string', 'First argument must be either URL string or Request');
assert((options.data === undefined ? 0 : 1) + (options.form === undefined ? 0 : 1) + (options.multipart === undefined ? 0 : 1) <= 1, `Only one of 'data', 'form' or 'multipart' can be specified`);
assert(options.maxRedirects === undefined || options.maxRedirects >= 0, `'maxRedirects' must be greater than or equal to '0'`);
@ -177,10 +179,11 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
const url = options.url !== undefined ? options.url : options.request!.url();
const method = options.method || options.request?.method();
let encodedParams = undefined;
if (typeof options.params === 'string')
if (typeof options.params === 'string') {
encodedParams = options.params;
else if (options.params instanceof URLSearchParams)
} else if (options.params instanceof URLSearchParams) {
encodedParams = options.params.toString();
}
// Cannot call allHeaders() here as the request may be paused inside route handler.
const headersObj = options.headers || options.request?.headers();
const headers = headersObj ? headersObjectToArray(headersObj) : undefined;
@ -190,10 +193,11 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
let postDataBuffer: Buffer | undefined;
if (options.data !== undefined) {
if (isString(options.data)) {
if (isJsonContentType(headers))
if (isJsonContentType(headers)) {
jsonData = isJsonParsable(options.data) ? options.data : JSON.stringify(options.data);
else
} else {
postDataBuffer = Buffer.from(options.data, 'utf8');
}
} else if (Buffer.isBuffer(options.data)) {
postDataBuffer = options.data;
} else if (typeof options.data === 'object' || typeof options.data === 'number' || typeof options.data === 'boolean') {
@ -205,8 +209,9 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
if (globalThis.FormData && options.form instanceof FormData) {
formData = [];
for (const [name, value] of options.form.entries()) {
if (typeof value !== 'string')
if (typeof value !== 'string') {
throw new Error(`Expected string for options.form["${name}"], found File. Please use options.multipart instead.`);
}
formData.push({ name, value });
}
} else {
@ -230,12 +235,14 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
}
} else {
// Convert file-like values to ServerFilePayload structs.
for (const [name, value] of Object.entries(options.multipart))
for (const [name, value] of Object.entries(options.multipart)) {
multipartData.push(await toFormField(name, value));
}
}
}
if (postDataBuffer === undefined && jsonData === undefined && formData === undefined && multipartData === undefined)
if (postDataBuffer === undefined && jsonData === undefined && formData === undefined && multipartData === undefined) {
postDataBuffer = options.request?.postDataBuffer() || undefined;
}
const fixtures = {
__testHookLookup: (options as any).__testHookLookup
};
@ -273,8 +280,9 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
async function toFormField(name: string, value: string|number|boolean|fs.ReadStream|FilePayload): Promise<channels.FormField> {
if (isFilePayload(value)) {
const payload = value as FilePayload;
if (!Buffer.isBuffer(payload.buffer))
if (!Buffer.isBuffer(payload.buffer)) {
throw new Error(`Unexpected buffer type of 'data.${name}'`);
}
return { name, file: filePayloadToJson(payload) };
} else if (value instanceof fs.ReadStream) {
return { name, file: await readStreamToJson(value as fs.ReadStream) };
@ -284,16 +292,18 @@ async function toFormField(name: string, value: string|number|boolean|fs.ReadStr
}
function isJsonParsable(value: any) {
if (typeof value !== 'string')
if (typeof value !== 'string') {
return false;
}
try {
JSON.parse(value);
return true;
} catch (e) {
if (e instanceof SyntaxError)
if (e instanceof SyntaxError) {
return false;
else
} else {
throw e;
}
}
}
@ -335,12 +345,14 @@ export class APIResponse implements api.APIResponse {
async body(): Promise<Buffer> {
try {
const result = await this._request._channel.fetchResponseBody({ fetchUid: this._fetchUid() });
if (result.binary === undefined)
if (result.binary === undefined) {
throw new Error('Response has been disposed');
}
return result.binary;
} catch (e) {
if (isTargetClosedError(e))
if (isTargetClosedError(e)) {
throw new Error('Response has been disposed');
}
throw e;
}
}
@ -403,21 +415,25 @@ async function readStreamToJson(stream: fs.ReadStream): Promise<ServerFilePayloa
}
function isJsonContentType(headers?: HeadersArray): boolean {
if (!headers)
if (!headers) {
return false;
}
for (const { name, value } of headers) {
if (name.toLocaleLowerCase() === 'content-type')
if (name.toLocaleLowerCase() === 'content-type') {
return value === 'application/json';
}
}
return false;
}
function objectToArray(map?: { [key: string]: any }): NameValue[] | undefined {
if (!map)
if (!map) {
return undefined;
}
const result = [];
for (const [name, value] of Object.entries(map))
for (const [name, value] of Object.entries(map)) {
result.push({ name, value: String(value) });
}
return result;
}

View file

@ -66,8 +66,9 @@ export class Frame extends ChannelOwner<channels.FrameChannel> implements api.Fr
this._eventEmitter = new EventEmitter();
this._eventEmitter.setMaxListeners(0);
this._parentFrame = Frame.fromNullable(initializer.parentFrame);
if (this._parentFrame)
if (this._parentFrame) {
this._parentFrame._childFrames.add(this);
}
this._name = initializer.name;
this._url = initializer.url;
this._loadStates = new Set(initializer.loadStates);
@ -76,19 +77,23 @@ export class Frame extends ChannelOwner<channels.FrameChannel> implements api.Fr
this._loadStates.add(event.add);
this._eventEmitter.emit('loadstate', event.add);
}
if (event.remove)
if (event.remove) {
this._loadStates.delete(event.remove);
if (!this._parentFrame && event.add === 'load' && this._page)
}
if (!this._parentFrame && event.add === 'load' && this._page) {
this._page.emit(Events.Page.Load, this._page);
if (!this._parentFrame && event.add === 'domcontentloaded' && this._page)
}
if (!this._parentFrame && event.add === 'domcontentloaded' && this._page) {
this._page.emit(Events.Page.DOMContentLoaded, this._page);
}
});
this._channel.on('navigated', event => {
this._url = event.url;
this._name = event.name;
this._eventEmitter.emit('navigated', event);
if (!event.error && this._page)
if (!event.error && this._page) {
this._page.emit(Events.Page.FrameNavigated, this);
}
});
}
@ -103,8 +108,9 @@ export class Frame extends ChannelOwner<channels.FrameChannel> implements api.Fr
private _setupNavigationWaiter(options: { timeout?: number }): Waiter {
const waiter = new Waiter(this._page!, '');
if (this._page!.isClosed())
if (this._page!.isClosed()) {
waiter.rejectImmediately(this._page!._closeErrorWithReason());
}
waiter.rejectOnEvent(this._page!, Events.Page.Close, () => this._page!._closeErrorWithReason());
waiter.rejectOnEvent(this._page!, Events.Page.Crash, new Error('Navigation failed because page crashed!'));
waiter.rejectOnEvent<Frame>(this._page!, Events.Page.FrameDetached, new Error('Navigating frame was detached!'), frame => frame === this);
@ -123,8 +129,9 @@ export class Frame extends ChannelOwner<channels.FrameChannel> implements api.Fr
const navigatedEvent = await waiter.waitForEvent<channels.FrameNavigatedEvent>(this._eventEmitter, 'navigated', event => {
// Any failed navigation results in a rejection.
if (event.error)
if (event.error) {
return true;
}
waiter.log(` navigated to "${event.url}"`);
return urlMatches(this._page?.context()._options.baseURL, event.url, options.url);
});
@ -165,8 +172,9 @@ export class Frame extends ChannelOwner<channels.FrameChannel> implements api.Fr
}
async waitForURL(url: URLMatch, options: { waitUntil?: LifecycleEvent, timeout?: number } = {}): Promise<void> {
if (urlMatches(this._page?.context()._options.baseURL, this.url(), url))
if (urlMatches(this._page?.context()._options.baseURL, this.url(), url)) {
return await this.waitForLoadState(options.waitUntil, options);
}
await this.waitForNavigation({ url, ...options });
}
@ -201,10 +209,12 @@ export class Frame extends ChannelOwner<channels.FrameChannel> implements api.Fr
waitForSelector(selector: string, options: channels.FrameWaitForSelectorOptions & { state: 'attached' | 'visible' }): Promise<ElementHandle<SVGElement | HTMLElement>>;
waitForSelector(selector: string, options?: channels.FrameWaitForSelectorOptions): Promise<ElementHandle<SVGElement | HTMLElement> | null>;
async waitForSelector(selector: string, options: channels.FrameWaitForSelectorOptions = {}): Promise<ElementHandle<SVGElement | HTMLElement> | null> {
if ((options as any).visibility)
if ((options as any).visibility) {
throw new Error('options.visibility is not supported, did you mean options.state?');
if ((options as any).waitFor && (options as any).waitFor !== 'visible')
}
if ((options as any).waitFor && (options as any).waitFor !== 'visible') {
throw new Error('options.waitFor is not supported, did you mean options.state?');
}
const result = await this._channel.waitForSelector({ selector, ...options });
return ElementHandle.fromNullable(result.element) as ElementHandle<SVGElement | HTMLElement> | null;
}
@ -421,10 +431,11 @@ export class Frame extends ChannelOwner<channels.FrameChannel> implements api.Fr
}
async setChecked(selector: string, checked: boolean, options?: channels.FrameCheckOptions) {
if (checked)
if (checked) {
await this.check(selector, options);
else
} else {
await this.uncheck(selector, options);
}
}
async waitForTimeout(timeout: number) {
@ -432,8 +443,9 @@ export class Frame extends ChannelOwner<channels.FrameChannel> implements api.Fr
}
async waitForFunction<R, Arg>(pageFunction: structs.PageFunction<Arg, R>, arg?: Arg, options: WaitForFunctionOptions = {}): Promise<structs.SmartHandle<R>> {
if (typeof options.polling === 'string')
if (typeof options.polling === 'string') {
assert(options.polling === 'raf', 'Unknown polling option: ' + options.polling);
}
const result = await this._channel.waitForFunction({
...options,
pollingInterval: options.polling === 'raf' ? undefined : options.polling,
@ -450,9 +462,11 @@ export class Frame extends ChannelOwner<channels.FrameChannel> implements api.Fr
}
export function verifyLoadState(name: string, waitUntil: LifecycleEvent): LifecycleEvent {
if (waitUntil as unknown === 'networkidle0')
if (waitUntil as unknown === 'networkidle0') {
waitUntil = 'networkidle';
if (!kLifecycleEvents.has(waitUntil))
}
if (!kLifecycleEvents.has(waitUntil)) {
throw new Error(`${name}: expected one of (load|domcontentloaded|networkidle|commit)`);
}
return waitUntil;
}

View file

@ -31,8 +31,9 @@ export class HarRouter {
static async create(localUtils: LocalUtils, file: string, notFoundAction: HarNotFoundAction, options: { urlMatch?: URLMatch }): Promise<HarRouter> {
const { harId, error } = await localUtils._channel.harOpen({ file });
if (error)
if (error) {
throw new Error(error);
}
return new HarRouter(localUtils, harId!, notFoundAction, options);
}
@ -67,8 +68,9 @@ export class HarRouter {
// TODO: it'd be better to abort such requests, but then we likely need to respect the timing,
// because the request might have been stalled for a long time until the very end of the
// test when HAR was recorded but we'd abort it immediately.
if (response.status === -1)
if (response.status === -1) {
return;
}
await route.fulfill({
status: response.status,
headers: Object.fromEntries(response.headers!.map(h => [h.name, h.value])),
@ -77,8 +79,9 @@ export class HarRouter {
return;
}
if (response.action === 'error')
if (response.action === 'error') {
debugLogger.log('api', 'HAR: ' + response.message!);
}
// Report the error, but fall through to the default handler.
if (this._notFoundAction === 'abort') {

View file

@ -51,8 +51,9 @@ export class JSHandle<T = any> extends ChannelOwner<channels.JSHandleChannel> im
async getProperties(): Promise<Map<string, JSHandle>> {
const map = new Map<string, JSHandle>();
for (const { name, value } of (await this._channel.getPropertyList()).properties)
for (const { name, value } of (await this._channel.getPropertyList()).properties) {
map.set(name, JSHandle.from(value));
}
return map;
}
@ -72,8 +73,9 @@ export class JSHandle<T = any> extends ChannelOwner<channels.JSHandleChannel> im
try {
await this._channel.dispose();
} catch (e) {
if (isTargetClosedError(e))
if (isTargetClosedError(e)) {
return;
}
throw e;
}
}
@ -92,8 +94,9 @@ export function serializeArgument(arg: any): channels.SerializedArgument {
return handles.length - 1;
};
const value = serializeValue(arg, value => {
if (value instanceof JSHandle)
if (value instanceof JSHandle) {
return { h: pushHandle(value._channel) };
}
return { fallThrough: value };
});
return { value, handles };
@ -104,6 +107,7 @@ export function parseResult(value: channels.SerializedValue): any {
}
export function assertMaxArguments(count: number, max: number): asserts count {
if (count > max)
if (count > max) {
throw new Error('Too many arguments. If you need to pass more than 1 argument to the function wrap them in an object.');
}
}

View file

@ -35,7 +35,8 @@ export class LocalUtils extends ChannelOwner<channels.LocalUtilsChannel> {
super(parent, type, guid, initializer);
this.markAsInternalType();
this.devices = {};
for (const { name, descriptor } of initializer.deviceDescriptors)
for (const { name, descriptor } of initializer.deviceDescriptors) {
this.devices[name] = descriptor;
}
}
}

View file

@ -42,23 +42,27 @@ export class Locator implements api.Locator {
this._frame = frame;
this._selector = selector;
if (options?.hasText)
if (options?.hasText) {
this._selector += ` >> internal:has-text=${escapeForTextSelector(options.hasText, false)}`;
}
if (options?.hasNotText)
if (options?.hasNotText) {
this._selector += ` >> internal:has-not-text=${escapeForTextSelector(options.hasNotText, false)}`;
}
if (options?.has) {
const locator = options.has;
if (locator._frame !== frame)
if (locator._frame !== frame) {
throw new Error(`Inner "has" locator must belong to the same frame.`);
}
this._selector += ` >> internal:has=` + JSON.stringify(locator._selector);
}
if (options?.hasNot) {
const locator = options.hasNot;
if (locator._frame !== frame)
if (locator._frame !== frame) {
throw new Error(`Inner "hasNot" locator must belong to the same frame.`);
}
this._selector += ` >> internal:has-not=` + JSON.stringify(locator._selector);
}
}
@ -70,8 +74,9 @@ export class Locator implements api.Locator {
return await this._frame._wrapApiCall<R>(async () => {
const result = await this._frame._channel.waitForSelector({ selector: this._selector, strict: true, state: 'attached', timeout });
const handle = ElementHandle.fromNullable(result.element) as ElementHandle<SVGElement | HTMLElement> | null;
if (!handle)
if (!handle) {
throw new Error(`Could not resolve ${this._selector} to DOM Element`);
}
try {
return await task(handle, deadline ? deadline - monotonicTime() : 0);
} finally {
@ -145,10 +150,12 @@ export class Locator implements api.Locator {
}
locator(selectorOrLocator: string | Locator, options?: LocatorOptions): Locator {
if (isString(selectorOrLocator))
if (isString(selectorOrLocator)) {
return new Locator(this._frame, this._selector + ' >> ' + selectorOrLocator, options);
if (selectorOrLocator._frame !== this._frame)
}
if (selectorOrLocator._frame !== this._frame) {
throw new Error(`Locators must belong to the same frame.`);
}
return new Locator(this._frame, this._selector + ' >> internal:chain=' + JSON.stringify(selectorOrLocator._selector), options);
}
@ -213,14 +220,16 @@ export class Locator implements api.Locator {
}
and(locator: Locator): Locator {
if (locator._frame !== this._frame)
if (locator._frame !== this._frame) {
throw new Error(`Locators must belong to the same frame.`);
}
return new Locator(this._frame, this._selector + ` >> internal:and=` + JSON.stringify(locator._selector));
}
or(locator: Locator): Locator {
if (locator._frame !== this._frame)
if (locator._frame !== this._frame) {
throw new Error(`Locators must belong to the same frame.`);
}
return new Locator(this._frame, this._selector + ` >> internal:or=` + JSON.stringify(locator._selector));
}
@ -306,10 +315,11 @@ export class Locator implements api.Locator {
}
async setChecked(checked: boolean, options?: channels.ElementHandleCheckOptions) {
if (checked)
if (checked) {
await this.check(options);
else
} else {
await this.uncheck(options);
}
}
async setInputFiles(files: string | FilePayload | string[] | FilePayload[], options: channels.ElementHandleSetInputFilesOptions = {}) {
@ -358,8 +368,9 @@ export class Locator implements api.Locator {
const params: channels.FrameExpectParams = { selector: this._selector, expression, ...options, isNot: !!options.isNot };
params.expectedValue = serializeArgument(options.expectedValue);
const result = (await this._frame._channel.expect(params));
if (result.received !== undefined)
if (result.received !== undefined) {
result.received = parseResult(result.received);
}
return result;
}
@ -382,10 +393,12 @@ export class FrameLocator implements api.FrameLocator {
}
locator(selectorOrLocator: string | Locator, options?: LocatorOptions): Locator {
if (isString(selectorOrLocator))
if (isString(selectorOrLocator)) {
return new Locator(this._frame, this._frameSelector + ' >> internal:control=enter-frame >> ' + selectorOrLocator, options);
if (selectorOrLocator._frame !== this._frame)
}
if (selectorOrLocator._frame !== this._frame) {
throw new Error(`Locators must belong to the same frame.`);
}
return new Locator(this._frame, this._frameSelector + ' >> internal:control=enter-frame >> ' + selectorOrLocator._selector, options);
}

View file

@ -99,8 +99,9 @@ export class Request extends ChannelOwner<channels.RequestChannel> implements ap
super(parent, type, guid, initializer);
this.markAsInternalType();
this._redirectedFrom = Request.fromNullable(initializer.redirectedFrom);
if (this._redirectedFrom)
if (this._redirectedFrom) {
this._redirectedFrom._redirectedTo = this;
}
this._provisionalHeaders = new RawHeaders(initializer.headers);
this._timing = {
startTime: 0,
@ -137,15 +138,17 @@ export class Request extends ChannelOwner<channels.RequestChannel> implements ap
postDataJSON(): Object | null {
const postData = this.postData();
if (!postData)
if (!postData) {
return null;
}
const contentType = this.headers()['content-type'];
if (contentType?.includes('application/x-www-form-urlencoded')) {
if (contentType.includes('application/x-www-form-urlencoded')) {
const entries: Record<string, string> = {};
const parsed = new URLSearchParams(postData);
for (const [k, v] of parsed.entries())
for (const [k, v] of parsed.entries()) {
entries[k] = v;
}
return entries;
}
@ -160,14 +163,16 @@ export class Request extends ChannelOwner<channels.RequestChannel> implements ap
* @deprecated
*/
headers(): Headers {
if (this._fallbackOverrides.headers)
if (this._fallbackOverrides.headers) {
return RawHeaders._fromHeadersObjectLossy(this._fallbackOverrides.headers).headers();
}
return this._provisionalHeaders.headers();
}
async _actualHeaders(): Promise<RawHeaders> {
if (this._fallbackOverrides.headers)
if (this._fallbackOverrides.headers) {
return RawHeaders._fromHeadersObjectLossy(this._fallbackOverrides.headers);
}
if (!this._actualHeadersPromise) {
this._actualHeadersPromise = this._wrapApiCall(async () => {
@ -236,8 +241,9 @@ export class Request extends ChannelOwner<channels.RequestChannel> implements ap
}
failure(): { errorText: string; } | null {
if (this._failureText === null)
if (this._failureText === null) {
return null;
}
return {
errorText: this._failureText
};
@ -249,15 +255,17 @@ export class Request extends ChannelOwner<channels.RequestChannel> implements ap
async sizes(): Promise<RequestSizes> {
const response = await this.response();
if (!response)
if (!response) {
throw new Error('Unable to fetch sizes for failed request');
}
return (await response._channel.sizes()).sizes;
}
_setResponseEndTiming(responseEndTiming: number) {
this._timing.responseEnd = responseEndTiming;
if (this._timing.responseStart === -1)
if (this._timing.responseStart === -1) {
this._timing.responseStart = responseEndTiming;
}
}
_finalRequest(): Request {
@ -265,19 +273,23 @@ export class Request extends ChannelOwner<channels.RequestChannel> implements ap
}
_applyFallbackOverrides(overrides: FallbackOverrides) {
if (overrides.url)
if (overrides.url) {
this._fallbackOverrides.url = overrides.url;
if (overrides.method)
}
if (overrides.method) {
this._fallbackOverrides.method = overrides.method;
if (overrides.headers)
}
if (overrides.headers) {
this._fallbackOverrides.headers = overrides.headers;
}
if (isString(overrides.postData))
if (isString(overrides.postData)) {
this._fallbackOverrides.postDataBuffer = Buffer.from(overrides.postData, 'utf-8');
else if (overrides.postData instanceof Buffer)
} else if (overrides.postData instanceof Buffer) {
this._fallbackOverrides.postDataBuffer = overrides.postData;
else if (overrides.postData)
} else if (overrides.postData) {
this._fallbackOverrides.postDataBuffer = Buffer.from(JSON.stringify(overrides.postData), 'utf-8');
}
}
_fallbackOverridesForContinue() {
@ -375,10 +387,11 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
statusOption ??= options.response.status();
headersOption ??= options.response.headers();
if (body === undefined && options.path === undefined) {
if (options.response._request._connection === this._connection)
if (options.response._request._connection === this._connection) {
fetchResponseUid = (options.response as APIResponse)._fetchUid();
else
} else {
body = await options.response.body();
}
}
}
@ -399,16 +412,19 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
}
const headers: Headers = {};
for (const header of Object.keys(headersOption || {}))
for (const header of Object.keys(headersOption || {})) {
headers[header.toLowerCase()] = String(headersOption![header]);
if (options.contentType)
}
if (options.contentType) {
headers['content-type'] = String(options.contentType);
else if (options.json)
} else if (options.json) {
headers['content-type'] = 'application/json';
else if (options.path)
} else if (options.path) {
headers['content-type'] = mime.getType(options.path) || 'application/octet-stream';
if (length && !('content-length' in headers))
}
if (length && !('content-length' in headers)) {
headers['content-length'] = String(length);
}
await this._raceWithTargetClose(this._channel.fulfill({
status: statusOption || 200,
@ -427,8 +443,9 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
}
_checkNotHandled() {
if (!this._handlingPromise)
if (!this._handlingPromise) {
throw new Error('Route is already handled!');
}
}
_reportHandled(done: boolean) {
@ -487,10 +504,11 @@ export class WebSocketRoute extends ChannelOwner<channels.WebSocketRouteChannel>
},
send: (message: string | Buffer) => {
if (isString(message))
if (isString(message)) {
this._channel.sendToServer({ message, isBase64: false }).catch(() => {});
else
} else {
this._channel.sendToServer({ message: message.toString('base64'), isBase64: true }).catch(() => {});
}
},
async [Symbol.asyncDispose]() {
@ -499,31 +517,35 @@ export class WebSocketRoute extends ChannelOwner<channels.WebSocketRouteChannel>
};
this._channel.on('messageFromPage', ({ message, isBase64 }) => {
if (this._onPageMessage)
if (this._onPageMessage) {
this._onPageMessage(isBase64 ? Buffer.from(message, 'base64') : message);
else if (this._connected)
} else if (this._connected) {
this._channel.sendToServer({ message, isBase64 }).catch(() => {});
}
});
this._channel.on('messageFromServer', ({ message, isBase64 }) => {
if (this._onServerMessage)
if (this._onServerMessage) {
this._onServerMessage(isBase64 ? Buffer.from(message, 'base64') : message);
else
} else {
this._channel.sendToPage({ message, isBase64 }).catch(() => {});
}
});
this._channel.on('closePage', ({ code, reason, wasClean }) => {
if (this._onPageClose)
if (this._onPageClose) {
this._onPageClose(code, reason);
else
} else {
this._channel.closeServer({ code, reason, wasClean }).catch(() => {});
}
});
this._channel.on('closeServer', ({ code, reason, wasClean }) => {
if (this._onServerClose)
if (this._onServerClose) {
this._onServerClose(code, reason);
else
} else {
this._channel.closePage({ code, reason, wasClean }).catch(() => {});
}
});
}
@ -536,18 +558,20 @@ export class WebSocketRoute extends ChannelOwner<channels.WebSocketRouteChannel>
}
connectToServer() {
if (this._connected)
if (this._connected) {
throw new Error('Already connected to the server');
}
this._connected = true;
this._channel.connect().catch(() => {});
return this._server;
}
send(message: string | Buffer) {
if (isString(message))
if (isString(message)) {
this._channel.sendToPage({ message, isBase64: false }).catch(() => {});
else
} else {
this._channel.sendToPage({ message: message.toString('base64'), isBase64: true }).catch(() => {});
}
}
onMessage(handler: (message: string | Buffer) => any) {
@ -563,8 +587,9 @@ export class WebSocketRoute extends ChannelOwner<channels.WebSocketRouteChannel>
}
async _afterHandle() {
if (this._connected)
if (this._connected) {
return;
}
// Ensure that websocket is "open" and can send messages without an actual server connection.
await this._channel.ensureOpened();
}
@ -585,15 +610,17 @@ export class WebSocketRouteHandler {
const patterns: channels.BrowserContextSetWebSocketInterceptionPatternsParams['patterns'] = [];
let all = false;
for (const handler of handlers) {
if (isString(handler.url))
if (isString(handler.url)) {
patterns.push({ glob: handler.url });
else if (isRegExp(handler.url))
} else if (isRegExp(handler.url)) {
patterns.push({ regexSource: handler.url.source, regexFlags: handler.url.flags });
else
} else {
all = true;
}
}
if (all)
if (all) {
return [{ glob: '**/*' }];
}
return patterns;
}
@ -753,16 +780,18 @@ export class WebSocket extends ChannelOwner<channels.WebSocketChannel> implement
this._isClosed = false;
this._page = parent as Page;
this._channel.on('frameSent', event => {
if (event.opcode === 1)
if (event.opcode === 1) {
this.emit(Events.WebSocket.FrameSent, { payload: event.data });
else if (event.opcode === 2)
} else if (event.opcode === 2) {
this.emit(Events.WebSocket.FrameSent, { payload: Buffer.from(event.data, 'base64') });
}
});
this._channel.on('frameReceived', event => {
if (event.opcode === 1)
if (event.opcode === 1) {
this.emit(Events.WebSocket.FrameReceived, { payload: event.data });
else if (event.opcode === 2)
} else if (event.opcode === 2) {
this.emit(Events.WebSocket.FrameReceived, { payload: Buffer.from(event.data, 'base64') });
}
});
this._channel.on('socketError', ({ error }) => this.emit(Events.WebSocket.Error, error));
this._channel.on('close', () => {
@ -785,10 +814,12 @@ export class WebSocket extends ChannelOwner<channels.WebSocketChannel> implement
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
const waiter = Waiter.createForEvent(this, event);
waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`);
if (event !== Events.WebSocket.Error)
if (event !== Events.WebSocket.Error) {
waiter.rejectOnEvent(this, Events.WebSocket.Error, new Error('Socket error'));
if (event !== Events.WebSocket.Close)
}
if (event !== Events.WebSocket.Close) {
waiter.rejectOnEvent(this, Events.WebSocket.Close, new Error('Socket closed'));
}
waiter.rejectOnEvent(this._page, Events.Page.Close, () => this._page._closeErrorWithReason());
const result = await waiter.waitForEvent(this, event, predicate as any);
waiter.dispose();
@ -800,8 +831,9 @@ export class WebSocket extends ChannelOwner<channels.WebSocketChannel> implement
export function validateHeaders(headers: Headers) {
for (const key of Object.keys(headers)) {
const value = headers[key];
if (!Object.is(value, undefined) && !isString(value))
if (!Object.is(value, undefined) && !isString(value)) {
throw new Error(`Expected value of header "${key}" to be String, but "${typeof value}" is found.`);
}
}
}
@ -827,15 +859,17 @@ export class RouteHandler {
const patterns: channels.BrowserContextSetNetworkInterceptionPatternsParams['patterns'] = [];
let all = false;
for (const handler of handlers) {
if (isString(handler.url))
if (isString(handler.url)) {
patterns.push({ glob: handler.url });
else if (isRegExp(handler.url))
} else if (isRegExp(handler.url)) {
patterns.push({ regexSource: handler.url.source, regexFlags: handler.url.flags });
else
} else {
all = true;
}
}
if (all)
if (all) {
return [{ glob: '**/*' }];
}
return patterns;
}
@ -854,8 +888,9 @@ export class RouteHandler {
return await this._handleInternal(route);
} catch (e) {
// If the handler was stopped (without waiting for completion), we ignore all exceptions.
if (this._ignoreException)
if (this._ignoreException) {
return false;
}
if (isTargetClosedError(e)) {
// We are failing in the handler because the target close closed.
// Give user a hint!
@ -878,8 +913,9 @@ export class RouteHandler {
} else {
const promises = [];
for (const activation of this._activeInvocations) {
if (!activation.route._didThrow)
if (!activation.route._didThrow) {
promises.push(activation.complete);
}
}
await Promise.all(promises);
}
@ -915,14 +951,16 @@ export class RawHeaders {
constructor(headers: HeadersArray) {
this._headersArray = headers;
for (const header of headers)
for (const header of headers) {
this._headersMap.set(header.name.toLowerCase(), header.value);
}
}
get(name: string): string | null {
const values = this.getAll(name);
if (!values || !values.length)
if (!values || !values.length) {
return null;
}
return values.join(name.toLowerCase() === 'set-cookie' ? '\n' : ', ');
}
@ -932,8 +970,9 @@ export class RawHeaders {
headers(): Headers {
const result: Headers = {};
for (const name of this._headersMap.keys())
for (const name of this._headersMap.keys()) {
result[name] = this.get(name)!;
}
return result;
}

View file

@ -166,16 +166,18 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
private _onFrameAttached(frame: Frame) {
frame._page = this;
this._frames.add(frame);
if (frame._parentFrame)
if (frame._parentFrame) {
frame._parentFrame._childFrames.add(frame);
}
this.emit(Events.Page.FrameAttached, frame);
}
private _onFrameDetached(frame: Frame) {
this._frames.delete(frame);
frame._detached = true;
if (frame._parentFrame)
if (frame._parentFrame) {
frame._parentFrame._childFrames.delete(frame);
}
this.emit(Events.Page.FrameDetached, frame);
}
@ -184,20 +186,26 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
const routeHandlers = this._routes.slice();
for (const routeHandler of routeHandlers) {
// If the page was closed we stall all requests right away.
if (this._closeWasCalled || this._browserContext._closeWasCalled)
if (this._closeWasCalled || this._browserContext._closeWasCalled) {
return;
if (!routeHandler.matches(route.request().url()))
}
if (!routeHandler.matches(route.request().url())) {
continue;
}
const index = this._routes.indexOf(routeHandler);
if (index === -1)
if (index === -1) {
continue;
if (routeHandler.willExpire())
}
if (routeHandler.willExpire()) {
this._routes.splice(index, 1);
}
const handled = await routeHandler.handle(route);
if (!this._routes.length)
if (!this._routes.length) {
this._wrapApiCall(() => this._updateInterceptionPatterns(), true).catch(() => {});
if (handled)
}
if (handled) {
return;
}
}
await this._browserContext._onRoute(route);
@ -205,10 +213,11 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
private async _onWebSocketRoute(webSocketRoute: WebSocketRoute) {
const routeHandler = this._webSocketRoutes.find(route => route.matches(webSocketRoute.url()));
if (routeHandler)
if (routeHandler) {
await routeHandler.handle(webSocketRoute);
else
} else {
await this._browserContext._onWebSocketRoute(webSocketRoute);
}
}
async _onBinding(bindingCall: BindingCall) {
@ -243,8 +252,9 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
}
async opener(): Promise<Page | null> {
if (!this._opener || this._opener.isClosed())
if (!this._opener || this._opener.isClosed()) {
return null;
}
return this._opener;
}
@ -257,8 +267,9 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
const url = isObject(frameSelector) ? frameSelector.url : undefined;
assert(name || url, 'Either name or url matcher should be specified');
return this.frames().find(f => {
if (name)
if (name) {
return f.name() === name;
}
return urlMatches(this._browserContext._options.baseURL, f.url(), url);
}) || null;
}
@ -282,8 +293,9 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
}
private _forceVideo(): Video {
if (!this._video)
if (!this._video) {
this._video = new Video(this, this._connection);
}
return this._video;
}
@ -291,8 +303,9 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
// Note: we are creating Video object lazily, because we do not know
// BrowserContextOptions when constructing the page - it is assigned
// too late during launchPersistentContext.
if (!this._browserContext._options.recordVideo)
if (!this._browserContext._options.recordVideo) {
return null;
}
return this._forceVideo();
}
@ -375,10 +388,12 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
}
async addLocatorHandler(locator: Locator, handler: (locator: Locator) => any, options: { times?: number, noWaitAfter?: boolean } = {}): Promise<void> {
if (locator._frame !== this._mainFrame)
if (locator._frame !== this._mainFrame) {
throw new Error(`Locator must belong to the main frame of this page`);
if (options.times === 0)
}
if (options.times === 0) {
return;
}
const { uid } = await this._channel.registerLocatorHandler({ selector: locator._selector, noWaitAfter: options.noWaitAfter });
this._locatorHandlers.set(uid, { locator, handler, times: options.times });
}
@ -388,14 +403,16 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
try {
const handler = this._locatorHandlers.get(uid);
if (handler && handler.times !== 0) {
if (handler.times !== undefined)
if (handler.times !== undefined) {
handler.times--;
}
await handler.handler(handler.locator);
}
remove = handler?.times === 0;
} finally {
if (remove)
if (remove) {
this._locatorHandlers.delete(uid);
}
this._wrapApiCall(() => this._channel.resolveLocatorHandlerNoReply({ uid, remove }), true).catch(() => {});
}
}
@ -423,8 +440,9 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
async waitForRequest(urlOrPredicate: string | RegExp | ((r: Request) => boolean | Promise<boolean>), options: { timeout?: number } = {}): Promise<Request> {
const predicate = async (request: Request) => {
if (isString(urlOrPredicate) || isRegExp(urlOrPredicate))
if (isString(urlOrPredicate) || isRegExp(urlOrPredicate)) {
return urlMatches(this._browserContext._options.baseURL, request.url(), urlOrPredicate);
}
return await urlOrPredicate(request);
};
const trimmedUrl = trimUrl(urlOrPredicate);
@ -434,8 +452,9 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
async waitForResponse(urlOrPredicate: string | RegExp | ((r: Response) => boolean | Promise<boolean>), options: { timeout?: number } = {}): Promise<Response> {
const predicate = async (response: Response) => {
if (isString(urlOrPredicate) || isRegExp(urlOrPredicate))
if (isString(urlOrPredicate) || isRegExp(urlOrPredicate)) {
return urlMatches(this._browserContext._options.baseURL, response.url(), urlOrPredicate);
}
return await urlOrPredicate(response);
};
const trimmedUrl = trimUrl(urlOrPredicate);
@ -456,13 +475,16 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
const waiter = Waiter.createForEvent(this, event);
if (logLine)
if (logLine) {
waiter.log(logLine);
}
waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`);
if (event !== Events.Page.Crash)
if (event !== Events.Page.Crash) {
waiter.rejectOnEvent(this, Events.Page.Crash, new Error('Page crashed'));
if (event !== Events.Page.Close)
}
if (event !== Events.Page.Close) {
waiter.rejectOnEvent(this, Events.Page.Close, () => this._closeErrorWithReason());
}
const result = await waiter.waitForEvent(this, event, predicate as any);
waiter.dispose();
return result;
@ -545,10 +567,11 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
const removed = [];
const remaining = [];
for (const route of this._routes) {
if (urlMatchesEqual(route.url, url) && (!handler || route.handler === handler))
if (urlMatchesEqual(route.url, url) && (!handler || route.handler === handler)) {
removed.push(route);
else
} else {
remaining.push(route);
}
}
await this._unrouteInternal(removed, remaining, 'default');
}
@ -556,8 +579,9 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
private async _unrouteInternal(removed: RouteHandler[], remaining: RouteHandler[], behavior?: 'wait'|'ignoreErrors'|'default'): Promise<void> {
this._routes = remaining;
await this._updateInterceptionPatterns();
if (!behavior || behavior === 'default')
if (!behavior || behavior === 'default') {
return;
}
const promises = removed.map(routeHandler => routeHandler.stop(behavior));
await Promise.all(promises);
}
@ -574,8 +598,9 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
async screenshot(options: Omit<channels.PageScreenshotOptions, 'mask'> & { path?: string, mask?: Locator[] } = {}): Promise<Buffer> {
const copy: channels.PageScreenshotOptions = { ...options, mask: undefined };
if (!copy.type)
if (!copy.type) {
copy.type = determineScreenshotType(options);
}
if (options.mask) {
copy.mask = options.mask.map(locator => ({
frame: locator._frame._channel,
@ -591,7 +616,7 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
}
async _expectScreenshot(options: ExpectScreenshotOptions): Promise<{ actual?: Buffer, previous?: Buffer, diff?: Buffer, errorMessage?: string, log?: string[], timedOut?: boolean}> {
const mask = options?.mask ? options?.mask.map(locator => ({
const mask = options.mask ? options.mask.map(locator => ({
frame: (locator as Locator)._frame._channel,
selector: (locator as Locator)._selector,
})) : undefined;
@ -623,13 +648,15 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
this._closeReason = options.reason;
this._closeWasCalled = true;
try {
if (this._ownedContext)
if (this._ownedContext) {
await this._ownedContext.close();
else
} else {
await this._channel.close(options);
}
} catch (e) {
if (isTargetClosedError(e) && !options.runBeforeUnload)
if (isTargetClosedError(e) && !options.runBeforeUnload) {
return;
}
throw e;
}
}
@ -787,13 +814,14 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
}
async pause(_options?: { __testHookKeepTestTimeout: boolean }) {
if (require('inspector').url())
if (require('inspector').url()) {
return;
}
const defaultNavigationTimeout = this._browserContext._timeoutSettings.defaultNavigationTimeout();
const defaultTimeout = this._browserContext._timeoutSettings.defaultTimeout();
this._browserContext.setDefaultNavigationTimeout(0);
this._browserContext.setDefaultTimeout(0);
this._instrumentation?.onWillPause({ keepTestTimeout: !!_options?.__testHookKeepTestTimeout });
this._instrumentation.onWillPause({ keepTestTimeout: !!_options?.__testHookKeepTestTimeout });
await this._closedOrCrashedScope.safeRace(this.context()._channel.pause());
this._browserContext.setDefaultNavigationTimeout(defaultNavigationTimeout);
this._browserContext.setDefaultTimeout(defaultTimeout);
@ -801,16 +829,20 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
async pdf(options: PDFOptions = {}): Promise<Buffer> {
const transportOptions: channels.PagePdfParams = { ...options } as channels.PagePdfParams;
if (transportOptions.margin)
if (transportOptions.margin) {
transportOptions.margin = { ...transportOptions.margin };
if (typeof options.width === 'number')
}
if (typeof options.width === 'number') {
transportOptions.width = options.width + 'px';
if (typeof options.height === 'number')
}
if (typeof options.height === 'number') {
transportOptions.height = options.height + 'px';
}
for (const margin of ['top', 'right', 'bottom', 'left']) {
const index = margin as 'top' | 'right' | 'bottom' | 'left';
if (options.margin && typeof options.margin[index] === 'number')
transportOptions.margin![index] = transportOptions.margin![index] + 'px';
if (options.margin && typeof options.margin[index] === 'number') {
transportOptions.margin![index] = transportOptions.margin![index] + 'px';
}
}
const result = await this._channel.pdf(transportOptions);
if (options.path) {
@ -839,10 +871,11 @@ export class BindingCall extends ChannelOwner<channels.BindingCallChannel> {
frame
};
let result: any;
if (this._initializer.handle)
if (this._initializer.handle) {
result = await func(source, JSHandle.from(this._initializer.handle));
else
} else {
result = await func(source, ...this._initializer.args!.map(parseResult));
}
this._channel.resolve({ result: serializeArgument(result) }).catch(() => {});
} catch (e) {
this._channel.reject({ error: serializeError(e) }).catch(() => {});
@ -851,8 +884,10 @@ export class BindingCall extends ChannelOwner<channels.BindingCallChannel> {
}
function trimUrl(param: any): string | undefined {
if (isRegExp(param))
if (isRegExp(param)) {
return `/${trimStringWithEllipsis(param.source, 50)}/${param.flags}`;
if (isString(param))
}
if (isString(param)) {
return `"${trimStringWithEllipsis(param, 50)}"`;
}
}

View file

@ -51,7 +51,7 @@ export class Playwright extends ChannelOwner<channels.PlaywrightChannel> {
this._bidiChromium._playwright = this;
this._bidiFirefox = BrowserType.from(initializer.bidiFirefox);
this._bidiFirefox._playwright = this;
this.devices = this._connection.localUtils()?.devices ?? {};
this.devices = this._connection.localUtils().devices ?? {};
this.selectors = new Selectors();
this.errors = { TimeoutError };

View file

@ -28,15 +28,17 @@ export class Selectors implements api.Selectors {
async register(name: string, script: string | (() => SelectorEngine) | { path?: string, content?: string }, options: { contentScript?: boolean } = {}): Promise<void> {
const source = await evaluationScript(script, undefined, false);
const params = { ...options, name, source };
for (const channel of this._channels)
for (const channel of this._channels) {
await channel._channel.register(params);
}
this._registrations.push(params);
}
setTestIdAttribute(attributeName: string) {
setTestIdAttribute(attributeName);
for (const channel of this._channels)
for (const channel of this._channels) {
channel._channel.setTestIdAttributeName({ testIdAttributeName: attributeName }).catch(() => {});
}
}
_addChannel(channel: SelectorsOwner) {

View file

@ -42,10 +42,11 @@ class StreamImpl extends Readable {
override async _read() {
const result = await this._channel.read({ size: 1024 * 1024 });
if (result.binary.byteLength)
if (result.binary.byteLength) {
this.push(result.binary);
else
} else {
this.push(null);
}
}
override _destroy(error: Error | null, callback: (error: Error | null | undefined) => void): void {

View file

@ -87,8 +87,9 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
if (!filePath) {
// Not interested in artifacts.
await this._channel.tracingStopChunk({ mode: 'discard' });
if (this._stacksId)
if (this._stacksId) {
await this._connection.localUtils()._channel.traceDiscarded({ stacksId: this._stacksId });
}
return;
}
@ -104,8 +105,9 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
// The artifact may be missing if the browser closed while stopping tracing.
if (!result.artifact) {
if (this._stacksId)
if (this._stacksId) {
await this._connection.localUtils()._channel.traceDiscarded({ stacksId: this._stacksId });
}
return;
}

View file

@ -35,24 +35,28 @@ export class Video implements api.Video {
}
async path(): Promise<string> {
if (this._isRemote)
if (this._isRemote) {
throw new Error(`Path is not available when connecting remotely. Use saveAs() to save a local copy.`);
}
const artifact = await this._artifact;
if (!artifact)
if (!artifact) {
throw new Error('Page did not produce any video frames');
}
return artifact._initializer.absolutePath;
}
async saveAs(path: string): Promise<void> {
const artifact = await this._artifact;
if (!artifact)
if (!artifact) {
throw new Error('Page did not produce any video frames');
}
return await artifact.saveAs(path);
}
async delete(): Promise<void> {
const artifact = await this._artifact;
if (artifact)
if (artifact) {
await artifact.delete();
}
}
}

View file

@ -56,14 +56,19 @@ export class Waiter {
rejectOnEvent<T = void>(emitter: EventEmitter, event: string, error: Error | (() => Error), predicate?: (arg: T) => boolean | Promise<boolean>) {
const { promise, dispose } = waitForEvent(emitter, event, this._savedZone, predicate);
this._rejectOn(promise.then(() => { throw (typeof error === 'function' ? error() : error); }), dispose);
this._rejectOn(promise.then(() => {
throw (typeof error === 'function' ? error() : error);
}), dispose);
}
rejectOnTimeout(timeout: number, message: string) {
if (!timeout)
if (!timeout) {
return;
}
const { promise, dispose } = waitForTimeout(timeout);
this._rejectOn(promise.then(() => { throw new TimeoutError(message); }), dispose);
this._rejectOn(promise.then(() => {
throw new TimeoutError(message);
}), dispose);
}
rejectImmediately(error: Error) {
@ -71,21 +76,25 @@ export class Waiter {
}
dispose() {
for (const dispose of this._dispose)
for (const dispose of this._dispose) {
dispose();
}
}
async waitForPromise<T>(promise: Promise<T>, dispose?: () => void): Promise<T> {
try {
if (this._immediateError)
if (this._immediateError) {
throw this._immediateError;
}
const result = await Promise.race([promise, ...this._failures]);
if (dispose)
if (dispose) {
dispose();
}
return result;
} catch (e) {
if (dispose)
if (dispose) {
dispose();
}
this._error = e.message;
this.dispose();
rewriteErrorMessage(e, e.message + formatLogRecording(this._logs));
@ -102,8 +111,9 @@ export class Waiter {
private _rejectOn(promise: Promise<any>, dispose?: () => void) {
this._failures.push(promise);
if (dispose)
if (dispose) {
this._dispose.push(dispose);
}
}
}
@ -113,8 +123,9 @@ function waitForEvent<T = void>(emitter: EventEmitter, event: string, savedZone:
listener = async (eventArg: any) => {
await savedZone.run(async () => {
try {
if (predicate && !(await predicate(eventArg)))
if (predicate && !(await predicate(eventArg))) {
return;
}
emitter.removeListener(event, listener);
resolve(eventArg);
} catch (e) {
@ -137,8 +148,9 @@ function waitForTimeout(timeout: number): { promise: Promise<void>, dispose: ()
}
function formatLogRecording(log: string[]): string {
if (!log.length)
if (!log.length) {
return '';
}
const header = ` logs `;
const headerLength = 60;
const leftLength = (headerLength - header.length) / 2;

View file

@ -37,10 +37,12 @@ export class Worker extends ChannelOwner<channels.WorkerChannel> implements api.
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.WorkerInitializer) {
super(parent, type, guid, initializer);
this._channel.on('close', () => {
if (this._page)
if (this._page) {
this._page._workers.delete(this);
if (this._context)
}
if (this._context) {
this._context._serviceWorkers.delete(this);
}
this.emit(Events.Worker.Close, this);
});
this.once(Events.Worker.Close, () => this._closedScope.close(this._page?._closeErrorWithReason() || new TargetClosedError()));

View file

@ -174,8 +174,9 @@ class SocksConnection {
case SocksAddressType.IPv6:
const bytes = await this._readBytes(16);
const tokens: string[] = [];
for (let i = 0; i < 8; ++i)
for (let i = 0; i < 8; ++i) {
tokens.push(bytes.readUInt16BE(i * 2).toString(16));
}
host = tokens.join(':');
break;
}
@ -199,15 +200,17 @@ class SocksConnection {
private async _readBytes(length: number): Promise<Buffer> {
this._fence = this._offset + length;
if (!this._buffer || this._buffer.length < this._fence)
if (!this._buffer || this._buffer.length < this._fence) {
await new Promise<void>(f => this._fenceCallback = f);
}
this._offset += length;
return this._buffer.slice(this._offset - length, this._offset);
}
private _writeBytes(buffer: Buffer) {
if (this._socket.writable)
if (this._socket.writable) {
this._socket.write(buffer);
}
}
private _onClose() {
@ -280,12 +283,18 @@ function hexToNumber(hex: string): number {
// Note: parseInt has a few issues including ignoring trailing characters and allowing leading 0x.
return [...hex].reduce((value, digit) => {
const code = digit.charCodeAt(0);
if (code >= 48 && code <= 57) // 0..9
if (code >= 48 && code <= 57) {
// 0..9
return value + code;
if (code >= 97 && code <= 102) // a..f
}
if (code >= 97 && code <= 102) {
// a..f
return value + (code - 97) + 10;
if (code >= 65 && code <= 70) // A..F
}
if (code >= 65 && code <= 70) {
// A..F
return value + (code - 65) + 10;
}
throw new Error('Invalid IPv6 token ' + hex);
}, 0);
}
@ -300,8 +309,9 @@ function ipToSocksAddress(address: string): number[] {
if (net.isIPv6(address)) {
const result = [0x04]; // IPv6
const tokens = address.split(':', 8);
while (tokens.length < 8)
while (tokens.length < 8) {
tokens.unshift('');
}
for (const token of tokens) {
const value = hexToNumber(token);
result.push((value >> 8) & 0xFF, value & 0xFF); // Big-endian
@ -324,21 +334,24 @@ function starMatchToRegex(pattern: string) {
// This follows "Proxy bypass rules" syntax without implicit and negative rules.
// https://source.chromium.org/chromium/chromium/src/+/main:net/docs/proxy.md;l=331
export function parsePattern(pattern: string | undefined): PatternMatcher {
if (!pattern)
if (!pattern) {
return () => false;
}
const matchers: PatternMatcher[] = pattern.split(',').map(token => {
const match = token.match(/^(.*?)(?::(\d+))?$/);
if (!match)
if (!match) {
throw new Error(`Unsupported token "${token}" in pattern "${pattern}"`);
}
const tokenPort = match[2] ? +match[2] : undefined;
const portMatches = (port: number) => tokenPort === undefined || tokenPort === port;
let tokenHost = match[1];
if (tokenHost === '<loopback>') {
return (host, port) => {
if (!portMatches(port))
if (!portMatches(port)) {
return false;
}
return host === 'localhost'
|| host.endsWith('.localhost')
|| host === '127.0.0.1'
@ -346,20 +359,25 @@ export function parsePattern(pattern: string | undefined): PatternMatcher {
};
}
if (tokenHost === '*')
if (tokenHost === '*') {
return (host, port) => portMatches(port);
}
if (net.isIPv4(tokenHost) || net.isIPv6(tokenHost))
if (net.isIPv4(tokenHost) || net.isIPv6(tokenHost)) {
return (host, port) => host === tokenHost && portMatches(port);
}
if (tokenHost[0] === '.')
if (tokenHost[0] === '.') {
tokenHost = '*' + tokenHost;
}
const tokenRegex = starMatchToRegex(tokenHost);
return (host, port) => {
if (!portMatches(port))
if (!portMatches(port)) {
return false;
if (net.isIPv4(host) || net.isIPv6(host))
}
if (net.isIPv4(host) || net.isIPv6(host)) {
return false;
}
return !!host.match(tokenRegex);
};
});
@ -442,11 +460,13 @@ export class SocksProxy extends EventEmitter implements SocksConnectionClient {
}
async close() {
if (this._closed)
if (this._closed) {
return;
}
this._closed = true;
for (const socket of this._sockets)
for (const socket of this._sockets) {
socket.destroy();
}
this._sockets.clear();
await new Promise(f => this._server.close(f));
}
@ -519,8 +539,9 @@ export class SocksProxyHandler extends EventEmitter {
}
cleanup() {
for (const uid of this._sockets.keys())
for (const uid of this._sockets.keys()) {
this.socketClosed({ uid });
}
}
async socketRequested({ uid, host, port }: SocksSocketRequestedPayload): Promise<void> {
@ -532,11 +553,13 @@ export class SocksProxyHandler extends EventEmitter {
return;
}
if (host === 'local.playwright')
if (host === 'local.playwright') {
host = 'localhost';
}
try {
if (this._redirectPortForTest)
if (this._redirectPortForTest) {
port = this._redirectPortForTest;
}
const socket = await createSocket(host, port);
socket.on('data', data => {
const payload: SocksSocketDataPayload = { uid, data };

View file

@ -46,44 +46,57 @@ export class TimeoutSettings {
}
navigationTimeout(options: { timeout?: number }): number {
if (typeof options.timeout === 'number')
if (typeof options.timeout === 'number') {
return options.timeout;
if (this._defaultNavigationTimeout !== undefined)
}
if (this._defaultNavigationTimeout !== undefined) {
return this._defaultNavigationTimeout;
if (debugMode())
}
if (debugMode()) {
return 0;
if (this._defaultTimeout !== undefined)
}
if (this._defaultTimeout !== undefined) {
return this._defaultTimeout;
if (this._parent)
}
if (this._parent) {
return this._parent.navigationTimeout(options);
}
return DEFAULT_TIMEOUT;
}
timeout(options: { timeout?: number }): number {
if (typeof options.timeout === 'number')
if (typeof options.timeout === 'number') {
return options.timeout;
if (debugMode())
}
if (debugMode()) {
return 0;
if (this._defaultTimeout !== undefined)
}
if (this._defaultTimeout !== undefined) {
return this._defaultTimeout;
if (this._parent)
}
if (this._parent) {
return this._parent.timeout(options);
}
return DEFAULT_TIMEOUT;
}
static timeout(options: { timeout?: number }): number {
if (typeof options.timeout === 'number')
if (typeof options.timeout === 'number') {
return options.timeout;
if (debugMode())
}
if (debugMode()) {
return 0;
}
return DEFAULT_TIMEOUT;
}
static launchTimeout(options: { timeout?: number }): number {
if (typeof options.timeout === 'number')
if (typeof options.timeout === 'number') {
return options.timeout;
if (debugMode())
}
if (debugMode()) {
return 0;
}
return DEFAULT_LAUNCH_TIMEOUT;
}
}

View file

@ -67,12 +67,15 @@ export class FastStats implements Stats {
const recalc = (mx: number[], idx: number, initial: number, x: number, y: number) => {
mx[idx] = initial;
if (y > 0)
if (y > 0) {
mx[idx] += mx[(y - 1) * width + x];
if (x > 0)
}
if (x > 0) {
mx[idx] += mx[y * width + x - 1];
if (x > 0 && y > 0)
}
if (x > 0 && y > 0) {
mx[idx] -= mx[(y - 1) * width + x - 1];
}
};
for (let y = 0; y < height; ++y) {
@ -90,12 +93,15 @@ export class FastStats implements Stats {
_sum(partialSum: number[], x1: number, y1: number, x2: number, y2: number): number {
const width = this.c1.width;
let result = partialSum[y2 * width + x2];
if (y1 > 0)
if (y1 > 0) {
result -= partialSum[(y1 - 1) * width + x2];
if (x1 > 0)
}
if (x1 > 0) {
result -= partialSum[y2 * width + x1 - 1];
if (x1 > 0 && y1 > 0)
}
if (x1 > 0 && y1 > 0) {
result += partialSum[(y1 - 1) * width + x1 - 1];
}
return result;
}

View file

@ -21,60 +21,77 @@ export function parseSerializedValue(value: SerializedValue, handles: any[] | un
}
function innerParseSerializedValue(value: SerializedValue, handles: any[] | undefined, refs: Map<number, object>): any {
if (value.ref !== undefined)
if (value.ref !== undefined) {
return refs.get(value.ref);
if (value.n !== undefined)
return value.n;
if (value.s !== undefined)
return value.s;
if (value.b !== undefined)
return value.b;
if (value.v !== undefined) {
if (value.v === 'undefined')
return undefined;
if (value.v === 'null')
return null;
if (value.v === 'NaN')
return NaN;
if (value.v === 'Infinity')
return Infinity;
if (value.v === '-Infinity')
return -Infinity;
if (value.v === '-0')
return -0;
}
if (value.d !== undefined)
if (value.n !== undefined) {
return value.n;
}
if (value.s !== undefined) {
return value.s;
}
if (value.b !== undefined) {
return value.b;
}
if (value.v !== undefined) {
if (value.v === 'undefined') {
return undefined;
}
if (value.v === 'null') {
return null;
}
if (value.v === 'NaN') {
return NaN;
}
if (value.v === 'Infinity') {
return Infinity;
}
if (value.v === '-Infinity') {
return -Infinity;
}
if (value.v === '-0') {
return -0;
}
}
if (value.d !== undefined) {
return new Date(value.d);
if (value.u !== undefined)
}
if (value.u !== undefined) {
return new URL(value.u);
if (value.bi !== undefined)
}
if (value.bi !== undefined) {
return BigInt(value.bi);
}
if (value.e !== undefined) {
const error = new Error(value.e.m);
error.name = value.e.n;
error.stack = value.e.s;
return error;
}
if (value.r !== undefined)
if (value.r !== undefined) {
return new RegExp(value.r.p, value.r.f);
}
if (value.a !== undefined) {
const result: any[] = [];
refs.set(value.id!, result);
for (const v of value.a)
for (const v of value.a) {
result.push(innerParseSerializedValue(v, handles, refs));
}
return result;
}
if (value.o !== undefined) {
const result: any = {};
refs.set(value.id!, result);
for (const { k, v } of value.o)
for (const { k, v } of value.o) {
result[k] = innerParseSerializedValue(v, handles, refs);
}
return result;
}
if (value.h !== undefined) {
if (handles === undefined)
if (handles === undefined) {
throw new Error('Unexpected handle');
}
return handles[value.h];
}
throw new Error('Unexpected value');
@ -92,60 +109,79 @@ export function serializeValue(value: any, handleSerializer: (value: any) => Han
function innerSerializeValue(value: any, handleSerializer: (value: any) => HandleOrValue, visitorInfo: VisitorInfo): SerializedValue {
const handle = handleSerializer(value);
if ('fallThrough' in handle)
if ('fallThrough' in handle) {
value = handle.fallThrough;
else
} else {
return handle;
}
if (typeof value === 'symbol')
if (typeof value === 'symbol') {
return { v: 'undefined' };
if (Object.is(value, undefined))
}
if (Object.is(value, undefined)) {
return { v: 'undefined' };
if (Object.is(value, null))
}
if (Object.is(value, null)) {
return { v: 'null' };
if (Object.is(value, NaN))
}
if (Object.is(value, NaN)) {
return { v: 'NaN' };
if (Object.is(value, Infinity))
}
if (Object.is(value, Infinity)) {
return { v: 'Infinity' };
if (Object.is(value, -Infinity))
}
if (Object.is(value, -Infinity)) {
return { v: '-Infinity' };
if (Object.is(value, -0))
}
if (Object.is(value, -0)) {
return { v: '-0' };
if (typeof value === 'boolean')
}
if (typeof value === 'boolean') {
return { b: value };
if (typeof value === 'number')
}
if (typeof value === 'number') {
return { n: value };
if (typeof value === 'string')
}
if (typeof value === 'string') {
return { s: value };
if (typeof value === 'bigint')
}
if (typeof value === 'bigint') {
return { bi: value.toString() };
if (isError(value))
}
if (isError(value)) {
return { e: { n: value.name, m: value.message, s: value.stack || '' } };
if (isDate(value))
}
if (isDate(value)) {
return { d: value.toJSON() };
if (isURL(value))
}
if (isURL(value)) {
return { u: value.toJSON() };
if (isRegExp(value))
}
if (isRegExp(value)) {
return { r: { p: value.source, f: value.flags } };
}
const id = visitorInfo.visited.get(value);
if (id)
if (id) {
return { ref: id };
}
if (Array.isArray(value)) {
const a = [];
const id = ++visitorInfo.lastId;
visitorInfo.visited.set(value, id);
for (let i = 0; i < value.length; ++i)
for (let i = 0; i < value.length; ++i) {
a.push(innerSerializeValue(value[i], handleSerializer, visitorInfo));
}
return { a, id };
}
if (typeof value === 'object') {
const o: { k: string, v: SerializedValue }[] = [];
const id = ++visitorInfo.lastId;
visitorInfo.visited.set(value, id);
for (const name of Object.keys(value))
for (const name of Object.keys(value)) {
o.push({ k: name, v: innerSerializeValue(value[name], handleSerializer, visitorInfo) });
}
return { o, id };
}
throw new Error('Unexpected value');

View file

@ -49,22 +49,25 @@ export class PipeTransport {
pipeRead.on('data', buffer => this._dispatch(buffer));
pipeRead.on('close', () => {
this._closed = true;
if (this.onclose)
if (this.onclose) {
this.onclose();
}
});
this.onmessage = undefined;
this.onclose = undefined;
}
send(message: string) {
if (this._closed)
if (this._closed) {
throw new Error('Pipe has been closed');
}
const data = Buffer.from(message, 'utf-8');
const dataLength = Buffer.alloc(4);
if (this._endian === 'be')
if (this._endian === 'be') {
dataLength.writeUInt32BE(data.length, 0);
else
} else {
dataLength.writeUInt32LE(data.length, 0);
}
this._pipeWrite.write(dataLength);
this._pipeWrite.write(data);
}
@ -96,8 +99,9 @@ export class PipeTransport {
this._data = this._data.slice(this._bytesLeft);
this._bytesLeft = 0;
this._waitForNextTask(() => {
if (this.onmessage)
if (this.onmessage) {
this.onmessage(message.toString('utf-8'));
}
});
}
}

View file

@ -26,8 +26,9 @@ export const scheme: { [key: string]: Validator } = {};
export function findValidator(type: string, method: string, kind: 'Initializer' | 'Event' | 'Params' | 'Result'): Validator {
const validator = maybeFindValidator(type, method, kind);
if (!validator)
if (!validator) {
throw new ValidationError(`Unknown scheme for ${kind}: ${type}.${method}`);
}
return validator;
}
export function maybeFindValidator(type: string, method: string, kind: 'Initializer' | 'Event' | 'Params' | 'Result'): Validator | undefined {
@ -39,49 +40,60 @@ export function createMetadataValidator(): Validator {
}
export const tNumber: Validator = (arg: any, path: string, context: ValidatorContext) => {
if (arg instanceof Number)
if (arg instanceof Number) {
return arg.valueOf();
if (typeof arg === 'number')
}
if (typeof arg === 'number') {
return arg;
}
throw new ValidationError(`${path}: expected number, got ${typeof arg}`);
};
export const tBoolean: Validator = (arg: any, path: string, context: ValidatorContext) => {
if (arg instanceof Boolean)
if (arg instanceof Boolean) {
return arg.valueOf();
if (typeof arg === 'boolean')
}
if (typeof arg === 'boolean') {
return arg;
}
throw new ValidationError(`${path}: expected boolean, got ${typeof arg}`);
};
export const tString: Validator = (arg: any, path: string, context: ValidatorContext) => {
if (arg instanceof String)
if (arg instanceof String) {
return arg.valueOf();
if (typeof arg === 'string')
}
if (typeof arg === 'string') {
return arg;
}
throw new ValidationError(`${path}: expected string, got ${typeof arg}`);
};
export const tBinary: Validator = (arg: any, path: string, context: ValidatorContext) => {
if (context.binary === 'fromBase64') {
if (arg instanceof String)
if (arg instanceof String) {
return Buffer.from(arg.valueOf(), 'base64');
if (typeof arg === 'string')
}
if (typeof arg === 'string') {
return Buffer.from(arg, 'base64');
}
throw new ValidationError(`${path}: expected base64-encoded buffer, got ${typeof arg}`);
}
if (context.binary === 'toBase64') {
if (!(arg instanceof Buffer))
if (!(arg instanceof Buffer)) {
throw new ValidationError(`${path}: expected Buffer, got ${typeof arg}`);
}
return (arg as Buffer).toString('base64');
}
if (context.binary === 'buffer') {
if (!(arg instanceof Buffer))
if (!(arg instanceof Buffer)) {
throw new ValidationError(`${path}: expected Buffer, got ${typeof arg}`);
}
return arg;
}
throw new ValidationError(`Unsupported binary behavior "${context.binary}"`);
};
export const tUndefined: Validator = (arg: any, path: string, context: ValidatorContext) => {
if (Object.is(arg, undefined))
if (Object.is(arg, undefined)) {
return arg;
}
throw new ValidationError(`${path}: expected undefined, got ${typeof arg}`);
};
export const tAny: Validator = (arg: any, path: string, context: ValidatorContext) => {
@ -89,34 +101,40 @@ export const tAny: Validator = (arg: any, path: string, context: ValidatorContex
};
export const tOptional = (v: Validator): Validator => {
return (arg: any, path: string, context: ValidatorContext) => {
if (Object.is(arg, undefined))
if (Object.is(arg, undefined)) {
return arg;
}
return v(arg, path, context);
};
};
export const tArray = (v: Validator): Validator => {
return (arg: any, path: string, context: ValidatorContext) => {
if (!Array.isArray(arg))
if (!Array.isArray(arg)) {
throw new ValidationError(`${path}: expected array, got ${typeof arg}`);
}
return arg.map((x, index) => v(x, path + '[' + index + ']', context));
};
};
export const tObject = (s: { [key: string]: Validator }): Validator => {
return (arg: any, path: string, context: ValidatorContext) => {
if (Object.is(arg, null))
if (Object.is(arg, null)) {
throw new ValidationError(`${path}: expected object, got null`);
if (typeof arg !== 'object')
}
if (typeof arg !== 'object') {
throw new ValidationError(`${path}: expected object, got ${typeof arg}`);
}
const result: any = {};
for (const [key, v] of Object.entries(s)) {
const value = v(arg[key], path ? path + '.' + key : key, context);
if (!Object.is(value, undefined))
if (!Object.is(value, undefined)) {
result[key] = value;
}
}
if (isUnderTest()) {
for (const [key, value] of Object.entries(arg)) {
if (key.startsWith('__testHook'))
if (key.startsWith('__testHook')) {
result[key] = value;
}
}
}
return result;
@ -124,8 +142,9 @@ export const tObject = (s: { [key: string]: Validator }): Validator => {
};
export const tEnum = (e: string[]): Validator => {
return (arg: any, path: string, context: ValidatorContext) => {
if (!e.includes(arg))
if (!e.includes(arg)) {
throw new ValidationError(`${path}: expected one of (${e.join('|')})`);
}
return arg;
};
};
@ -137,8 +156,9 @@ export const tChannel = (names: '*' | string[]): Validator => {
export const tType = (name: string): Validator => {
return (arg: any, path: string, context: ValidatorContext) => {
const v = scheme[name];
if (!v)
if (!v) {
throw new ValidationError(path + ': unknown type "' + name + '"');
}
return v(arg, path, context);
};
};

View file

@ -61,10 +61,12 @@ export class PlaywrightConnection {
this._preLaunched = preLaunched;
this._options = options;
options.launchOptions = filterLaunchOptions(options.launchOptions);
if (clientType === 'reuse-browser' || clientType === 'pre-launched-browser-or-android')
if (clientType === 'reuse-browser' || clientType === 'pre-launched-browser-or-android') {
assert(preLaunched.playwright);
if (clientType === 'pre-launched-browser-or-android')
}
if (clientType === 'pre-launched-browser-or-android') {
assert(preLaunched.browser || preLaunched.androidDevice);
}
this._onClose = onClose;
this._id = id;
this._profileName = `${new Date().toISOString()}-${clientType}`;
@ -74,10 +76,12 @@ export class PlaywrightConnection {
await lock;
if (ws.readyState !== ws.CLOSING) {
const messageString = JSON.stringify(message);
if (debugLogger.isEnabled('server:channel'))
if (debugLogger.isEnabled('server:channel')) {
debugLogger.log('server:channel', `[${this._id}] ${monotonicTime() * 1000} SEND ► ${messageString}`);
if (debugLogger.isEnabled('server:metadata'))
}
if (debugLogger.isEnabled('server:metadata')) {
this.logServerMetadata(message, messageString, 'SEND');
}
ws.send(messageString);
}
};
@ -85,10 +89,12 @@ export class PlaywrightConnection {
await lock;
const messageString = Buffer.from(message).toString();
const jsonMessage = JSON.parse(messageString);
if (debugLogger.isEnabled('server:channel'))
if (debugLogger.isEnabled('server:channel')) {
debugLogger.log('server:channel', `[${this._id}] ${monotonicTime() * 1000} ◀ RECV ${messageString}`);
if (debugLogger.isEnabled('server:metadata'))
}
if (debugLogger.isEnabled('server:metadata')) {
this.logServerMetadata(jsonMessage, messageString, 'RECV');
}
this._dispatcherConnection.dispatch(jsonMessage);
});
@ -102,12 +108,15 @@ export class PlaywrightConnection {
this._root = new RootDispatcher(this._dispatcherConnection, async (scope, options) => {
await startProfiling();
if (clientType === 'reuse-browser')
if (clientType === 'reuse-browser') {
return await this._initReuseBrowsersMode(scope);
if (clientType === 'pre-launched-browser-or-android')
}
if (clientType === 'pre-launched-browser-or-android') {
return this._preLaunched.browser ? await this._initPreLaunchedBrowserMode(scope) : await this._initPreLaunchedAndroidMode(scope);
if (clientType === 'launch-browser')
}
if (clientType === 'launch-browser') {
return await this._initLaunchBrowserMode(scope, options);
}
throw new Error('Unsupported client type: ' + clientType);
});
}
@ -120,8 +129,9 @@ export class PlaywrightConnection {
const browser = await playwright[this._options.browserName as 'chromium'].launch(serverSideCallMetadata(), this._options.launchOptions);
this._cleanups.push(async () => {
for (const browser of playwright.allBrowsers())
for (const browser of playwright.allBrowsers()) {
await browser.close({ reason: 'Connection terminated' });
}
});
browser.on(Browser.Events.Disconnected, () => {
// Underlying browser did close for some reason - force disconnect the client.
@ -147,8 +157,9 @@ export class PlaywrightConnection {
const playwrightDispatcher = new PlaywrightDispatcher(scope, playwright, this._preLaunched.socksProxy, browser);
// In pre-launched mode, keep only the pre-launched browser.
for (const b of playwright.allBrowsers()) {
if (b !== browser)
if (b !== browser) {
await b.close({ reason: 'Connection terminated' });
}
}
this._cleanups.push(() => playwrightDispatcher.cleanup());
return playwrightDispatcher;
@ -183,18 +194,21 @@ export class PlaywrightConnection {
const requestedOptions = launchOptionsHash(this._options.launchOptions);
let browser = playwright.allBrowsers().find(b => {
if (b.options.name !== this._options.browserName)
if (b.options.name !== this._options.browserName) {
return false;
}
const existingOptions = launchOptionsHash(b.options.originalLaunchOptions);
return existingOptions === requestedOptions;
});
// Close remaining browsers of this type+channel. Keep different browser types for the speed.
for (const b of playwright.allBrowsers()) {
if (b === browser)
if (b === browser) {
continue;
if (b.options.name === this._options.browserName && b.options.channel === this._options.launchOptions.channel)
}
if (b.options.name === this._options.browserName && b.options.channel === this._options.launchOptions.channel) {
await b.close({ reason: 'Connection terminated' });
}
}
if (!browser) {
@ -213,13 +227,15 @@ export class PlaywrightConnection {
// but close all the empty browsers and contexts to clean up.
for (const browser of playwright.allBrowsers()) {
for (const context of browser.contexts()) {
if (!context.pages().length)
if (!context.pages().length) {
await context.close({ reason: 'Connection terminated' });
else
} else {
await context.stopPendingOperations('Connection closed');
}
}
if (!browser.contexts())
if (!browser.contexts()) {
await browser.close({ reason: 'Connection terminated' });
}
}
});
@ -228,8 +244,9 @@ export class PlaywrightConnection {
}
private async _createOwnedSocksProxy(playwright: Playwright): Promise<SocksProxy | undefined> {
if (!this._options.socksProxyPattern)
if (!this._options.socksProxyPattern) {
return;
}
const socksProxy = new SocksProxy();
socksProxy.setPattern(this._options.socksProxyPattern);
playwright.options.socksProxyPort = await socksProxy.listen(0);
@ -243,8 +260,9 @@ export class PlaywrightConnection {
debugLogger.log('server', `[${this._id}] disconnected. error: ${error}`);
this._root._dispose();
debugLogger.log('server', `[${this._id}] starting cleanup`);
for (const cleanup of this._cleanups)
for (const cleanup of this._cleanups) {
await cleanup().catch(() => {});
}
await stopProfiling(this._profileName);
this._onClose();
debugLogger.log('server', `[${this._id}] finished cleanup`);
@ -262,8 +280,9 @@ export class PlaywrightConnection {
}
async close(reason?: { code: number, reason: string }) {
if (this._disconnected)
if (this._disconnected) {
return;
}
debugLogger.log('server', `[${this._id}] force closing connection: ${reason?.reason || ''} (${reason?.code || 0})`);
try {
this._ws.close(reason?.code, reason?.reason);
@ -276,11 +295,13 @@ function launchOptionsHash(options: LaunchOptions) {
const copy = { ...options };
for (const k of Object.keys(copy)) {
const key = k as keyof LaunchOptions;
if (copy[key] === defaultLaunchOptions[key])
if (copy[key] === defaultLaunchOptions[key]) {
delete copy[key];
}
}
for (const key of optionsThatAllowBrowserReuse)
for (const key of optionsThatAllowBrowserReuse) {
delete copy[key];
}
return JSON.stringify(copy);
}

View file

@ -43,10 +43,12 @@ export class PlaywrightServer {
constructor(options: ServerOptions) {
this._options = options;
if (options.preLaunchedBrowser)
if (options.preLaunchedBrowser) {
this._preLaunchedPlaywright = options.preLaunchedBrowser.attribution.playwright;
if (options.preLaunchedAndroidDevice)
}
if (options.preLaunchedAndroidDevice) {
this._preLaunchedPlaywright = options.preLaunchedAndroidDevice._android.attribution.playwright;
}
const browserSemaphore = new Semaphore(this._options.maxConnections);
const controllerSemaphore = new Semaphore(1);
@ -55,13 +57,15 @@ export class PlaywrightServer {
this._wsServer = new WSServer({
onUpgrade: (request, socket) => {
const uaError = userAgentVersionMatchesErrorMessage(request.headers['user-agent'] || '');
if (uaError)
if (uaError) {
return { error: `HTTP/${request.httpVersion} 428 Precondition Required\r\n\r\n${uaError}` };
}
},
onHeaders: headers => {
if (process.env.PWTEST_SERVER_WS_HEADERS)
if (process.env.PWTEST_SERVER_WS_HEADERS) {
headers.push(process.env.PWTEST_SERVER_WS_HEADERS!);
}
},
onConnection: (request, url, ws, id) => {
@ -82,8 +86,9 @@ export class PlaywrightServer {
// Instantiate playwright for the extension modes.
const isExtension = this._options.mode === 'extension';
if (isExtension) {
if (!this._preLaunchedPlaywright)
if (!this._preLaunchedPlaywright) {
this._preLaunchedPlaywright = createPlaywright({ sdkLanguage: 'javascript', isServer: true });
}
}
let clientType: ClientType = 'launch-browser';
@ -114,8 +119,9 @@ export class PlaywrightServer {
onClose: async () => {
debugLogger.log('server', 'closing browsers');
if (this._preLaunchedPlaywright)
if (this._preLaunchedPlaywright) {
await Promise.all(this._preLaunchedPlaywright.allBrowsers().map(browser => browser.close({ reason: 'Playwright Server stopped' })));
}
debugLogger.log('server', 'closed browsers');
}
});

View file

@ -42,39 +42,47 @@ export class Accessibility {
} = options;
const { tree, needle } = await this._getAXTree(root || undefined);
if (!interestingOnly) {
if (root)
if (root) {
return needle && serializeTree(needle)[0];
}
return serializeTree(tree)[0];
}
const interestingNodes: Set<AXNode> = new Set();
collectInterestingNodes(interestingNodes, tree, false);
if (root && (!needle || !interestingNodes.has(needle)))
if (root && (!needle || !interestingNodes.has(needle))) {
return null;
}
return serializeTree(needle || tree, interestingNodes)[0];
}
}
function collectInterestingNodes(collection: Set<AXNode>, node: AXNode, insideControl: boolean) {
if (node.isInteresting(insideControl))
if (node.isInteresting(insideControl)) {
collection.add(node);
if (node.isLeafNode())
}
if (node.isLeafNode()) {
return;
}
insideControl = insideControl || node.isControl();
for (const child of node.children())
for (const child of node.children()) {
collectInterestingNodes(collection, child, insideControl);
}
}
function serializeTree(node: AXNode, whitelistedNodes?: Set<AXNode>): channels.AXNode[] {
const children: channels.AXNode[] = [];
for (const child of node.children())
for (const child of node.children()) {
children.push(...serializeTree(child, whitelistedNodes));
}
if (whitelistedNodes && !whitelistedNodes.has(node))
if (whitelistedNodes && !whitelistedNodes.has(node)) {
return children;
}
const serializedNode = node.serialize();
if (children.length)
if (children.length) {
serializedNode.children = children;
}
return [serializedNode];
}

View file

@ -80,14 +80,16 @@ export class Android extends SdkObject {
const newSerials = new Set<string>();
for (const d of devices) {
newSerials.add(d.serial);
if (this._devices.has(d.serial))
if (this._devices.has(d.serial)) {
continue;
}
const device = await AndroidDevice.create(this, d, options);
this._devices.set(d.serial, device);
}
for (const d of this._devices.keys()) {
if (!newSerials.has(d))
if (!newSerials.has(d)) {
this._devices.delete(d);
}
}
return [...this._devices.values()];
}
@ -168,10 +170,12 @@ export class AndroidDevice extends SdkObject {
}
private async _driver(): Promise<PipeTransport | undefined> {
if (this._isClosed)
if (this._isClosed) {
return;
if (!this._driverPromise)
}
if (!this._driverPromise) {
this._driverPromise = this._installDriver();
}
return this._driverPromise;
}
@ -190,8 +194,9 @@ export class AndroidDevice extends SdkObject {
const packageManagerCommand = getPackageManagerExecCommand();
for (const file of ['android-driver.apk', 'android-driver-target.apk']) {
const fullName = path.join(executable.directory!, file);
if (!fs.existsSync(fullName))
if (!fs.existsSync(fullName)) {
throw new Error(`Please install Android driver apk using '${packageManagerCommand} playwright install android'`);
}
await this.installApk(await fs.promises.readFile(fullName));
}
} else {
@ -206,12 +211,14 @@ export class AndroidDevice extends SdkObject {
const response = JSON.parse(message);
const { id, result, error } = response;
const callback = this._callbacks.get(id);
if (!callback)
if (!callback) {
return;
if (error)
}
if (error) {
callback.reject(new Error(error));
else
} else {
callback.fulfill(result);
}
this._callbacks.delete(id);
};
return transport;
@ -235,8 +242,9 @@ export class AndroidDevice extends SdkObject {
// Patch the timeout in!
params.timeout = this._timeoutSettings.timeout(params);
const driver = await this._driver();
if (!driver)
if (!driver) {
throw new Error('Device is closed');
}
const id = ++this._lastId;
const result = new Promise((fulfill, reject) => this._callbacks.set(id, { fulfill, reject }));
driver.send(JSON.stringify({ id, method, params }));
@ -244,13 +252,16 @@ export class AndroidDevice extends SdkObject {
}
async close() {
if (this._isClosed)
if (this._isClosed) {
return;
}
this._isClosed = true;
if (this._pollingWebViews)
if (this._pollingWebViews) {
clearTimeout(this._pollingWebViews);
for (const connection of this._browserConnections)
}
for (const connection of this._browserConnections) {
await connection.close();
}
if (this._driverPromise) {
const driver = await this._driver();
driver?.close();
@ -292,12 +303,15 @@ export class AndroidDevice extends SdkObject {
if (proxy) {
chromeArguments.push(`--proxy-server=${proxy.server}`);
const proxyBypassRules = [];
if (proxy.bypass)
if (proxy.bypass) {
proxyBypassRules.push(...proxy.bypass.split(',').map(t => t.trim()).map(t => t.startsWith('.') ? '*' + t : t));
if (!process.env.PLAYWRIGHT_DISABLE_FORCED_CHROMIUM_PROXIED_LOOPBACK && !proxyBypassRules.includes('<-loopback>'))
}
if (!process.env.PLAYWRIGHT_DISABLE_FORCED_CHROMIUM_PROXIED_LOOPBACK && !proxyBypassRules.includes('<-loopback>')) {
proxyBypassRules.push('<-loopback>');
if (proxyBypassRules.length > 0)
}
if (proxyBypassRules.length > 0) {
chromeArguments.push(`--proxy-bypass-list=${proxyBypassRules.join(';')}`);
}
}
chromeArguments.push(...args);
return chromeArguments;
@ -305,8 +319,9 @@ export class AndroidDevice extends SdkObject {
async connectToWebView(socketName: string): Promise<BrowserContext> {
const webView = this._webViews.get(socketName);
if (!webView)
if (!webView) {
throw new Error('WebView has been closed');
}
return await this._connectToBrowser(socketName);
}
@ -319,8 +334,9 @@ export class AndroidDevice extends SdkObject {
const artifactsDir = await fs.promises.mkdtemp(ARTIFACTS_FOLDER);
const cleanupArtifactsDir = async () => {
const errors = await removeFolders([artifactsDir]);
for (let i = 0; i < (errors || []).length; ++i)
for (let i = 0; i < (errors || []).length; ++i) {
debug('pw:android')(`exception while removing ${artifactsDir}: ${errors[i]}`);
}
};
gracefullyCloseSet.add(cleanupArtifactsDir);
socket.on('close', async () => {
@ -381,43 +397,50 @@ export class AndroidDevice extends SdkObject {
};
await send('SEND', Buffer.from(`${path},${mode}`));
const maxChunk = 65535;
for (let i = 0; i < content.length; i += maxChunk)
for (let i = 0; i < content.length; i += maxChunk) {
await send('DATA', content.slice(i, i + maxChunk));
}
await sendHeader('DONE', (Date.now() / 1000) | 0);
const result = await new Promise<Buffer>(f => socket.once('data', f));
const code = result.slice(0, 4).toString();
if (code !== 'OKAY')
if (code !== 'OKAY') {
throw new Error('Could not push: ' + code);
}
socket.close();
}
private async _refreshWebViews() {
// possible socketName, eg: webview_devtools_remote_32327, webview_devtools_remote_32327_zeus, webview_devtools_remote_zeus
const sockets = (await this._backend.runCommand(`shell:cat /proc/net/unix | grep webview_devtools_remote`)).toString().split('\n');
if (this._isClosed)
if (this._isClosed) {
return;
}
const socketNames = new Set<string>();
for (const line of sockets) {
const matchSocketName = line.match(/[^@]+@(.*?webview_devtools_remote_?.*)/);
if (!matchSocketName)
if (!matchSocketName) {
continue;
}
const socketName = matchSocketName[1];
socketNames.add(socketName);
if (this._webViews.has(socketName))
if (this._webViews.has(socketName)) {
continue;
}
// possible line: 0000000000000000: 00000002 00000000 00010000 0001 01 5841881 @webview_devtools_remote_zeus
// the result: match[1] = ''
const match = line.match(/[^@]+@.*?webview_devtools_remote_?(\d*)/);
let pid = -1;
if (match && match[1])
if (match && match[1]) {
pid = +match[1];
}
const pkg = await this._extractPkg(pid);
if (this._isClosed)
if (this._isClosed) {
return;
}
const webView = { pid, pkg, socketName };
this._webViews.set(socketName, webView);
@ -433,14 +456,16 @@ export class AndroidDevice extends SdkObject {
private async _extractPkg(pid: number) {
let pkg = '';
if (pid === -1)
if (pid === -1) {
return pkg;
}
const procs = (await this._backend.runCommand(`shell:ps -A | grep ${pid}`)).toString().split('\n');
for (const proc of procs) {
const match = proc.match(/[^\s]+\s+(\d+).*$/);
if (!match)
if (!match) {
continue;
}
pkg = proc.substring(proc.lastIndexOf(' ') + 1);
}
return pkg;
@ -462,15 +487,17 @@ class AndroidBrowser extends EventEmitter {
this._socket = socket;
this._socket.on('close', () => {
this._waitForNextTask(() => {
if (this.onclose)
if (this.onclose) {
this.onclose();
}
});
});
this._receiver = new wsReceiver() as stream.Writable;
this._receiver.on('message', message => {
this._waitForNextTask(() => {
if (this.onmessage)
if (this.onmessage) {
this.onmessage(JSON.parse(message));
}
});
});
}

View file

@ -54,14 +54,16 @@ class AdbDevice implements DeviceBackend {
}
runCommand(command: string): Promise<Buffer> {
if (this._closed)
if (this._closed) {
throw new Error('Device is closed');
}
return runCommand(command, this.host, this.port, this.serial);
}
async open(command: string): Promise<SocketBackend> {
if (this._closed)
if (this._closed) {
throw new Error('Device is closed');
}
const result = await open(command, this.host, this.port, this.serial);
result.becomeSocket();
return result;
@ -134,13 +136,15 @@ class BufferedSocketWrapper extends EventEmitter implements SocketBackend {
return;
}
this._buffer = Buffer.concat([this._buffer, data]);
if (this._notifyReader)
if (this._notifyReader) {
this._notifyReader();
}
});
this._socket.on('close', () => {
this._isClosed = true;
if (this._notifyReader)
if (this._notifyReader) {
this._notifyReader();
}
this.close();
this.emit('close');
});
@ -154,8 +158,9 @@ class BufferedSocketWrapper extends EventEmitter implements SocketBackend {
}
close() {
if (this._isClosed)
if (this._isClosed) {
return;
}
debug('pw:adb')('Close ' + this._command);
this._socket.destroy();
}
@ -163,8 +168,9 @@ class BufferedSocketWrapper extends EventEmitter implements SocketBackend {
async read(length: number): Promise<Buffer> {
await this._connectPromise;
assert(!this._isSocket, 'Can not read by length in socket mode');
while (this._buffer.length < length)
while (this._buffer.length < length) {
await new Promise<void>(f => this._notifyReader = f);
}
const result = this._buffer.slice(0, length);
this._buffer = this._buffer.slice(length);
debug('pw:adb:recv')(result.toString().substring(0, 100) + '...');
@ -172,8 +178,9 @@ class BufferedSocketWrapper extends EventEmitter implements SocketBackend {
}
async readAll(): Promise<Buffer> {
while (!this._isClosed)
while (!this._isClosed) {
await new Promise<void>(f => this._notifyReader = f);
}
return this._buffer;
}

View file

@ -24,7 +24,8 @@ export function parseAriaSnapshot(text: string): AriaTemplateNode {
export function parseYamlForAriaSnapshot(text: string): ParsedYaml {
const parsed = yaml.parse(text);
if (!Array.isArray(parsed))
if (!Array.isArray(parsed)) {
throw new Error('Expected object key starting with "- ":\n\n' + text + '\n');
}
return parsed;
}

View file

@ -49,21 +49,26 @@ export class Artifact extends SdkObject {
}
async localPathAfterFinished(): Promise<string> {
if (this._unaccessibleErrorMessage)
if (this._unaccessibleErrorMessage) {
throw new Error(this._unaccessibleErrorMessage);
}
await this._finishedPromise;
if (this._failureError)
if (this._failureError) {
throw this._failureError;
}
return this._localPath;
}
saveAs(saveCallback: SaveCallback) {
if (this._unaccessibleErrorMessage)
if (this._unaccessibleErrorMessage) {
throw new Error(this._unaccessibleErrorMessage);
if (this._deleted)
}
if (this._deleted) {
throw new Error(`File already deleted. Save before deleting.`);
if (this._failureError)
}
if (this._failureError) {
throw this._failureError;
}
if (this._finished) {
saveCallback(this._localPath).catch(() => {});
@ -73,8 +78,9 @@ export class Artifact extends SdkObject {
}
async failureError(): Promise<string | null> {
if (this._unaccessibleErrorMessage)
if (this._unaccessibleErrorMessage) {
return this._unaccessibleErrorMessage;
}
await this._finishedPromise;
return this._failureError?.message || null;
}
@ -85,39 +91,47 @@ export class Artifact extends SdkObject {
}
async delete(): Promise<void> {
if (this._unaccessibleErrorMessage)
if (this._unaccessibleErrorMessage) {
return;
}
const fileName = await this.localPathAfterFinished();
if (this._deleted)
if (this._deleted) {
return;
}
this._deleted = true;
if (fileName)
if (fileName) {
await fs.promises.unlink(fileName).catch(e => {});
}
}
async deleteOnContextClose(): Promise<void> {
// Compared to "delete", this method does not wait for the artifact to finish.
// We use it when closing the context to avoid stalling.
if (this._deleted)
if (this._deleted) {
return;
}
this._deleted = true;
if (!this._unaccessibleErrorMessage)
if (!this._unaccessibleErrorMessage) {
await fs.promises.unlink(this._localPath).catch(e => {});
}
await this.reportFinished(new TargetClosedError());
}
async reportFinished(error?: Error) {
if (this._finished)
if (this._finished) {
return;
}
this._finished = true;
this._failureError = error;
if (error) {
for (const callback of this._saveCallbacks)
for (const callback of this._saveCallbacks) {
await callback('', error);
}
} else {
for (const callback of this._saveCallbacks)
for (const callback of this._saveCallbacks) {
await callback(this._localPath);
}
}
this._saveCallbacks = [];

View file

@ -41,8 +41,9 @@ export class BidiBrowser extends Browser {
static async connect(parent: SdkObject, transport: ConnectionTransport, options: BrowserOptions): Promise<BidiBrowser> {
const browser = new BidiBrowser(parent, transport, options);
if ((options as any).__testHookOnConnectToBrowser)
if ((options as any).__testHookOnConnectToBrowser) {
await (options as any).__testHookOnConnectToBrowser();
}
let proxy: bidi.Session.ManualProxyConfiguration | undefined;
if (options.proxy) {
@ -68,8 +69,9 @@ export class BidiBrowser extends Browser {
default:
throw new Error('Invalid proxy server protocol: ' + options.proxy.server);
}
if (options.proxy.bypass)
if (options.proxy.bypass) {
proxy.noProxy = options.proxy.bypass.split(',');
}
// TODO: support authentication.
}
@ -148,8 +150,9 @@ export class BidiBrowser extends Browser {
const parentFrameId = event.parent;
for (const page of this._bidiPages.values()) {
const parentFrame = page._page._frameManager.frame(parentFrameId);
if (!parentFrame)
if (!parentFrame) {
continue;
}
page._session.addFrameBrowsingContext(event.context);
page._page._frameManager.frameAttached(event.context, parentFrameId);
return;
@ -157,10 +160,12 @@ export class BidiBrowser extends Browser {
return;
}
let context = this._contexts.get(event.userContext);
if (!context)
if (!context) {
context = this._defaultContext as BidiBrowserContext;
if (!context)
}
if (!context) {
return;
}
const session = this._connection.createMainFrameBrowsingContextSession(event.context);
const opener = event.originalOpener && this._bidiPages.get(event.originalOpener);
const page = new BidiPage(context, session, opener || null);
@ -173,24 +178,27 @@ export class BidiBrowser extends Browser {
const parentFrameId = event.parent;
for (const page of this._bidiPages.values()) {
const parentFrame = page._page._frameManager.frame(parentFrameId);
if (!parentFrame)
if (!parentFrame) {
continue;
}
page._page._frameManager.frameDetached(event.context);
return;
}
return;
}
const bidiPage = this._bidiPages.get(event.context);
if (!bidiPage)
if (!bidiPage) {
return;
}
bidiPage.didClose();
this._bidiPages.delete(event.context);
}
private _onScriptRealmDestroyed(event: bidi.Script.RealmDestroyedParameters) {
for (const page of this._bidiPages.values()) {
if (page._onRealmDestroyed(event))
if (page._onRealmDestroyed(event)) {
return;
}
}
}
}
@ -282,8 +290,9 @@ export class BidiBrowserContext extends BrowserContext {
async doSetHTTPCredentials(httpCredentials?: types.Credentials): Promise<void> {
this._options.httpCredentials = httpCredentials;
for (const page of this.pages())
for (const page of this.pages()) {
await (page._delegate as BidiPage).updateHttpCredentials();
}
}
async doAddInitScript(initScript: InitScript) {

View file

@ -43,14 +43,17 @@ export class BidiChromium extends BrowserType {
}
override doRewriteStartupLog(error: ProtocolError): ProtocolError {
if (!error.logs)
if (!error.logs) {
return error;
if (error.logs.includes('Missing X server'))
}
if (error.logs.includes('Missing X server')) {
error.logs = '\n' + wrapInASCIIBox(kNoXServerRunningError, 1);
}
// These error messages are taken from Chromium source code as of July, 2020:
// https://github.com/chromium/chromium/blob/70565f67e79f79e17663ad1337dc6e63ee207ce9/content/browser/zygote_host/zygote_host_impl_linux.cc
if (!error.logs.includes('crbug.com/357670') && !error.logs.includes('No usable sandbox!') && !error.logs.includes('crbug.com/638180'))
if (!error.logs.includes('crbug.com/357670') && !error.logs.includes('No usable sandbox!') && !error.logs.includes('crbug.com/638180')) {
return error;
}
error.logs = [
`Chromium sandboxing failed!`,
`================================`,
@ -69,8 +72,9 @@ export class BidiChromium extends BrowserType {
override attemptToGracefullyCloseBrowser(transport: ConnectionTransport): void {
const bidiTransport = (transport as any)[kBidiOverCdpWrapper];
if (bidiTransport)
if (bidiTransport) {
transport = bidiTransport;
}
transport.send({ method: 'browser.close', params: {}, id: kBrowserCloseMessageId });
}
@ -78,10 +82,11 @@ export class BidiChromium extends BrowserType {
const chromeArguments = this._innerDefaultArgs(options);
chromeArguments.push(`--user-data-dir=${userDataDir}`);
chromeArguments.push('--remote-debugging-port=0');
if (isPersistent)
if (isPersistent) {
chromeArguments.push('about:blank');
else
} else {
chromeArguments.push('--no-startup-window');
}
return chromeArguments;
}
@ -93,24 +98,29 @@ export class BidiChromium extends BrowserType {
private _innerDefaultArgs(options: types.LaunchOptions): string[] {
const { args = [] } = options;
const userDataDirArg = args.find(arg => arg.startsWith('--user-data-dir'));
if (userDataDirArg)
if (userDataDirArg) {
throw this._createUserDataDirArgMisuseError('--user-data-dir');
if (args.find(arg => arg.startsWith('--remote-debugging-pipe')))
}
if (args.find(arg => arg.startsWith('--remote-debugging-pipe'))) {
throw new Error('Playwright manages remote debugging connection itself.');
if (args.find(arg => !arg.startsWith('-')))
}
if (args.find(arg => !arg.startsWith('-'))) {
throw new Error('Arguments can not specify page to be opened');
}
const chromeArguments = [...chromiumSwitches];
if (os.platform() === 'darwin') {
// See https://github.com/microsoft/playwright/issues/7362
chromeArguments.push('--enable-use-zoom-for-dsf=false');
// See https://bugs.chromium.org/p/chromium/issues/detail?id=1407025.
if (options.headless)
if (options.headless) {
chromeArguments.push('--use-angle');
}
}
if (options.devtools)
if (options.devtools) {
chromeArguments.push('--auto-open-devtools-for-tabs');
}
if (options.headless) {
chromeArguments.push('--headless');
@ -120,8 +130,9 @@ export class BidiChromium extends BrowserType {
'--blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4',
);
}
if (options.chromiumSandbox !== true)
if (options.chromiumSandbox !== true) {
chromeArguments.push('--no-sandbox');
}
const proxy = options.proxyOverride || options.proxy;
if (proxy) {
const proxyURL = new URL(proxy.server);
@ -134,14 +145,18 @@ export class BidiChromium extends BrowserType {
chromeArguments.push(`--proxy-server=${proxy.server}`);
const proxyBypassRules = [];
// https://source.chromium.org/chromium/chromium/src/+/master:net/docs/proxy.md;l=548;drc=71698e610121078e0d1a811054dcf9fd89b49578
if (this.attribution.playwright.options.socksProxyPort)
if (this.attribution.playwright.options.socksProxyPort) {
proxyBypassRules.push('<-loopback>');
if (proxy.bypass)
}
if (proxy.bypass) {
proxyBypassRules.push(...proxy.bypass.split(',').map(t => t.trim()).map(t => t.startsWith('.') ? '*' + t : t));
if (!process.env.PLAYWRIGHT_DISABLE_FORCED_CHROMIUM_PROXIED_LOOPBACK && !proxyBypassRules.includes('<-loopback>'))
}
if (!process.env.PLAYWRIGHT_DISABLE_FORCED_CHROMIUM_PROXIED_LOOPBACK && !proxyBypassRules.includes('<-loopback>')) {
proxyBypassRules.push('<-loopback>');
if (proxyBypassRules.length > 0)
}
if (proxyBypassRules.length > 0) {
chromeArguments.push(`--proxy-bypass-list=${proxyBypassRules.join(';')}`);
}
}
chromeArguments.push(...args);
return chromeArguments;
@ -151,8 +166,9 @@ export class BidiChromium extends BrowserType {
class ChromiumReadyState extends BrowserReadyState {
override onBrowserOutput(message: string): void {
const match = message.match(/DevTools listening on (.*)/);
if (match)
if (match) {
this._wsEndpoint.resolve(match[1]);
}
}
}

View file

@ -69,10 +69,11 @@ export class BidiConnection {
if (object.type === 'event') {
// Route page events to the right session.
let context;
if ('context' in object.params)
if ('context' in object.params) {
context = object.params.context;
else if (object.method === 'log.entryAdded' || object.method === 'script.message')
context = object.params.source?.context;
} else if (object.method === 'log.entryAdded' || object.method === 'script.message') {
context = object.params.source.context;
}
if (context) {
const session = this._browsingContextToSession.get(context);
if (session) {
@ -106,8 +107,9 @@ export class BidiConnection {
}
close() {
if (!this._closed)
if (!this._closed) {
this._transport.close();
}
}
createMainFrameBrowsingContextSession(bowsingContextId: bidi.BrowsingContext.BrowsingContext): BidiSession {
@ -165,8 +167,9 @@ export class BidiSession extends EventEmitter {
method: T,
params?: bidiCommands.Commands[T]['params']
): Promise<bidiCommands.Commands[T]['returnType']> {
if (this._crashed || this._disposed || this.connection._browserDisconnectedLogs)
if (this._crashed || this._disposed || this.connection._browserDisconnectedLogs) {
throw new ProtocolError(this._crashed ? 'crashed' : 'closed', undefined, this.connection._browserDisconnectedLogs);
}
const id = this.connection.nextMessageId();
const messageObj = { id, method, params };
this._rawSend(messageObj);
@ -190,8 +193,9 @@ export class BidiSession extends EventEmitter {
dispose() {
this._disposed = true;
this.connection._browsingContextToSession.delete(this.sessionId);
for (const context of this._browsingContexts)
for (const context of this._browsingContexts) {
this.connection._browsingContextToSession.delete(context);
}
this._browsingContexts.clear();
for (const callback of this._callbacks.values()) {
callback.error.type = this._crashed ? 'crashed' : 'closed';
@ -207,8 +211,9 @@ export class BidiSession extends EventEmitter {
dispatchMessage(message: any) {
const object = message as bidi.Message;
if (object.id === kBrowserCloseMessageId)
if (object.id === kBrowserCloseMessageId) {
return;
}
if (object.id && this._callbacks.has(object.id)) {
const callback = this._callbacks.get(object.id)!;
this._callbacks.delete(object.id);

View file

@ -51,10 +51,12 @@ export class BidiExecutionContext implements js.ExecutionContextDelegate {
awaitPromise: true,
userActivation: true,
});
if (response.type === 'success')
if (response.type === 'success') {
return BidiDeserializer.deserialize(response.result);
if (response.type === 'exception')
}
if (response.type === 'exception') {
throw new js.JavaScriptErrorInEvaluate(response.exceptionDetails.text + '\nFull val: ' + JSON.stringify(response.exceptionDetails));
}
throw new js.JavaScriptErrorInEvaluate('Unexpected response type: ' + JSON.stringify(response));
}
@ -68,12 +70,14 @@ export class BidiExecutionContext implements js.ExecutionContextDelegate {
userActivation: true,
});
if (response.type === 'success') {
if ('handle' in response.result)
if ('handle' in response.result) {
return response.result.handle!;
}
throw new js.JavaScriptErrorInEvaluate('Cannot get handle: ' + JSON.stringify(response.result));
}
if (response.type === 'exception')
if (response.type === 'exception') {
throw new js.JavaScriptErrorInEvaluate(response.exceptionDetails.text + '\nFull val: ' + JSON.stringify(response.exceptionDetails));
}
throw new js.JavaScriptErrorInEvaluate('Unexpected response type: ' + JSON.stringify(response));
}
@ -91,11 +95,13 @@ export class BidiExecutionContext implements js.ExecutionContextDelegate {
awaitPromise: true,
userActivation: true,
});
if (response.type === 'exception')
if (response.type === 'exception') {
throw new js.JavaScriptErrorInEvaluate(response.exceptionDetails.text + '\nFull val: ' + JSON.stringify(response.exceptionDetails));
}
if (response.type === 'success') {
if (returnByValue)
if (returnByValue) {
return parseEvaluationResultValue(BidiDeserializer.deserialize(response.result));
}
const objectId = 'handle' in response.result ? response.result.handle : undefined ;
return utilityScript._context.createHandle({ objectId, ...response.result });
}
@ -128,32 +134,41 @@ export class BidiExecutionContext implements js.ExecutionContextDelegate {
awaitPromise: true,
userActivation: true,
});
if (response.type === 'exception')
if (response.type === 'exception') {
throw new js.JavaScriptErrorInEvaluate(response.exceptionDetails.text + '\nFull val: ' + JSON.stringify(response.exceptionDetails));
if (response.type === 'success')
}
if (response.type === 'success') {
return response.result;
}
throw new js.JavaScriptErrorInEvaluate('Unexpected response type: ' + JSON.stringify(response));
}
}
function renderPreview(remoteObject: bidi.Script.RemoteValue): string | undefined {
if (remoteObject.type === 'undefined')
if (remoteObject.type === 'undefined') {
return 'undefined';
if (remoteObject.type === 'null')
}
if (remoteObject.type === 'null') {
return 'null';
if ('value' in remoteObject)
}
if ('value' in remoteObject) {
return String(remoteObject.value);
}
return `<${remoteObject.type}>`;
}
function remoteObjectValue(remoteObject: bidi.Script.RemoteValue): any {
if (remoteObject.type === 'undefined')
if (remoteObject.type === 'undefined') {
return undefined;
if (remoteObject.type === 'null')
}
if (remoteObject.type === 'null') {
return null;
if (remoteObject.type === 'number' && typeof remoteObject.value === 'string')
}
if (remoteObject.type === 'number' && typeof remoteObject.value === 'string') {
return js.parseUnserializableValue(remoteObject.value);
if ('value' in remoteObject)
}
if ('value' in remoteObject) {
return remoteObject.value;
}
return undefined;
}

View file

@ -39,19 +39,23 @@ export class BidiFirefox extends BrowserType {
}
override doRewriteStartupLog(error: ProtocolError): ProtocolError {
if (!error.logs)
if (!error.logs) {
return error;
}
// https://github.com/microsoft/playwright/issues/6500
if (error.logs.includes(`as root in a regular user's session is not supported.`))
if (error.logs.includes(`as root in a regular user's session is not supported.`)) {
error.logs = '\n' + wrapInASCIIBox(`Firefox is unable to launch if the $HOME folder isn't owned by the current user.\nWorkaround: Set the HOME=/root environment variable${process.env.GITHUB_ACTION ? ' in your GitHub Actions workflow file' : ''} when running Playwright.`, 1);
if (error.logs.includes('no DISPLAY environment variable specified'))
}
if (error.logs.includes('no DISPLAY environment variable specified')) {
error.logs = '\n' + wrapInASCIIBox(kNoXServerRunningError, 1);
}
return error;
}
override amendEnvironment(env: Env, userDataDir: string, executable: string, browserArguments: string[]): Env {
if (!path.isAbsolute(os.homedir()))
if (!path.isAbsolute(os.homedir())) {
throw new Error(`Cannot launch Firefox with relative home directory. Did you set ${os.platform() === 'win32' ? 'USERPROFILE' : 'HOME'} to a relative path?`);
}
env = {
...env,
@ -83,13 +87,15 @@ export class BidiFirefox extends BrowserType {
override defaultArgs(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string): string[] {
const { args = [], headless } = options;
const userDataDirArg = args.find(arg => arg.startsWith('-profile') || arg.startsWith('--profile'));
if (userDataDirArg)
if (userDataDirArg) {
throw this._createUserDataDirArgMisuseError('--profile');
}
const firefoxArguments = ['--remote-debugging-port=0'];
if (headless)
if (headless) {
firefoxArguments.push('--headless');
else
} else {
firefoxArguments.push('--foreground');
}
firefoxArguments.push(`--profile`, userDataDir);
firefoxArguments.push(...args);
return firefoxArguments;
@ -105,7 +111,8 @@ class FirefoxReadyState extends BrowserReadyState {
override onBrowserOutput(message: string): void {
// Bidi WebSocket in Firefox.
const match = message.match(/WebDriver BiDi listening on (ws:\/\/.*)$/);
if (match)
if (match) {
this._wsEndpoint.resolve(match[1] + '/session');
}
}
}

View file

@ -55,14 +55,17 @@ export class BidiNetworkManager {
}
private _onBeforeRequestSent(param: bidi.Network.BeforeRequestSentParameters) {
if (param.request.url.startsWith('data:'))
if (param.request.url.startsWith('data:')) {
return;
}
const redirectedFrom = param.redirectCount ? (this._requests.get(param.request.request) || null) : null;
const frame = redirectedFrom ? redirectedFrom.request.frame() : (param.context ? this._page._frameManager.frame(param.context) : null);
if (!frame)
if (!frame) {
return;
if (redirectedFrom)
}
if (redirectedFrom) {
this._requests.delete(redirectedFrom._id);
}
let route;
if (param.intercepts) {
// We do not support intercepting redirects.
@ -82,16 +85,18 @@ export class BidiNetworkManager {
private _onResponseStarted(params: bidi.Network.ResponseStartedParameters) {
const request = this._requests.get(params.request.request);
if (!request)
if (!request) {
return;
}
const getResponseBody = async () => {
throw new Error(`Response body is not available for requests in Bidi`);
};
const timings = params.request.timings;
const startTime = timings.requestTime;
function relativeToStart(time: number): number {
if (!time)
if (!time) {
return -1;
}
return (time - startTime) / 1000;
}
const timing: network.ResourceTiming = {
@ -111,14 +116,16 @@ export class BidiNetworkManager {
response.setRawResponseHeaders(null);
response.setResponseHeadersSize(params.response.headersSize);
this._page._frameManager.requestReceivedResponse(response);
if (params.navigation)
if (params.navigation) {
this._onNavigationResponseStarted(params);
}
}
private _onResponseCompleted(params: bidi.Network.ResponseCompletedParameters) {
const request = this._requests.get(params.request.request);
if (!request)
if (!request) {
return;
}
const response = request.request._existingResponse()!;
// TODO: body size is the encoded size
response.setTransferSize(params.response.bodySize);
@ -140,8 +147,9 @@ export class BidiNetworkManager {
private _onFetchError(params: bidi.Network.FetchErrorParameters) {
const request = this._requests.get(params.request.request);
if (!request)
if (!request) {
return;
}
this._requests.delete(request._id);
const response = request.request._existingResponse();
if (response) {
@ -187,11 +195,13 @@ export class BidiNetworkManager {
async _updateProtocolRequestInterception(initial?: boolean) {
const enabled = this._userRequestInterceptionEnabled || !!this._credentials;
if (enabled === this._protocolRequestInterceptionEnabled)
if (enabled === this._protocolRequestInterceptionEnabled) {
return;
}
this._protocolRequestInterceptionEnabled = enabled;
if (initial && !enabled)
if (initial && !enabled) {
return;
}
const cachePromise = this._session.send('network.setCacheBehavior', { cacheBehavior: enabled ? 'bypass' : 'default' });
let interceptPromise = Promise.resolve<any>(undefined);
if (enabled) {
@ -221,8 +231,9 @@ class BidiRequest {
constructor(frame: frames.Frame, redirectedFrom: BidiRequest | null, payload: bidi.Network.BeforeRequestSentParameters, route: BidiRouteImpl | undefined) {
this._id = payload.request.request;
if (redirectedFrom)
if (redirectedFrom) {
redirectedFrom._redirectedTo = this;
}
// TODO: missing in the spec?
const postDataBuffer = null;
this.request = new network.Request(frame._page._browserContext, frame, null, redirectedFrom ? redirectedFrom.request : null, payload.navigation ?? undefined,
@ -236,8 +247,9 @@ class BidiRequest {
_finalRequest(): BidiRequest {
let request: BidiRequest = this;
while (request._redirectedTo)
while (request._redirectedTo) {
request = request._redirectedTo;
}
return request;
}
}
@ -262,8 +274,9 @@ class BidiRouteImpl implements network.RouteDelegate {
let headers = overrides.headers || this._request.headers();
if (overrides.postData && headers) {
headers = headers.map(header => {
if (header.name.toLowerCase() === 'content-length')
if (header.name.toLowerCase() === 'content-length') {
return { name: header.name, value: overrides.postData!.byteLength.toString() };
}
return header;
});
}
@ -297,8 +310,9 @@ class BidiRouteImpl implements network.RouteDelegate {
function fromBidiHeaders(bidiHeaders: bidi.Network.Header[]): types.HeadersArray {
const result: types.HeadersArray = [];
for (const { name, value } of bidiHeaders)
for (const { name, value } of bidiHeaders) {
result.push({ name, value: bidiBytesValueToString(value) });
}
return result;
}
@ -328,20 +342,25 @@ function toBidiHeaders(headers: types.HeadersArray): bidi.Network.Header[] {
}
export function bidiBytesValueToString(value: bidi.Network.BytesValue): string {
if (value.type === 'string')
if (value.type === 'string') {
return value.value;
if (value.type === 'base64')
}
if (value.type === 'base64') {
return Buffer.from(value.type, 'base64').toString('binary');
}
return 'unknown value type: ' + (value as any).type;
}
function toBidiSameSite(sameSite?: 'Strict' | 'Lax' | 'None'): bidi.Network.SameSite | undefined {
if (!sameSite)
if (!sameSite) {
return undefined;
if (sameSite === 'Strict')
}
if (sameSite === 'Strict') {
return bidi.Network.SameSite.Strict;
if (sameSite === 'Lax')
}
if (sameSite === 'Lax') {
return bidi.Network.SameSite.Lax;
}
return bidi.Network.SameSite.None;
}

View file

@ -115,20 +115,24 @@ export class BidiPage implements PageDelegate {
for (const [contextId, context] of this._realmToContext) {
if (context.frame === frame) {
this._realmToContext.delete(contextId);
if (notifyFrame)
if (notifyFrame) {
frame._contextDestroyed(context);
}
}
}
}
private _onRealmCreated(realmInfo: bidi.Script.RealmInfo) {
if (this._realmToContext.has(realmInfo.realm))
if (this._realmToContext.has(realmInfo.realm)) {
return;
if (realmInfo.type !== 'window')
}
if (realmInfo.type !== 'window') {
return;
}
const frame = this._page._frameManager.frame(realmInfo.context);
if (!frame)
if (!frame) {
return;
}
const delegate = new BidiExecutionContext(this._session, realmInfo);
let worldName: types.World;
if (!realmInfo.sandbox) {
@ -164,8 +168,9 @@ export class BidiPage implements PageDelegate {
_onRealmDestroyed(params: bidi.Script.RealmDestroyedParameters): boolean {
const context = this._realmToContext.get(params.realm);
if (!context)
if (!context) {
return false;
}
this._realmToContext.delete(params.realm);
context.frame._contextDestroyed(context);
return true;
@ -185,8 +190,9 @@ export class BidiPage implements PageDelegate {
// Navigation to file urls doesn't emit network events, so we fire 'commit' event right when navigation is started.
// Doing it in domcontentload would be too late as we'd clear frame tree.
const frame = this._page._frameManager.frame(frameId)!;
if (frame)
if (frame) {
this._page._frameManager.frameCommittedNewDocumentNavigation(frameId, params.url, '', params.navigation!, /* initial */ false);
}
}
}
@ -233,12 +239,14 @@ export class BidiPage implements PageDelegate {
}
private _onLogEntryAdded(params: bidi.Log.Entry) {
if (params.type !== 'console')
if (params.type !== 'console') {
return;
}
const entry: bidi.Log.ConsoleLogEntry = params as bidi.Log.ConsoleLogEntry;
const context = this._realmToContext.get(params.source.realm);
if (!context)
if (!context) {
return;
}
const callFrame = params.stackTrace?.callFrames[0];
const location = callFrame ?? { url: '', lineNumber: 1, columnNumber: 1 };
this._page._addConsoleMessage(entry.method, entry.args.map(arg => context.createHandle({ objectId: (arg as any).handle, ...arg })), location, params.text || undefined);
@ -274,8 +282,9 @@ export class BidiPage implements PageDelegate {
private async _updateViewport(): Promise<void> {
const options = this._browserContext._options;
const deviceSize = this._page.emulatedSize();
if (deviceSize === null)
if (deviceSize === null) {
return;
}
const viewportSize = deviceSize.viewport;
await this._session.send('browsingContext.setViewport', {
context: this._session.sessionId,
@ -353,16 +362,20 @@ export class BidiPage implements PageDelegate {
}
private async _onScriptMessage(event: bidi.Script.MessageParameters) {
if (event.channel !== kPlaywrightBindingChannel)
if (event.channel !== kPlaywrightBindingChannel) {
return;
}
const pageOrError = await this._page.waitForInitializedOrError();
if (pageOrError instanceof Error)
if (pageOrError instanceof Error) {
return;
}
const context = this._realmToContext.get(event.source.realm);
if (!context)
if (!context) {
return;
if (event.data.type !== 'string')
}
if (event.data.type !== 'string') {
return;
}
await this._page._onBindingCalled(event.data.value, context);
}
@ -373,8 +386,9 @@ export class BidiPage implements PageDelegate {
// TODO: push to iframes?
contexts: [this._session.sessionId],
});
if (!initScript.internal)
if (!initScript.internal) {
this._initScriptIds.push(script);
}
}
async removeNonInternalInitScripts() {
@ -431,16 +445,19 @@ export class BidiPage implements PageDelegate {
async getBoundingBox(handle: dom.ElementHandle): Promise<types.Rect | null> {
const box = await handle.evaluate(element => {
if (!(element instanceof Element))
if (!(element instanceof Element)) {
return null;
}
const rect = element.getBoundingClientRect();
return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
});
if (!box)
if (!box) {
return null;
}
const position = await this._framePosition(handle._frame);
if (!position)
if (!position) {
return null;
}
box.x += position.x;
box.y += position.y;
return box;
@ -448,15 +465,18 @@ export class BidiPage implements PageDelegate {
// TODO: move to Frame.
private async _framePosition(frame: frames.Frame): Promise<types.Point | null> {
if (frame === this._page.mainFrame())
if (frame === this._page.mainFrame()) {
return { x: 0, y: 0 };
}
const element = await frame.frameElement();
const box = await element.boundingBox();
if (!box)
if (!box) {
return null;
}
const style = await element.evaluateInUtility(([injected, iframe]) => injected.describeIFrameStyle(iframe as Element), {}).catch(e => 'error:notconnected' as const);
if (style === 'error:notconnected' || style === 'transformed')
if (style === 'error:notconnected' || style === 'transformed') {
return null;
}
// Content box is offset by border and padding widths.
box.x += style.left;
box.y += style.top;
@ -471,10 +491,12 @@ export class BidiPage implements PageDelegate {
behavior: 'instant',
});
}, null).then(() => 'done' as const).catch(e => {
if (e instanceof Error && e.message.includes('Node is detached from document'))
if (e instanceof Error && e.message.includes('Node is detached from document')) {
return 'error:notconnected';
if (e instanceof Error && e.message.includes('Node does not have a layout object'))
}
if (e instanceof Error && e.message.includes('Node does not have a layout object')) {
return 'error:notvisible';
}
throw e;
});
}
@ -488,11 +510,13 @@ export class BidiPage implements PageDelegate {
async getContentQuads(handle: dom.ElementHandle<Element>): Promise<types.Quad[] | null | 'error:notconnected'> {
const quads = await handle.evaluateInUtility(([injected, node]) => {
if (!node.isConnected)
if (!node.isConnected) {
return 'error:notconnected';
}
const rects = node.getClientRects();
if (!rects)
if (!rects) {
return null;
}
return [...rects].map(rect => [
{ x: rect.left, y: rect.top },
{ x: rect.right, y: rect.top },
@ -500,12 +524,14 @@ export class BidiPage implements PageDelegate {
{ x: rect.left, y: rect.bottom },
]);
}, null);
if (!quads || quads === 'error:notconnected')
if (!quads || quads === 'error:notconnected') {
return quads;
}
// TODO: consider transforming quads to support clicks in iframes.
const position = await this._framePosition(handle._frame);
if (!position)
if (!position) {
return null;
}
quads.forEach(quad => quad.forEach(point => {
point.x += position.x;
point.y += position.y;
@ -525,13 +551,15 @@ export class BidiPage implements PageDelegate {
const fromContext = toBidiExecutionContext(handle._context);
const shared = await fromContext.rawCallFunction('x => x', { handle: handle._objectId });
// TODO: store sharedId in the handle.
if (!('sharedId' in shared))
if (!('sharedId' in shared)) {
throw new Error('Element is not a node');
}
const sharedId = shared.sharedId!;
const executionContext = toBidiExecutionContext(to);
const result = await executionContext.rawCallFunction('x => x', { sharedId });
if ('handle' in result)
if ('handle' in result) {
return to.createHandle({ objectId: result.handle!, ...result }) as dom.ElementHandle<T>;
}
throw new Error('Failed to adopt element handle.');
}
@ -551,10 +579,13 @@ export class BidiPage implements PageDelegate {
async getFrameElement(frame: frames.Frame): Promise<dom.ElementHandle> {
const parent = frame.parentFrame();
if (!parent)
if (!parent) {
throw new Error('Frame has been detached.');
}
const parentContext = await parent._mainContext();
const list = await parentContext.evaluateHandle(() => { return [...document.querySelectorAll('iframe,frame')]; });
const list = await parentContext.evaluateHandle(() => {
return [...document.querySelectorAll('iframe,frame')];
});
const length = await list.evaluate(list => list.length);
let foundElement = null;
for (let i = 0; i < length; i++) {
@ -568,8 +599,9 @@ export class BidiPage implements PageDelegate {
}
}
list.dispose();
if (!foundElement)
if (!foundElement) {
throw new Error('Frame has been detached.');
}
return foundElement;
}

View file

@ -41,8 +41,9 @@ const unitToPixels: { [key: string]: number } = {
};
function convertPrintParameterToInches(text: string | undefined): number | undefined {
if (text === undefined)
if (text === undefined) {
return undefined;
}
let unit = text.substring(text.length - 2).toLowerCase();
let valueText = '';
if (unitToPixels.hasOwnProperty(unit)) {

View file

@ -15,8 +15,9 @@ import type * as Bidi from './bidiProtocol';
*/
export class BidiDeserializer {
static deserialize(result: Bidi.Script.RemoteValue): any {
if (!result)
if (!result) {
return undefined;
}
switch (result.type) {
case 'array':

View file

@ -5,7 +5,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
/* eslint-disable curly */
export const getBidiKeyValue = (key: string) => {
switch (key) {

View file

@ -7,7 +7,7 @@
import type * as Bidi from './bidiProtocol';
/* eslint-disable curly, indent */
/* eslint-disable indent */
/**
* @internal

View file

@ -7,7 +7,7 @@
import fs from 'fs';
import path from 'path';
/* eslint-disable curly, indent */
/* eslint-disable indent */
interface ProfileOptions {
preferences: Record<string, unknown>;

View file

@ -98,16 +98,18 @@ export abstract class Browser extends SdkObject {
throw error;
}
context._clientCertificatesProxy = clientCertificatesProxy;
if (options.storageState)
if (options.storageState) {
await context.setStorageState(metadata, options.storageState);
}
return context;
}
async newContextForReuse(params: channels.BrowserNewContextForReuseParams, metadata: CallMetadata): Promise<{ context: BrowserContext, needsReset: boolean }> {
const hash = BrowserContext.reusableContextHash(params);
if (!this._contextForReuse || hash !== this._contextForReuse.hash || !this._contextForReuse.context.canResetForReuse()) {
if (this._contextForReuse)
if (this._contextForReuse) {
await this._contextForReuse.context.close({ reason: 'Context reused' });
}
this._contextForReuse = { context: await this.newContext(metadata, params), hash };
return { context: this._contextForReuse.context, needsReset: false };
}
@ -116,7 +118,7 @@ export abstract class Browser extends SdkObject {
}
async stopPendingOperations(reason: string) {
await this._contextForReuse?.context?.stopPendingOperations(reason);
await this._contextForReuse?.context.stopPendingOperations(reason);
}
_downloadCreated(page: Page, uuid: string, url: string, suggestedFilename?: string) {
@ -126,15 +128,17 @@ export abstract class Browser extends SdkObject {
_downloadFilenameSuggested(uuid: string, suggestedFilename: string) {
const download = this._downloads.get(uuid);
if (!download)
if (!download) {
return;
}
download._filenameSuggested(suggestedFilename);
}
_downloadFinished(uuid: string, error?: string) {
const download = this._downloads.get(uuid);
if (!download)
if (!download) {
return;
}
download.artifact.reportFinished(error ? new Error(error) : undefined);
this._downloads.delete(uuid);
}
@ -158,23 +162,27 @@ export abstract class Browser extends SdkObject {
}
_didClose() {
for (const context of this.contexts())
for (const context of this.contexts()) {
context._browserClosed();
if (this._defaultContext)
}
if (this._defaultContext) {
this._defaultContext._browserClosed();
}
this.emit(Browser.Events.Disconnected);
this.instrumentation.onBrowserClose(this);
}
async close(options: { reason?: string }) {
if (!this._startedClosing) {
if (options.reason)
if (options.reason) {
this._closeReason = options.reason;
}
this._startedClosing = true;
await this.options.browserProcess.close();
}
if (this.isConnected())
if (this.isConnected()) {
await new Promise(x => this.once(Browser.Events.Disconnected, x));
}
}
async killForTests() {

View file

@ -103,8 +103,9 @@ export abstract class BrowserContext extends SdkObject {
this.fetchRequest = new BrowserContextAPIRequestContext(this);
if (this._options.recordHar)
if (this._options.recordHar) {
this._harRecorders.set('', new HarRecorder(this, null, this._options.recordHar));
}
this.tracing = new Tracing(this, browser.options.tracesDir);
this.clock = new Clock(this);
@ -123,31 +124,38 @@ export abstract class BrowserContext extends SdkObject {
}
async _initialize() {
if (this.attribution.playwright.options.isInternalPlaywright)
if (this.attribution.playwright.options.isInternalPlaywright) {
return;
}
// Debugger will pause execution upon page.pause in headed mode.
this._debugger = new Debugger(this);
// When PWDEBUG=1, show inspector for each context.
if (debugMode() === 'inspector')
if (debugMode() === 'inspector') {
await Recorder.show('actions', this, RecorderApp.factory(this), { pauseOnNextStatement: true });
}
// When paused, show inspector.
if (this._debugger.isPaused())
if (this._debugger.isPaused()) {
Recorder.showInspectorNoReply(this, RecorderApp.factory(this));
}
this._debugger.on(Debugger.Events.PausedStateChanged, () => {
if (this._debugger.isPaused())
if (this._debugger.isPaused()) {
Recorder.showInspectorNoReply(this, RecorderApp.factory(this));
}
});
if (debugMode() === 'console')
if (debugMode() === 'console') {
await this.extendInjectedScript(consoleApiSource.source);
if (this._options.serviceWorkers === 'block')
}
if (this._options.serviceWorkers === 'block') {
await this.addInitScript(`\nif (navigator.serviceWorker) navigator.serviceWorker.register = async () => { console.warn('Service Worker registration blocked by Playwright'); };\n`);
}
if (this._options.permissions)
if (this._options.permissions) {
await this.grantPermissions(this._options.permissions);
}
}
debugger(): Debugger {
@ -155,21 +163,24 @@ export abstract class BrowserContext extends SdkObject {
}
async _ensureVideosPath() {
if (this._options.recordVideo)
if (this._options.recordVideo) {
await mkdirIfNeeded(path.join(this._options.recordVideo.dir, 'dummy'));
}
}
canResetForReuse(): boolean {
if (this._closedStatus !== 'open')
if (this._closedStatus !== 'open') {
return false;
}
return true;
}
async stopPendingOperations(reason: string) {
// When using context reuse, stop pending operations to gracefully terminate all the actions
// with a user-friendly error message containing operation log.
for (const controller of this._activeProgressControllers)
for (const controller of this._activeProgressControllers) {
controller.abort(new Error(reason));
}
// Let rejections in microtask generate events before returning.
await new Promise(f => setTimeout(f, 0));
}
@ -179,12 +190,14 @@ export abstract class BrowserContext extends SdkObject {
for (const k of Object.keys(paramsCopy)) {
const key = k as keyof channels.BrowserNewContextForReuseParams;
if (paramsCopy[key] === defaultNewContextParamValues[key])
if (paramsCopy[key] === defaultNewContextParamValues[key]) {
delete paramsCopy[key];
}
}
for (const key of paramsThatAllowContextReuse)
for (const key of paramsThatAllowContextReuse) {
delete paramsCopy[key];
}
return JSON.stringify(paramsCopy);
}
@ -194,8 +207,9 @@ export abstract class BrowserContext extends SdkObject {
this.tracing.resetForReuse();
if (params) {
for (const key of paramsThatAllowContextReuse)
for (const key of paramsThatAllowContextReuse) {
(this._options as any)[key] = params[key];
}
}
await this._cancelAllRoutesInFlight();
@ -203,8 +217,9 @@ export abstract class BrowserContext extends SdkObject {
// Close extra pages early.
let page: Page | undefined = this.pages()[0];
const [, ...otherPages] = this.pages();
for (const p of otherPages)
for (const p of otherPages) {
await p.close(metadata);
}
if (page && page.hasCrashed()) {
await page.close(metadata);
page = undefined;
@ -222,10 +237,11 @@ export abstract class BrowserContext extends SdkObject {
await this._removeInitScripts();
this.clock.markAsUninstalled();
// TODO: following can be optimized to not perform noops.
if (this._options.permissions)
if (this._options.permissions) {
await this.grantPermissions(this._options.permissions);
else
} else {
await this.clearPermissions();
}
await this.setExtraHTTPHeaders(this._options.extraHTTPHeaders || []);
await this.setGeolocation(this._options.geolocation);
await this.setOffline(!!this._options.offline);
@ -237,8 +253,9 @@ export abstract class BrowserContext extends SdkObject {
}
_browserClosed() {
for (const page of this.pages())
for (const page of this.pages()) {
page._didClose();
}
this._didCloseInternal();
}
@ -250,8 +267,9 @@ export abstract class BrowserContext extends SdkObject {
}
this._clientCertificatesProxy?.close().catch(() => {});
this.tracing.abort();
if (this._isPersistentContext)
if (this._isPersistentContext) {
this.onClosePersistent();
}
this._closePromiseFulfill!(new Error('Context closed'));
this.emit(BrowserContext.Events.Close);
}
@ -282,8 +300,9 @@ export abstract class BrowserContext extends SdkObject {
protected abstract onClosePersistent(): void;
async cookies(urls: string | string[] | undefined = []): Promise<channels.NetworkCookie[]> {
if (urls && !Array.isArray(urls))
if (urls && !Array.isArray(urls)) {
urls = [urls];
}
return await this.doGetCookies(urls as string[]);
}
@ -292,8 +311,9 @@ export abstract class BrowserContext extends SdkObject {
await this.doClearCookies();
const matches = (cookie: channels.NetworkCookie, prop: 'name' | 'domain' | 'path', value: string | RegExp | undefined) => {
if (!value)
if (!value) {
return true;
}
if (value instanceof RegExp) {
value.lastIndex = 0;
return value.test(cookie[prop]);
@ -315,11 +335,13 @@ export abstract class BrowserContext extends SdkObject {
}
async exposeBinding(name: string, needsHandle: boolean, playwrightBinding: frames.FunctionWithSource): Promise<void> {
if (this._pageBindings.has(name))
if (this._pageBindings.has(name)) {
throw new Error(`Function "${name}" has been already registered`);
}
for (const page of this.pages()) {
if (page.getBinding(name))
if (page.getBinding(name)) {
throw new Error(`Function "${name}" has been already registered in one of the pages`);
}
}
const binding = new PageBinding(name, playwrightBinding, needsHandle);
this._pageBindings.set(name, binding);
@ -330,8 +352,9 @@ export abstract class BrowserContext extends SdkObject {
async _removeExposedBindings() {
for (const [key, binding] of this._pageBindings) {
if (!binding.internal)
if (!binding.internal) {
this._pageBindings.delete(key);
}
}
}
@ -369,19 +392,22 @@ export abstract class BrowserContext extends SdkObject {
await Promise.race([waitForEvent.promise, this._closePromise]);
}
const page = this.possiblyUninitializedPages()[0];
if (!page)
if (!page) {
return;
}
const pageOrError = await page.waitForInitializedOrError();
if (pageOrError instanceof Error)
if (pageOrError instanceof Error) {
throw pageOrError;
}
await page.mainFrame()._waitForLoadState(progress, 'load');
return page;
}
async _loadDefaultContext(progress: Progress) {
const defaultPage = await this._loadDefaultContextAsIs(progress);
if (!defaultPage)
if (!defaultPage) {
return;
}
const browserName = this._browser.options.name;
if ((this._options.isMobile && browserName === 'chromium') || (this._options.locale && browserName === 'webkit')) {
// Workaround for:
@ -407,11 +433,13 @@ export abstract class BrowserContext extends SdkObject {
protected _authenticateProxyViaCredentials() {
const proxy = this._options.proxy || this._browser.options.proxy;
if (!proxy)
if (!proxy) {
return;
}
const { username, password } = proxy;
if (username)
if (username) {
this._options.httpCredentials = { username, password: password || '' };
}
}
async addInitScript(source: string) {
@ -448,21 +476,24 @@ export abstract class BrowserContext extends SdkObject {
async close(options: { reason?: string }) {
if (this._closedStatus === 'open') {
if (options.reason)
if (options.reason) {
this._closeReason = options.reason;
}
this.emit(BrowserContext.Events.BeforeClose);
this._closedStatus = 'closing';
for (const harRecorder of this._harRecorders.values())
for (const harRecorder of this._harRecorders.values()) {
await harRecorder.flush();
}
await this.tracing.flush();
// Cleanup.
const promises: Promise<void>[] = [];
for (const { context, artifact } of this._browser._idToVideo.values()) {
// Wait for the videos to finish.
if (context === this)
if (context === this) {
promises.push(artifact.finishedPromise());
}
}
if (this._customCloseHandler) {
@ -479,20 +510,23 @@ export abstract class BrowserContext extends SdkObject {
await Promise.all(promises);
// Custom handler should trigger didCloseInternal itself.
if (!this._customCloseHandler)
if (!this._customCloseHandler) {
this._didCloseInternal();
}
}
await this._closePromise;
}
async newPage(metadata: CallMetadata): Promise<Page> {
const page = await this.doCreateNewPage();
if (metadata.isServerSide)
if (metadata.isServerSide) {
page.markAsServerSideOnly();
}
const pageOrError = await page.waitForInitializedOrError();
if (pageOrError instanceof Page) {
if (pageOrError.isClosed())
if (pageOrError.isClosed()) {
throw new Error('Page has been closed.');
}
return pageOrError;
}
throw pageOrError;
@ -512,14 +546,16 @@ export abstract class BrowserContext extends SdkObject {
// First try collecting storage stage from existing pages.
for (const page of this.pages()) {
const origin = page.mainFrame().origin();
if (!origin || !originsToSave.has(origin))
if (!origin || !originsToSave.has(origin)) {
continue;
}
try {
const storage = await page.mainFrame().nonStallingEvaluateInExistingContext(`({
localStorage: Object.keys(localStorage).map(name => ({ name, value: localStorage.getItem(name) })),
})`, 'utility');
if (storage.localStorage.length)
if (storage.localStorage.length) {
result.origins.push({ origin, localStorage: storage.localStorage } as channels.OriginStorage);
}
originsToSave.delete(origin);
} catch {
// When failed on the live page, we'll retry on the blank page below.
@ -542,8 +578,9 @@ export abstract class BrowserContext extends SdkObject {
localStorage: Object.keys(localStorage).map(name => ({ name, value: localStorage.getItem(name) })),
})`, { world: 'utility' });
originStorage.localStorage = storage.localStorage;
if (storage.localStorage.length)
if (storage.localStorage.length) {
result.origins.push(originStorage);
}
}
await page.close(internalMetadata);
}
@ -553,8 +590,9 @@ export abstract class BrowserContext extends SdkObject {
async _resetStorage() {
const oldOrigins = this._origins;
const newOrigins = new Map(this._options.storageState?.origins?.map(p => [p.origin, p]) || []);
if (!oldOrigins.size && !newOrigins.size)
if (!oldOrigins.size && !newOrigins.size) {
return;
}
let page = this.pages()[0];
const internalMetadata = serverSideCallMetadata();
@ -583,8 +621,9 @@ export abstract class BrowserContext extends SdkObject {
async _resetCookies() {
await this.doClearCookies();
if (this._options.storageState?.cookies)
await this.addCookies(this._options.storageState?.cookies);
if (this._options.storageState?.cookies) {
await this.addCookies(this._options.storageState.cookies);
}
}
isSettingStorageState(): boolean {
@ -594,8 +633,9 @@ export abstract class BrowserContext extends SdkObject {
async setStorageState(metadata: CallMetadata, state: NonNullable<channels.BrowserNewContextParams['storageState']>) {
this._settingStorageState = true;
try {
if (state.cookies)
if (state.cookies) {
await this.addCookies(state.cookies);
}
if (state.origins && state.origins.length) {
const internalMetadata = serverSideCallMetadata();
const page = await this.newPage(internalMetadata);
@ -660,25 +700,30 @@ export abstract class BrowserContext extends SdkObject {
export function assertBrowserContextIsNotOwned(context: BrowserContext) {
for (const page of context.pages()) {
if (page._ownedContext)
if (page._ownedContext) {
throw new Error('Please use browser.newContext() for multi-page scripts that share the context.');
}
}
}
export function validateBrowserContextOptions(options: types.BrowserContextOptions, browserOptions: BrowserOptions) {
if (options.noDefaultViewport && options.deviceScaleFactor !== undefined)
if (options.noDefaultViewport && options.deviceScaleFactor !== undefined) {
throw new Error(`"deviceScaleFactor" option is not supported with null "viewport"`);
if (options.noDefaultViewport && !!options.isMobile)
}
if (options.noDefaultViewport && !!options.isMobile) {
throw new Error(`"isMobile" option is not supported with null "viewport"`);
if (options.acceptDownloads === undefined && browserOptions.name !== 'electron')
}
if (options.acceptDownloads === undefined && browserOptions.name !== 'electron') {
options.acceptDownloads = 'accept';
// Electron requires explicit acceptDownloads: true since we wait for
// https://github.com/electron/electron/pull/41718 to be widely shipped.
// In 6-12 months, we can remove this check.
else if (options.acceptDownloads === undefined && browserOptions.name === 'electron')
} else if (options.acceptDownloads === undefined && browserOptions.name === 'electron') {
// Electron requires explicit acceptDownloads: true since we wait for
// https://github.com/electron/electron/pull/41718 to be widely shipped.
// In 6-12 months, we can remove this check.
options.acceptDownloads = 'internal-browser-default';
if (!options.viewport && !options.noDefaultViewport)
}
if (!options.viewport && !options.noDefaultViewport) {
options.viewport = { width: 1280, height: 720 };
}
if (options.recordVideo) {
if (!options.recordVideo.size) {
if (options.noDefaultViewport) {
@ -696,38 +741,49 @@ export function validateBrowserContextOptions(options: types.BrowserContextOptio
options.recordVideo.size!.width &= ~1;
options.recordVideo.size!.height &= ~1;
}
if (options.proxy)
if (options.proxy) {
options.proxy = normalizeProxySettings(options.proxy);
}
verifyGeolocation(options.geolocation);
}
export function verifyGeolocation(geolocation?: types.Geolocation) {
if (!geolocation)
if (!geolocation) {
return;
}
geolocation.accuracy = geolocation.accuracy || 0;
const { longitude, latitude, accuracy } = geolocation;
if (longitude < -180 || longitude > 180)
if (longitude < -180 || longitude > 180) {
throw new Error(`geolocation.longitude: precondition -180 <= LONGITUDE <= 180 failed.`);
if (latitude < -90 || latitude > 90)
}
if (latitude < -90 || latitude > 90) {
throw new Error(`geolocation.latitude: precondition -90 <= LATITUDE <= 90 failed.`);
if (accuracy < 0)
}
if (accuracy < 0) {
throw new Error(`geolocation.accuracy: precondition 0 <= ACCURACY failed.`);
}
}
export function verifyClientCertificates(clientCertificates?: types.BrowserContextOptions['clientCertificates']) {
if (!clientCertificates)
if (!clientCertificates) {
return;
}
for (const cert of clientCertificates) {
if (!cert.origin)
if (!cert.origin) {
throw new Error(`clientCertificates.origin is required`);
if (!cert.cert && !cert.key && !cert.passphrase && !cert.pfx)
}
if (!cert.cert && !cert.key && !cert.passphrase && !cert.pfx) {
throw new Error('None of cert, key, passphrase or pfx is specified');
if (cert.cert && !cert.key)
}
if (cert.cert && !cert.key) {
throw new Error('cert is specified without key');
if (!cert.cert && cert.key)
}
if (!cert.cert && cert.key) {
throw new Error('key is specified without cert');
if (cert.pfx && (cert.cert || cert.key))
}
if (cert.pfx && (cert.cert || cert.key)) {
throw new Error('pfx is specified together with cert, key or passphrase');
}
}
}
@ -739,18 +795,22 @@ export function normalizeProxySettings(proxy: types.ProxySettings): types.ProxyS
// new URL('localhost:8080') fails to parse host or protocol
// In both of these cases, we need to try re-parse URL with `http://` prefix.
url = new URL(server);
if (!url.host || !url.protocol)
if (!url.host || !url.protocol) {
url = new URL('http://' + server);
}
} catch (e) {
url = new URL('http://' + server);
}
if (url.protocol === 'socks4:' && (proxy.username || proxy.password))
if (url.protocol === 'socks4:' && (proxy.username || proxy.password)) {
throw new Error(`Socks4 proxy protocol does not support authentication`);
if (url.protocol === 'socks5:' && (proxy.username || proxy.password))
}
if (url.protocol === 'socks5:' && (proxy.username || proxy.password)) {
throw new Error(`Browser does not support socks5 proxy authentication`);
}
server = url.protocol + '//' + url.host;
if (bypass)
if (bypass) {
bypass = bypass.split(',').map(t => t.trim()).join(',');
}
return { ...proxy, server, bypass };
}

View file

@ -80,23 +80,28 @@ export abstract class BrowserType extends SdkObject {
async launch(metadata: CallMetadata, options: types.LaunchOptions, protocolLogger?: types.ProtocolLogger): Promise<Browser> {
options = this._validateLaunchOptions(options);
if (this._useBidi)
if (this._useBidi) {
options.useWebSocket = true;
}
const controller = new ProgressController(metadata, this);
controller.setLogName('browser');
const browser = await controller.run(progress => {
const seleniumHubUrl = (options as any).__testHookSeleniumRemoteURL || process.env.SELENIUM_REMOTE_URL;
if (seleniumHubUrl)
if (seleniumHubUrl) {
return this._launchWithSeleniumHub(progress, seleniumHubUrl, options);
return this._innerLaunchWithRetries(progress, options, undefined, helper.debugProtocolLogger(protocolLogger)).catch(e => { throw this._rewriteStartupLog(e); });
}
return this._innerLaunchWithRetries(progress, options, undefined, helper.debugProtocolLogger(protocolLogger)).catch(e => {
throw this._rewriteStartupLog(e);
});
}, TimeoutSettings.launchTimeout(options));
return browser;
}
async launchPersistentContext(metadata: CallMetadata, userDataDir: string, options: channels.BrowserTypeLaunchPersistentContextOptions & { useWebSocket?: boolean, internalIgnoreHTTPSErrors?: boolean }): Promise<BrowserContext> {
const launchOptions = this._validateLaunchOptions(options);
if (this._useBidi)
if (this._useBidi) {
launchOptions.useWebSocket = true;
}
const controller = new ProgressController(metadata, this);
controller.setLogName('browser');
const browser = await controller.run(async progress => {
@ -104,12 +109,14 @@ export abstract class BrowserType extends SdkObject {
let clientCertificatesProxy: ClientCertificatesProxy | undefined;
if (options.clientCertificates?.length) {
clientCertificatesProxy = new ClientCertificatesProxy(options);
launchOptions.proxyOverride = await clientCertificatesProxy?.listen();
launchOptions.proxyOverride = await clientCertificatesProxy.listen();
options = { ...options };
options.internalIgnoreHTTPSErrors = true;
}
progress.cleanupWhenAborted(() => clientCertificatesProxy?.close());
const browser = await this._innerLaunchWithRetries(progress, launchOptions, options, helper.debugProtocolLogger(), userDataDir).catch(e => { throw this._rewriteStartupLog(e); });
const browser = await this._innerLaunchWithRetries(progress, launchOptions, options, helper.debugProtocolLogger(), userDataDir).catch(e => {
throw this._rewriteStartupLog(e);
});
browser._defaultContext!._clientCertificatesProxy = clientCertificatesProxy;
return browser;
}, TimeoutSettings.launchTimeout(launchOptions));
@ -134,8 +141,9 @@ export abstract class BrowserType extends SdkObject {
options.proxy = options.proxy ? normalizeProxySettings(options.proxy) : undefined;
const browserLogsCollector = new RecentLogsCollector();
const { browserProcess, userDataDir, artifactsDir, transport } = await this._launchProcess(progress, options, !!persistent, browserLogsCollector, maybeUserDataDir);
if ((options as any).__testHookBeforeCreateBrowser)
if ((options as any).__testHookBeforeCreateBrowser) {
await (options as any).__testHookBeforeCreateBrowser();
}
const browserOptions: BrowserOptions = {
name: this._name,
isChromium: this._name === 'chromium',
@ -154,14 +162,16 @@ export abstract class BrowserType extends SdkObject {
wsEndpoint: options.useWebSocket ? (transport as WebSocketTransport).wsEndpoint : undefined,
originalLaunchOptions: options,
};
if (persistent)
if (persistent) {
validateBrowserContextOptions(persistent, browserOptions);
}
copyTestHooks(options, browserOptions);
const browser = await this.connectToTransport(transport, browserOptions);
(browser as any)._userDataDirForTest = userDataDir;
// We assume no control when using custom arguments, and do not prepare the default context in that case.
if (persistent && !options.ignoreAllDefaultArgs)
if (persistent && !options.ignoreAllDefaultArgs) {
await browser._defaultContext!._loadDefaultContext(progress);
}
return browser;
}
@ -186,8 +196,9 @@ export abstract class BrowserType extends SdkObject {
if (userDataDir) {
// Firefox bails if the profile directory does not exist, Chrome creates it. We ensure consistent behavior here.
if (!await existsAsync(userDataDir))
if (!await existsAsync(userDataDir)) {
await fs.promises.mkdir(userDataDir, { recursive: true, mode: 0o700 });
}
} else {
userDataDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), `playwright_${this._name}dev_profile-`));
tempDirectories.push(userDataDir);
@ -195,22 +206,25 @@ export abstract class BrowserType extends SdkObject {
await this.prepareUserDataDir(options, userDataDir);
const browserArguments = [];
if (ignoreAllDefaultArgs)
if (ignoreAllDefaultArgs) {
browserArguments.push(...args);
else if (ignoreDefaultArgs)
} else if (ignoreDefaultArgs) {
browserArguments.push(...this.defaultArgs(options, isPersistent, userDataDir).filter(arg => ignoreDefaultArgs.indexOf(arg) === -1));
else
} else {
browserArguments.push(...this.defaultArgs(options, isPersistent, userDataDir));
}
let executable: string;
if (executablePath) {
if (!(await existsAsync(executablePath)))
if (!(await existsAsync(executablePath))) {
throw new Error(`Failed to launch ${this._name} because executable doesn't exist at ${executablePath}`);
}
executable = executablePath;
} else {
const registryExecutable = registry.findExecutable(this.getExecutableName(options));
if (!registryExecutable || registryExecutable.browserName !== this._name)
if (!registryExecutable || registryExecutable.browserName !== this._name) {
throw new Error(`Unsupported ${this._name} channel "${options.channel}"`);
}
executable = registryExecutable.executablePathOrDie(this.attribution.playwright.options.sdkLanguage);
await registry.validateHostRequirementsForExecutablesIfNeeded([registryExecutable], this.attribution.playwright.options.sdkLanguage);
}
@ -235,8 +249,9 @@ export abstract class BrowserType extends SdkObject {
stdio: 'pipe',
tempDirectories,
attemptToGracefullyClose: async () => {
if ((options as any).__testHookGracefullyClose)
if ((options as any).__testHookGracefullyClose) {
await (options as any).__testHookGracefullyClose();
}
// We try to gracefully close to prevent crash reporting and core dumps.
// Note that it's fine to reuse the pipe transport, since
// our connection ignores kBrowserCloseMessageId.
@ -245,8 +260,9 @@ export abstract class BrowserType extends SdkObject {
onExit: (exitCode, signal) => {
// Unblock launch when browser prematurely exits.
readyState?.onBrowserExit();
if (browserProcess && browserProcess.onclose)
if (browserProcess && browserProcess.onclose) {
browserProcess.onclose(exitCode, signal);
}
},
});
async function closeOrKill(timeout: number): Promise<void> {
@ -280,10 +296,12 @@ export abstract class BrowserType extends SdkObject {
}
async _createArtifactDirs(options: types.LaunchOptions): Promise<void> {
if (options.downloadsPath)
if (options.downloadsPath) {
await fs.promises.mkdir(options.downloadsPath, { recursive: true });
if (options.tracesDir)
}
if (options.tracesDir) {
await fs.promises.mkdir(options.tracesDir, { recursive: true });
}
}
async connectOverCDP(metadata: CallMetadata, endpointURL: string, options: { slowMo?: number }, timeout?: number): Promise<Browser> {
@ -297,12 +315,15 @@ export abstract class BrowserType extends SdkObject {
private _validateLaunchOptions(options: types.LaunchOptions): types.LaunchOptions {
const { devtools = false } = options;
let { headless = !devtools, downloadsPath, proxy } = options;
if (debugMode())
if (debugMode()) {
headless = false;
if (downloadsPath && !path.isAbsolute(downloadsPath))
}
if (downloadsPath && !path.isAbsolute(downloadsPath)) {
downloadsPath = path.join(process.cwd(), downloadsPath);
if (this.attribution.playwright.options.socksProxyPort)
}
if (this.attribution.playwright.options.socksProxyPort) {
proxy = { server: `socks5://127.0.0.1:${this.attribution.playwright.options.socksProxyPort}` };
}
return { ...options, devtools, headless, downloadsPath, proxy };
}
@ -320,8 +341,9 @@ export abstract class BrowserType extends SdkObject {
}
_rewriteStartupLog(error: Error): Error {
if (!isProtocolError(error))
if (!isProtocolError(error)) {
return error;
}
return this.doRewriteStartupLog(error);
}
@ -345,7 +367,8 @@ export abstract class BrowserType extends SdkObject {
function copyTestHooks(from: object, to: object) {
for (const [key, value] of Object.entries(from)) {
if (key.startsWith('__testHook'))
if (key.startsWith('__testHook')) {
(to as any)[key] = value;
}
}
}

View file

@ -58,8 +58,9 @@ export class Chromium extends BrowserType {
constructor(parent: SdkObject) {
super(parent, 'chromium');
if (debugMode())
if (debugMode()) {
this._devtools = this._createDevTools();
}
}
override async connectOverCDP(metadata: CallMetadata, endpointURL: string, options: { slowMo?: number, headers?: types.HeadersArray }, timeout?: number) {
@ -72,13 +73,15 @@ export class Chromium extends BrowserType {
async _connectOverCDPInternal(progress: Progress, endpointURL: string, options: types.LaunchOptions & { headers?: types.HeadersArray }, onClose?: () => Promise<void>) {
let headersMap: { [key: string]: string; } | undefined;
if (options.headers)
if (options.headers) {
headersMap = headersArrayToObject(options.headers, false);
}
if (!headersMap)
if (!headersMap) {
headersMap = { 'User-Agent': getUserAgent() };
else if (headersMap && !Object.keys(headersMap).some(key => key.toLowerCase() === 'user-agent'))
} else if (headersMap && !Object.keys(headersMap).some(key => key.toLowerCase() === 'user-agent')) {
headersMap['User-Agent'] = getUserAgent();
}
const artifactsDir = await fs.promises.mkdtemp(ARTIFACTS_FOLDER);
@ -135,14 +138,17 @@ export class Chromium extends BrowserType {
}
override doRewriteStartupLog(error: ProtocolError): ProtocolError {
if (!error.logs)
if (!error.logs) {
return error;
if (error.logs.includes('Missing X server'))
}
if (error.logs.includes('Missing X server')) {
error.logs = '\n' + wrapInASCIIBox(kNoXServerRunningError, 1);
}
// These error messages are taken from Chromium source code as of July, 2020:
// https://github.com/chromium/chromium/blob/70565f67e79f79e17663ad1337dc6e63ee207ce9/content/browser/zygote_host/zygote_host_impl_linux.cc
if (!error.logs.includes('crbug.com/357670') && !error.logs.includes('No usable sandbox!') && !error.logs.includes('crbug.com/638180'))
if (!error.logs.includes('crbug.com/357670') && !error.logs.includes('No usable sandbox!') && !error.logs.includes('crbug.com/638180')) {
return error;
}
error.logs = [
`Chromium sandboxing failed!`,
`================================`,
@ -167,8 +173,9 @@ export class Chromium extends BrowserType {
override async _launchWithSeleniumHub(progress: Progress, hubUrl: string, options: types.LaunchOptions): Promise<CRBrowser> {
await this._createArtifactDirs(options);
if (!hubUrl.endsWith('/'))
if (!hubUrl.endsWith('/')) {
hubUrl = hubUrl + '/';
}
const args = this._innerDefaultArgs(options);
args.push('--remote-debugging-port=0');
@ -180,15 +187,17 @@ export class Chromium extends BrowserType {
if (process.env.SELENIUM_REMOTE_CAPABILITIES) {
const remoteCapabilities = parseSeleniumRemoteParams({ name: 'capabilities', value: process.env.SELENIUM_REMOTE_CAPABILITIES }, progress);
if (remoteCapabilities)
if (remoteCapabilities) {
desiredCapabilities = { ...desiredCapabilities, ...remoteCapabilities };
}
}
let headers: { [key: string]: string } = {};
if (process.env.SELENIUM_REMOTE_HEADERS) {
const remoteHeaders = parseSeleniumRemoteParams({ name: 'headers', value: process.env.SELENIUM_REMOTE_HEADERS }, progress);
if (remoteHeaders)
if (remoteHeaders) {
headers = remoteHeaders;
}
}
progress.log(`<selenium> connecting to ${hubUrl}`);
@ -229,8 +238,9 @@ export class Chromium extends BrowserType {
progress.log(`<selenium> using selenium v4`);
const endpointURLString = addProtocol(capabilities['se:cdp']);
endpointURL = new URL(endpointURLString);
if (endpointURL.hostname === 'localhost' || endpointURL.hostname === '127.0.0.1')
if (endpointURL.hostname === 'localhost' || endpointURL.hostname === '127.0.0.1') {
endpointURL.hostname = new URL(hubUrl).hostname;
}
progress.log(`<selenium> retrieved endpoint ${endpointURL.toString()} for sessionId=${sessionId}`);
} else {
// Selenium 3 - resolve target node IP to use instead of localhost ws url.
@ -274,38 +284,45 @@ export class Chromium extends BrowserType {
override defaultArgs(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string): string[] {
const chromeArguments = this._innerDefaultArgs(options);
chromeArguments.push(`--user-data-dir=${userDataDir}`);
if (options.useWebSocket)
if (options.useWebSocket) {
chromeArguments.push('--remote-debugging-port=0');
else
} else {
chromeArguments.push('--remote-debugging-pipe');
if (isPersistent)
}
if (isPersistent) {
chromeArguments.push('about:blank');
else
} else {
chromeArguments.push('--no-startup-window');
}
return chromeArguments;
}
private _innerDefaultArgs(options: types.LaunchOptions): string[] {
const { args = [] } = options;
const userDataDirArg = args.find(arg => arg.startsWith('--user-data-dir'));
if (userDataDirArg)
if (userDataDirArg) {
throw this._createUserDataDirArgMisuseError('--user-data-dir');
if (args.find(arg => arg.startsWith('--remote-debugging-pipe')))
}
if (args.find(arg => arg.startsWith('--remote-debugging-pipe'))) {
throw new Error('Playwright manages remote debugging connection itself.');
if (args.find(arg => !arg.startsWith('-')))
}
if (args.find(arg => !arg.startsWith('-'))) {
throw new Error('Arguments can not specify page to be opened');
}
const chromeArguments = [...chromiumSwitches];
if (os.platform() === 'darwin') {
// See https://github.com/microsoft/playwright/issues/7362
chromeArguments.push('--enable-use-zoom-for-dsf=false');
// See https://bugs.chromium.org/p/chromium/issues/detail?id=1407025.
if (options.headless && (!options.channel || options.channel === 'chromium-headless-shell'))
if (options.headless && (!options.channel || options.channel === 'chromium-headless-shell')) {
chromeArguments.push('--use-angle');
}
}
if (options.devtools)
if (options.devtools) {
chromeArguments.push('--auto-open-devtools-for-tabs');
}
if (options.headless) {
chromeArguments.push('--headless');
@ -315,8 +332,9 @@ export class Chromium extends BrowserType {
'--blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4',
);
}
if (options.chromiumSandbox !== true)
if (options.chromiumSandbox !== true) {
chromeArguments.push('--no-sandbox');
}
const proxy = options.proxyOverride || options.proxy;
if (proxy) {
const proxyURL = new URL(proxy.server);
@ -329,28 +347,34 @@ export class Chromium extends BrowserType {
chromeArguments.push(`--proxy-server=${proxy.server}`);
const proxyBypassRules = [];
// https://source.chromium.org/chromium/chromium/src/+/master:net/docs/proxy.md;l=548;drc=71698e610121078e0d1a811054dcf9fd89b49578
if (this.attribution.playwright.options.socksProxyPort)
if (this.attribution.playwright.options.socksProxyPort) {
proxyBypassRules.push('<-loopback>');
if (proxy.bypass)
}
if (proxy.bypass) {
proxyBypassRules.push(...proxy.bypass.split(',').map(t => t.trim()).map(t => t.startsWith('.') ? '*' + t : t));
if (!process.env.PLAYWRIGHT_DISABLE_FORCED_CHROMIUM_PROXIED_LOOPBACK && !proxyBypassRules.includes('<-loopback>'))
}
if (!process.env.PLAYWRIGHT_DISABLE_FORCED_CHROMIUM_PROXIED_LOOPBACK && !proxyBypassRules.includes('<-loopback>')) {
proxyBypassRules.push('<-loopback>');
if (proxyBypassRules.length > 0)
}
if (proxyBypassRules.length > 0) {
chromeArguments.push(`--proxy-bypass-list=${proxyBypassRules.join(';')}`);
}
}
chromeArguments.push(...args);
return chromeArguments;
}
override readyState(options: types.LaunchOptions): BrowserReadyState | undefined {
if (options.useWebSocket || options.args?.some(a => a.startsWith('--remote-debugging-port')))
if (options.useWebSocket || options.args?.some(a => a.startsWith('--remote-debugging-port'))) {
return new ChromiumReadyState();
}
return undefined;
}
override getExecutableName(options: types.LaunchOptions): string {
if (options.channel)
if (options.channel) {
return options.channel;
}
return options.headless ? 'chromium-headless-shell' : 'chromium';
}
}
@ -358,14 +382,16 @@ export class Chromium extends BrowserType {
class ChromiumReadyState extends BrowserReadyState {
override onBrowserOutput(message: string): void {
const match = message.match(/DevTools listening on (.*)/);
if (match)
if (match) {
this._wsEndpoint.resolve(match[1]);
}
}
}
async function urlToWSEndpoint(progress: Progress, endpointURL: string, headers: { [key: string]: string; }) {
if (endpointURL.startsWith('ws'))
if (endpointURL.startsWith('ws')) {
return endpointURL;
}
progress.log(`<ws preparing> retrieving websocket url from ${endpointURL}`);
const httpURL = endpointURL.endsWith('/') ? `${endpointURL}json/version/` : `${endpointURL}/json/version/`;
const json = await fetchData({
@ -389,8 +415,9 @@ async function seleniumErrorHandler(params: HTTPRequestParams, response: http.In
}
function addProtocol(url: string) {
if (!['ws://', 'wss://', 'http://', 'https://'].some(protocol => url.startsWith(protocol)))
if (!['ws://', 'wss://', 'http://', 'https://'].some(protocol => url.startsWith(protocol))) {
return 'http://' + url;
}
return url;
}

View file

@ -55,20 +55,25 @@ class CRAXNode implements accessibility.AXNode {
this._richlyEditable = property.value.value === 'richtext';
this._editable = true;
}
if (property.name === 'focusable')
if (property.name === 'focusable') {
this._focusable = property.value.value;
if (property.name === 'expanded')
}
if (property.name === 'expanded') {
this._expanded = property.value.value;
if (property.name === 'hidden')
}
if (property.name === 'hidden') {
this._hidden = property.value.value;
}
}
}
private _isPlainTextField(): boolean {
if (this._richlyEditable)
if (this._richlyEditable) {
return false;
if (this._editable)
}
if (this._editable) {
return true;
}
return this._role === 'textbox' || this._role === 'ComboBox' || this._role === 'searchbox';
}
@ -103,26 +108,30 @@ class CRAXNode implements accessibility.AXNode {
}
find(predicate: (arg0: CRAXNode) => boolean): CRAXNode | null {
if (predicate(this))
if (predicate(this)) {
return this;
}
for (const child of this._children) {
const result = child.find(predicate);
if (result)
if (result) {
return result;
}
}
return null;
}
isLeafNode(): boolean {
if (!this._children.length)
if (!this._children.length) {
return true;
}
// These types of objects may have children that we use as internal
// implementation details, but we want to expose them as leaves to platform
// accessibility APIs because screen readers might be confused if they find
// any children.
if (this._isPlainTextField() || this._isTextOnlyObject())
if (this._isPlainTextField() || this._isTextOnlyObject()) {
return true;
}
// Roles whose children are only presentational according to the ARIA and
// HTML5 Specs should be hidden from screen readers.
@ -143,12 +152,15 @@ class CRAXNode implements accessibility.AXNode {
}
// Here and below: Android heuristics
if (this._hasFocusableChild())
if (this._hasFocusableChild()) {
return false;
if (this._focusable && this._role !== 'WebArea' && this._role !== 'RootWebArea' && this._name)
}
if (this._focusable && this._role !== 'WebArea' && this._role !== 'RootWebArea' && this._name) {
return true;
if (this._role === 'heading' && this._name)
}
if (this._role === 'heading' && this._name) {
return true;
}
return false;
}
@ -182,19 +194,23 @@ class CRAXNode implements accessibility.AXNode {
isInteresting(insideControl: boolean): boolean {
const role = this._role;
if (role === 'Ignored' || this._hidden)
if (role === 'Ignored' || this._hidden) {
return false;
}
if (this._focusable || this._richlyEditable)
if (this._focusable || this._richlyEditable) {
return true;
}
// If it's not focusable but has a control role, then it's interesting.
if (this.isControl())
if (this.isControl()) {
return true;
}
// A non focusable child of a control is not interesting
if (insideControl)
if (insideControl) {
return false;
}
return this.isLeafNode() && !!this._name;
}
@ -212,10 +228,12 @@ class CRAXNode implements accessibility.AXNode {
serialize(): channels.AXNode {
const properties: Map<string, number | string | boolean> = new Map();
for (const property of this._payload.properties || [])
for (const property of this._payload.properties || []) {
properties.set(property.name.toLowerCase(), property.value.value);
if (this._payload.description)
}
if (this._payload.description) {
properties.set('description', this._payload.description.value);
}
const node: {[x in keyof channels.AXNode]: any} = {
role: this.normalizedRole(),
@ -229,8 +247,9 @@ class CRAXNode implements accessibility.AXNode {
'valuetext',
];
for (const userStringProperty of userStringProperties) {
if (!properties.has(userStringProperty))
if (!properties.has(userStringProperty)) {
continue;
}
node[userStringProperty] = properties.get(userStringProperty);
}
const booleanProperties: Array<keyof channels.AXNode> = [
@ -247,11 +266,13 @@ class CRAXNode implements accessibility.AXNode {
for (const booleanProperty of booleanProperties) {
// WebArea's treat focus differently than other nodes. They report whether their frame has focus,
// not whether focus is specifically on the root node.
if (booleanProperty === 'focused' && (this._role === 'WebArea' || this._role === 'RootWebArea'))
if (booleanProperty === 'focused' && (this._role === 'WebArea' || this._role === 'RootWebArea')) {
continue;
}
const value = properties.get(booleanProperty);
if (!value)
if (!value) {
continue;
}
node[booleanProperty] = value;
}
const numericalProperties: Array<keyof channels.AXNode> = [
@ -260,8 +281,9 @@ class CRAXNode implements accessibility.AXNode {
'valuemin',
];
for (const numericalProperty of numericalProperties) {
if (!properties.has(numericalProperty))
if (!properties.has(numericalProperty)) {
continue;
}
node[numericalProperty] = properties.get(numericalProperty);
}
const tokenProperties: Array<keyof channels.AXNode> = [
@ -272,32 +294,39 @@ class CRAXNode implements accessibility.AXNode {
];
for (const tokenProperty of tokenProperties) {
const value = properties.get(tokenProperty);
if (!value || value === 'false')
if (!value || value === 'false') {
continue;
}
node[tokenProperty] = value;
}
const axNode = node as channels.AXNode;
if (this._payload.value) {
if (typeof this._payload.value.value === 'string')
if (typeof this._payload.value.value === 'string') {
axNode.valueString = this._payload.value.value;
if (typeof this._payload.value.value === 'number')
}
if (typeof this._payload.value.value === 'number') {
axNode.valueNumber = this._payload.value.value;
}
}
if (properties.has('checked'))
if (properties.has('checked')) {
axNode.checked = properties.get('checked') === 'true' ? 'checked' : properties.get('checked') === 'false' ? 'unchecked' : 'mixed';
if (properties.has('pressed'))
}
if (properties.has('pressed')) {
axNode.pressed = properties.get('pressed') === 'true' ? 'pressed' : properties.get('pressed') === 'false' ? 'released' : 'mixed';
}
return axNode;
}
static createTree(client: CRSession, payloads: Protocol.Accessibility.AXNode[]): CRAXNode {
const nodeById: Map<string, CRAXNode> = new Map();
for (const payload of payloads)
for (const payload of payloads) {
nodeById.set(payload.nodeId, new CRAXNode(client, payload));
}
for (const node of nodeById.values()) {
for (const childId of node._payload.childIds || [])
for (const childId of node._payload.childIds || []) {
node._children.push(nodeById.get(childId)!);
}
}
return nodeById.values().next().value!;
}

View file

@ -59,11 +59,13 @@ export class CRBrowser extends Browser {
const connection = new CRConnection(transport, options.protocolLogger, options.browserLogsCollector);
const browser = new CRBrowser(parent, connection, options);
browser._devtools = devtools;
if (browser.isClank())
if (browser.isClank()) {
browser._isCollocatedWithServer = false;
}
const session = connection.rootSession;
if ((options as any).__testHookOnConnectToBrowser)
if ((options as any).__testHookOnConnectToBrowser) {
await (options as any).__testHookOnConnectToBrowser();
}
const version = await session.send('Browser.getVersion');
browser._version = version.product.substring(version.product.indexOf('/') + 1);
@ -104,10 +106,11 @@ export class CRBrowser extends Browser {
const proxy = options.proxyOverride || options.proxy;
let proxyBypassList = undefined;
if (proxy) {
if (process.env.PLAYWRIGHT_DISABLE_FORCED_CHROMIUM_PROXIED_LOOPBACK)
if (process.env.PLAYWRIGHT_DISABLE_FORCED_CHROMIUM_PROXIED_LOOPBACK) {
proxyBypassList = proxy.bypass;
else
} else {
proxyBypassList = '<-loopback>' + (proxy.bypass ? `,${proxy.bypass}` : '');
}
}
const { browserContextId } = await this._session.send('Target.createBrowserContext', {
@ -134,10 +137,12 @@ export class CRBrowser extends Browser {
}
_platform(): 'mac' | 'linux' | 'win' {
if (this._userAgent.includes('Windows'))
if (this._userAgent.includes('Windows')) {
return 'win';
if (this._userAgent.includes('Macintosh'))
}
if (this._userAgent.includes('Macintosh')) {
return 'mac';
}
return 'linux';
}
@ -150,8 +155,9 @@ export class CRBrowser extends Browser {
}
_onAttachedToTarget({ targetInfo, sessionId, waitingForDebugger }: Protocol.Target.attachedToTargetPayload) {
if (targetInfo.type === 'browser')
if (targetInfo.type === 'browser') {
return;
}
const session = this._session.createChildSession(sessionId);
assert(targetInfo.browserContextId, 'targetInfo: ' + JSON.stringify(targetInfo, null, 2));
let context = this._contexts.get(targetInfo.browserContextId) || null;
@ -228,14 +234,17 @@ export class CRBrowser extends Browser {
}
private _didDisconnect() {
for (const crPage of this._crPages.values())
for (const crPage of this._crPages.values()) {
crPage.didClose();
}
this._crPages.clear();
for (const backgroundPage of this._backgroundPages.values())
for (const backgroundPage of this._backgroundPages.values()) {
backgroundPage.didClose();
}
this._backgroundPages.clear();
for (const serviceWorker of this._serviceWorkers.values())
for (const serviceWorker of this._serviceWorkers.values()) {
serviceWorker.didClose();
}
this._serviceWorkers.clear();
this._didClose();
}
@ -243,8 +252,9 @@ export class CRBrowser extends Browser {
private _findOwningPage(frameId: string) {
for (const crPage of this._crPages.values()) {
const frame = crPage._page._frameManager.frame(frameId);
if (frame)
if (frame) {
return crPage;
}
}
return null;
}
@ -261,18 +271,22 @@ export class CRBrowser extends Browser {
let originPage = page._page.initializedOrUndefined();
// If it's a new window download, report it on the opener page.
if (!originPage && page._opener)
if (!originPage && page._opener) {
originPage = page._opener._page.initializedOrUndefined();
if (!originPage)
}
if (!originPage) {
return;
}
this._downloadCreated(originPage, payload.guid, payload.url, payload.suggestedFilename);
}
_onDownloadProgress(payload: any) {
if (payload.state === 'completed')
if (payload.state === 'completed') {
this._downloadFinished(payload.guid, '');
if (payload.state === 'canceled')
}
if (payload.state === 'canceled') {
this._downloadFinished(payload.guid, this._closeReason || 'canceled');
}
}
async _closePage(crPage: CRPage) {
@ -298,8 +312,9 @@ export class CRBrowser extends Browser {
categories = defaultCategories,
} = options;
if (screenshots)
if (screenshots) {
categories.push('disabled-by-default-devtools.screenshot');
}
this._tracingRecording = true;
await this._tracingClient.send('Tracing.start', {
@ -327,8 +342,9 @@ export class CRBrowser extends Browser {
}
async _clientRootSession(): Promise<CDPSession> {
if (!this._clientRootSessionPromise)
if (!this._clientRootSessionPromise) {
this._clientRootSessionPromise = this._connection.createBrowserSession();
}
return this._clientRootSessionPromise;
}
}
@ -380,13 +396,15 @@ export class CRBrowserContext extends BrowserContext {
// heuristic assuming that there is only one page created at a time.
const newKeys = new Set(this._browser._crPages.keys());
// Remove old keys.
for (const key of oldKeys)
for (const key of oldKeys) {
newKeys.delete(key);
}
// Remove potential concurrent popups.
for (const key of newKeys) {
const page = this._browser._crPages.get(key)!;
if (page._opener)
if (page._opener) {
newKeys.delete(key);
}
}
assert(newKeys.size === 1);
[targetId] = [...newKeys];
@ -437,8 +455,9 @@ export class CRBrowserContext extends BrowserContext {
]);
const filtered = permissions.map(permission => {
const protocolPermission = webPermissionToProtocol.get(permission);
if (!protocolPermission)
if (!protocolPermission) {
throw new Error('Unknown permission: ' + permission);
}
return protocolPermission;
});
await this._browser._session.send('Browser.grantPermissions', { origin: origin === '*' ? undefined : origin, browserContextId: this._browserContextId, permissions: filtered });
@ -451,56 +470,68 @@ export class CRBrowserContext extends BrowserContext {
async setGeolocation(geolocation?: types.Geolocation): Promise<void> {
verifyGeolocation(geolocation);
this._options.geolocation = geolocation;
for (const page of this.pages())
for (const page of this.pages()) {
await (page._delegate as CRPage).updateGeolocation();
}
}
async setExtraHTTPHeaders(headers: types.HeadersArray): Promise<void> {
this._options.extraHTTPHeaders = headers;
for (const page of this.pages())
for (const page of this.pages()) {
await (page._delegate as CRPage).updateExtraHTTPHeaders();
for (const sw of this.serviceWorkers())
}
for (const sw of this.serviceWorkers()) {
await (sw as CRServiceWorker).updateExtraHTTPHeaders();
}
}
async setUserAgent(userAgent: string | undefined): Promise<void> {
this._options.userAgent = userAgent;
for (const page of this.pages())
for (const page of this.pages()) {
await (page._delegate as CRPage).updateUserAgent();
}
// TODO: service workers don't have Emulation domain?
}
async setOffline(offline: boolean): Promise<void> {
this._options.offline = offline;
for (const page of this.pages())
for (const page of this.pages()) {
await (page._delegate as CRPage).updateOffline();
for (const sw of this.serviceWorkers())
}
for (const sw of this.serviceWorkers()) {
await (sw as CRServiceWorker).updateOffline();
}
}
async doSetHTTPCredentials(httpCredentials?: types.Credentials): Promise<void> {
this._options.httpCredentials = httpCredentials;
for (const page of this.pages())
for (const page of this.pages()) {
await (page._delegate as CRPage).updateHttpCredentials();
for (const sw of this.serviceWorkers())
}
for (const sw of this.serviceWorkers()) {
await (sw as CRServiceWorker).updateHttpCredentials();
}
}
async doAddInitScript(initScript: InitScript) {
for (const page of this.pages())
for (const page of this.pages()) {
await (page._delegate as CRPage).addInitScript(initScript);
}
}
async doRemoveNonInternalInitScripts() {
for (const page of this.pages())
for (const page of this.pages()) {
await (page._delegate as CRPage).removeNonInternalInitScripts();
}
}
async doUpdateRequestInterception(): Promise<void> {
for (const page of this.pages())
for (const page of this.pages()) {
await (page._delegate as CRPage).updateRequestInterception();
for (const sw of this.serviceWorkers())
}
for (const sw of this.serviceWorkers()) {
await (sw as CRServiceWorker).updateRequestInterception();
}
}
async doClose(reason: string | undefined) {
@ -525,8 +556,9 @@ export class CRBrowserContext extends BrowserContext {
await this._browser._session.send('Target.disposeBrowserContext', { browserContextId: this._browserContextId });
this._browser._contexts.delete(this._browserContextId);
for (const [targetId, serviceWorker] of this._browser._serviceWorkers) {
if (serviceWorker._browserContext !== this)
if (serviceWorker._browserContext !== this) {
continue;
}
// When closing a browser context, service workers are shutdown
// asynchronously and we get detached from them later.
// To avoid the wrong order of notifications, we manually fire
@ -552,8 +584,9 @@ export class CRBrowserContext extends BrowserContext {
}
override async clearCache(): Promise<void> {
for (const page of this._crPages())
for (const page of this._crPages()) {
await page._networkManager.clearCache();
}
}
async cancelDownload(guid: string) {
@ -569,8 +602,9 @@ export class CRBrowserContext extends BrowserContext {
backgroundPages(): Page[] {
const result: Page[] = [];
for (const backgroundPage of this._browser._backgroundPages.values()) {
if (backgroundPage._browserContext === this && backgroundPage._page.initializedOrUndefined())
if (backgroundPage._browserContext === this && backgroundPage._page.initializedOrUndefined()) {
result.push(backgroundPage._page);
}
}
return result;
}
@ -585,8 +619,9 @@ export class CRBrowserContext extends BrowserContext {
targetId = (page._delegate as CRPage)._targetId;
} else if (page instanceof Frame) {
const session = (page._page._delegate as CRPage)._sessions.get(page._id);
if (!session)
if (!session) {
throw new Error(`This frame does not have a separate CDP session, it is a part of the parent frame's session`);
}
targetId = session._targetId;
} else {
throw new Error('page: expected Page or Frame');

View file

@ -59,8 +59,9 @@ export class CRConnection extends EventEmitter {
_rawSend(sessionId: string, method: string, params: any): number {
const id = ++this._lastId;
const message: ProtocolRequest = { id, method, params };
if (sessionId)
if (sessionId) {
message.sessionId = sessionId;
}
this._protocolLogger('send', message);
this._transport.send(message);
return id;
@ -68,11 +69,13 @@ export class CRConnection extends EventEmitter {
async _onMessage(message: ProtocolResponse) {
this._protocolLogger('receive', message);
if (message.id === kBrowserCloseMessageId)
if (message.id === kBrowserCloseMessageId) {
return;
}
const session = this._sessions.get(message.sessionId || '');
if (session)
if (session) {
session._onMessage(message);
}
}
_onClose(reason?: string) {
@ -85,8 +88,9 @@ export class CRConnection extends EventEmitter {
}
close() {
if (!this._closed)
if (!this._closed) {
this._transport.close();
}
}
async createBrowserSession(): Promise<CDPSession> {
@ -140,8 +144,9 @@ export class CRSession extends EventEmitter {
method: T,
params?: Protocol.CommandParameters[T]
): Promise<Protocol.CommandReturnValues[T]> {
if (this._crashed || this._closed || this._connection._closed || this._connection._browserDisconnectedLogs)
if (this._crashed || this._closed || this._connection._closed || this._connection._browserDisconnectedLogs) {
throw new ProtocolError(this._crashed ? 'crashed' : 'closed', undefined, this._connection._browserDisconnectedLogs);
}
const id = this._connection._rawSend(this._sessionId, method, params);
return new Promise((resolve, reject) => {
this._callbacks.set(id, { resolve, reject, error: new ProtocolError('error', method) });
@ -165,20 +170,23 @@ export class CRSession extends EventEmitter {
} else if (object.id && object.error?.code === -32001) {
// Message to a closed session, just ignore it.
} else {
assert(!object.id, object?.error?.message || undefined);
assert(!object.id, object.error?.message || undefined);
Promise.resolve().then(() => {
if (this._eventListener)
if (this._eventListener) {
this._eventListener(object.method!, object.params);
}
this.emit(object.method!, object.params);
});
}
}
async detach() {
if (this._closed)
if (this._closed) {
throw new Error(`Session already detached. Most likely the page has been closed.`);
if (!this._parentSession)
}
if (!this._parentSession) {
throw new Error('Root session cannot be closed');
}
// Ideally, detaching should resume any target, but there is a bug in the backend,
// so we must Runtime.runIfWaitingForDebugger first.
await this._sendMayFail('Runtime.runIfWaitingForDebugger');
@ -214,8 +222,9 @@ export class CDPSession extends EventEmitter {
this.guid = `cdp-session@${sessionId}`;
this._session = parentSession.createChildSession(sessionId, (method, params) => this.emit(CDPSession.Events.Event, { method, params }));
this._listeners = [eventsHelper.addEventListener(parentSession, 'Target.detachedFromTarget', (event: Protocol.Target.detachedFromTargetPayload) => {
if (event.sessionId === sessionId)
if (event.sessionId === sessionId) {
this._onClose();
}
})];
}

View file

@ -95,8 +95,9 @@ class JSCoverage {
}
_onExecutionContextsCleared() {
if (!this._resetOnNavigation)
if (!this._resetOnNavigation) {
return;
}
this._scriptIds.clear();
this._scriptSources.clear();
}
@ -104,12 +105,14 @@ class JSCoverage {
async _onScriptParsed(event: Protocol.Debugger.scriptParsedPayload) {
this._scriptIds.add(event.scriptId);
// Ignore other anonymous scripts unless the reportAnonymousScripts option is true.
if (!event.url && !this._reportAnonymousScripts)
if (!event.url && !this._reportAnonymousScripts) {
return;
}
// This might fail if the page has already navigated away.
const response = await this._client._sendMayFail('Debugger.getScriptSource', { scriptId: event.scriptId });
if (response)
if (response) {
this._scriptSources.set(event.scriptId, response.scriptSource);
}
}
async stop(): Promise<channels.PageStopJSCoverageResult> {
@ -125,15 +128,18 @@ class JSCoverage {
const coverage: channels.PageStopJSCoverageResult = { entries: [] };
for (const entry of profileResponse.result) {
if (!this._scriptIds.has(entry.scriptId))
if (!this._scriptIds.has(entry.scriptId)) {
continue;
if (!entry.url && !this._reportAnonymousScripts)
}
if (!entry.url && !this._reportAnonymousScripts) {
continue;
}
const source = this._scriptSources.get(entry.scriptId);
if (source)
if (source) {
coverage.entries.push({ ...entry, source });
else
} else {
coverage.entries.push(entry);
}
}
return coverage;
}
@ -175,8 +181,9 @@ class CSSCoverage {
}
_onExecutionContextsCleared() {
if (!this._resetOnNavigation)
if (!this._resetOnNavigation) {
return;
}
this._stylesheetURLs.clear();
this._stylesheetSources.clear();
}
@ -184,8 +191,9 @@ class CSSCoverage {
async _onStyleSheet(event: Protocol.CSS.styleSheetAddedPayload) {
const header = event.header;
// Ignore anonymous scripts
if (!header.sourceURL)
if (!header.sourceURL) {
return;
}
// This might fail if the page has already navigated away.
const response = await this._client._sendMayFail('CSS.getStyleSheetText', { styleSheetId: header.styleSheetId });
if (response) {
@ -243,16 +251,19 @@ function convertToDisjointRanges(nestedRanges: {
// Sort points to form a valid parenthesis sequence.
points.sort((a, b) => {
// Sort with increasing offsets.
if (a.offset !== b.offset)
if (a.offset !== b.offset) {
return a.offset - b.offset;
}
// All "end" points should go before "start" points.
if (a.type !== b.type)
if (a.type !== b.type) {
return b.type - a.type;
}
const aLength = a.range.endOffset - a.range.startOffset;
const bLength = b.range.endOffset - b.range.startOffset;
// For two "start" points, the one with longer range goes first.
if (a.type === 0)
if (a.type === 0) {
return bLength - aLength;
}
// For two "end" points, the one with shorter range goes first.
return aLength - bLength;
});
@ -264,16 +275,18 @@ function convertToDisjointRanges(nestedRanges: {
for (const point of points) {
if (hitCountStack.length && lastOffset < point.offset && hitCountStack[hitCountStack.length - 1] > 0) {
const lastResult = results.length ? results[results.length - 1] : null;
if (lastResult && lastResult.end === lastOffset)
if (lastResult && lastResult.end === lastOffset) {
lastResult.end = point.offset;
else
} else {
results.push({ start: lastOffset, end: point.offset });
}
}
lastOffset = point.offset;
if (point.type === 0)
if (point.type === 0) {
hitCountStack.push(point.range.count);
else
} else {
hitCountStack.pop();
}
}
// Filter out empty ranges.
return results.filter(range => range.end - range.start > 1);

View file

@ -34,12 +34,14 @@ export class CRDevTools {
install(session: CRSession) {
session.on('Runtime.bindingCalled', async event => {
if (event.name !== kBindingName)
if (event.name !== kBindingName) {
return;
}
const parsed = JSON.parse(event.payload);
let result = undefined;
if (this.__testHookOnBinding)
if (this.__testHookOnBinding) {
this.__testHookOnBinding(parsed);
}
if (parsed.method === 'getPreferences') {
if (this._prefs === undefined) {
try {

View file

@ -34,8 +34,9 @@ export class DragManager {
}
async cancelDrag() {
if (!this._dragState)
if (!this._dragState) {
return false;
}
await this._crPage._mainFrameSession._client.send('Input.dispatchDragEvent', {
type: 'dragCancel',
x: this._lastPosition.x,
@ -61,8 +62,9 @@ export class DragManager {
});
return;
}
if (button !== 'left')
if (button !== 'left') {
return moveCallback();
}
const client = this._crPage._mainFrameSession._client;
let onDragIntercepted: (payload: Protocol.Input.dragInterceptedPayload) => void;

View file

@ -38,8 +38,9 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
contextId: this._contextId,
returnByValue: true,
}).catch(rewriteError);
if (exceptionDetails)
if (exceptionDetails) {
throw new js.JavaScriptErrorInEvaluate(getExceptionMessage(exceptionDetails));
}
return remoteObject.value;
}
@ -48,8 +49,9 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
expression,
contextId: this._contextId,
}).catch(rewriteError);
if (exceptionDetails)
if (exceptionDetails) {
throw new js.JavaScriptErrorInEvaluate(getExceptionMessage(exceptionDetails));
}
return remoteObject.objectId!;
}
@ -66,8 +68,9 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
awaitPromise: true,
userGesture: true
}).catch(rewriteError);
if (exceptionDetails)
if (exceptionDetails) {
throw new js.JavaScriptErrorInEvaluate(getExceptionMessage(exceptionDetails));
}
return returnByValue ? parseEvaluationResultValue(remoteObject.value) : utilityScript._context.createHandle(remoteObject);
}
@ -78,8 +81,9 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
});
const result = new Map();
for (const property of response.result) {
if (!property.enumerable || !property.value)
if (!property.enumerable || !property.value) {
continue;
}
result.set(property.name, context.createHandle(property.value));
}
return result;
@ -95,15 +99,19 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
}
function rewriteError(error: Error): Protocol.Runtime.evaluateReturnValue {
if (error.message.includes('Object reference chain is too long'))
if (error.message.includes('Object reference chain is too long')) {
throw new Error('Cannot serialize result: object reference chain is too long.');
if (error.message.includes('Object couldn\'t be returned by value'))
}
if (error.message.includes('Object couldn\'t be returned by value')) {
return { result: { type: 'undefined' } };
}
if (error instanceof TypeError && error.message.startsWith('Converting circular structure to JSON'))
if (error instanceof TypeError && error.message.startsWith('Converting circular structure to JSON')) {
rewriteErrorMessage(error, error.message + ' Are you passing a nested JSHandle?');
if (!js.isJavaScriptErrorInEvaluate(error) && !isSessionClosedError(error))
}
if (!js.isJavaScriptErrorInEvaluate(error) && !isSessionClosedError(error)) {
throw new Error('Execution context was destroyed, most likely because of a navigation.');
}
throw error;
}
@ -114,20 +122,25 @@ function potentiallyUnserializableValue(remoteObject: Protocol.Runtime.RemoteObj
}
function renderPreview(object: Protocol.Runtime.RemoteObject): string | undefined {
if (object.type === 'undefined')
if (object.type === 'undefined') {
return 'undefined';
if ('value' in object)
}
if ('value' in object) {
return String(object.value);
if (object.unserializableValue)
}
if (object.unserializableValue) {
return String(object.unserializableValue);
}
if (object.description === 'Object' && object.preview) {
const tokens = [];
for (const { name, value } of object.preview.properties)
for (const { name, value } of object.preview.properties) {
tokens.push(`${name}: ${value}`);
}
return `{${tokens.join(', ')}}`;
}
if (object.subtype === 'array' && object.preview)
if (object.subtype === 'array' && object.preview) {
return js.sparseArrayToString(object.preview.properties);
}
return object.description;
}

View file

@ -32,18 +32,21 @@ export class RawKeyboardImpl implements input.RawKeyboard {
) { }
_commandsForCode(code: string, modifiers: Set<types.KeyboardModifier>) {
if (!this._isMac)
if (!this._isMac) {
return [];
}
const parts = [];
for (const modifier of (['Shift', 'Control', 'Alt', 'Meta']) as types.KeyboardModifier[]) {
if (modifiers.has(modifier))
if (modifiers.has(modifier)) {
parts.push(modifier);
}
}
parts.push(code);
const shortcut = parts.join('+');
let commands = macEditingCommands[shortcut] || [];
if (isString(commands))
if (isString(commands)) {
commands = [commands];
}
// Commands that insert text are not supported
commands = commands.filter(x => !x.startsWith('insert'));
// remove the trailing : to match the Chromium command names.
@ -51,8 +54,9 @@ export class RawKeyboardImpl implements input.RawKeyboard {
}
async keydown(modifiers: Set<types.KeyboardModifier>, code: string, keyCode: number, keyCodeWithoutLocation: number, key: string, location: number, autoRepeat: boolean, text: string | undefined): Promise<void> {
if (code === 'Escape' && await this._dragManger.cancelDrag())
if (code === 'Escape' && await this._dragManger.cancelDrag()) {
return;
}
const commands = this._commandsForCode(code, modifiers);
await this._client.send('Input.dispatchKeyEvent', {
type: text ? 'keyDown' : 'rawKeyDown',
@ -116,8 +120,9 @@ export class RawMouseImpl implements input.RawMouse {
}
async down(x: number, y: number, button: types.MouseButton, buttons: Set<types.MouseButton>, modifiers: Set<types.KeyboardModifier>, clickCount: number): Promise<void> {
if (this._dragManager.isDragging())
if (this._dragManager.isDragging()) {
return;
}
await this._client.send('Input.dispatchMouseEvent', {
type: 'mousePressed',
button,

View file

@ -92,19 +92,22 @@ export class CRNetworkManager {
removeSession(session: CRSession) {
const info = this._sessions.get(session);
if (info)
if (info) {
eventsHelper.removeEventListeners(info.eventListeners);
}
this._sessions.delete(session);
}
private async _forEachSession(cb: (sessionInfo: SessionInfo) => Promise<any>) {
await Promise.all([...this._sessions.values()].map(info => {
if (info.isMain)
if (info.isMain) {
return cb(info);
}
return cb(info).catch(e => {
// Broadcasting a message to the closed target should be a noop.
if (isSessionClosedError(e))
if (isSessionClosedError(e)) {
return;
}
throw e;
});
}));
@ -116,18 +119,21 @@ export class CRNetworkManager {
}
async setOffline(offline: boolean) {
if (offline === this._offline)
if (offline === this._offline) {
return;
}
this._offline = offline;
await this._forEachSession(info => this._setOfflineForSession(info));
}
private async _setOfflineForSession(info: SessionInfo, initial?: boolean) {
if (initial && !this._offline)
if (initial && !this._offline) {
return;
}
// Workers are affected by the owner frame's Network.emulateNetworkConditions.
if (info.workerFrame)
if (info.workerFrame) {
return;
}
await info.session.send('Network.emulateNetworkConditions', {
offline: this._offline,
// values of 0 remove any active throttling. crbug.com/456324#c9
@ -144,37 +150,42 @@ export class CRNetworkManager {
async _updateProtocolRequestInterception() {
const enabled = this._userRequestInterceptionEnabled || !!this._credentials;
if (enabled === this._protocolRequestInterceptionEnabled)
if (enabled === this._protocolRequestInterceptionEnabled) {
return;
}
this._protocolRequestInterceptionEnabled = enabled;
await this._forEachSession(info => this._updateProtocolRequestInterceptionForSession(info));
}
private async _updateProtocolRequestInterceptionForSession(info: SessionInfo, initial?: boolean) {
const enabled = this._protocolRequestInterceptionEnabled;
if (initial && !enabled)
if (initial && !enabled) {
return;
}
const cachePromise = info.session.send('Network.setCacheDisabled', { cacheDisabled: enabled });
let fetchPromise = Promise.resolve<any>(undefined);
if (!info.workerFrame) {
if (enabled)
if (enabled) {
fetchPromise = info.session.send('Fetch.enable', { handleAuthRequests: true, patterns: [{ urlPattern: '*', requestStage: 'Request' }] });
else
} else {
fetchPromise = info.session.send('Fetch.disable');
}
}
await Promise.all([cachePromise, fetchPromise]);
}
async setExtraHTTPHeaders(extraHTTPHeaders: types.HeadersArray) {
if (!this._extraHTTPHeaders.length && !extraHTTPHeaders.length)
if (!this._extraHTTPHeaders.length && !extraHTTPHeaders.length) {
return;
}
this._extraHTTPHeaders = extraHTTPHeaders;
await this._forEachSession(info => this._setExtraHTTPHeadersForSession(info));
}
private async _setExtraHTTPHeadersForSession(info: SessionInfo, initial?: boolean) {
if (initial && !this._extraHTTPHeaders.length)
if (initial && !this._extraHTTPHeaders.length) {
return;
}
await info.session.send('Network.setExtraHTTPHeaders', { headers: headersArrayToObject(this._extraHTTPHeaders, false /* lowerCase */) });
}
@ -182,10 +193,12 @@ export class CRNetworkManager {
await this._forEachSession(async info => {
// Sending 'Network.setCacheDisabled' with 'cacheDisabled = true' will clear the MemoryCache.
await info.session.send('Network.setCacheDisabled', { cacheDisabled: true });
if (!this._protocolRequestInterceptionEnabled)
if (!this._protocolRequestInterceptionEnabled) {
await info.session.send('Network.setCacheDisabled', { cacheDisabled: false });
if (!info.workerFrame)
}
if (!info.workerFrame) {
await info.session.send('Network.clearBrowserCache');
}
});
}
@ -230,8 +243,9 @@ export class CRNetworkManager {
}
_shouldProvideCredentials(url: string): boolean {
if (!this._credentials)
if (!this._credentials) {
return false;
}
return !this._credentials.origin || new URL(url).origin.toLowerCase() === this._credentials.origin.toLowerCase();
}
@ -242,8 +256,9 @@ export class CRNetworkManager {
sessionInfo.session._sendMayFail('Fetch.continueRequest', { requestId: event.requestId });
return;
}
if (event.request.url.startsWith('data:'))
if (event.request.url.startsWith('data:')) {
return;
}
const requestId = event.networkId;
const requestWillBeSentEvent = this._requestIdToRequestWillBeSentEvent.get(requestId);
@ -274,8 +289,9 @@ export class CRNetworkManager {
}
_onRequest(requestWillBeSentSessionInfo: SessionInfo, requestWillBeSentEvent: Protocol.Network.requestWillBeSentPayload, requestPausedSessionInfo: SessionInfo | undefined, requestPausedEvent: Protocol.Fetch.requestPausedPayload | undefined) {
if (requestWillBeSentEvent.request.url.startsWith('data:'))
if (requestWillBeSentEvent.request.url.startsWith('data:')) {
return;
}
let redirectedFrom: InterceptableRequest | null = null;
if (requestWillBeSentEvent.redirectResponse) {
const request = this._requestIdToRequest.get(requestWillBeSentEvent.requestId);
@ -289,11 +305,12 @@ export class CRNetworkManager {
// Requests from workers lack frameId, because we receive Network.requestWillBeSent
// on the worker target. However, we receive Fetch.requestPaused on the page target,
// and lack workerFrame there. Luckily, Fetch.requestPaused provides a frameId.
if (!frame && this._page && requestPausedEvent && requestPausedEvent.frameId)
if (!frame && this._page && requestPausedEvent && requestPausedEvent.frameId) {
frame = this._page._frameManager.frame(requestPausedEvent.frameId);
}
// Check if it's main resource request interception (targetId === main frame id).
if (!frame && this._page && requestWillBeSentEvent.frameId === (this._page?._delegate as CRPage)._targetId) {
if (!frame && this._page && requestWillBeSentEvent.frameId === (this._page._delegate as CRPage)._targetId) {
// Main resource request for the page is being intercepted so the Frame is not created
// yet. Precreate it here for the purposes of request interception. It will be updated
// later as soon as the request continues and we receive frame tree from the page.
@ -312,8 +329,9 @@ export class CRNetworkManager {
{ name: 'Access-Control-Allow-Methods', value: requestHeaders['Access-Control-Request-Method'] || 'GET, POST, OPTIONS, DELETE' },
{ name: 'Access-Control-Allow-Credentials', value: 'true' }
];
if (requestHeaders['Access-Control-Request-Headers'])
if (requestHeaders['Access-Control-Request-Headers']) {
responseHeaders.push({ name: 'Access-Control-Allow-Headers', value: requestHeaders['Access-Control-Request-Headers'] });
}
requestPausedSessionInfo!.session._sendMayFail('Fetch.fulfillRequest', {
requestId: requestPausedEvent.requestId,
responseCode: 204,
@ -326,8 +344,9 @@ export class CRNetworkManager {
// Non-service-worker requests MUST have a frame—if they don't, we pretend there was no request
if (!frame && !this._serviceWorker) {
if (requestPausedEvent)
requestPausedSessionInfo!.session._sendMayFail('Fetch.continueRequest', { requestId: requestPausedEvent.requestId });
if (requestPausedEvent) {
requestPausedSessionInfo!.session._sendMayFail('Fetch.continueRequest', { requestId: requestPausedEvent.requestId });
}
return;
}
@ -375,12 +394,14 @@ export class CRNetworkManager {
const session = request.session;
const response = await session.send('Network.getResponseBody', { requestId: request._requestId });
if (response.body || !expectedLength)
if (response.body || !expectedLength) {
return Buffer.from(response.body, response.base64Encoded ? 'base64' : 'utf8');
}
// Make sure no network requests sent while reading the body for fulfilled requests.
if (request._route?._fulfilled)
if (request._route?._fulfilled) {
return Buffer.from('');
}
// For <link prefetch we are going to receive empty body with non-empty content-length expectation. Reach out for the actual content.
const resource = await session.send('Network.loadNetworkResource', { url: request.request.url(), frameId: this._serviceWorker ? undefined : request.request.frame()!._id, options: { disableCache: false, includeCredentials: true } });
@ -421,7 +442,7 @@ export class CRNetworkManager {
};
}
const response = new network.Response(request.request, responsePayload.status, responsePayload.statusText, headersObjectToArray(responsePayload.headers), timing, getResponseBody, !!responsePayload.fromServiceWorker, responsePayload.protocol);
if (responsePayload?.remoteIPAddress && typeof responsePayload?.remotePort === 'number') {
if (responsePayload.remoteIPAddress && typeof responsePayload.remotePort === 'number') {
response._serverAddrFinished({
ipAddress: responsePayload.remoteIPAddress,
port: responsePayload.remotePort,
@ -430,11 +451,11 @@ export class CRNetworkManager {
response._serverAddrFinished();
}
response._securityDetailsFinished({
protocol: responsePayload?.securityDetails?.protocol,
subjectName: responsePayload?.securityDetails?.subjectName,
issuer: responsePayload?.securityDetails?.issuer,
validFrom: responsePayload?.securityDetails?.validFrom,
validTo: responsePayload?.securityDetails?.validTo,
protocol: responsePayload.securityDetails?.protocol,
subjectName: responsePayload.securityDetails?.subjectName,
issuer: responsePayload.securityDetails?.issuer,
validFrom: responsePayload.securityDetails?.validFrom,
validTo: responsePayload.securityDetails?.validTo,
});
this._responseExtraInfoTracker.processResponse(request._requestId, response, hasExtraInfo);
return response;
@ -442,8 +463,9 @@ export class CRNetworkManager {
_deleteRequest(request: InterceptableRequest) {
this._requestIdToRequest.delete(request._requestId);
if (request._interceptionId)
if (request._interceptionId) {
this._attemptedAuthentications.delete(request._interceptionId);
}
}
_handleRequestRedirect(request: InterceptableRequest, responsePayload: Protocol.Network.Response, timestamp: number, hasExtraInfo: boolean) {
@ -474,8 +496,9 @@ export class CRNetworkManager {
}
}
// FileUpload sends a response without a matching request.
if (!request)
if (!request) {
return;
}
const response = this._createResponse(request, event.response, event.hasExtraInfo);
(this._page?._frameManager || this._serviceWorker)!.requestReceivedResponse(response);
}
@ -486,8 +509,9 @@ export class CRNetworkManager {
const request = this._requestIdToRequest.get(event.requestId);
// For certain requestIds we never receive requestWillBeSent event.
// @see https://crbug.com/750469
if (!request)
if (!request) {
return;
}
this._maybeUpdateOOPIFMainRequest(sessionInfo, request);
// Under certain conditions we never get the Network.responseReceived
@ -521,8 +545,9 @@ export class CRNetworkManager {
// For certain requestIds we never receive requestWillBeSent event.
// @see https://crbug.com/750469
if (!request)
if (!request) {
return;
}
this._maybeUpdateOOPIFMainRequest(sessionInfo, request);
const response = request.request._existingResponse();
if (response) {
@ -542,8 +567,9 @@ export class CRNetworkManager {
// OOPIF has a main request that starts in the parent session but finishes in the child session.
// We check for the main request by matching loaderId and requestId, and if it now belongs to
// a child session, migrate it there.
if (request.session !== sessionInfo.session && !sessionInfo.isMain && request._documentId === request._requestId)
if (request.session !== sessionInfo.session && !sessionInfo.isMain && request._documentId === request._requestId) {
request.session = sessionInfo.session;
}
}
}
@ -591,8 +617,9 @@ class InterceptableRequest {
const type = (requestWillBeSentEvent.type || '').toLowerCase();
let postDataBuffer = null;
const entries = postDataEntries?.filter(entry => entry.bytes);
if (entries && entries.length)
if (entries && entries.length) {
postDataBuffer = Buffer.concat(entries.map(entry => Buffer.from(entry.bytes!, 'base64')));
}
this.request = new network.Request(context, frame, serviceWorker, redirectedFrom?.request || null, documentId, url, type, method, postDataBuffer, headersOverride || headersObjectToArray(headers));
}
@ -656,21 +683,24 @@ async function catchDisallowedErrors(callback: () => Promise<void>) {
try {
return await callback();
} catch (e) {
if (isProtocolError(e) && e.message.includes('Invalid http status code or phrase'))
if (isProtocolError(e) && e.message.includes('Invalid http status code or phrase')) {
throw e;
}
}
}
function splitSetCookieHeader(headers: types.HeadersArray): types.HeadersArray {
const index = headers.findIndex(({ name }) => name.toLowerCase() === 'set-cookie');
if (index === -1)
if (index === -1) {
return headers;
}
const header = headers[index];
const values = header.value.split('\n');
if (values.length === 1)
if (values.length === 1) {
return headers;
}
const result = headers.slice();
result.splice(index, 1, ...values.map(value => ({ name: header.name, value })));
return result;
@ -767,16 +797,18 @@ class ResponseExtraInfoTracker {
loadingFinished(event: Protocol.Network.loadingFinishedPayload) {
const info = this._requests.get(event.requestId);
if (!info)
if (!info) {
return;
}
info.loadingFinished = event;
this._checkFinished(info);
}
loadingFailed(event: Protocol.Network.loadingFailedPayload) {
const info = this._requests.get(event.requestId);
if (!info)
if (!info) {
return;
}
info.loadingFailed = event;
this._checkFinished(info);
}
@ -811,8 +843,9 @@ class ResponseExtraInfoTracker {
}
private _checkFinished(info: RequestInfo) {
if (!info.loadingFinished && !info.loadingFailed)
if (!info.loadingFinished && !info.loadingFailed) {
return;
}
if (info.responses.length <= info.responseReceivedExtraInfo.length) {
// We have extra info for each response.

View file

@ -103,8 +103,9 @@ export class CRPage implements PageDelegate {
if (opener && !browserContext._options.noDefaultViewport) {
const features = opener._nextWindowOpenPopupFeatures.shift() || [];
const viewportSize = helper.getViewportSizeFromWindowFeatures(features);
if (viewportSize)
if (viewportSize) {
this._page._emulatedSize = { viewport: viewportSize, screen: viewportSize };
}
}
const createdEvent = this._isBackgroundPage ? CRBrowserContext.CREvents.BackgroundPage : BrowserContext.Events.Page;
@ -116,12 +117,14 @@ export class CRPage implements PageDelegate {
private async _forAllFrameSessions(cb: (frame: FrameSession) => Promise<any>) {
const frameSessions = Array.from(this._sessions.values());
await Promise.all(frameSessions.map(frameSession => {
if (frameSession._isMainFrame())
if (frameSession._isMainFrame()) {
return cb(frameSession);
}
return cb(frameSession).catch(e => {
// Broadcasting a message to the closed iframe should be a noop.
if (isSessionClosedError(e))
if (isSessionClosedError(e)) {
return;
}
throw e;
});
}));
@ -131,8 +134,9 @@ export class CRPage implements PageDelegate {
// Frame id equals target id.
while (!this._sessions.has(frame._id)) {
const parent = frame.parentFrame();
if (!parent)
if (!parent) {
throw new Error(`Frame has been detached.`);
}
frame = parent;
}
return this._sessions.get(frame._id)!;
@ -148,8 +152,9 @@ export class CRPage implements PageDelegate {
}
didClose() {
for (const session of this._sessions.values())
for (const session of this._sessions.values()) {
session.dispose();
}
this._page._didClose();
}
@ -208,8 +213,9 @@ export class CRPage implements PageDelegate {
private async _go(delta: number): Promise<boolean> {
const history = await this._mainFrameSession._client.send('Page.getNavigationHistory');
const entry = history.entries[history.currentIndex + delta];
if (!entry)
if (!entry) {
return false;
}
await this._mainFrameSession._client.send('Page.navigateToHistoryEntry', { entryId: entry.id });
return true;
}
@ -235,10 +241,11 @@ export class CRPage implements PageDelegate {
}
async closePage(runBeforeUnload: boolean): Promise<void> {
if (runBeforeUnload)
if (runBeforeUnload) {
await this._mainFrameSession._client.send('Page.close');
else
} else {
await this._browserContext._browser._closePage(this);
}
}
async setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise<void> {
@ -317,8 +324,9 @@ export class CRPage implements PageDelegate {
async setInputFilePaths(handle: dom.ElementHandle<HTMLInputElement>, files: string[]): Promise<void> {
const frame = await handle.ownerFrame();
if (!frame)
if (!frame) {
throw new Error('Cannot set input files to detached input element');
}
const parentSession = this._sessionForFrame(frame);
await parentSession._client.send('DOM.setFileInputFiles', {
objectId: handle._objectId,
@ -353,17 +361,20 @@ export class CRPage implements PageDelegate {
async getFrameElement(frame: frames.Frame): Promise<dom.ElementHandle> {
let parent = frame.parentFrame();
if (!parent)
if (!parent) {
throw new Error('Frame has been detached.');
}
const parentSession = this._sessionForFrame(parent);
const { backendNodeId } = await parentSession._client.send('DOM.getFrameOwner', { frameId: frame._id }).catch(e => {
if (e instanceof Error && e.message.includes('Frame with the given id was not found.'))
if (e instanceof Error && e.message.includes('Frame with the given id was not found.')) {
rewriteErrorMessage(e, 'Frame has been detached.');
}
throw e;
});
parent = frame.parentFrame();
if (!parent)
if (!parent) {
throw new Error('Frame has been detached.');
}
return parentSession._adoptBackendNodeId(backendNodeId, await parent._mainContext());
}
@ -401,8 +412,9 @@ class FrameSession {
this._page = crPage._page;
this._targetId = targetId;
this._parentSession = parentSession;
if (parentSession)
if (parentSession) {
parentSession._childSessions.add(this);
}
this._firstNonInitialNavigationCommittedPromise = new Promise((f, r) => {
this._firstNonInitialNavigationCommittedFulfill = f;
this._firstNonInitialNavigationCommittedReject = r;
@ -468,14 +480,16 @@ class FrameSession {
// and it is equally important to send Page.startScreencast before sending Runtime.runIfWaitingForDebugger.
await this._createVideoRecorder(screencastId, screencastOptions);
this._crPage._page.waitForInitializedOrError().then(p => {
if (p instanceof Error)
if (p instanceof Error) {
this._stopVideoRecording().catch(() => {});
}
});
}
let lifecycleEventsEnabled: Promise<any>;
if (!this._isMainFrame())
if (!this._isMainFrame()) {
this._addRendererListeners();
}
this._addBrowserListeners();
const promises: Promise<any>[] = [
this._client.send('Page.enable'),
@ -493,8 +507,9 @@ class FrameSession {
grantUniveralAccess: true,
worldName: UTILITY_WORLD_NAME,
});
for (const initScript of this._crPage._page.allInitScripts())
for (const initScript of this._crPage._page.allInitScripts()) {
frame.evaluateExpression(initScript.source).catch(e => {});
}
}
const isInitialEmptyPage = this._isMainFrame() && this._page.mainFrame().url() === ':';
@ -522,34 +537,46 @@ class FrameSession {
this._client.send('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: true, flatten: true }),
];
if (!isSettingStorageState) {
if (this._isMainFrame())
if (this._isMainFrame()) {
promises.push(this._client.send('Emulation.setFocusEmulationEnabled', { enabled: true }));
}
const options = this._crPage._browserContext._options;
if (options.bypassCSP)
if (options.bypassCSP) {
promises.push(this._client.send('Page.setBypassCSP', { enabled: true }));
if (options.ignoreHTTPSErrors || options.internalIgnoreHTTPSErrors)
}
if (options.ignoreHTTPSErrors || options.internalIgnoreHTTPSErrors) {
promises.push(this._client.send('Security.setIgnoreCertificateErrors', { ignore: true }));
if (this._isMainFrame())
}
if (this._isMainFrame()) {
promises.push(this._updateViewport());
if (options.hasTouch)
}
if (options.hasTouch) {
promises.push(this._client.send('Emulation.setTouchEmulationEnabled', { enabled: true }));
if (options.javaScriptEnabled === false)
}
if (options.javaScriptEnabled === false) {
promises.push(this._client.send('Emulation.setScriptExecutionDisabled', { value: true }));
if (options.userAgent || options.locale)
}
if (options.userAgent || options.locale) {
promises.push(this._updateUserAgent());
if (options.locale)
}
if (options.locale) {
promises.push(emulateLocale(this._client, options.locale));
if (options.timezoneId)
}
if (options.timezoneId) {
promises.push(emulateTimezone(this._client, options.timezoneId));
if (!this._crPage._browserContext._browser.options.headful)
}
if (!this._crPage._browserContext._browser.options.headful) {
promises.push(this._setDefaultFontFamilies(this._client));
}
promises.push(this._updateGeolocation(true));
promises.push(this._updateEmulateMedia());
promises.push(this._updateFileChooserInterception(true));
for (const initScript of this._crPage._page.allInitScripts())
for (const initScript of this._crPage._page.allInitScripts()) {
promises.push(this._evaluateOnNewDocument(initScript, 'main'));
if (screencastOptions)
}
if (screencastOptions) {
promises.push(this._startVideoRecording(screencastOptions));
}
}
promises.push(this._client.send('Runtime.runIfWaitingForDebugger'));
promises.push(this._firstNonInitialNavigationCommittedPromise);
@ -558,10 +585,12 @@ class FrameSession {
dispose() {
this._firstNonInitialNavigationCommittedReject(new TargetClosedError());
for (const childSession of this._childSessions)
for (const childSession of this._childSessions) {
childSession.dispose();
if (this._parentSession)
}
if (this._parentSession) {
this._parentSession._childSessions.delete(this);
}
eventsHelper.removeEventListeners(this._eventListeners);
this._crPage._networkManager.removeSession(this._client);
this._crPage._sessions.delete(this._targetId);
@ -570,35 +599,41 @@ class FrameSession {
async _navigate(frame: frames.Frame, url: string, referrer: string | undefined): Promise<frames.GotoResult> {
const response = await this._client.send('Page.navigate', { url, referrer, frameId: frame._id, referrerPolicy: 'unsafeUrl' });
if (response.errorText)
if (response.errorText) {
throw new frames.NavigationAbortedError(response.loaderId, `${response.errorText} at ${url}`);
}
return { newDocumentId: response.loaderId };
}
_onLifecycleEvent(event: Protocol.Page.lifecycleEventPayload) {
if (this._eventBelongsToStaleFrame(event.frameId))
if (this._eventBelongsToStaleFrame(event.frameId)) {
return;
if (event.name === 'load')
}
if (event.name === 'load') {
this._page._frameManager.frameLifecycleEvent(event.frameId, 'load');
else if (event.name === 'DOMContentLoaded')
} else if (event.name === 'DOMContentLoaded') {
this._page._frameManager.frameLifecycleEvent(event.frameId, 'domcontentloaded');
}
}
_handleFrameTree(frameTree: Protocol.Page.FrameTree) {
this._onFrameAttached(frameTree.frame.id, frameTree.frame.parentId || null);
this._onFrameNavigated(frameTree.frame, true);
if (!frameTree.childFrames)
if (!frameTree.childFrames) {
return;
}
for (const child of frameTree.childFrames)
for (const child of frameTree.childFrames) {
this._handleFrameTree(child);
}
}
private _eventBelongsToStaleFrame(frameId: string) {
const frame = this._page._frameManager.frame(frameId);
// Subtree may be already gone because some ancestor navigation destroyed the oopif.
if (!frame)
if (!frame) {
return true;
}
// When frame goes remote, parent process may still send some events
// related to the local frame before it sends frameDetached.
// In this case, we already have a new session for this frame, so events
@ -614,8 +649,9 @@ class FrameSession {
frameSession._swappedIn = true;
const frame = this._page._frameManager.frame(frameId);
// Frame or even a whole subtree may be already gone, because some ancestor did navigate.
if (frame)
if (frame) {
this._page._frameManager.removeChildFramesRecursively(frame);
}
return;
}
if (parentFrameId && !this._page._frameManager.frame(parentFrameId)) {
@ -629,23 +665,28 @@ class FrameSession {
}
_onFrameNavigated(framePayload: Protocol.Page.Frame, initial: boolean) {
if (this._eventBelongsToStaleFrame(framePayload.id))
if (this._eventBelongsToStaleFrame(framePayload.id)) {
return;
}
this._page._frameManager.frameCommittedNewDocumentNavigation(framePayload.id, framePayload.url + (framePayload.urlFragment || ''), framePayload.name || '', framePayload.loaderId, initial);
if (!initial)
if (!initial) {
this._firstNonInitialNavigationCommittedFulfill();
}
}
_onFrameRequestedNavigation(payload: Protocol.Page.frameRequestedNavigationPayload) {
if (this._eventBelongsToStaleFrame(payload.frameId))
if (this._eventBelongsToStaleFrame(payload.frameId)) {
return;
if (payload.disposition === 'currentTab')
}
if (payload.disposition === 'currentTab') {
this._page._frameManager.frameRequestedNavigation(payload.frameId);
}
}
_onFrameNavigatedWithinDocument(frameId: string, url: string) {
if (this._eventBelongsToStaleFrame(frameId))
if (this._eventBelongsToStaleFrame(frameId)) {
return;
}
this._page._frameManager.frameCommittedSameDocumentNavigation(frameId, url);
}
@ -661,8 +702,9 @@ class FrameSession {
// Page.frameDetached arrives before Target.attachedToTarget.
// We should keep the frame in the tree, and it will be used for the new target.
const frame = this._page._frameManager.frame(frameId);
if (frame)
if (frame) {
this._page._frameManager.removeChildFramesRecursively(frame);
}
return;
}
// Just a regular frame detach.
@ -671,32 +713,37 @@ class FrameSession {
_onExecutionContextCreated(contextPayload: Protocol.Runtime.ExecutionContextDescription) {
const frame = contextPayload.auxData ? this._page._frameManager.frame(contextPayload.auxData.frameId) : null;
if (!frame || this._eventBelongsToStaleFrame(frame._id))
if (!frame || this._eventBelongsToStaleFrame(frame._id)) {
return;
}
const delegate = new CRExecutionContext(this._client, contextPayload);
let worldName: types.World|null = null;
if (contextPayload.auxData && !!contextPayload.auxData.isDefault)
if (contextPayload.auxData && !!contextPayload.auxData.isDefault) {
worldName = 'main';
else if (contextPayload.name === UTILITY_WORLD_NAME)
} else if (contextPayload.name === UTILITY_WORLD_NAME) {
worldName = 'utility';
}
const context = new dom.FrameExecutionContext(delegate, frame, worldName);
(context as any)[contextDelegateSymbol] = delegate;
if (worldName)
if (worldName) {
frame._contextCreated(worldName, context);
}
this._contextIdToContext.set(contextPayload.id, context);
}
_onExecutionContextDestroyed(executionContextId: number) {
const context = this._contextIdToContext.get(executionContextId);
if (!context)
if (!context) {
return;
}
this._contextIdToContext.delete(executionContextId);
context.frame._contextDestroyed(context);
}
_onExecutionContextsCleared() {
for (const contextId of Array.from(this._contextIdToContext.keys()))
for (const contextId of Array.from(this._contextIdToContext.keys())) {
this._onExecutionContextDestroyed(contextId);
}
}
_onAttachedToTarget(event: Protocol.Target.attachedToTargetPayload) {
@ -706,12 +753,14 @@ class FrameSession {
// Frame id equals target id.
const targetId = event.targetInfo.targetId;
const frame = this._page._frameManager.frame(targetId);
if (!frame)
return; // Subtree may be already gone due to renderer/browser race.
if (!frame) {
return;
} // Subtree may be already gone due to renderer/browser race.
this._page._frameManager.removeChildFramesRecursively(frame);
for (const [contextId, context] of this._contextIdToContext) {
if (context.frame === frame)
if (context.frame === frame) {
this._onExecutionContextDestroyed(contextId);
}
}
const frameSession = new FrameSession(this._crPage, session, targetId, this);
this._crPage._sessions.set(targetId, frameSession);
@ -757,8 +806,9 @@ class FrameSession {
// ... or an oopif.
const childFrameSession = this._crPage._sessions.get(event.targetId!);
if (!childFrameSession)
if (!childFrameSession) {
return;
}
// Usually, we get frameAttached in this session first and mark child as swappedIn.
if (childFrameSession._swappedIn) {
@ -773,8 +823,9 @@ class FrameSession {
this._client.send('Page.enable').catch(e => null).then(() => {
// Child was not swapped in - that means frameAttached did not happen and
// this is remote detach rather than remote -> local swap.
if (!childFrameSession._swappedIn)
if (!childFrameSession._swappedIn) {
this._page._frameManager.frameDetached(event.targetId!);
}
childFrameSession.dispose();
});
}
@ -801,8 +852,9 @@ class FrameSession {
return;
}
const context = this._contextIdToContext.get(event.executionContextId);
if (!context)
if (!context) {
return;
}
const values = event.args.map(arg => context.createHandle(arg));
this._page._addConsoleMessage(event.type, values, toConsoleMessageLocation(event.stackTrace));
}
@ -811,22 +863,25 @@ class FrameSession {
const pageOrError = await this._crPage._page.waitForInitializedOrError();
if (!(pageOrError instanceof Error)) {
const context = this._contextIdToContext.get(event.executionContextId);
if (context)
if (context) {
await this._page._onBindingCalled(event.payload, context);
}
}
}
_onDialog(event: Protocol.Page.javascriptDialogOpeningPayload) {
if (!this._page._frameManager.frame(this._targetId))
return; // Our frame/subtree may be gone already.
if (!this._page._frameManager.frame(this._targetId)) {
return;
} // Our frame/subtree may be gone already.
this._page.emitOnContext(BrowserContext.Events.Dialog, new dialog.Dialog(
this._page,
event.type,
event.message,
async (accept: boolean, promptText?: string) => {
// TODO: this should actually be a CDP event that notifies about a cancelled navigation attempt.
if (this._isMainFrame() && event.type === 'beforeunload' && !accept)
if (this._isMainFrame() && event.type === 'beforeunload' && !accept) {
this._page._frameManager.frameAbortedNavigation(this._page.mainFrame()._id, 'navigation cancelled by beforeunload dialog');
}
await this._client.send('Page.handleJavaScriptDialog', { accept, promptText });
},
event.defaultPrompt));
@ -843,8 +898,9 @@ class FrameSession {
_onLogEntryAdded(event: Protocol.Log.entryAddedPayload) {
const { level, text, args, source, url, lineNumber } = event.entry;
if (args)
if (args) {
args.map(arg => releaseObject(this._client, arg.objectId!));
}
if (source !== 'worker') {
const location: types.ConsoleMessageLocation = {
url: url || '',
@ -856,11 +912,13 @@ class FrameSession {
}
async _onFileChooserOpened(event: Protocol.Page.fileChooserOpenedPayload) {
if (!event.backendNodeId)
if (!event.backendNodeId) {
return;
}
const frame = this._page._frameManager.frame(event.frameId);
if (!frame)
if (!frame) {
return;
}
let handle;
try {
const utilityContext = await frame._utilityContext();
@ -918,8 +976,9 @@ class FrameSession {
}
async _stopVideoRecording(): Promise<void> {
if (!this._screencastId)
if (!this._screencastId) {
return;
}
const screencastId = this._screencastId;
this._screencastId = null;
const recorder = this._videoRecorder!;
@ -934,30 +993,35 @@ class FrameSession {
async _startScreencast(client: any, options: Protocol.Page.startScreencastParameters = {}) {
this._screencastClients.add(client);
if (this._screencastClients.size === 1)
if (this._screencastClients.size === 1) {
await this._client.send('Page.startScreencast', options);
}
}
async _stopScreencast(client: any) {
this._screencastClients.delete(client);
if (!this._screencastClients.size)
if (!this._screencastClients.size) {
await this._client._sendMayFail('Page.stopScreencast');
}
}
async _updateGeolocation(initial: boolean): Promise<void> {
const geolocation = this._crPage._browserContext._options.geolocation;
if (!initial || geolocation)
if (!initial || geolocation) {
await this._client.send('Emulation.setGeolocationOverride', geolocation || {});
}
}
async _updateViewport(preserveWindowBoundaries?: boolean): Promise<void> {
if (this._crPage._browserContext._browser.isClank())
if (this._crPage._browserContext._browser.isClank()) {
return;
}
assert(this._isMainFrame());
const options = this._crPage._browserContext._options;
const emulatedSize = this._page.emulatedSize();
if (emulatedSize === null)
if (emulatedSize === null) {
return;
}
const viewportSize = emulatedSize.viewport;
const screenSize = emulatedSize.screen;
const isLandscape = screenSize.width > screenSize.height;
@ -973,8 +1037,9 @@ class FrameSession {
) : { angle: 0, type: 'landscapePrimary' },
dontSetVisibleSize: preserveWindowBoundaries
};
if (JSON.stringify(this._metricsOverride) === JSON.stringify(metricsOverride))
if (JSON.stringify(this._metricsOverride) === JSON.stringify(metricsOverride)) {
return;
}
const promises = [
this._client.send('Emulation.setDeviceMetricsOverride', metricsOverride),
];
@ -983,12 +1048,13 @@ class FrameSession {
if (this._crPage._browserContext._browser.options.headful) {
// TODO: popup windows have their own insets.
insets = { width: 24, height: 88 };
if (process.platform === 'win32')
if (process.platform === 'win32') {
insets = { width: 16, height: 88 };
else if (process.platform === 'linux')
} else if (process.platform === 'linux') {
insets = { width: 8, height: 85 };
else if (process.platform === 'darwin')
} else if (process.platform === 'darwin') {
insets = { width: 2, height: 80 };
}
if (this._crPage._browserContext.isPersistentContext()) {
// FIXME: Chrome bug: OOPIF router is confused when hit target is
// outside browser window.
@ -1050,16 +1116,18 @@ class FrameSession {
async _updateFileChooserInterception(initial: boolean) {
const enabled = this._page.fileChooserIntercepted();
if (initial && !enabled)
if (initial && !enabled) {
return;
}
await this._client.send('Page.setInterceptFileChooserDialog', { enabled }).catch(() => {}); // target can be closed.
}
async _evaluateOnNewDocument(initScript: InitScript, world: types.World): Promise<void> {
const worldName = world === 'utility' ? UTILITY_WORLD_NAME : undefined;
const { identifier } = await this._client.send('Page.addScriptToEvaluateOnNewDocument', { source: initScript.source, worldName });
if (!initScript.internal)
if (!initScript.internal) {
this._evaluateOnNewDocumentIdentifiers.push(identifier);
}
}
async _removeEvaluatesOnNewDocument(): Promise<void> {
@ -1072,8 +1140,9 @@ class FrameSession {
const nodeInfo = await this._client.send('DOM.describeNode', {
objectId: handle._objectId
});
if (!nodeInfo || typeof nodeInfo.node.frameId !== 'string')
if (!nodeInfo || typeof nodeInfo.node.frameId !== 'string') {
return null;
}
return this._page._frameManager.frame(nodeInfo.node.frameId);
}
@ -1081,14 +1150,17 @@ class FrameSession {
// document.documentElement has frameId of the owner frame.
const documentElement = await handle.evaluateHandle(node => {
const doc = node as Document;
if (doc.documentElement && doc.documentElement.ownerDocument === doc)
if (doc.documentElement && doc.documentElement.ownerDocument === doc) {
return doc.documentElement;
}
return node.ownerDocument ? node.ownerDocument.documentElement : null;
});
if (!documentElement)
if (!documentElement) {
return null;
if (!documentElement._objectId)
}
if (!documentElement._objectId) {
return null;
}
const nodeInfo = await this._client.send('DOM.describeNode', {
objectId: documentElement._objectId
});
@ -1102,25 +1174,29 @@ class FrameSession {
const result = await this._client._sendMayFail('DOM.getBoxModel', {
objectId: handle._objectId
});
if (!result)
if (!result) {
return null;
}
const quad = result.model.border;
const x = Math.min(quad[0], quad[2], quad[4], quad[6]);
const y = Math.min(quad[1], quad[3], quad[5], quad[7]);
const width = Math.max(quad[0], quad[2], quad[4], quad[6]) - x;
const height = Math.max(quad[1], quad[3], quad[5], quad[7]) - y;
const position = await this._framePosition();
if (!position)
if (!position) {
return null;
}
return { x: x + position.x, y: y + position.y, width, height };
}
private async _framePosition(): Promise<types.Point | null> {
const frame = this._page._frameManager.frame(this._targetId);
if (!frame)
if (!frame) {
return null;
if (frame === this._page.mainFrame())
}
if (frame === this._page.mainFrame()) {
return { x: 0, y: 0 };
}
const element = await frame.frameElement();
const box = await element.boundingBox();
return box;
@ -1131,10 +1207,12 @@ class FrameSession {
objectId: handle._objectId,
rect,
}).then(() => 'done' as const).catch(e => {
if (e instanceof Error && e.message.includes('Node does not have a layout object'))
if (e instanceof Error && e.message.includes('Node does not have a layout object')) {
return 'error:notvisible';
if (e instanceof Error && e.message.includes('Node is detached from document'))
}
if (e instanceof Error && e.message.includes('Node is detached from document')) {
return 'error:notconnected';
}
throw e;
});
}
@ -1143,11 +1221,13 @@ class FrameSession {
const result = await this._client._sendMayFail('DOM.getContentQuads', {
objectId: handle._objectId
});
if (!result)
if (!result) {
return null;
}
const position = await this._framePosition();
if (!position)
if (!position) {
return null;
}
return result.quads.map(quad => [
{ x: quad[0] + position.x, y: quad[1] + position.y },
{ x: quad[2] + position.x, y: quad[3] + position.y },
@ -1168,8 +1248,9 @@ class FrameSession {
backendNodeId,
executionContextId: ((to as any)[contextDelegateSymbol] as CRExecutionContext)._contextId,
});
if (!result || result.object.subtype === 'null')
if (!result || result.object.subtype === 'null') {
throw new Error(dom.kUnableToAdoptErrorMessage);
}
return to.createHandle(result.object).asElement()!;
}
}
@ -1181,8 +1262,9 @@ async function emulateLocale(session: CRSession, locale: string) {
// All pages in the same renderer share locale. All such pages belong to the same
// context and if locale is overridden for one of them its value is the same as
// we are trying to set so it's not a problem.
if (exception.message.includes('Another locale override is already in effect'))
if (exception.message.includes('Another locale override is already in effect')) {
return;
}
throw exception;
}
}
@ -1191,10 +1273,12 @@ async function emulateTimezone(session: CRSession, timezoneId: string) {
try {
await session.send('Emulation.setTimezoneOverride', { timezoneId: timezoneId });
} catch (exception) {
if (exception.message.includes('Timezone override is already in effect'))
if (exception.message.includes('Timezone override is already in effect')) {
return;
if (exception.message.includes('Invalid timezone'))
}
if (exception.message.includes('Invalid timezone')) {
throw new Error(`Invalid timezone ID: ${timezoneId}`);
}
throw exception;
}
}
@ -1204,8 +1288,9 @@ const contextDelegateSymbol = Symbol('delegate');
// Chromium reference: https://source.chromium.org/chromium/chromium/src/+/main:components/embedder_support/user_agent_utils.cc;l=434;drc=70a6711e08e9f9e0d8e4c48e9ba5cab62eb010c2
function calculateUserAgentMetadata(options: types.BrowserContextOptions) {
const ua = options.userAgent;
if (!ua)
if (!ua) {
return undefined;
}
const metadata: Protocol.Emulation.UserAgentMetadata = {
mobile: !!options.isMobile,
model: '',
@ -1233,15 +1318,17 @@ function calculateUserAgentMetadata(options: types.BrowserContextOptions) {
} else if (macOSMatch) {
metadata.platform = 'macOS';
metadata.platformVersion = macOSMatch[1];
if (!ua.includes('Intel'))
if (!ua.includes('Intel')) {
metadata.architecture = 'arm';
}
} else if (windowsMatch) {
metadata.platform = 'Windows';
metadata.platformVersion = windowsMatch[1];
} else if (ua.toLowerCase().includes('linux')) {
metadata.platform = 'Linux';
}
if (ua.includes('ARM'))
if (ua.includes('ARM')) {
metadata.architecture = 'arm';
}
return metadata;
}

View file

@ -42,8 +42,9 @@ const unitToPixels: { [key: string]: number } = {
};
function convertPrintParameterToInches(text: string | undefined): number | undefined {
if (text === undefined)
if (text === undefined) {
return undefined;
}
let unit = text.substring(text.length - 2).toLowerCase();
let valueText = '';
if (unitToPixels.hasOwnProperty(unit)) {

View file

@ -23,8 +23,9 @@ import { mkdirIfNeeded } from '../../utils/fileUtils';
import { splitErrorMessage } from '../../utils/stackTrace';
export function getExceptionMessage(exceptionDetails: Protocol.Runtime.ExceptionDetails): string {
if (exceptionDetails.exception)
if (exceptionDetails.exception) {
return exceptionDetails.exception.description || String(exceptionDetails.exception.value);
}
let message = exceptionDetails.text;
if (exceptionDetails.stackTrace) {
for (const callframe of exceptionDetails.stackTrace.callFrames) {
@ -98,24 +99,31 @@ export function exceptionToError(exceptionDetails: Protocol.Runtime.ExceptionDet
export function toModifiersMask(modifiers: Set<types.KeyboardModifier>): number {
let mask = 0;
if (modifiers.has('Alt'))
if (modifiers.has('Alt')) {
mask |= 1;
if (modifiers.has('Control'))
}
if (modifiers.has('Control')) {
mask |= 2;
if (modifiers.has('Meta'))
}
if (modifiers.has('Meta')) {
mask |= 4;
if (modifiers.has('Shift'))
}
if (modifiers.has('Shift')) {
mask |= 8;
}
return mask;
}
export function toButtonsMask(buttons: Set<types.MouseButton>): number {
let mask = 0;
if (buttons.has('left'))
if (buttons.has('left')) {
mask |= 1;
if (buttons.has('right'))
}
if (buttons.has('right')) {
mask |= 2;
if (buttons.has('middle'))
}
if (buttons.has('middle')) {
mask |= 4;
}
return mask;
}

View file

@ -30,8 +30,9 @@ export class CRServiceWorker extends Worker {
super(browserContext, url);
this._session = session;
this._browserContext = browserContext;
if (!!process.env.PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS)
if (!!process.env.PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS) {
this._networkManager = new CRNetworkManager(null, this);
}
session.once('Runtime.executionContextCreated', event => {
this._createExecutionContext(new CRExecutionContext(session, event.context));
});
@ -59,26 +60,30 @@ export class CRServiceWorker extends Worker {
}
async updateOffline(): Promise<void> {
if (!this._isNetworkInspectionEnabled())
if (!this._isNetworkInspectionEnabled()) {
return;
}
await this._networkManager?.setOffline(!!this._browserContext._options.offline).catch(() => {});
}
async updateHttpCredentials(): Promise<void> {
if (!this._isNetworkInspectionEnabled())
if (!this._isNetworkInspectionEnabled()) {
return;
}
await this._networkManager?.authenticate(this._browserContext._options.httpCredentials || null).catch(() => {});
}
async updateExtraHTTPHeaders(): Promise<void> {
if (!this._isNetworkInspectionEnabled())
if (!this._isNetworkInspectionEnabled()) {
return;
}
await this._networkManager?.setExtraHTTPHeaders(this._browserContext._options.extraHTTPHeaders || []).catch(() => {});
}
async updateRequestInterception(): Promise<void> {
if (!this._isNetworkInspectionEnabled())
if (!this._isNetworkInspectionEnabled()) {
return;
}
await this._networkManager?.setRequestInterception(this.needsRequestInterception()).catch(() => {});
}
@ -102,8 +107,9 @@ export class CRServiceWorker extends Worker {
this._browserContext.emit(BrowserContext.Events.Request, request);
if (route) {
const r = new network.Route(request, route);
if (this._browserContext._requestInterceptor?.(r, request))
if (this._browserContext._requestInterceptor?.(r, request)) {
return;
}
r.continue({ isFallback: true }).catch(() => {});
}
}

View file

@ -38,8 +38,9 @@ export class VideoRecorder {
private _ffmpegPath: string;
static async launch(page: Page, ffmpegPath: string, options: types.PageScreencastOptions): Promise<VideoRecorder> {
if (!options.outputFile.endsWith('.webm'))
if (!options.outputFile.endsWith('.webm')) {
throw new Error('File must have .webm extension');
}
const controller = new ProgressController(serverSideCallMetadata(), page);
controller.setLogName('browser');
@ -128,14 +129,16 @@ export class VideoRecorder {
writeFrame(frame: Buffer, timestamp: number) {
assert(this._process);
if (this._isStopped)
if (this._isStopped) {
return;
}
if (this._lastFrameBuffer) {
const durationSec = timestamp - this._lastFrameTimestamp;
const repeatCount = Math.max(1, Math.round(fps * durationSec));
for (let i = 0; i < repeatCount; ++i)
for (let i = 0; i < repeatCount; ++i) {
this._frameQueue.push(this._lastFrameBuffer);
}
this._lastWritePromise = this._lastWritePromise.then(() => this._sendFrames());
}
@ -145,20 +148,23 @@ export class VideoRecorder {
}
private async _sendFrames() {
while (this._frameQueue.length)
while (this._frameQueue.length) {
await this._sendFrame(this._frameQueue.shift()!);
}
}
private async _sendFrame(frame: Buffer) {
return new Promise(f => this._process!.stdin!.write(frame, f)).then(error => {
if (error)
if (error) {
this._progress.log(`ffmpeg failed to write: ${String(error)}`);
}
});
}
async stop() {
if (this._isStopped)
if (this._isStopped) {
return;
}
this.writeFrame(Buffer.from([]), this._lastFrameTimestamp + (monotonicTime() - this._lastWriteTimestamp) / 1000);
this._isStopped = true;
await this._lastWritePromise;

View file

@ -78,8 +78,9 @@ export class Clock {
}
private async _installIfNeeded() {
if (this._scriptInstalled)
if (this._scriptInstalled) {
return;
}
this._scriptInstalled = true;
const script = `(() => {
const module = {};
@ -101,10 +102,12 @@ export class Clock {
* to clock.tick()
*/
function parseTicks(value: number | string): number {
if (typeof value === 'number')
if (typeof value === 'number') {
return value;
if (!value)
}
if (!value) {
return 0;
}
const str = value;
const strings = str.split(':');
@ -121,8 +124,9 @@ function parseTicks(value: number | string): number {
while (i--) {
parsed = parseInt(strings[i], 10);
if (parsed >= 60)
if (parsed >= 60) {
throw new Error(`Invalid time ${str}`);
}
ms += parsed * Math.pow(60, l - i - 1);
}
@ -130,12 +134,15 @@ function parseTicks(value: number | string): number {
}
function parseTime(epoch: string | number | undefined): number {
if (!epoch)
if (!epoch) {
return 0;
if (typeof epoch === 'number')
}
if (typeof epoch === 'number') {
return epoch;
}
const parsed = new Date(epoch);
if (!isFinite(parsed.getTime()))
if (!isFinite(parsed.getTime())) {
throw new Error(`Invalid date: ${epoch}`);
}
return parsed.getTime();
}

View file

@ -48,24 +48,28 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
generateAction(actionInContext: actions.ActionInContext): string {
const action = this._generateActionInner(actionInContext);
if (action)
if (action) {
return action;
}
return '';
}
_generateActionInner(actionInContext: actions.ActionInContext): string {
const action = actionInContext.action;
if (this._mode !== 'library' && (action.name === 'openPage' || action.name === 'closePage'))
if (this._mode !== 'library' && (action.name === 'openPage' || action.name === 'closePage')) {
return '';
}
let pageAlias = actionInContext.frame.pageAlias;
if (this._mode !== 'library')
if (this._mode !== 'library') {
pageAlias = pageAlias.replace('page', 'Page');
}
const formatter = new CSharpFormatter(this._mode === 'library' ? 0 : 8);
if (action.name === 'openPage') {
formatter.add(`var ${pageAlias} = await context.NewPageAsync();`);
if (action.url && action.url !== 'about:blank' && action.url !== 'chrome://newtab/')
if (action.url && action.url !== 'about:blank' && action.url !== 'chrome://newtab/') {
formatter.add(`await ${pageAlias}.GotoAsync(${quote(action.url)});`);
}
return formatter.format();
}
@ -96,8 +100,9 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
lines.push(`});`);
}
for (const line of lines)
for (const line of lines) {
formatter.add(line);
}
return formatter.format();
}
@ -111,11 +116,13 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
return `await ${subject}.CloseAsync();`;
case 'click': {
let method = 'Click';
if (action.clickCount === 2)
if (action.clickCount === 2) {
method = 'DblClick';
}
const options = toClickOptionsForSourceCode(action);
if (!Object.entries(options).length)
if (!Object.entries(options).length) {
return `await ${subject}.${this._asLocator(action.selector)}.${method}Async();`;
}
const optionsString = formatObject(options, ' ', 'Locator' + method + 'Options');
return `await ${subject}.${this._asLocator(action.selector)}.${method}Async(${optionsString});`;
}
@ -156,8 +163,9 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
}
generateHeader(options: LanguageGeneratorOptions): string {
if (this._mode === 'library')
if (this._mode === 'library') {
return this.generateStandaloneHeader(options);
}
return this.generateTestRunnerHeader(options);
}
@ -171,8 +179,9 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
using var playwright = await Playwright.CreateAsync();
await using var browser = await playwright.${toPascal(options.browserName)}.LaunchAsync(${formatObject(options.launchOptions, ' ', 'BrowserTypeLaunchOptions')});
var context = await browser.NewContextAsync(${formatContextOptions(options.contextOptions, options.deviceName)});`);
if (options.contextOptions.recordHar)
if (options.contextOptions.recordHar) {
formatter.add(` await context.RouteFromHARAsync(${quote(options.contextOptions.recordHar.path)});`);
}
formatter.newLine();
return formatter.format();
}
@ -198,43 +207,50 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
formatter.add(` [${this._mode === 'nunit' ? 'Test' : 'TestMethod'}]
public async Task MyTest()
{`);
if (options.contextOptions.recordHar)
if (options.contextOptions.recordHar) {
formatter.add(` await context.RouteFromHARAsync(${quote(options.contextOptions.recordHar.path)});`);
}
return formatter.format();
}
generateFooter(saveStorage: string | undefined): string {
const offset = this._mode === 'library' ? '' : ' ';
let storageStateLine = saveStorage ? `\n${offset}await context.StorageStateAsync(new BrowserContextStorageStateOptions\n${offset}{\n${offset} Path = ${quote(saveStorage)}\n${offset}});\n` : '';
if (this._mode !== 'library')
if (this._mode !== 'library') {
storageStateLine += ` }\n}\n`;
}
return storageStateLine;
}
}
function formatObject(value: any, indent = ' ', name = ''): string {
if (typeof value === 'string') {
if (['permissions', 'colorScheme', 'modifiers', 'button', 'recordHarContent', 'recordHarMode', 'serviceWorkers'].includes(name))
if (['permissions', 'colorScheme', 'modifiers', 'button', 'recordHarContent', 'recordHarMode', 'serviceWorkers'].includes(name)) {
return `${getClassName(name)}.${toPascal(value)}`;
}
return quote(value);
}
if (Array.isArray(value))
if (Array.isArray(value)) {
return `new[] { ${value.map(o => formatObject(o, indent, name)).join(', ')} }`;
}
if (typeof value === 'object') {
const keys = Object.keys(value).filter(key => value[key] !== undefined).sort();
if (!keys.length)
if (!keys.length) {
return name ? `new ${getClassName(name)}` : '';
}
const tokens: string[] = [];
for (const key of keys) {
const property = getPropertyName(key);
tokens.push(`${property} = ${formatObject(value[key], indent, key)},`);
}
if (name)
if (name) {
return `new ${getClassName(name)}\n{\n${indent}${tokens.join(`\n${indent}`)}\n${indent}}`;
}
return `{\n${indent}${tokens.join(`\n${indent}`)}\n${indent}}`;
}
if (name === 'latitude' || name === 'longitude')
if (name === 'latitude' || name === 'longitude') {
return String(value) + 'm';
}
return String(value);
}
@ -271,14 +287,16 @@ function formatContextOptions(contextOptions: BrowserContextOptions, deviceName:
delete options.recordHar;
const device = deviceName && deviceDescriptors[deviceName];
if (!device) {
if (!Object.entries(options).length)
if (!Object.entries(options).length) {
return '';
}
return formatObject(options, ' ', 'BrowserNewContextOptions');
}
options = sanitizeDeviceOptions(device, options);
if (!Object.entries(options).length)
if (!Object.entries(options).length) {
return `playwright.Devices[${quote(deviceName!)}]`;
}
return formatObject(options, ' ', `BrowserNewContextOptions(playwright.Devices[${quote(deviceName!)}])`);
}
@ -309,19 +327,23 @@ class CSharpFormatter {
let spaces = '';
let previousLine = '';
return this._lines.map((line: string) => {
if (line === '')
if (line === '') {
return line;
if (line.startsWith('}') || line.startsWith(']') || line.includes('});') || line === ');')
}
if (line.startsWith('}') || line.startsWith(']') || line.includes('});') || line === ');') {
spaces = spaces.substring(this._baseIndent.length);
}
const extraSpaces = /^(for|while|if).*\(.*\)$/.test(previousLine) ? this._baseIndent : '';
previousLine = line;
line = spaces + extraSpaces + line;
if (line.endsWith('{') || line.endsWith('[') || line.endsWith('('))
if (line.endsWith('{') || line.endsWith('[') || line.endsWith('(')) {
spaces += this._baseIndent;
if (line.endsWith('));'))
}
if (line.endsWith('));')) {
spaces = spaces.substring(this._baseIndent.length);
}
return this._baseOffset + line;
}).join('\n');

View file

@ -51,13 +51,15 @@ export class JavaLanguageGenerator implements LanguageGenerator {
const offset = this._mode === 'junit' ? 4 : 6;
const formatter = new JavaScriptFormatter(offset);
if (this._mode !== 'library' && (action.name === 'openPage' || action.name === 'closePage'))
if (this._mode !== 'library' && (action.name === 'openPage' || action.name === 'closePage')) {
return '';
}
if (action.name === 'openPage') {
formatter.add(`Page ${pageAlias} = context.newPage();`);
if (action.url && action.url !== 'about:blank' && action.url !== 'chrome://newtab/')
if (action.url && action.url !== 'about:blank' && action.url !== 'chrome://newtab/') {
formatter.add(`${pageAlias}.navigate(${quote(action.url)});`);
}
return formatter.format();
}
@ -100,8 +102,9 @@ export class JavaLanguageGenerator implements LanguageGenerator {
return `${subject}.close();`;
case 'click': {
let method = 'click';
if (action.clickCount === 2)
if (action.clickCount === 2) {
method = 'dblclick';
}
const options = toClickOptionsForSourceCode(action);
const optionsText = formatClickOptions(options);
return `${subject}.${this._asLocator(action.selector, inFrameLocator)}.${method}(${optionsText});`;
@ -170,8 +173,9 @@ export class JavaLanguageGenerator implements LanguageGenerator {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.${options.browserName}().launch(${formatLaunchOptions(options.launchOptions)});
BrowserContext context = browser.newContext(${formatContextOptions(options.contextOptions, options.deviceName)});`);
if (options.contextOptions.recordHar)
if (options.contextOptions.recordHar) {
formatter.add(` context.routeFromHAR(${quote(options.contextOptions.recordHar.path)});`);
}
return formatter.format();
}
@ -189,8 +193,9 @@ export class JavaLanguageGenerator implements LanguageGenerator {
function formatPath(files: string | string[]): string {
if (Array.isArray(files)) {
if (files.length === 0)
if (files.length === 0) {
return 'new Path[0]';
}
return `new Path[] {${files.map(s => 'Paths.get(' + quote(s) + ')').join(', ')}}`;
}
return `Paths.get(${quote(files)})`;
@ -198,8 +203,9 @@ function formatPath(files: string | string[]): string {
function formatSelectOption(options: string | string[]): string {
if (Array.isArray(options)) {
if (options.length === 0)
if (options.length === 0) {
return 'new String[0]';
}
return `new String[] {${options.map(s => quote(s)).join(', ')}}`;
}
return quote(options);
@ -207,66 +213,89 @@ function formatSelectOption(options: string | string[]): string {
function formatLaunchOptions(options: any): string {
const lines = [];
if (!Object.keys(options).filter(key => options[key] !== undefined).length)
if (!Object.keys(options).filter(key => options[key] !== undefined).length) {
return '';
}
lines.push('new BrowserType.LaunchOptions()');
if (options.channel)
if (options.channel) {
lines.push(` .setChannel(${quote(options.channel)})`);
if (typeof options.headless === 'boolean')
}
if (typeof options.headless === 'boolean') {
lines.push(` .setHeadless(false)`);
}
return lines.join('\n');
}
function formatContextOptions(contextOptions: BrowserContextOptions, deviceName: string | undefined): string {
const lines = [];
if (!Object.keys(contextOptions).length && !deviceName)
if (!Object.keys(contextOptions).length && !deviceName) {
return '';
}
const device = deviceName ? deviceDescriptors[deviceName] : {};
const options: BrowserContextOptions = { ...device, ...contextOptions };
lines.push('new Browser.NewContextOptions()');
if (options.acceptDownloads)
if (options.acceptDownloads) {
lines.push(` .setAcceptDownloads(true)`);
if (options.bypassCSP)
}
if (options.bypassCSP) {
lines.push(` .setBypassCSP(true)`);
if (options.colorScheme)
}
if (options.colorScheme) {
lines.push(` .setColorScheme(ColorScheme.${options.colorScheme.toUpperCase()})`);
if (options.deviceScaleFactor)
}
if (options.deviceScaleFactor) {
lines.push(` .setDeviceScaleFactor(${options.deviceScaleFactor})`);
if (options.geolocation)
}
if (options.geolocation) {
lines.push(` .setGeolocation(${options.geolocation.latitude}, ${options.geolocation.longitude})`);
if (options.hasTouch)
}
if (options.hasTouch) {
lines.push(` .setHasTouch(${options.hasTouch})`);
if (options.isMobile)
}
if (options.isMobile) {
lines.push(` .setIsMobile(${options.isMobile})`);
if (options.locale)
}
if (options.locale) {
lines.push(` .setLocale(${quote(options.locale)})`);
if (options.proxy)
}
if (options.proxy) {
lines.push(` .setProxy(new Proxy(${quote(options.proxy.server)}))`);
if (options.serviceWorkers)
}
if (options.serviceWorkers) {
lines.push(` .setServiceWorkers(ServiceWorkerPolicy.${options.serviceWorkers.toUpperCase()})`);
if (options.storageState)
}
if (options.storageState) {
lines.push(` .setStorageStatePath(Paths.get(${quote(options.storageState as string)}))`);
if (options.timezoneId)
}
if (options.timezoneId) {
lines.push(` .setTimezoneId(${quote(options.timezoneId)})`);
if (options.userAgent)
}
if (options.userAgent) {
lines.push(` .setUserAgent(${quote(options.userAgent)})`);
if (options.viewport)
}
if (options.viewport) {
lines.push(` .setViewportSize(${options.viewport.width}, ${options.viewport.height})`);
}
return lines.join('\n');
}
function formatClickOptions(options: types.MouseClickOptions) {
const lines = [];
if (options.button)
if (options.button) {
lines.push(` .setButton(MouseButton.${options.button.toUpperCase()})`);
if (options.modifiers)
}
if (options.modifiers) {
lines.push(` .setModifiers(Arrays.asList(${options.modifiers.map(m => `KeyboardModifier.${m.toUpperCase()}`).join(', ')}))`);
if (options.clickCount)
}
if (options.clickCount) {
lines.push(` .setClickCount(${options.clickCount})`);
if (options.position)
}
if (options.position) {
lines.push(` .setPosition(${options.position.x}, ${options.position.y})`);
if (!lines.length)
}
if (!lines.length) {
return '';
}
lines.unshift(`new Locator.ClickOptions()`);
return lines.join('\n');
}

View file

@ -36,16 +36,18 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
generateAction(actionInContext: actions.ActionInContext): string {
const action = actionInContext.action;
if (this._isTest && (action.name === 'openPage' || action.name === 'closePage'))
if (this._isTest && (action.name === 'openPage' || action.name === 'closePage')) {
return '';
}
const pageAlias = actionInContext.frame.pageAlias;
const formatter = new JavaScriptFormatter(2);
if (action.name === 'openPage') {
formatter.add(`const ${pageAlias} = await context.newPage();`);
if (action.url && action.url !== 'about:blank' && action.url !== 'chrome://newtab/')
if (action.url && action.url !== 'about:blank' && action.url !== 'chrome://newtab/') {
formatter.add(`await ${pageAlias}.goto(${quote(action.url)});`);
}
return formatter.format();
}
@ -60,17 +62,21 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
});`);
}
if (signals.popup)
if (signals.popup) {
formatter.add(`const ${signals.popup.popupAlias}Promise = ${pageAlias}.waitForEvent('popup');`);
if (signals.download)
}
if (signals.download) {
formatter.add(`const download${signals.download.downloadAlias}Promise = ${pageAlias}.waitForEvent('download');`);
}
formatter.add(wrapWithStep(actionInContext.description, this._generateActionCall(subject, actionInContext)));
if (signals.popup)
if (signals.popup) {
formatter.add(`const ${signals.popup.popupAlias} = await ${signals.popup.popupAlias}Promise;`);
if (signals.download)
}
if (signals.download) {
formatter.add(`const download${signals.download.downloadAlias} = await download${signals.download.downloadAlias}Promise;`);
}
return formatter.format();
}
@ -84,8 +90,9 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
return `await ${subject}.close();`;
case 'click': {
let method = 'click';
if (action.clickCount === 2)
if (action.clickCount === 2) {
method = 'dblclick';
}
const options = toClickOptionsForSourceCode(action);
const optionsString = formatOptions(options, false);
return `await ${subject}.${this._asLocator(action.selector)}.${method}(${optionsString});`;
@ -129,14 +136,16 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
}
generateHeader(options: LanguageGeneratorOptions): string {
if (this._isTest)
if (this._isTest) {
return this.generateTestHeader(options);
}
return this.generateStandaloneHeader(options);
}
generateFooter(saveStorage: string | undefined): string {
if (this._isTest)
if (this._isTest) {
return this.generateTestFooter(saveStorage);
}
return this.generateStandaloneFooter(saveStorage);
}
@ -147,8 +156,9 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
import { test, expect${options.deviceName ? ', devices' : ''} } from '@playwright/test';
${useText ? '\ntest.use(' + useText + ');\n' : ''}
test('test', async ({ page }) => {`);
if (options.contextOptions.recordHar)
if (options.contextOptions.recordHar) {
formatter.add(` await page.routeFromHAR(${quote(options.contextOptions.recordHar.path)});`);
}
return formatter.format();
}
@ -164,8 +174,9 @@ ${useText ? '\ntest.use(' + useText + ');\n' : ''}
(async () => {
const browser = await ${options.browserName}.launch(${formatObjectOrVoid(options.launchOptions)});
const context = await browser.newContext(${formatContextOptions(options.contextOptions, options.deviceName, false)});`);
if (options.contextOptions.recordHar)
if (options.contextOptions.recordHar) {
formatter.add(` await context.routeFromHAR(${quote(options.contextOptions.recordHar.path)});`);
}
return formatter.format();
}
@ -180,23 +191,28 @@ ${useText ? '\ntest.use(' + useText + ');\n' : ''}
function formatOptions(value: any, hasArguments: boolean): string {
const keys = Object.keys(value);
if (!keys.length)
if (!keys.length) {
return '';
}
return (hasArguments ? ', ' : '') + formatObject(value);
}
function formatObject(value: any, indent = ' '): string {
if (typeof value === 'string')
if (typeof value === 'string') {
return quote(value);
if (Array.isArray(value))
}
if (Array.isArray(value)) {
return `[${value.map(o => formatObject(o)).join(', ')}]`;
}
if (typeof value === 'object') {
const keys = Object.keys(value).filter(key => value[key] !== undefined).sort();
if (!keys.length)
if (!keys.length) {
return '{}';
}
const tokens: string[] = [];
for (const key of keys)
for (const key of keys) {
tokens.push(`${key}: ${formatObject(value[key])}`);
}
return `{\n${indent}${tokens.join(`,\n${indent}`)}\n}`;
}
return String(value);
@ -211,13 +227,15 @@ function formatContextOptions(options: BrowserContextOptions, deviceName: string
const device = deviceName && deviceDescriptors[deviceName];
// recordHAR is replaced with routeFromHAR in the generated code.
options = { ...options, recordHar: undefined };
if (!device)
if (!device) {
return formatObjectOrVoid(options);
}
// Filter out all the properties from the device descriptor.
let serializedObject = formatObjectOrVoid(sanitizeDeviceOptions(device, options));
// When there are no additional context options, we still want to spread the device inside.
if (!serializedObject)
if (!serializedObject) {
serializedObject = '{\n}';
}
const lines = serializedObject.split('\n');
lines.splice(1, 0, `...devices[${quote(deviceName!)}],`);
return lines.join('\n');
@ -251,18 +269,21 @@ export class JavaScriptFormatter {
let spaces = '';
let previousLine = '';
return this._lines.map((line: string) => {
if (line === '')
if (line === '') {
return line;
if (line.startsWith('}') || line.startsWith(']'))
}
if (line.startsWith('}') || line.startsWith(']')) {
spaces = spaces.substring(this._baseIndent.length);
}
const extraSpaces = /^(for|while|if|try).*\(.*\)$/.test(previousLine) ? this._baseIndent : '';
previousLine = line;
const callCarryOver = line.startsWith('.set');
line = spaces + extraSpaces + (callCarryOver ? this._baseIndent : '') + line;
if (line.endsWith('{') || line.endsWith('['))
if (line.endsWith('{') || line.endsWith('[')) {
spaces += this._baseIndent;
}
return this._baseOffset + line;
}).join('\n');
}
@ -283,8 +304,9 @@ export function quoteMultiline(text: string, indent = ' ') {
.replace(/`/g, '\\`')
.replace(/\$\{/g, '\\${');
const lines = text.split('\n');
if (lines.length === 1)
if (lines.length === 1) {
return '`' + escape(text) + '`';
}
return '`\n' + lines.map(line => indent + escape(line).replace(/\${/g, '\\${')).join('\n') + `\n${indent}\``;
}

View file

@ -31,8 +31,9 @@ export function sanitizeDeviceOptions(device: any, options: BrowserContextOption
// Filter out all the properties from the device descriptor.
const cleanedOptions: Record<string, any> = {};
for (const property in options) {
if (JSON.stringify(device[property]) !== JSON.stringify((options as any)[property]))
if (JSON.stringify(device[property]) !== JSON.stringify((options as any)[property])) {
cleanedOptions[property] = (options as any)[property];
}
}
return cleanedOptions;
}
@ -42,12 +43,13 @@ export function toSignalMap(action: actions.Action) {
let download: actions.DownloadSignal | undefined;
let dialog: actions.DialogSignal | undefined;
for (const signal of action.signals) {
if (signal.name === 'popup')
if (signal.name === 'popup') {
popup = signal;
else if (signal.name === 'download')
} else if (signal.name === 'download') {
download = signal;
else if (signal.name === 'dialog')
} else if (signal.name === 'dialog') {
dialog = signal;
}
}
return {
popup,
@ -58,45 +60,59 @@ export function toSignalMap(action: actions.Action) {
export function toKeyboardModifiers(modifiers: number): types.SmartKeyboardModifier[] {
const result: types.SmartKeyboardModifier[] = [];
if (modifiers & 1)
if (modifiers & 1) {
result.push('Alt');
if (modifiers & 2)
}
if (modifiers & 2) {
result.push('ControlOrMeta');
if (modifiers & 4)
}
if (modifiers & 4) {
result.push('ControlOrMeta');
if (modifiers & 8)
}
if (modifiers & 8) {
result.push('Shift');
}
return result;
}
export function fromKeyboardModifiers(modifiers?: types.SmartKeyboardModifier[]): number {
let result = 0;
if (!modifiers)
if (!modifiers) {
return result;
if (modifiers.includes('Alt'))
}
if (modifiers.includes('Alt')) {
result |= 1;
if (modifiers.includes('Control'))
}
if (modifiers.includes('Control')) {
result |= 2;
if (modifiers.includes('ControlOrMeta'))
}
if (modifiers.includes('ControlOrMeta')) {
result |= 2;
if (modifiers.includes('Meta'))
}
if (modifiers.includes('Meta')) {
result |= 4;
if (modifiers.includes('Shift'))
}
if (modifiers.includes('Shift')) {
result |= 8;
}
return result;
}
export function toClickOptionsForSourceCode(action: actions.ClickAction): types.MouseClickOptions {
const modifiers = toKeyboardModifiers(action.modifiers);
const options: types.MouseClickOptions = {};
if (action.button !== 'left')
if (action.button !== 'left') {
options.button = action.button;
if (modifiers.length)
}
if (modifiers.length) {
options.modifiers = modifiers;
}
// Do not render clickCount === 2 for dblclick.
if (action.clickCount > 2)
if (action.clickCount > 2) {
options.clickCount = action.clickCount;
if (action.position)
}
if (action.position) {
options.position = action.position;
}
return options;
}

View file

@ -43,16 +43,18 @@ export class PythonLanguageGenerator implements LanguageGenerator {
generateAction(actionInContext: actions.ActionInContext): string {
const action = actionInContext.action;
if (this._isPyTest && (action.name === 'openPage' || action.name === 'closePage'))
if (this._isPyTest && (action.name === 'openPage' || action.name === 'closePage')) {
return '';
}
const pageAlias = actionInContext.frame.pageAlias;
const formatter = new PythonFormatter(4);
if (action.name === 'openPage') {
formatter.add(`${pageAlias} = ${this._awaitPrefix}context.new_page()`);
if (action.url && action.url !== 'about:blank' && action.url !== 'chrome://newtab/')
if (action.url && action.url !== 'about:blank' && action.url !== 'chrome://newtab/') {
formatter.add(`${this._awaitPrefix}${pageAlias}.goto(${quote(action.url)})`);
}
return formatter.format();
}
@ -60,8 +62,9 @@ export class PythonLanguageGenerator implements LanguageGenerator {
const subject = `${pageAlias}${locators.join('')}`;
const signals = toSignalMap(action);
if (signals.dialog)
if (signals.dialog) {
formatter.add(` ${pageAlias}.once("dialog", lambda dialog: dialog.dismiss())`);
}
let code = `${this._awaitPrefix}${this._generateActionCall(subject, actionInContext)}`;
@ -93,8 +96,9 @@ export class PythonLanguageGenerator implements LanguageGenerator {
return `${subject}.close()`;
case 'click': {
let method = 'click';
if (action.clickCount === 2)
if (action.clickCount === 2) {
method = 'dblclick';
}
const options = toClickOptionsForSourceCode(action);
const optionsString = formatOptions(options, false);
return `${subject}.${this._asLocator(action.selector)}.${method}(${optionsString})`;
@ -151,8 +155,9 @@ from playwright.sync_api import Page, expect
${fixture}
def test_example(page: Page) -> None {`);
if (options.contextOptions.recordHar)
if (options.contextOptions.recordHar) {
formatter.add(` page.route_from_har(${quote(options.contextOptions.recordHar.path)})`);
}
} else if (this._isAsync) {
formatter.add(`
import asyncio
@ -163,8 +168,9 @@ from playwright.async_api import Playwright, async_playwright, expect
async def run(playwright: Playwright) -> None {
browser = await playwright.${options.browserName}.launch(${formatOptions(options.launchOptions, false)})
context = await browser.new_context(${formatContextOptions(options.contextOptions, options.deviceName)})`);
if (options.contextOptions.recordHar)
if (options.contextOptions.recordHar) {
formatter.add(` await page.route_from_har(${quote(options.contextOptions.recordHar.path)})`);
}
} else {
formatter.add(`
import re
@ -174,8 +180,9 @@ from playwright.sync_api import Playwright, sync_playwright, expect
def run(playwright: Playwright) -> None {
browser = playwright.${options.browserName}.launch(${formatOptions(options.launchOptions, false)})
context = browser.new_context(${formatContextOptions(options.contextOptions, options.deviceName)})`);
if (options.contextOptions.recordHar)
if (options.contextOptions.recordHar) {
formatter.add(` context.route_from_har(${quote(options.contextOptions.recordHar.path)})`);
}
}
return formatter.format();
}
@ -212,28 +219,36 @@ with sync_playwright() as playwright:
}
function formatValue(value: any): string {
if (value === false)
if (value === false) {
return 'False';
if (value === true)
}
if (value === true) {
return 'True';
if (value === undefined)
}
if (value === undefined) {
return 'None';
if (Array.isArray(value))
}
if (Array.isArray(value)) {
return `[${value.map(formatValue).join(', ')}]`;
if (typeof value === 'string')
}
if (typeof value === 'string') {
return quote(value);
if (typeof value === 'object')
}
if (typeof value === 'object') {
return JSON.stringify(value);
}
return String(value);
}
function formatOptions(value: any, hasArguments: boolean, asDict?: boolean): string {
const keys = Object.keys(value).filter(key => value[key] !== undefined).sort();
if (!keys.length)
if (!keys.length) {
return '';
}
return (hasArguments ? ', ' : '') + keys.map(key => {
if (asDict)
if (asDict) {
return `"${toSnakeCase(key)}": ${formatValue(value[key])}`;
}
return `${toSnakeCase(key)}=${formatValue(value[key])}`;
}).join(', ');
}
@ -242,8 +257,9 @@ function formatContextOptions(options: BrowserContextOptions, deviceName: string
// recordHAR is replaced with routeFromHAR in the generated code.
options = { ...options, recordHar: undefined };
const device = deviceName && deviceDescriptors[deviceName];
if (!device)
if (!device) {
return formatOptions(options, false, asDict);
}
return `**playwright.devices[${quote(deviceName!)}]` + formatOptions(sanitizeDeviceOptions(device, options), true, asDict);
}
@ -273,8 +289,9 @@ class PythonFormatter {
let spaces = '';
const lines: string[] = [];
this._lines.forEach((line: string) => {
if (line === '')
if (line === '') {
return lines.push(line);
}
if (line === '}') {
spaces = spaces.substring(this._baseIndent.length);
return;

View file

@ -42,8 +42,9 @@ export class ConsoleMessage {
}
text(): string {
if (this._text === undefined)
if (this._text === undefined) {
this._text = this._args.map(arg => arg.preview()).join(' ');
}
return this._text;
}

View file

@ -29,12 +29,15 @@ class Cookie {
// https://datatracker.ietf.org/doc/html/rfc6265#section-5.4
matches(url: URL): boolean {
if (this._raw.secure && (url.protocol !== 'https:' && url.hostname !== 'localhost'))
if (this._raw.secure && (url.protocol !== 'https:' && url.hostname !== 'localhost')) {
return false;
if (!domainMatches(url.hostname, this._raw.domain))
}
if (!domainMatches(url.hostname, this._raw.domain)) {
return false;
if (!pathMatches(url.pathname, this._raw.path))
}
if (!pathMatches(url.pathname, this._raw.path)) {
return false;
}
return true;
}
@ -53,8 +56,9 @@ class Cookie {
}
expired() {
if (this._raw.expires === -1)
if (this._raw.expires === -1) {
return false;
}
return this._raw.expires * 1000 < Date.now();
}
}
@ -63,23 +67,26 @@ export class CookieStore {
private readonly _nameToCookies: Map<string, Set<Cookie>> = new Map();
addCookies(cookies: channels.NetworkCookie[]) {
for (const cookie of cookies)
for (const cookie of cookies) {
this._addCookie(new Cookie(cookie));
}
}
cookies(url: URL): channels.NetworkCookie[] {
const result = [];
for (const cookie of this._cookiesIterator()) {
if (cookie.matches(url))
if (cookie.matches(url)) {
result.push(cookie.networkCookie());
}
}
return result;
}
allCookies(): channels.NetworkCookie[] {
const result = [];
for (const cookie of this._cookiesIterator())
for (const cookie of this._cookiesIterator()) {
result.push(cookie.networkCookie());
}
return result;
}
@ -91,8 +98,9 @@ export class CookieStore {
}
// https://datatracker.ietf.org/doc/html/rfc6265#section-5.3
for (const other of set) {
if (other.equals(cookie))
if (other.equals(cookie)) {
set.delete(other);
}
}
set.add(cookie);
CookieStore.pruneExpired(set);
@ -101,17 +109,20 @@ export class CookieStore {
private *_cookiesIterator(): IterableIterator<Cookie> {
for (const [name, cookies] of this._nameToCookies) {
CookieStore.pruneExpired(cookies);
for (const cookie of cookies)
for (const cookie of cookies) {
yield cookie;
if (cookies.size === 0)
}
if (cookies.size === 0) {
this._nameToCookies.delete(name);
}
}
}
private static pruneExpired(cookies: Set<Cookie>) {
for (const cookie of cookies) {
if (cookie.expired())
if (cookie.expired()) {
cookies.delete(cookie);
}
}
}
}
@ -143,8 +154,9 @@ export function parseRawCookie(header: string): RawCookie | null {
}
return [key, value];
});
if (!pairs.length)
if (!pairs.length) {
return null;
}
const [name, value] = pairs[0];
const cookie: RawCookie = {
name,
@ -157,10 +169,11 @@ export function parseRawCookie(header: string): RawCookie | null {
const expiresMs = (+new Date(value));
// https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.1
if (isFinite(expiresMs)) {
if (expiresMs <= 0)
if (expiresMs <= 0) {
cookie.expires = 0;
else
} else {
cookie.expires = Math.min(expiresMs / 1000, kMaxCookieExpiresDateInSeconds);
}
}
break;
case 'max-age':
@ -169,16 +182,18 @@ export function parseRawCookie(header: string): RawCookie | null {
// From https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.2
// If delta-seconds is less than or equal to zero (0), let expiry-time
// be the earliest representable date and time.
if (maxAgeSec <= 0)
if (maxAgeSec <= 0) {
cookie.expires = 0;
else
} else {
cookie.expires = Math.min(Date.now() / 1000 + maxAgeSec, kMaxCookieExpiresDateInSeconds);
}
}
break;
case 'domain':
cookie.domain = value.toLocaleLowerCase() || '';
if (cookie.domain && !cookie.domain.startsWith('.') && cookie.domain.includes('.'))
if (cookie.domain && !cookie.domain.startsWith('.') && cookie.domain.includes('.')) {
cookie.domain = '.' + cookie.domain;
}
break;
case 'path':
cookie.path = value || '';
@ -208,21 +223,26 @@ export function parseRawCookie(header: string): RawCookie | null {
}
export function domainMatches(value: string, domain: string): boolean {
if (value === domain)
if (value === domain) {
return true;
}
// Only strict match is allowed if domain doesn't start with '.' (host-only-flag is true in the spec)
if (!domain.startsWith('.'))
if (!domain.startsWith('.')) {
return false;
}
value = '.' + value;
return value.endsWith(domain);
}
function pathMatches(value: string, path: string): boolean {
if (value === path)
if (value === path) {
return true;
if (!value.endsWith('/'))
}
if (!value.endsWith('/')) {
value = value + '/';
if (!path.endsWith('/'))
}
if (!path.endsWith('/')) {
path = path + '/';
}
return value.startsWith(path);
}

View file

@ -82,15 +82,18 @@ export class DebugController extends SdkObject {
async resetForReuse() {
const contexts = new Set<BrowserContext>();
for (const page of this._playwright.allPages())
for (const page of this._playwright.allPages()) {
contexts.add(page.context());
for (const context of contexts)
}
for (const context of contexts) {
await context.resetForReuse(internalMetadata, null);
}
}
async navigate(url: string) {
for (const p of this._playwright.allPages())
for (const p of this._playwright.allPages()) {
await p.mainFrame().goto(internalMetadata, url);
}
}
async setRecorderMode(params: { mode: Mode, file?: string, testIdAttributeName?: string }) {
@ -106,8 +109,9 @@ export class DebugController extends SdkObject {
return;
}
if (!this._playwright.allBrowsers().length)
if (!this._playwright.allBrowsers().length) {
await this._playwright.chromium.launch(internalMetadata, { headless: !!process.env.PW_DEBUG_CONTROLLER_HEADLESS });
}
// Create page if none.
const pages = this._playwright.allPages();
if (!pages.length) {
@ -117,56 +121,65 @@ export class DebugController extends SdkObject {
}
// Update test id attribute.
if (params.testIdAttributeName) {
for (const page of this._playwright.allPages())
for (const page of this._playwright.allPages()) {
page.context().selectors().setTestIdAttributeName(params.testIdAttributeName);
}
}
// Toggle the mode.
for (const recorder of await this._allRecorders()) {
recorder.hideHighlightedSelector();
if (params.mode !== 'inspecting')
if (params.mode !== 'inspecting') {
recorder.setOutput(this._codegenId, params.file);
}
recorder.setMode(params.mode);
}
this.setAutoCloseEnabled(true);
}
async setAutoCloseEnabled(enabled: boolean) {
if (!this._autoCloseAllowed)
if (!this._autoCloseAllowed) {
return;
if (this._autoCloseTimer)
}
if (this._autoCloseTimer) {
clearTimeout(this._autoCloseTimer);
if (!enabled)
}
if (!enabled) {
return;
}
const heartBeat = () => {
if (!this._playwright.allPages().length)
if (!this._playwright.allPages().length) {
gracefullyProcessExitDoNotHang(0);
else
} else {
this._autoCloseTimer = setTimeout(heartBeat, 5000);
}
};
this._autoCloseTimer = setTimeout(heartBeat, 30000);
}
async highlight(params: { selector?: string, ariaTemplate?: string }) {
// Assert parameters validity.
if (params.selector)
if (params.selector) {
unsafeLocatorOrSelectorAsSelector(this._sdkLanguage, params.selector, 'data-testid');
}
let parsedYaml: ParsedYaml | undefined;
if (params.ariaTemplate) {
parsedYaml = parseYamlForAriaSnapshot(params.ariaTemplate);
parseYamlTemplate(parsedYaml);
}
for (const recorder of await this._allRecorders()) {
if (parsedYaml)
if (parsedYaml) {
recorder.setHighlightedAriaTemplate(parsedYaml);
else if (params.selector)
} else if (params.selector) {
recorder.setHighlightedSelector(this._sdkLanguage, params.selector);
}
}
}
async hideHighlight() {
// Hide all active recorder highlights.
for (const recorder of await this._allRecorders())
for (const recorder of await this._allRecorders()) {
recorder.hideHighlightedSelector();
}
// Hide all locator.highlight highlights.
await this._playwright.hideHighlight();
}
@ -176,8 +189,9 @@ export class DebugController extends SdkObject {
}
async resume() {
for (const recorder of await this._allRecorders())
for (const recorder of await this._allRecorders()) {
recorder.resume();
}
}
async kill() {
@ -201,8 +215,9 @@ export class DebugController extends SdkObject {
pages: [] as any[]
};
b.contexts.push(c);
for (const page of context.pages())
for (const page of context.pages()) {
c.pages.push(page.mainFrame().url());
}
pageCount += context.pages().length;
}
}
@ -211,8 +226,9 @@ export class DebugController extends SdkObject {
private async _allRecorders(): Promise<Recorder[]> {
const contexts = new Set<BrowserContext>();
for (const page of this._playwright.allPages())
for (const page of this._playwright.allPages()) {
contexts.add(page.context());
}
const result = await Promise.all([...contexts].map(c => Recorder.showInspector(c, { omitCallTracking: true }, () => Promise.resolve(new InspectingRecorderApp(this)))));
return result.filter(Boolean) as Recorder[];
}
@ -220,11 +236,13 @@ export class DebugController extends SdkObject {
private async _closeBrowsersWithoutPages() {
for (const browser of this._playwright.allBrowsers()) {
for (const context of browser.contexts()) {
if (!context.pages().length)
if (!context.pages().length) {
await context.close({ reason: 'Browser collected' });
}
}
if (!browser.contexts())
if (!browser.contexts()) {
await browser.close({ reason: 'Browser collected' });
}
}
}
}

View file

@ -39,8 +39,9 @@ export class Debugger extends EventEmitter implements InstrumentationListener {
this._context = context;
(this._context as any)[symbol] = this;
this._enabled = debugMode() === 'inspector';
if (this._enabled)
if (this._enabled) {
this.pauseOnNextStatement();
}
context.instrumentation.addListener(this, context);
this._context.once(BrowserContext.Events.Close, () => {
this._context.instrumentation.removeListener(this);
@ -53,10 +54,12 @@ export class Debugger extends EventEmitter implements InstrumentationListener {
}
async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
if (this._muted)
if (this._muted) {
return;
if (shouldPauseOnCall(sdkObject, metadata) || (this._pauseOnNextStatement && shouldPauseBeforeStep(metadata)))
}
if (shouldPauseOnCall(sdkObject, metadata) || (this._pauseOnNextStatement && shouldPauseBeforeStep(metadata))) {
await this.pause(sdkObject, metadata);
}
}
async _doSlowMo() {
@ -64,20 +67,24 @@ export class Debugger extends EventEmitter implements InstrumentationListener {
}
async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
if (this._slowMo && shouldSlowMo(metadata))
if (this._slowMo && shouldSlowMo(metadata)) {
await this._doSlowMo();
}
}
async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
if (this._muted)
if (this._muted) {
return;
if (this._enabled && this._pauseOnNextStatement)
}
if (this._enabled && this._pauseOnNextStatement) {
await this.pause(sdkObject, metadata);
}
}
async pause(sdkObject: SdkObject, metadata: CallMetadata) {
if (this._muted)
if (this._muted) {
return;
}
this._enabled = true;
metadata.pauseStartTime = monotonicTime();
const result = new Promise<void>(resolve => {
@ -88,8 +95,9 @@ export class Debugger extends EventEmitter implements InstrumentationListener {
}
resume(step: boolean) {
if (!this.isPaused())
if (!this.isPaused()) {
return;
}
this._pauseOnNextStatement = step;
const endTime = monotonicTime();
@ -106,36 +114,43 @@ export class Debugger extends EventEmitter implements InstrumentationListener {
}
isPaused(metadata?: CallMetadata): boolean {
if (metadata)
if (metadata) {
return this._pausedCallsMetadata.has(metadata);
}
return !!this._pausedCallsMetadata.size;
}
pausedDetails(): { metadata: CallMetadata, sdkObject: SdkObject }[] {
const result: { metadata: CallMetadata, sdkObject: SdkObject }[] = [];
for (const [metadata, { sdkObject }] of this._pausedCallsMetadata)
for (const [metadata, { sdkObject }] of this._pausedCallsMetadata) {
result.push({ metadata, sdkObject });
}
return result;
}
}
function shouldPauseOnCall(sdkObject: SdkObject, metadata: CallMetadata): boolean {
if (sdkObject.attribution.playwright.options.isServer)
if (sdkObject.attribution.playwright.options.isServer) {
return false;
if (!sdkObject.attribution.browser?.options.headful && !isUnderTest())
}
if (!sdkObject.attribution.browser?.options.headful && !isUnderTest()) {
return false;
}
return metadata.method === 'pause';
}
function shouldPauseBeforeStep(metadata: CallMetadata): boolean {
// Don't stop on internal.
if (!metadata.apiName)
if (!metadata.apiName) {
return false;
}
// Always stop on 'close'
if (metadata.method === 'close')
if (metadata.method === 'close') {
return true;
if (metadata.method === 'waitForSelector' || metadata.method === 'waitForEventInfo')
return false; // Never stop on those, primarily for the test harness.
}
if (metadata.method === 'waitForSelector' || metadata.method === 'waitForEventInfo') {
return false;
} // Never stop on those, primarily for the test harness.
const step = metadata.type + '.' + metadata.method;
// Stop before everything that generates snapshot. But don't stop before those marked as pausesBeforeInputActions
// since we stop in them on a separate instrumentation signal.

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