diff --git a/packages/html-reporter/bundle.ts b/packages/html-reporter/bundle.ts index 4c6bc02632..36c786de58 100644 --- a/packages/html-reporter/bundle.ts +++ b/packages/html-reporter/bundle.ts @@ -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(/(?=/, ''); 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(/`); - else + } else { html = html.replace(/]*>/, () => ``); + } } return html; }, diff --git a/packages/html-reporter/src/filter.ts b/packages/html-reporter/src/filter.ts index 280c9a566b..b6fd32e7b0 100644 --- a/packages/html-reporter/src/filter.ts +++ b/packages/html-reporter/src/filter.ts @@ -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); diff --git a/packages/html-reporter/src/index.tsx b/packages/html-reporter/src/index.tsx index 4ae3b02591..2f650be4d1 100644 --- a/packages/html-reporter/src/index.tsx +++ b/packages/html-reporter/src/index.tsx @@ -36,8 +36,9 @@ document.head.appendChild(link); const ReportLoader: React.FC = () => { const [report, setReport] = React.useState(); 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(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; } diff --git a/packages/html-reporter/src/links.tsx b/packages/html-reporter/src/links.tsx index a6ea1e6695..ef7b6752ac 100644 --- a/packages/html-reporter/src/links.tsx +++ b/packages/html-reporter/src/links.tsx @@ -101,11 +101,13 @@ export const SearchParamsProvider: React.FunctionComponent 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; } diff --git a/packages/html-reporter/src/metadataView.tsx b/packages/html-reporter/src/metadataView.tsx index 0f4fe6e73c..fbe1e4ff4b 100644 --- a/packages/html-reporter/src/metadataView.tsx +++ b/packages/html-reporter/src/metadataView.tsx @@ -62,8 +62,9 @@ class ErrorBoundary extends React.Component, { error export const MetadataView: React.FC = metadata => ; const InnerMetadataView: React.FC = 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 ( { const map = new Map(); 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; } diff --git a/packages/html-reporter/src/tabbedPane.tsx b/packages/html-reporter/src/tabbedPane.tsx index 02d0c6f3b1..38ba00f865 100644 --- a/packages/html-reporter/src/tabbedPane.tsx +++ b/packages/html-reporter/src/tabbedPane.tsx @@ -45,8 +45,9 @@ export const TabbedPane: React.FunctionComponent<{ { tabs.map(tab => { - if (selectedTab === tab.id) + if (selectedTab === tab.id) { return
{tab.render()}
; + } }) } diff --git a/packages/html-reporter/src/testCaseView.tsx b/packages/html-reporter/src/testCaseView.tsx index e4ffa9c15b..38eaeb3f13 100644 --- a/packages/html-reporter/src/testCaseView.tsx +++ b/packages/html-reporter/src/testCaseView.tsx @@ -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}`; } diff --git a/packages/html-reporter/src/testFileView.tsx b/packages/html-reporter/src/testFileView.tsx index f8fad1d646..2113c008ff 100644 --- a/packages/html-reporter/src/testFileView.tsx +++ b/packages/html-reporter/src/testFileView.tsx @@ -74,8 +74,9 @@ export const TestFileView: React.FC{image()}; + } } } } diff --git a/packages/html-reporter/src/testFilesView.tsx b/packages/html-reporter/src/testFilesView.tsx index d21088f575..332cff7ee3 100644 --- a/packages/html-reporter/src/testFilesView.tsx +++ b/packages/html-reporter/src/testFilesView.tsx @@ -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 <>
{report.projectNames.length === 1 && !!report.projectNames[0] &&
Project: {report.projectNames[0]}
} diff --git a/packages/html-reporter/src/testResultView.tsx b/packages/html-reporter/src/testResultView.tsx index 410677cb02..bdaec18d24 100644 --- a/packages/html-reporter/src/testResultView.tsx +++ b/packages/html-reporter/src/testResultView.tsx @@ -36,8 +36,9 @@ function groupImageDiffs(screenshots: Set): ImageDiffWithAnchors const snapshotNameToImageDiff = new Map(); 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): 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
{!!errors.length && {errors.map((error, index) => { - if (error.type === 'screenshot') + if (error.type === 'screenshot') { return ; + } return ; })} } @@ -177,15 +183,18 @@ const StepTreeItem: React.FC<{ const attachmentName = step.title.match(/^attach "(.*)"$/)?.[1]; return {msToString(step.duration)} - {attachmentName && { evt.stopPropagation(); }}>{icons.attachment()}} + {attachmentName && { + evt.stopPropagation(); + }}>{icons.attachment()}} {statusIcon(step.error || step.duration === -1 ? 'failed' : 'passed')} {step.title} {step.count > 1 && <> ✕ {step.count}} {step.location && — {step.location.file}:{step.location.line}} } loadChildren={step.steps.length + (step.snippet ? 1 : 0) ? () => { const children = step.steps.map((s, i) => ); - if (step.snippet) + if (step.snippet) { children.unshift(); + } return children; } : undefined} depth={depth}/>; }; diff --git a/packages/html-reporter/src/treeItem.tsx b/packages/html-reporter/src/treeItem.tsx index 926a398a05..7f06cf1fb7 100644 --- a/packages/html-reporter/src/treeItem.tsx +++ b/packages/html-reporter/src/treeItem.tsx @@ -30,7 +30,9 @@ export const TreeItem: React.FunctionComponent<{ }> = ({ title, loadChildren, onClick, expandByDefault, depth, selected, style }) => { const [expanded, setExpanded] = React.useState(expandByDefault || false); return
- { onClick?.(); setExpanded(!expanded); }} > + { + onClick?.(); setExpanded(!expanded); + }} > {loadChildren && !!expanded && icons.downArrow()} {loadChildren && !expanded && icons.rightArrow()} {!loadChildren && {icons.rightArrow()}} diff --git a/packages/html-reporter/src/utils.ts b/packages/html-reporter/src/utils.ts index 65404b2fe7..60dc3c1be9 100644 --- a/packages/html-reporter/src/utils.ts +++ b/packages/html-reporter/src/utils.ts @@ -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); } diff --git a/packages/playwright-core/src/androidServerImpl.ts b/packages/playwright-core/src/androidServerImpl.ts index f0108e67c7..c45f4ca6bf 100644 --- a/packages/playwright-core/src/androidServerImpl.ts +++ b/packages/playwright-core/src/androidServerImpl.ts @@ -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]; diff --git a/packages/playwright-core/src/browserServerImpl.ts b/packages/playwright-core/src/browserServerImpl.ts index dfe960c5ea..73455aa5c0 100644 --- a/packages/playwright-core/src/browserServerImpl.ts +++ b/packages/playwright-core/src/browserServerImpl.ts @@ -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; } diff --git a/packages/playwright-core/src/cli/driver.ts b/packages/playwright-core/src/cli/driver.ts index e48dc2a4d2..5448618562 100644 --- a/packages/playwright-core/src/cli/driver.ts +++ b/packages/playwright-core/src/cli/driver.ts @@ -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()); diff --git a/packages/playwright-core/src/cli/program.ts b/packages/playwright-core/src/cli/program.ts index 7ce1c4f928..2c0a4b519f 100644 --- a/packages/playwright-core/src/cli/program.ts +++ b/packages/playwright-core/src/cli/program.ts @@ -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 { 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 ', 'browser to use, one of cr, chromium, ff, firefox, wk, webkit', 'chromium') .option('--block-service-workers', 'block service workers') diff --git a/packages/playwright-core/src/cli/programWithTestStub.ts b/packages/playwright-core/src/cli/programWithTestStub.ts index 1c11c14ec2..2ccd71b64a 100644 --- a/packages/playwright-core/src/cli/programWithTestStub.ts +++ b/packages/playwright-core/src/cli/programWithTestStub.ts @@ -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(); +} diff --git a/packages/playwright-core/src/client/android.ts b/packages/playwright-core/src/client/android.ts index 07912bfbdd..3e9ea44341 100644 --- a/packages/playwright-core/src/client/android.ts +++ b/packages/playwright-core/src/client/android.ts @@ -58,8 +58,9 @@ export class Android extends ChannelOwner implements ap } async launchServer(options: types.LaunchServerOptions = {}): Promise { - 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 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 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 i async webView(selector: { pkg?: string; socketName?: string; }, options?: types.TimeoutOptions): Promise { 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 i async screenshot(options: { path?: string } = {}): Promise { 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 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 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 i } async function loadFile(file: string | Buffer): Promise { - 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 { - if (!this._pagePromise) + if (!this._pagePromise) { this._pagePromise = this._fetchPage(); + } return await this._pagePromise; } diff --git a/packages/playwright-core/src/client/artifact.ts b/packages/playwright-core/src/client/artifact.ts index c3a53c0294..7f07d8e0f2 100644 --- a/packages/playwright-core/src/client/artifact.ts +++ b/packages/playwright-core/src/client/artifact.ts @@ -27,8 +27,9 @@ export class Artifact extends ChannelOwner { } async pathAfterFinished(): Promise { - 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; } diff --git a/packages/playwright-core/src/client/browser.ts b/packages/playwright-core/src/client/browser.ts index be47ddeb51..b5f4bd56f3 100644 --- a/packages/playwright-core/src/client/browser.ts +++ b/packages/playwright-core/src/client/browser.ts @@ -65,8 +65,9 @@ export class Browser extends ChannelOwner 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 implements ap async close(options: { reason?: string } = {}): Promise { 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; } } diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index 5ff432ec60..28afee001e 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -79,8 +79,9 @@ export class BrowserContext extends ChannelOwner 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 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 _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 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 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 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 } async newPage(): Promise { - 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 { - 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 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 private async _unrouteInternal(removed: network.RouteHandler[], remaining: network.RouteHandler[], behavior?: 'wait'|'ignoreErrors'|'default'): Promise { 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 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 async newCDPSession(page: Page | Frame): Promise { // 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 } async close(options: { reason?: string } = {}): Promise { - 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 } async function prepareStorageState(options: BrowserContextOptions): Promise { - 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 { - 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 { - if (!certs) + if (!certs) { return undefined; + } const bufferizeContent = async (value?: Buffer, path?: string): Promise => { - if (value) + if (value) { return value; - if (path) + } + if (path) { return await fs.promises.readFile(path); + } }; return await Promise.all(certs.map(async cert => ({ diff --git a/packages/playwright-core/src/client/browserType.ts b/packages/playwright-core/src/client/browserType.ts index 06672cc64e..d4eb59ff5a 100644 --- a/packages/playwright-core/src/client/browserType.ts +++ b/packages/playwright-core/src/client/browserType.ts @@ -56,8 +56,9 @@ export class BrowserType extends ChannelOwner 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 imple } async launchServer(options: LaunchServerOptions = {}): Promise { - 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 imple connect(options: api.ConnectOptions & { wsEndpoint: string }): Promise; connect(wsEndpoint: string, options?: api.ConnectOptions): Promise; async connect(optionsOrWsEndpoint: string | (api.ConnectOptions & { wsEndpoint: string }), options?: api.ConnectOptions): Promise{ - 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 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 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 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 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 imple async connectOverCDP(options: api.ConnectOverCDPOptions & { wsEndpoint?: string }): Promise; async connectOverCDP(endpointURL: string, options?: api.ConnectOverCDPOptions): Promise; 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 { - 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 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 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); } diff --git a/packages/playwright-core/src/client/channelOwner.ts b/packages/playwright-core/src/client/channelOwner.ts index a5d753507b..a0f5e127f0 100644 --- a/packages/playwright-core/src/client/channelOwner.ts +++ b/packages/playwright-core/src/client/channelOwner.ts @@ -80,37 +80,42 @@ export abstract class ChannelOwner(func: (apiZone: ApiZone) => Promise, isInternal?: boolean): Promise { const logger = this._logger; const apiZone = zones.zoneData('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'); 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\n' + e.stack : ''; - if (apiName && !apiName.includes('')) + if (apiName && !apiName.includes('')) { 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 { - 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]; diff --git a/packages/playwright-core/src/client/clock.ts b/packages/playwright-core/src/client/clock.ts index 73eb538c26..338bf634ed 100644 --- a/packages/playwright-core/src/client/clock.ts +++ b/packages/playwright-core/src/client/clock.ts @@ -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() }; } diff --git a/packages/playwright-core/src/client/connection.ts b/packages/playwright-core/src/client/connection.ts index 75d1878da3..dba0cdd303 100644 --- a/packages/playwright-core/src/client/connection.ts +++ b/packages/playwright-core/src/client/connection.ts @@ -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 { - 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', '; 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); diff --git a/packages/playwright-core/src/client/electron.ts b/packages/playwright-core/src/client/electron.ts index fe5b6e189e..7d73be023e 100644 --- a/packages/playwright-core/src/client/electron.ts +++ b/packages/playwright-core/src/client/electron.ts @@ -74,8 +74,9 @@ export class ElectronApplication extends ChannelOwner this._onPage(page)); this._channel.on('close', () => { this.emit(Events.ElectronApplication.Close); @@ -102,8 +103,9 @@ export class ElectronApplication extends ChannelOwner { - 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 new TargetClosedError()); + } const result = await waiter.waitForEvent(this, event, predicate as any); waiter.dispose(); return result; diff --git a/packages/playwright-core/src/client/elementHandle.ts b/packages/playwright-core/src/client/elementHandle.ts index 0a89d3091b..c731781ffb 100644 --- a/packages/playwright-core/src/client/elementHandle.ts +++ b/packages/playwright-core/src/client/elementHandle.ts @@ -147,8 +147,9 @@ export class ElementHandle extends JSHandle 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 extends JSHandle 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 { @@ -187,8 +189,9 @@ export class ElementHandle extends JSHandle implements async screenshot(options: Omit & { path?: string, mask?: Locator[] } = {}): Promise { 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 extends JSHandle 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; diff --git a/packages/playwright-core/src/client/errors.ts b/packages/playwright-core/src/client/errors.ts index 51555202e5..fdcfc1e1ac 100644 --- a/packages/playwright-core/src/client/errors.ts +++ b/packages/playwright-core/src/client/errors.ts @@ -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') { diff --git a/packages/playwright-core/src/client/eventEmitter.ts b/packages/playwright-core/src/client/eventEmitter.ts index c0375a70dc..01d620225d 100644 --- a/packages/playwright-core/src/client/eventEmitter.ts +++ b/packages/playwright-core/src/client/eventEmitter.ts @@ -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; removeAllListeners(type?: string, options?: { behavior?: 'wait'|'ignoreErrors'|'default' }): this | Promise { 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); diff --git a/packages/playwright-core/src/client/fetch.ts b/packages/playwright-core/src/client/fetch.ts index 58928532ac..d508b6fd3a 100644 --- a/packages/playwright-core/src/client/fetch.ts +++ b/packages/playwright-core/src/client/fetch.ts @@ -110,8 +110,9 @@ export class APIRequestContext extends ChannelOwner { 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 { 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 { 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 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 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 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(this._page!, Events.Page.FrameDetached, new Error('Navigating frame was detached!'), frame => frame === this); @@ -123,8 +129,9 @@ export class Frame extends ChannelOwner implements api.Fr const navigatedEvent = await waiter.waitForEvent(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 implements api.Fr } async waitForURL(url: URLMatch, options: { waitUntil?: LifecycleEvent, timeout?: number } = {}): Promise { - 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 implements api.Fr waitForSelector(selector: string, options: channels.FrameWaitForSelectorOptions & { state: 'attached' | 'visible' }): Promise>; waitForSelector(selector: string, options?: channels.FrameWaitForSelectorOptions): Promise | null>; async waitForSelector(selector: string, options: channels.FrameWaitForSelectorOptions = {}): Promise | 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 | null; } @@ -421,10 +431,11 @@ export class Frame extends ChannelOwner 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 implements api.Fr } async waitForFunction(pageFunction: structs.PageFunction, arg?: Arg, options: WaitForFunctionOptions = {}): Promise> { - 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 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; } diff --git a/packages/playwright-core/src/client/harRouter.ts b/packages/playwright-core/src/client/harRouter.ts index 5e617d7ce9..17320ffd78 100644 --- a/packages/playwright-core/src/client/harRouter.ts +++ b/packages/playwright-core/src/client/harRouter.ts @@ -31,8 +31,9 @@ export class HarRouter { static async create(localUtils: LocalUtils, file: string, notFoundAction: HarNotFoundAction, options: { urlMatch?: URLMatch }): Promise { 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') { diff --git a/packages/playwright-core/src/client/jsHandle.ts b/packages/playwright-core/src/client/jsHandle.ts index 8577400f3d..3561b28e8e 100644 --- a/packages/playwright-core/src/client/jsHandle.ts +++ b/packages/playwright-core/src/client/jsHandle.ts @@ -51,8 +51,9 @@ export class JSHandle extends ChannelOwner im async getProperties(): Promise> { const map = new Map(); - 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 extends ChannelOwner 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.'); + } } diff --git a/packages/playwright-core/src/client/localUtils.ts b/packages/playwright-core/src/client/localUtils.ts index 530547b227..6c4bc331fc 100644 --- a/packages/playwright-core/src/client/localUtils.ts +++ b/packages/playwright-core/src/client/localUtils.ts @@ -35,7 +35,8 @@ export class LocalUtils extends ChannelOwner { 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; + } } } diff --git a/packages/playwright-core/src/client/locator.ts b/packages/playwright-core/src/client/locator.ts index 84e68e1b20..6e7a7df0e1 100644 --- a/packages/playwright-core/src/client/locator.ts +++ b/packages/playwright-core/src/client/locator.ts @@ -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(async () => { const result = await this._frame._channel.waitForSelector({ selector: this._selector, strict: true, state: 'attached', timeout }); const handle = ElementHandle.fromNullable(result.element) as ElementHandle | 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); } diff --git a/packages/playwright-core/src/client/network.ts b/packages/playwright-core/src/client/network.ts index cb18681ccf..56e5e879bb 100644 --- a/packages/playwright-core/src/client/network.ts +++ b/packages/playwright-core/src/client/network.ts @@ -99,8 +99,9 @@ export class Request extends ChannelOwner 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 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 = {}; 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 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 { - 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 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 implements ap async sizes(): Promise { 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 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 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 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 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 }, 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 }; 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 } 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 } 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 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 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 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; } diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index f1d90fece2..0f0cd18b09 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -166,16 +166,18 @@ export class Page extends ChannelOwner 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 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 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 implements api.Page } async opener(): Promise { - 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 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 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 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 implements api.Page } async addLocatorHandler(locator: Locator, handler: (locator: Locator) => any, options: { times?: number, noWaitAfter?: boolean } = {}): Promise { - 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 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 implements api.Page async waitForRequest(urlOrPredicate: string | RegExp | ((r: Request) => boolean | Promise), options: { timeout?: number } = {}): Promise { 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 implements api.Page async waitForResponse(urlOrPredicate: string | RegExp | ((r: Response) => boolean | Promise), options: { timeout?: number } = {}): Promise { 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 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 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 implements api.Page private async _unrouteInternal(removed: RouteHandler[], remaining: RouteHandler[], behavior?: 'wait'|'ignoreErrors'|'default'): Promise { 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 implements api.Page async screenshot(options: Omit & { path?: string, mask?: Locator[] } = {}): Promise { 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 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 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 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 implements api.Page async pdf(options: PDFOptions = {}): Promise { 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 { 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 { } 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)}"`; + } } diff --git a/packages/playwright-core/src/client/playwright.ts b/packages/playwright-core/src/client/playwright.ts index 9933ce15de..7221a04115 100644 --- a/packages/playwright-core/src/client/playwright.ts +++ b/packages/playwright-core/src/client/playwright.ts @@ -51,7 +51,7 @@ export class Playwright extends ChannelOwner { 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 }; diff --git a/packages/playwright-core/src/client/selectors.ts b/packages/playwright-core/src/client/selectors.ts index 2739be0e8d..989cf8d233 100644 --- a/packages/playwright-core/src/client/selectors.ts +++ b/packages/playwright-core/src/client/selectors.ts @@ -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 { 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) { diff --git a/packages/playwright-core/src/client/stream.ts b/packages/playwright-core/src/client/stream.ts index bb202ac4ad..115d35b8be 100644 --- a/packages/playwright-core/src/client/stream.ts +++ b/packages/playwright-core/src/client/stream.ts @@ -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 { diff --git a/packages/playwright-core/src/client/tracing.ts b/packages/playwright-core/src/client/tracing.ts index 2481741e3e..dfb1c7be3b 100644 --- a/packages/playwright-core/src/client/tracing.ts +++ b/packages/playwright-core/src/client/tracing.ts @@ -87,8 +87,9 @@ export class Tracing extends ChannelOwner 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 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; } diff --git a/packages/playwright-core/src/client/video.ts b/packages/playwright-core/src/client/video.ts index 08c39d4247..4c010b0e16 100644 --- a/packages/playwright-core/src/client/video.ts +++ b/packages/playwright-core/src/client/video.ts @@ -35,24 +35,28 @@ export class Video implements api.Video { } async path(): Promise { - 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 { 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 { const artifact = await this._artifact; - if (artifact) + if (artifact) { await artifact.delete(); + } } } diff --git a/packages/playwright-core/src/client/waiter.ts b/packages/playwright-core/src/client/waiter.ts index 7b57fe8960..05ae4e80c2 100644 --- a/packages/playwright-core/src/client/waiter.ts +++ b/packages/playwright-core/src/client/waiter.ts @@ -56,14 +56,19 @@ export class Waiter { rejectOnEvent(emitter: EventEmitter, event: string, error: Error | (() => Error), predicate?: (arg: T) => boolean | Promise) { 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(promise: Promise, dispose?: () => void): Promise { 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, dispose?: () => void) { this._failures.push(promise); - if (dispose) + if (dispose) { this._dispose.push(dispose); + } } } @@ -113,8 +123,9 @@ function waitForEvent(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, 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; diff --git a/packages/playwright-core/src/client/worker.ts b/packages/playwright-core/src/client/worker.ts index 31d5d3057f..71145ad408 100644 --- a/packages/playwright-core/src/client/worker.ts +++ b/packages/playwright-core/src/client/worker.ts @@ -37,10 +37,12 @@ export class Worker extends ChannelOwner 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())); diff --git a/packages/playwright-core/src/common/socksProxy.ts b/packages/playwright-core/src/common/socksProxy.ts index 6566262d64..615d47eb11 100644 --- a/packages/playwright-core/src/common/socksProxy.ts +++ b/packages/playwright-core/src/common/socksProxy.ts @@ -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 { this._fence = this._offset + length; - if (!this._buffer || this._buffer.length < this._fence) + if (!this._buffer || this._buffer.length < this._fence) { await new Promise(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 === '') { 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 { @@ -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 }; diff --git a/packages/playwright-core/src/common/timeoutSettings.ts b/packages/playwright-core/src/common/timeoutSettings.ts index 65c8be1ecf..0a0a6f581a 100644 --- a/packages/playwright-core/src/common/timeoutSettings.ts +++ b/packages/playwright-core/src/common/timeoutSettings.ts @@ -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; } } diff --git a/packages/playwright-core/src/image_tools/stats.ts b/packages/playwright-core/src/image_tools/stats.ts index b35371b4a1..fb16f1d2f2 100644 --- a/packages/playwright-core/src/image_tools/stats.ts +++ b/packages/playwright-core/src/image_tools/stats.ts @@ -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; } diff --git a/packages/playwright-core/src/protocol/serializers.ts b/packages/playwright-core/src/protocol/serializers.ts index 559d4a062e..6794d20797 100644 --- a/packages/playwright-core/src/protocol/serializers.ts +++ b/packages/playwright-core/src/protocol/serializers.ts @@ -21,60 +21,77 @@ export function parseSerializedValue(value: SerializedValue, handles: any[] | un } function innerParseSerializedValue(value: SerializedValue, handles: any[] | undefined, refs: Map): 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'); diff --git a/packages/playwright-core/src/protocol/transport.ts b/packages/playwright-core/src/protocol/transport.ts index e647100c63..ad8fdd29d0 100644 --- a/packages/playwright-core/src/protocol/transport.ts +++ b/packages/playwright-core/src/protocol/transport.ts @@ -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')); + } }); } } diff --git a/packages/playwright-core/src/protocol/validatorPrimitives.ts b/packages/playwright-core/src/protocol/validatorPrimitives.ts index 9d4614512b..6c66e153d4 100644 --- a/packages/playwright-core/src/protocol/validatorPrimitives.ts +++ b/packages/playwright-core/src/protocol/validatorPrimitives.ts @@ -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); }; }; diff --git a/packages/playwright-core/src/remote/playwrightConnection.ts b/packages/playwright-core/src/remote/playwrightConnection.ts index cce31207a8..3b10592d9d 100644 --- a/packages/playwright-core/src/remote/playwrightConnection.ts +++ b/packages/playwright-core/src/remote/playwrightConnection.ts @@ -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 { - 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); } diff --git a/packages/playwright-core/src/remote/playwrightServer.ts b/packages/playwright-core/src/remote/playwrightServer.ts index 121cc2d83a..298827d57c 100644 --- a/packages/playwright-core/src/remote/playwrightServer.ts +++ b/packages/playwright-core/src/remote/playwrightServer.ts @@ -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'); } }); diff --git a/packages/playwright-core/src/server/accessibility.ts b/packages/playwright-core/src/server/accessibility.ts index b92c5411f5..000045d7bf 100644 --- a/packages/playwright-core/src/server/accessibility.ts +++ b/packages/playwright-core/src/server/accessibility.ts @@ -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 = 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, 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): 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]; } diff --git a/packages/playwright-core/src/server/android/android.ts b/packages/playwright-core/src/server/android/android.ts index 1af083916c..e0e437ab26 100644 --- a/packages/playwright-core/src/server/android/android.ts +++ b/packages/playwright-core/src/server/android/android.ts @@ -80,14 +80,16 @@ export class Android extends SdkObject { const newSerials = new Set(); 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 { - 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 { 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(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(); 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)); + } }); }); } diff --git a/packages/playwright-core/src/server/android/backendAdb.ts b/packages/playwright-core/src/server/android/backendAdb.ts index cd718b9d95..3cca53b07a 100644 --- a/packages/playwright-core/src/server/android/backendAdb.ts +++ b/packages/playwright-core/src/server/android/backendAdb.ts @@ -54,14 +54,16 @@ class AdbDevice implements DeviceBackend { } runCommand(command: string): Promise { - 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 { - 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 { 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(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 { - while (!this._isClosed) + while (!this._isClosed) { await new Promise(f => this._notifyReader = f); + } return this._buffer; } diff --git a/packages/playwright-core/src/server/ariaSnapshot.ts b/packages/playwright-core/src/server/ariaSnapshot.ts index 516688fef3..bfeecdf2cf 100644 --- a/packages/playwright-core/src/server/ariaSnapshot.ts +++ b/packages/playwright-core/src/server/ariaSnapshot.ts @@ -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; } diff --git a/packages/playwright-core/src/server/artifact.ts b/packages/playwright-core/src/server/artifact.ts index 8824365b80..940d646fb6 100644 --- a/packages/playwright-core/src/server/artifact.ts +++ b/packages/playwright-core/src/server/artifact.ts @@ -49,21 +49,26 @@ export class Artifact extends SdkObject { } async localPathAfterFinished(): Promise { - 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 { - 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 { - 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 { // 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 = []; diff --git a/packages/playwright-core/src/server/bidi/bidiBrowser.ts b/packages/playwright-core/src/server/bidi/bidiBrowser.ts index 955f6274a3..569998b115 100644 --- a/packages/playwright-core/src/server/bidi/bidiBrowser.ts +++ b/packages/playwright-core/src/server/bidi/bidiBrowser.ts @@ -41,8 +41,9 @@ export class BidiBrowser extends Browser { static async connect(parent: SdkObject, transport: ConnectionTransport, options: BrowserOptions): Promise { 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 { 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) { diff --git a/packages/playwright-core/src/server/bidi/bidiChromium.ts b/packages/playwright-core/src/server/bidi/bidiChromium.ts index 9572ac71ed..35c8ccf140 100644 --- a/packages/playwright-core/src/server/bidi/bidiChromium.ts +++ b/packages/playwright-core/src/server/bidi/bidiChromium.ts @@ -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]); + } } } diff --git a/packages/playwright-core/src/server/bidi/bidiConnection.ts b/packages/playwright-core/src/server/bidi/bidiConnection.ts index 48472bf748..6456302a9e 100644 --- a/packages/playwright-core/src/server/bidi/bidiConnection.ts +++ b/packages/playwright-core/src/server/bidi/bidiConnection.ts @@ -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 { - 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); diff --git a/packages/playwright-core/src/server/bidi/bidiExecutionContext.ts b/packages/playwright-core/src/server/bidi/bidiExecutionContext.ts index f53b160ccf..cfc691f52e 100644 --- a/packages/playwright-core/src/server/bidi/bidiExecutionContext.ts +++ b/packages/playwright-core/src/server/bidi/bidiExecutionContext.ts @@ -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; } diff --git a/packages/playwright-core/src/server/bidi/bidiFirefox.ts b/packages/playwright-core/src/server/bidi/bidiFirefox.ts index 204cabdef7..2bb2f69269 100644 --- a/packages/playwright-core/src/server/bidi/bidiFirefox.ts +++ b/packages/playwright-core/src/server/bidi/bidiFirefox.ts @@ -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'); + } } } diff --git a/packages/playwright-core/src/server/bidi/bidiNetworkManager.ts b/packages/playwright-core/src/server/bidi/bidiNetworkManager.ts index b7c314bd10..465e51e081 100644 --- a/packages/playwright-core/src/server/bidi/bidiNetworkManager.ts +++ b/packages/playwright-core/src/server/bidi/bidiNetworkManager.ts @@ -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(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; } diff --git a/packages/playwright-core/src/server/bidi/bidiPage.ts b/packages/playwright-core/src/server/bidi/bidiPage.ts index 9b501c5484..b25a4711ff 100644 --- a/packages/playwright-core/src/server/bidi/bidiPage.ts +++ b/packages/playwright-core/src/server/bidi/bidiPage.ts @@ -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 { 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 { 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 { - 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): Promise { 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; + } throw new Error('Failed to adopt element handle.'); } @@ -551,10 +579,13 @@ export class BidiPage implements PageDelegate { async getFrameElement(frame: frames.Frame): Promise { 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; } diff --git a/packages/playwright-core/src/server/bidi/bidiPdf.ts b/packages/playwright-core/src/server/bidi/bidiPdf.ts index 89fefb5260..b773e6b6f7 100644 --- a/packages/playwright-core/src/server/bidi/bidiPdf.ts +++ b/packages/playwright-core/src/server/bidi/bidiPdf.ts @@ -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)) { diff --git a/packages/playwright-core/src/server/bidi/third_party/bidiDeserializer.ts b/packages/playwright-core/src/server/bidi/third_party/bidiDeserializer.ts index 3637b4af36..4bdb3d081c 100644 --- a/packages/playwright-core/src/server/bidi/third_party/bidiDeserializer.ts +++ b/packages/playwright-core/src/server/bidi/third_party/bidiDeserializer.ts @@ -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': diff --git a/packages/playwright-core/src/server/bidi/third_party/bidiKeyboard.ts b/packages/playwright-core/src/server/bidi/third_party/bidiKeyboard.ts index 307d83fb87..e9819d0c8c 100644 --- a/packages/playwright-core/src/server/bidi/third_party/bidiKeyboard.ts +++ b/packages/playwright-core/src/server/bidi/third_party/bidiKeyboard.ts @@ -5,7 +5,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -/* eslint-disable curly */ export const getBidiKeyValue = (key: string) => { switch (key) { diff --git a/packages/playwright-core/src/server/bidi/third_party/bidiSerializer.ts b/packages/playwright-core/src/server/bidi/third_party/bidiSerializer.ts index 97e8381328..727336b5ad 100644 --- a/packages/playwright-core/src/server/bidi/third_party/bidiSerializer.ts +++ b/packages/playwright-core/src/server/bidi/third_party/bidiSerializer.ts @@ -7,7 +7,7 @@ import type * as Bidi from './bidiProtocol'; -/* eslint-disable curly, indent */ +/* eslint-disable indent */ /** * @internal diff --git a/packages/playwright-core/src/server/bidi/third_party/firefoxPrefs.ts b/packages/playwright-core/src/server/bidi/third_party/firefoxPrefs.ts index 7c08bebe6d..2bbc6d0418 100644 --- a/packages/playwright-core/src/server/bidi/third_party/firefoxPrefs.ts +++ b/packages/playwright-core/src/server/bidi/third_party/firefoxPrefs.ts @@ -7,7 +7,7 @@ import fs from 'fs'; import path from 'path'; -/* eslint-disable curly, indent */ +/* eslint-disable indent */ interface ProfileOptions { preferences: Record; diff --git a/packages/playwright-core/src/server/browser.ts b/packages/playwright-core/src/server/browser.ts index 252443bc44..1dc04a18c2 100644 --- a/packages/playwright-core/src/server/browser.ts +++ b/packages/playwright-core/src/server/browser.ts @@ -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() { diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index fc20c52bb5..32f4efaa99 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -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 { - 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 { - 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[] = []; 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 { 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) { 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 }; } diff --git a/packages/playwright-core/src/server/browserType.ts b/packages/playwright-core/src/server/browserType.ts index 128b80a352..6272f2a05b 100644 --- a/packages/playwright-core/src/server/browserType.ts +++ b/packages/playwright-core/src/server/browserType.ts @@ -80,23 +80,28 @@ export abstract class BrowserType extends SdkObject { async launch(metadata: CallMetadata, options: types.LaunchOptions, protocolLogger?: types.ProtocolLogger): Promise { 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 { 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 { @@ -280,10 +296,12 @@ export abstract class BrowserType extends SdkObject { } async _createArtifactDirs(options: types.LaunchOptions): Promise { - 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 { @@ -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; + } } } diff --git a/packages/playwright-core/src/server/chromium/chromium.ts b/packages/playwright-core/src/server/chromium/chromium.ts index 8fce8f51ca..7ac9c6f8be 100644 --- a/packages/playwright-core/src/server/chromium/chromium.ts +++ b/packages/playwright-core/src/server/chromium/chromium.ts @@ -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) { 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 { 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(` connecting to ${hubUrl}`); @@ -229,8 +238,9 @@ export class Chromium extends BrowserType { progress.log(` 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(` 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(` 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; } diff --git a/packages/playwright-core/src/server/chromium/crAccessibility.ts b/packages/playwright-core/src/server/chromium/crAccessibility.ts index 4114663a0e..c81866d734 100644 --- a/packages/playwright-core/src/server/chromium/crAccessibility.ts +++ b/packages/playwright-core/src/server/chromium/crAccessibility.ts @@ -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 = 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 = [ @@ -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 = [ @@ -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 = [ @@ -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 = 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!; } diff --git a/packages/playwright-core/src/server/chromium/crBrowser.ts b/packages/playwright-core/src/server/chromium/crBrowser.ts index 9f03803dcb..397725e34a 100644 --- a/packages/playwright-core/src/server/chromium/crBrowser.ts +++ b/packages/playwright-core/src/server/chromium/crBrowser.ts @@ -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 { - 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 { 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 { 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 { 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 { 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 { 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 { - 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 { - 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'); diff --git a/packages/playwright-core/src/server/chromium/crConnection.ts b/packages/playwright-core/src/server/chromium/crConnection.ts index c257d0d7ec..199d90fd07 100644 --- a/packages/playwright-core/src/server/chromium/crConnection.ts +++ b/packages/playwright-core/src/server/chromium/crConnection.ts @@ -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 { @@ -140,8 +144,9 @@ export class CRSession extends EventEmitter { method: T, params?: Protocol.CommandParameters[T] ): Promise { - 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(); + } })]; } diff --git a/packages/playwright-core/src/server/chromium/crCoverage.ts b/packages/playwright-core/src/server/chromium/crCoverage.ts index ccb9b40ae0..9e272431ac 100644 --- a/packages/playwright-core/src/server/chromium/crCoverage.ts +++ b/packages/playwright-core/src/server/chromium/crCoverage.ts @@ -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 { @@ -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); diff --git a/packages/playwright-core/src/server/chromium/crDevTools.ts b/packages/playwright-core/src/server/chromium/crDevTools.ts index 40cd134bcc..3a41d2671e 100644 --- a/packages/playwright-core/src/server/chromium/crDevTools.ts +++ b/packages/playwright-core/src/server/chromium/crDevTools.ts @@ -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 { diff --git a/packages/playwright-core/src/server/chromium/crDragDrop.ts b/packages/playwright-core/src/server/chromium/crDragDrop.ts index ea4af1ce7e..94b62ea6c7 100644 --- a/packages/playwright-core/src/server/chromium/crDragDrop.ts +++ b/packages/playwright-core/src/server/chromium/crDragDrop.ts @@ -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; diff --git a/packages/playwright-core/src/server/chromium/crExecutionContext.ts b/packages/playwright-core/src/server/chromium/crExecutionContext.ts index 661d216fb4..d057e783c6 100644 --- a/packages/playwright-core/src/server/chromium/crExecutionContext.ts +++ b/packages/playwright-core/src/server/chromium/crExecutionContext.ts @@ -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; } diff --git a/packages/playwright-core/src/server/chromium/crInput.ts b/packages/playwright-core/src/server/chromium/crInput.ts index bbfd973d10..30a947a4f7 100644 --- a/packages/playwright-core/src/server/chromium/crInput.ts +++ b/packages/playwright-core/src/server/chromium/crInput.ts @@ -32,18 +32,21 @@ export class RawKeyboardImpl implements input.RawKeyboard { ) { } _commandsForCode(code: string, modifiers: Set) { - 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, code: string, keyCode: number, keyCodeWithoutLocation: number, key: string, location: number, autoRepeat: boolean, text: string | undefined): Promise { - 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, modifiers: Set, clickCount: number): Promise { - if (this._dragManager.isDragging()) + if (this._dragManager.isDragging()) { return; + } await this._client.send('Input.dispatchMouseEvent', { type: 'mousePressed', button, diff --git a/packages/playwright-core/src/server/chromium/crNetworkManager.ts b/packages/playwright-core/src/server/chromium/crNetworkManager.ts index a8ff5a08dc..955f7e7b5f 100644 --- a/packages/playwright-core/src/server/chromium/crNetworkManager.ts +++ b/packages/playwright-core/src/server/chromium/crNetworkManager.ts @@ -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) { 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(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 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) { 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. diff --git a/packages/playwright-core/src/server/chromium/crPage.ts b/packages/playwright-core/src/server/chromium/crPage.ts index 0dc4703547..5346798870 100644 --- a/packages/playwright-core/src/server/chromium/crPage.ts +++ b/packages/playwright-core/src/server/chromium/crPage.ts @@ -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) { 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 { 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 { - 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 { @@ -317,8 +324,9 @@ export class CRPage implements PageDelegate { async setInputFilePaths(handle: dom.ElementHandle, files: string[]): Promise { 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 { 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; - if (!this._isMainFrame()) + if (!this._isMainFrame()) { this._addRendererListeners(); + } this._addBrowserListeners(); const promises: Promise[] = [ 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 { 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 { - 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 { 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 { - 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 { 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 { @@ -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 { 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; } diff --git a/packages/playwright-core/src/server/chromium/crPdf.ts b/packages/playwright-core/src/server/chromium/crPdf.ts index 8dae2d1fa1..ededb689f4 100644 --- a/packages/playwright-core/src/server/chromium/crPdf.ts +++ b/packages/playwright-core/src/server/chromium/crPdf.ts @@ -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)) { diff --git a/packages/playwright-core/src/server/chromium/crProtocolHelper.ts b/packages/playwright-core/src/server/chromium/crProtocolHelper.ts index 9458bed124..0099be334a 100644 --- a/packages/playwright-core/src/server/chromium/crProtocolHelper.ts +++ b/packages/playwright-core/src/server/chromium/crProtocolHelper.ts @@ -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): 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): 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; } diff --git a/packages/playwright-core/src/server/chromium/crServiceWorker.ts b/packages/playwright-core/src/server/chromium/crServiceWorker.ts index 730504866b..27b2fca631 100644 --- a/packages/playwright-core/src/server/chromium/crServiceWorker.ts +++ b/packages/playwright-core/src/server/chromium/crServiceWorker.ts @@ -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 { - if (!this._isNetworkInspectionEnabled()) + if (!this._isNetworkInspectionEnabled()) { return; + } await this._networkManager?.setOffline(!!this._browserContext._options.offline).catch(() => {}); } async updateHttpCredentials(): Promise { - if (!this._isNetworkInspectionEnabled()) + if (!this._isNetworkInspectionEnabled()) { return; + } await this._networkManager?.authenticate(this._browserContext._options.httpCredentials || null).catch(() => {}); } async updateExtraHTTPHeaders(): Promise { - if (!this._isNetworkInspectionEnabled()) + if (!this._isNetworkInspectionEnabled()) { return; + } await this._networkManager?.setExtraHTTPHeaders(this._browserContext._options.extraHTTPHeaders || []).catch(() => {}); } async updateRequestInterception(): Promise { - 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(() => {}); } } diff --git a/packages/playwright-core/src/server/chromium/videoRecorder.ts b/packages/playwright-core/src/server/chromium/videoRecorder.ts index 68bfbea037..b9b10d4911 100644 --- a/packages/playwright-core/src/server/chromium/videoRecorder.ts +++ b/packages/playwright-core/src/server/chromium/videoRecorder.ts @@ -38,8 +38,9 @@ export class VideoRecorder { private _ffmpegPath: string; static async launch(page: Page, ffmpegPath: string, options: types.PageScreencastOptions): Promise { - 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; diff --git a/packages/playwright-core/src/server/clock.ts b/packages/playwright-core/src/server/clock.ts index e77399c4fe..232a8e2984 100644 --- a/packages/playwright-core/src/server/clock.ts +++ b/packages/playwright-core/src/server/clock.ts @@ -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(); } diff --git a/packages/playwright-core/src/server/codegen/csharp.ts b/packages/playwright-core/src/server/codegen/csharp.ts index f9166c2a91..80a0c75304 100644 --- a/packages/playwright-core/src/server/codegen/csharp.ts +++ b/packages/playwright-core/src/server/codegen/csharp.ts @@ -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'); diff --git a/packages/playwright-core/src/server/codegen/java.ts b/packages/playwright-core/src/server/codegen/java.ts index 1fafa0642c..7b466049d6 100644 --- a/packages/playwright-core/src/server/codegen/java.ts +++ b/packages/playwright-core/src/server/codegen/java.ts @@ -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'); } diff --git a/packages/playwright-core/src/server/codegen/javascript.ts b/packages/playwright-core/src/server/codegen/javascript.ts index e5f72ce122..2b5dd5cfce 100644 --- a/packages/playwright-core/src/server/codegen/javascript.ts +++ b/packages/playwright-core/src/server/codegen/javascript.ts @@ -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}\``; } diff --git a/packages/playwright-core/src/server/codegen/language.ts b/packages/playwright-core/src/server/codegen/language.ts index b38959b89b..e6c970f39d 100644 --- a/packages/playwright-core/src/server/codegen/language.ts +++ b/packages/playwright-core/src/server/codegen/language.ts @@ -31,8 +31,9 @@ export function sanitizeDeviceOptions(device: any, options: BrowserContextOption // Filter out all the properties from the device descriptor. const cleanedOptions: Record = {}; 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; } diff --git a/packages/playwright-core/src/server/codegen/python.ts b/packages/playwright-core/src/server/codegen/python.ts index 8d4ea7659d..88dac10b74 100644 --- a/packages/playwright-core/src/server/codegen/python.ts +++ b/packages/playwright-core/src/server/codegen/python.ts @@ -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; diff --git a/packages/playwright-core/src/server/console.ts b/packages/playwright-core/src/server/console.ts index e1faa7fb8f..9cc852a4b1 100644 --- a/packages/playwright-core/src/server/console.ts +++ b/packages/playwright-core/src/server/console.ts @@ -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; } diff --git a/packages/playwright-core/src/server/cookieStore.ts b/packages/playwright-core/src/server/cookieStore.ts index d1842660c7..51f8f67930 100644 --- a/packages/playwright-core/src/server/cookieStore.ts +++ b/packages/playwright-core/src/server/cookieStore.ts @@ -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> = 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 { 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) { 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); } diff --git a/packages/playwright-core/src/server/debugController.ts b/packages/playwright-core/src/server/debugController.ts index b810e2fa65..ac6d6ac4f3 100644 --- a/packages/playwright-core/src/server/debugController.ts +++ b/packages/playwright-core/src/server/debugController.ts @@ -82,15 +82,18 @@ export class DebugController extends SdkObject { async resetForReuse() { const contexts = new Set(); - 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 { const contexts = new Set(); - 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' }); + } } } } diff --git a/packages/playwright-core/src/server/debugger.ts b/packages/playwright-core/src/server/debugger.ts index 420b438ccb..ba0314ea05 100644 --- a/packages/playwright-core/src/server/debugger.ts +++ b/packages/playwright-core/src/server/debugger.ts @@ -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 { - 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 { - if (this._slowMo && shouldSlowMo(metadata)) + if (this._slowMo && shouldSlowMo(metadata)) { await this._doSlowMo(); + } } async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise { - 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(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. diff --git a/packages/playwright-core/src/server/dialog.ts b/packages/playwright-core/src/server/dialog.ts index f0793d43fb..cdb3dadc21 100644 --- a/packages/playwright-core/src/server/dialog.ts +++ b/packages/playwright-core/src/server/dialog.ts @@ -73,9 +73,10 @@ export class Dialog extends SdkObject { } async close() { - if (this._type === 'beforeunload') + if (this._type === 'beforeunload') { await this.accept(); - else + } else { await this.dismiss(); + } } } diff --git a/packages/playwright-core/src/server/dispatchers/androidDispatcher.ts b/packages/playwright-core/src/server/dispatchers/androidDispatcher.ts index 77f38c5558..9d6284a786 100644 --- a/packages/playwright-core/src/server/dispatchers/androidDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/androidDispatcher.ts @@ -54,8 +54,9 @@ export class AndroidDeviceDispatcher extends Dispatcher this._dispatchEvent('webViewAdded', { webView })); this.addObjectListener(AndroidDevice.Events.WebViewRemoved, socketName => this._dispatchEvent('webViewRemoved', { socketName })); this.addObjectListener(AndroidDevice.Events.Close, socketName => this._dispatchEvent('close')); @@ -113,16 +114,18 @@ export class AndroidDeviceDispatcher extends Dispatcher this._object.send('inputPress', { keyCode }))); } async inputPress(params: channels.AndroidDeviceInputPressParams) { - if (!keyMap.has(params.key)) + if (!keyMap.has(params.key)) { throw new Error('Unknown key: ' + params.key); + } await this._object.send('inputPress', { keyCode: keyMap.get(params.key) }); } @@ -315,6 +318,7 @@ function fixupAndroidElementInfo(info: channels.AndroidElementInfo) { info.res = info.res || ''; info.desc = info.desc || ''; info.text = info.text || ''; - for (const child of info.children || []) + for (const child of info.children || []) { fixupAndroidElementInfo(child); + } } diff --git a/packages/playwright-core/src/server/dispatchers/artifactDispatcher.ts b/packages/playwright-core/src/server/dispatchers/artifactDispatcher.ts index 1996f11776..2140c4bbdb 100644 --- a/packages/playwright-core/src/server/dispatchers/artifactDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/artifactDispatcher.ts @@ -31,8 +31,9 @@ export class ArtifactDispatcher extends Dispatcher(artifact); return result || new ArtifactDispatcher(parentScope, artifact); } diff --git a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts index c6ffce49f7..2d77c77100 100644 --- a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts @@ -77,12 +77,14 @@ export class BrowserContextDispatcher extends Dispatcher { this._dispatchEvent('page', { page: PageDispatcher.from(this, page) }); }); @@ -107,18 +109,21 @@ export class BrowserContextDispatcher extends Dispatcher { - if (this._shouldDispatchEvent(dialog.page(), 'dialog')) + if (this._shouldDispatchEvent(dialog.page(), 'dialog')) { this._dispatchEvent('dialog', { dialog: new DialogDispatcher(this, dialog) }); - else + } else { dialog.close().catch(() => {}); + } }); if (context._browser.options.name === 'chromium') { - for (const page of (context as CRBrowserContext).backgroundPages()) + for (const page of (context as CRBrowserContext).backgroundPages()) { this._dispatchEvent('backgroundPage', { page: PageDispatcher.from(this, page) }); + } this.addObjectListener(CRBrowserContext.CREvents.BackgroundPage, page => this._dispatchEvent('backgroundPage', { page: PageDispatcher.from(this, page) })); - for (const serviceWorker of (context as CRBrowserContext).serviceWorkers()) + for (const serviceWorker of (context as CRBrowserContext).serviceWorkers()) { this._dispatchEvent('serviceWorker', { worker: new WorkerDispatcher(this, serviceWorker) }); + } this.addObjectListener(CRBrowserContext.CREvents.ServiceWorker, serviceWorker => this._dispatchEvent('serviceWorker', { worker: new WorkerDispatcher(this, serviceWorker) })); } this.addObjectListener(BrowserContext.Events.Request, (request: Request) => { @@ -128,8 +133,9 @@ export class BrowserContextDispatcher extends Dispatcher { const requestDispatcher = existingDispatcher(response.request()); - if (!requestDispatcher && !this._shouldDispatchNetworkEvent(response.request(), 'response')) + if (!requestDispatcher && !this._shouldDispatchNetworkEvent(response.request(), 'response')) { return; + } this._dispatchEvent('response', { response: ResponseDispatcher.from(this, response), page: PageDispatcher.fromNullable(this, response.frame()?._page.initializedOrUndefined()) @@ -147,8 +154,9 @@ export class BrowserContextDispatcher extends Dispatcher { const requestDispatcher = existingDispatcher(request); - if (!requestDispatcher && !this._shouldDispatchNetworkEvent(request, 'requestFailed')) + if (!requestDispatcher && !this._shouldDispatchNetworkEvent(request, 'requestFailed')) { return; + } this._dispatchEvent('requestFailed', { request: RequestDispatcher.from(this, request), failureText: request._failureText || undefined, @@ -158,8 +166,9 @@ export class BrowserContextDispatcher extends Dispatcher { const requestDispatcher = existingDispatcher(request); - if (!requestDispatcher && !this._shouldDispatchNetworkEvent(request, 'requestFinished')) + if (!requestDispatcher && !this._shouldDispatchNetworkEvent(request, 'requestFinished')) { return; + } this._dispatchEvent('requestFinished', { request: RequestDispatcher.from(this, request), response: ResponseDispatcher.fromNullable(this, response), @@ -170,15 +179,17 @@ export class BrowserContextDispatcher extends Dispatcher(page) : undefined; - if (pageDispatcher?._subscriptions.has(event)) + if (pageDispatcher?._subscriptions.has(event)) { return true; + } return false; } @@ -210,8 +221,9 @@ export class BrowserContextDispatcher extends Dispatcher { // When reusing the context, we might have some bindings called late enough, // after context and page dispatchers have been disposed. - if (this._disposed) + if (this._disposed) { return; + } const pageDispatcher = PageDispatcher.from(this, source.page); const binding = new BindingCallDispatcher(pageDispatcher, params.name, !!params.needsHandle, source, args); this._dispatchEvent('bindingCall', { binding }); @@ -278,8 +290,9 @@ export class BrowserContextDispatcher extends Dispatcher pattern.regexSource ? new RegExp(pattern.regexSource, pattern.regexFlags!) : pattern.glob!); await this._context.setRequestInterceptor((route, request) => { const matchesSome = urlMatchers.some(urlMatch => urlMatches(this._context._options.baseURL, request.url(), urlMatch)); - if (!matchesSome) + if (!matchesSome) { return false; + } this._dispatchEvent('route', { route: RouteDispatcher.from(RequestDispatcher.from(this, request), route) }); return true; }); @@ -287,8 +300,9 @@ export class BrowserContextDispatcher extends Dispatcher { this._webSocketInterceptionPatterns = params.patterns; - if (params.patterns.length) + if (params.patterns.length) { await WebSocketRouteDispatcher.installIfNeeded(this, this._context); + } } async storageState(params: channels.BrowserContextStorageStateParams, metadata: CallMetadata): Promise { @@ -319,10 +333,12 @@ export class BrowserContextDispatcher extends Dispatcher { - if (!this._object._browser.options.isChromium) + if (!this._object._browser.options.isChromium) { throw new Error(`CDP session is only available in Chromium`); - if (!params.page && !params.frame || params.page && params.frame) + } + if (!params.page && !params.frame || params.page && params.frame) { throw new Error(`CDP session must be initiated with either Page or Frame, not none or both`); + } const crBrowserContext = this._object as CRBrowserContext; return { session: new CDPSessionDispatcher(this, await crBrowserContext.newCDPSession((params.page ? params.page as PageDispatcher : params.frame as FrameDispatcher)._object)) }; } @@ -334,8 +350,9 @@ export class BrowserContextDispatcher extends Dispatcher { const artifact = await this._context._harExport(params.harId); - if (!artifact) + if (!artifact) { throw new Error('No HAR artifact. Ensure record.harPath is set.'); + } return { artifact: ArtifactDispatcher.from(this, artifact) }; } @@ -368,15 +385,17 @@ export class BrowserContextDispatcher extends Dispatcher { - if (params.enabled) + if (params.enabled) { this._subscriptions.add(params.event); - else + } else { this._subscriptions.delete(params.event); + } } override _onDispose() { // Avoid protocol calls for the closed context. - if (!this._context.isClosingOrClosed()) + if (!this._context.isClosingOrClosed()) { this._context.setRequestInterceptor(undefined).catch(() => {}); + } } } diff --git a/packages/playwright-core/src/server/dispatchers/browserDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserDispatcher.ts index 99ce5f961f..43223f9b15 100644 --- a/packages/playwright-core/src/server/dispatchers/browserDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserDispatcher.ts @@ -70,22 +70,25 @@ export class BrowserDispatcher extends Dispatcher { - if (!this._object.options.isChromium) + if (!this._object.options.isChromium) { throw new Error(`CDP session is only available in Chromium`); + } const crBrowser = this._object as CRBrowser; return { session: new CDPSessionDispatcher(this, await crBrowser.newBrowserCDPSession()) }; } async startTracing(params: channels.BrowserStartTracingParams): Promise { - if (!this._object.options.isChromium) + if (!this._object.options.isChromium) { throw new Error(`Tracing is only available in Chromium`); + } const crBrowser = this._object as CRBrowser; await crBrowser.startTracing(params.page ? (params.page as PageDispatcher)._object : undefined, params); } async stopTracing(): Promise { - if (!this._object.options.isChromium) + if (!this._object.options.isChromium) { throw new Error(`Tracing is only available in Chromium`); + } const crBrowser = this._object as CRBrowser; return { artifact: ArtifactDispatcher.from(this, await crBrowser.stopTracing()) }; } @@ -105,8 +108,9 @@ export class ConnectedBrowserDispatcher extends Dispatcher { - if (params.recordVideo) + if (params.recordVideo) { params.recordVideo.dir = this._object.options.artifactsDir; + } const context = await this._object.newContext(metadata, params); this._contexts.add(context); context.setSelectors(this.selectors); @@ -135,22 +139,25 @@ export class ConnectedBrowserDispatcher extends Dispatcher { - if (!this._object.options.isChromium) + if (!this._object.options.isChromium) { throw new Error(`CDP session is only available in Chromium`); + } const crBrowser = this._object as CRBrowser; return { session: new CDPSessionDispatcher(this as any as BrowserDispatcher, await crBrowser.newBrowserCDPSession()) }; } async startTracing(params: channels.BrowserStartTracingParams): Promise { - if (!this._object.options.isChromium) + if (!this._object.options.isChromium) { throw new Error(`Tracing is only available in Chromium`); + } const crBrowser = this._object as CRBrowser; await crBrowser.startTracing(params.page ? (params.page as PageDispatcher)._object : undefined, params); } async stopTracing(): Promise { - if (!this._object.options.isChromium) + if (!this._object.options.isChromium) { throw new Error(`Tracing is only available in Chromium`); + } const crBrowser = this._object as CRBrowser; return { artifact: ArtifactDispatcher.from(this, await crBrowser.stopTracing()) }; } @@ -164,12 +171,14 @@ async function newContextForReuse(browser: Browser, scope: BrowserDispatcher, pa const { context, needsReset } = await browser.newContextForReuse(params, metadata); if (needsReset) { const oldContextDispatcher = existingDispatcher(context); - if (oldContextDispatcher) + if (oldContextDispatcher) { oldContextDispatcher._dispose(); + } await context.resetForReuse(metadata, params); } - if (selectors) + if (selectors) { context.setSelectors(selectors); + } const contextDispatcher = new BrowserContextDispatcher(scope, context); return { context: contextDispatcher }; } diff --git a/packages/playwright-core/src/server/dispatchers/dispatcher.ts b/packages/playwright-core/src/server/dispatchers/dispatcher.ts index ce63891f4f..7653ffaa0f 100644 --- a/packages/playwright-core/src/server/dispatchers/dispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/dispatcher.ts @@ -79,8 +79,9 @@ export class Dispatcher>(method: T, params?: channels.EventsTraits[T]) { if (this._disposed) { - if (isUnderTest()) + if (isUnderTest()) { throw new Error(`${this._guid} is sending "${String(method)}" event after being disposed`); + } // Just ignore this event outside of tests. return; } @@ -144,8 +148,9 @@ export class Dispatcher { - if (!this._subscriptions.has('console')) + if (!this._subscriptions.has('console')) { return; + } this._dispatchEvent('console', { type: message.type(), text: message.text(), @@ -80,10 +81,11 @@ export class ElectronApplicationDispatcher extends Dispatcher { - if (params.enabled) + if (params.enabled) { this._subscriptions.add(params.event); - else + } else { this._subscriptions.delete(params.event); + } } async close(): Promise { diff --git a/packages/playwright-core/src/server/dispatchers/elementHandlerDispatcher.ts b/packages/playwright-core/src/server/dispatchers/elementHandlerDispatcher.ts index d4d0280ec4..cdbc298375 100644 --- a/packages/playwright-core/src/server/dispatchers/elementHandlerDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/elementHandlerDispatcher.ts @@ -36,15 +36,17 @@ export class ElementHandleDispatcher extends JSHandleDispatcher implements chann } static fromNullable(scope: JSHandleDispatcherParentScope, handle: ElementHandle | null): ElementHandleDispatcher | undefined { - if (!handle) + if (!handle) { return undefined; + } return existingDispatcher(handle) || new ElementHandleDispatcher(scope, handle); } static fromJSHandle(scope: JSHandleDispatcherParentScope, handle: js.JSHandle): JSHandleDispatcher { const result = existingDispatcher(handle); - if (result) + if (result) { return result; + } return handle.asElement() ? new ElementHandleDispatcher(scope, handle.asElement()!) : new JSHandleDispatcher(scope, handle); } @@ -212,14 +214,17 @@ export class ElementHandleDispatcher extends JSHandleDispatcher implements chann private _browserContextDispatcher(): BrowserContextDispatcher { const scope = this.parentScope(); - if (scope instanceof BrowserContextDispatcher) + if (scope instanceof BrowserContextDispatcher) { return scope; - if (scope instanceof PageDispatcher) + } + if (scope instanceof PageDispatcher) { return scope.parentScope(); + } if ((scope instanceof WorkerDispatcher) || (scope instanceof FrameDispatcher)) { const parentScope = scope.parentScope(); - if (parentScope instanceof BrowserContextDispatcher) + if (parentScope instanceof BrowserContextDispatcher) { return parentScope; + } return parentScope.parentScope(); } throw new Error('ElementHandle belongs to unexpected scope'); diff --git a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts index 2f172df694..8feb4c29df 100644 --- a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts @@ -39,8 +39,9 @@ export class FrameDispatcher extends Dispatcher { - if (!event.isPublic) + if (!event.isPublic) { return; + } const params = { url: event.url, name: event.name, error: event.error ? event.error.message : undefined }; - if (event.newDocument) + if (event.newDocument) { (params as any).newDocument = { request: RequestDispatcher.fromNullable(this._browserContextDispatcher, event.newDocument.request || null) }; + } this._dispatchEvent('navigated', params); }); } @@ -260,11 +263,13 @@ export class FrameDispatcher extends Dispatcher { metadata.potentiallyClosesScope = true; let expectedValue = params.expectedValue ? parseArgument(params.expectedValue) : undefined; - if (params.expression === 'to.match.aria' && expectedValue) + if (params.expression === 'to.match.aria' && expectedValue) { expectedValue = parseAriaSnapshot(expectedValue); + } const result = await this._frame.expect(metadata, params.selector, { ...params, expectedValue }); - if (result.received !== undefined) + if (result.received !== undefined) { result.received = serializeResult(result.received); + } return result; } diff --git a/packages/playwright-core/src/server/dispatchers/jsHandleDispatcher.ts b/packages/playwright-core/src/server/dispatchers/jsHandleDispatcher.ts index 33960e72d5..a1310cefcc 100644 --- a/packages/playwright-core/src/server/dispatchers/jsHandleDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/jsHandleDispatcher.ts @@ -54,8 +54,9 @@ export class JSHandleDispatcher extends Dispatcher { const map = await this._object.getProperties(); const properties = []; - for (const [name, value] of map) + for (const [name, value] of map) { properties.push({ name, value: ElementHandleDispatcher.fromJSHandle(this.parentScope(), value) }); + } return { properties }; } diff --git a/packages/playwright-core/src/server/dispatchers/jsonPipeDispatcher.ts b/packages/playwright-core/src/server/dispatchers/jsonPipeDispatcher.ts index dead26993f..7a22414a4a 100644 --- a/packages/playwright-core/src/server/dispatchers/jsonPipeDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/jsonPipeDispatcher.ts @@ -38,8 +38,9 @@ export class JsonPipeDispatcher extends Dispatcher<{ guid: string }, channels.Js } dispatch(message: Object) { - if (!this._disposed) + if (!this._disposed) { this._dispatchEvent('message', { message }); + } } wasClosed(reason?: string): void { diff --git a/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts b/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts index b6f8fe80ac..0bfa9a17b1 100644 --- a/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts @@ -69,14 +69,16 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels. const addFile = (file: string, name: string) => { try { - if (fs.statSync(file).isFile()) + if (fs.statSync(file).isFile()) { zipFile.addFile(file, name); + } } catch (e) { } }; - for (const entry of params.entries) + for (const entry of params.entries) { addFile(entry.value, entry.name); + } // Add stacks and the sources. const stackSession = params.stacksId ? this._stackSessions.get(params.stacksId) : undefined; @@ -94,13 +96,16 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels. if (params.includeSources) { const sourceFiles = new Set(); for (const { stack } of stackSession?.callStacks || []) { - if (!stack) + if (!stack) { continue; - for (const { file } of stack) + } + for (const { file } of stack) { sourceFiles.add(file); + } } - for (const sourceFile of sourceFiles) + for (const sourceFile of sourceFiles) { addFile(sourceFile, 'resources/src@' + calculateSha1(sourceFile) + '.txt'); + } } if (params.mode === 'write') { @@ -156,8 +161,9 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels. const zipFile = new ZipFile(params.file); const entryNames = await zipFile.entries(); const harEntryName = entryNames.find(e => e.endsWith('.har')); - if (!harEntryName) + if (!harEntryName) { return { error: 'Specified archive does not have a .har file' }; + } const har = await zipFile.read(harEntryName); const harFile = JSON.parse(har.toString()) as har.HARFile; harBackend = new HarBackend(harFile, null, zipFile); @@ -171,8 +177,9 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels. async harLookup(params: channels.LocalUtilsHarLookupParams, metadata: CallMetadata): Promise { const harBackend = this._harBackends.get(params.harId); - if (!harBackend) + if (!harBackend) { return { action: 'error', message: `Internal error: har was not opened` }; + } return await harBackend.lookup(params.url, params.method, params.headers, params.postData, params.isNavigationRequest); } @@ -189,10 +196,11 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels. const zipFile = new ZipFile(params.zipFile); for (const entry of await zipFile.entries()) { const buffer = await zipFile.read(entry); - if (entry === 'har.har') + if (entry === 'har.har') { await fs.promises.writeFile(params.harFile, buffer); - else + } else { await fs.promises.writeFile(path.join(dir, entry), buffer); + } } zipFile.close(); await fs.promises.unlink(params.zipFile); @@ -213,8 +221,9 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels. const socksInterceptor = new SocksInterceptor(transport, params.exposeNetwork, params.socksProxyRedirectPortForTest); const pipe = new JsonPipeDispatcher(this); transport.onmessage = json => { - if (socksInterceptor.interceptMessage(json)) + if (socksInterceptor.interceptMessage(json)) { return; + } const cb = () => { try { pipe.dispatch(json); @@ -222,16 +231,17 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels. transport.close(); } }; - if (params.slowMo) + if (params.slowMo) { setTimeout(cb, params.slowMo); - else + } else { cb(); + } }; pipe.on('message', message => { transport.send(message); }); transport.onclose = (reason?: string) => { - socksInterceptor?.cleanup(); + socksInterceptor.cleanup(); pipe.wasClosed(reason); }; pipe.on('close', () => transport.close()); @@ -241,8 +251,9 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels. async tracingStarted(params: channels.LocalUtilsTracingStartedParams, metadata?: CallMetadata | undefined): Promise { let tmpDir = undefined; - if (!params.tracesDir) + if (!params.tracesDir) { tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'playwright-tracing-')); + } const traceStacksFile = path.join(params.tracesDir || tmpDir!, params.traceName + '.stacks'); this._stackSessions.set(traceStacksFile, { callStacks: [], file: traceStacksFile, writer: Promise.resolve(), tmpDir }); return { stacksId: traceStacksFile }; @@ -266,11 +277,13 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels. private async _deleteStackSession(stacksId?: string) { const session = stacksId ? this._stackSessions.get(stacksId) : undefined; - if (!session) + if (!session) { return; + } await session.writer; - if (session.tmpDir) + if (session.tmpDir) { await removeFolders([session.tmpDir]); + } this._stackSessions.delete(stacksId!); } } @@ -303,12 +316,14 @@ class HarBackend { return { action: 'error', message: 'HAR error: ' + e.message }; } - if (!entry) + if (!entry) { return { action: 'noentry' }; + } // If navigation is being redirected, restart it with the final url to ensure the document's url changes. - if (entry.request.url !== url && isNavigationRequest) + if (entry.request.url !== url && isNavigationRequest) { return { action: 'redirect', redirectURL: entry.request.url }; + } const response = entry.response; try { @@ -328,10 +343,11 @@ class HarBackend { const file = content._file; let buffer: Buffer; if (file) { - if (this._zipFile) + if (this._zipFile) { buffer = await this._zipFile.read(file); - else + } else { buffer = await fs.promises.readFile(path.resolve(this._baseDir!, file)); + } } else { buffer = Buffer.from(content.text || '', content.encoding === 'base64' ? 'base64' : 'utf-8'); } @@ -344,27 +360,32 @@ class HarBackend { while (true) { const entries: har.Entry[] = []; for (const candidate of harLog.entries) { - if (candidate.request.url !== url || candidate.request.method !== method) + if (candidate.request.url !== url || candidate.request.method !== method) { continue; + } if (method === 'POST' && postData && candidate.request.postData) { const buffer = await this._loadContent(candidate.request.postData); if (!buffer.equals(postData)) { const boundary = multipartBoundary(headers); - if (!boundary) + if (!boundary) { continue; + } const candidataBoundary = multipartBoundary(candidate.request.headers); - if (!candidataBoundary) + if (!candidataBoundary) { continue; + } // Try to match multipart/form-data ignroing boundary as it changes between requests. - if (postData.toString().replaceAll(boundary, '') !== buffer.toString().replaceAll(candidataBoundary, '')) + if (postData.toString().replaceAll(boundary, '') !== buffer.toString().replaceAll(candidataBoundary, '')) { continue; + } } } entries.push(candidate); } - if (!entries.length) + if (!entries.length) { return; + } let entry = entries[0]; @@ -379,8 +400,9 @@ class HarBackend { entry = list[0].candidate; } - if (visited.has(entry)) + if (visited.has(entry)) { throw new Error(`Found redirect cycle for ${url}`); + } visited.add(entry); @@ -410,20 +432,23 @@ function countMatchingHeaders(harHeaders: har.Header[], headers: HeadersArray): const set = new Set(headers.map(h => h.name.toLowerCase() + ':' + h.value)); let matches = 0; for (const h of harHeaders) { - if (set.has(h.name.toLowerCase() + ':' + h.value)) + if (set.has(h.name.toLowerCase() + ':' + h.value)) { ++matches; + } } return matches; } export async function urlToWSEndpoint(progress: Progress|undefined, endpointURL: string): Promise { - if (endpointURL.startsWith('ws')) + if (endpointURL.startsWith('ws')) { return endpointURL; + } progress?.log(` retrieving websocket url from ${endpointURL}`); const fetchUrl = new URL(endpointURL); - if (!fetchUrl.pathname.endsWith('/')) + if (!fetchUrl.pathname.endsWith('/')) { fetchUrl.pathname += '/'; + } fetchUrl.pathname += 'json'; const json = await fetchData({ url: fetchUrl.toString(), @@ -438,10 +463,12 @@ export async function urlToWSEndpoint(progress: Progress|undefined, endpointURL: const wsUrl = new URL(endpointURL); let wsEndpointPath = JSON.parse(json).wsEndpointPath; - if (wsEndpointPath.startsWith('/')) + if (wsEndpointPath.startsWith('/')) { wsEndpointPath = wsEndpointPath.substring(1); - if (!wsUrl.pathname.endsWith('/')) + } + if (!wsUrl.pathname.endsWith('/')) { wsUrl.pathname += '/'; + } wsUrl.pathname += wsEndpointPath; wsUrl.protocol = wsUrl.protocol === 'https:' ? 'wss:' : 'ws:'; return wsUrl.toString(); @@ -449,10 +476,12 @@ export async function urlToWSEndpoint(progress: Progress|undefined, endpointURL: function multipartBoundary(headers: HeadersArray) { const contentType = headers.find(h => h.name.toLowerCase() === 'content-type'); - if (!contentType?.value.includes('multipart/form-data')) + if (!contentType?.value.includes('multipart/form-data')) { return undefined; + } const boundary = contentType.value.match(/boundary=(\S+)/); - if (boundary) + if (boundary) { return boundary[1]; + } return undefined; } diff --git a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts index 5e3b73a73f..15bf621977 100644 --- a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts @@ -49,8 +49,9 @@ export class PageDispatcher extends Dispatcher(page); return result || new PageDispatcher(parentScope, page); } @@ -91,12 +92,14 @@ export class PageDispatcher extends Dispatcher this._dispatchEvent('webSocket', { webSocket: new WebSocketDispatcher(this, webSocket) })); this.addObjectListener(Page.Events.Worker, worker => this._dispatchEvent('worker', { worker: new WorkerDispatcher(this, worker) })); this.addObjectListener(Page.Events.Video, (artifact: Artifact) => this._dispatchEvent('video', { artifact: ArtifactDispatcher.from(parentScope, artifact) })); - if (page._video) + if (page._video) { this._dispatchEvent('video', { artifact: ArtifactDispatcher.from(this.parentScope(), page._video) }); + } // Ensure client knows about all frames. const frames = page._frameManager.frames(); - for (let i = 1; i < frames.length; i++) + for (let i = 1; i < frames.length; i++) { this._onFrameAttached(frames[i]); + } } page(): Page { @@ -115,8 +118,9 @@ export class PageDispatcher extends Dispatcher { // When reusing the context, we might have some bindings called late enough, // after context and page dispatchers have been disposed. - if (this._disposed) + if (this._disposed) { return; + } const binding = new BindingCallDispatcher(this, params.name, !!params.needsHandle, source, args); this._dispatchEvent('bindingCall', { binding }); return binding.promise(); @@ -181,8 +185,9 @@ export class PageDispatcher extends Dispatcher pattern.regexSource ? new RegExp(pattern.regexSource, pattern.regexFlags!) : pattern.glob!); await this._page.setClientRequestInterceptor((route, request) => { const matchesSome = urlMatchers.some(urlMatch => urlMatches(this._page._browserContext._options.baseURL, request.url(), urlMatch)); - if (!matchesSome) + if (!matchesSome) { return false; + } this._dispatchEvent('route', { route: RouteDispatcher.from(RequestDispatcher.from(this.parentScope(), request), route) }); return true; }); @@ -190,8 +195,9 @@ export class PageDispatcher extends Dispatcher { this._webSocketInterceptionPatterns = params.patterns; - if (params.patterns.length) + if (params.patterns.length) { await WebSocketRouteDispatcher.installIfNeeded(this.parentScope(), this._page); + } } async expectScreenshot(params: channels.PageExpectScreenshotParams, metadata: CallMetadata): Promise { @@ -219,18 +225,21 @@ export class PageDispatcher extends Dispatcher { - if (!params.runBeforeUnload) + if (!params.runBeforeUnload) { metadata.potentiallyClosesScope = true; + } await this._page.close(metadata, params); } async updateSubscription(params: channels.PageUpdateSubscriptionParams): Promise { - if (params.event === 'fileChooser') + if (params.event === 'fileChooser') { await this._page.setFileChooserIntercepted(params.enabled); - if (params.enabled) + } + if (params.enabled) { this._subscriptions.add(params.event); - else + } else { this._subscriptions.delete(params.event); + } } async keyboardDown(params: channels.PageKeyboardDownParams, metadata: CallMetadata): Promise { @@ -286,8 +295,9 @@ export class PageDispatcher extends Dispatcher { - if (!this._page.pdf) + if (!this._page.pdf) { throw new Error('PDF generation is only supported for Headless Chromium'); + } const buffer = await this._page.pdf(params); return { pdf: buffer }; } @@ -326,8 +336,9 @@ export class PageDispatcher extends Dispatcher {}); + } } } @@ -336,8 +347,9 @@ export class WorkerDispatcher extends Dispatcher(worker); return result || new WorkerDispatcher(scope, worker); } diff --git a/packages/playwright-core/src/server/dispatchers/playwrightDispatcher.ts b/packages/playwright-core/src/server/dispatchers/playwrightDispatcher.ts index c411f93198..ee1fb8e304 100644 --- a/packages/playwright-core/src/server/dispatchers/playwrightDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/playwrightDispatcher.ts @@ -88,23 +88,23 @@ class SocksSupportDispatcher extends Dispatcher<{ guid: string }, channels.Socks } async socksConnected(params: channels.SocksSupportSocksConnectedParams): Promise { - this._socksProxy?.socketConnected(params); + this._socksProxy.socketConnected(params); } async socksFailed(params: channels.SocksSupportSocksFailedParams): Promise { - this._socksProxy?.socketFailed(params); + this._socksProxy.socketFailed(params); } async socksData(params: channels.SocksSupportSocksDataParams): Promise { - this._socksProxy?.sendSocketData(params); + this._socksProxy.sendSocketData(params); } async socksError(params: channels.SocksSupportSocksErrorParams): Promise { - this._socksProxy?.sendSocketError(params); + this._socksProxy.sendSocketError(params); } async socksEnd(params: channels.SocksSupportSocksEndParams): Promise { - this._socksProxy?.sendSocketEnd(params); + this._socksProxy.sendSocketEnd(params); } override _onDispose() { diff --git a/packages/playwright-core/src/server/dispatchers/streamDispatcher.ts b/packages/playwright-core/src/server/dispatchers/streamDispatcher.ts index 4a45468389..584462333e 100644 --- a/packages/playwright-core/src/server/dispatchers/streamDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/streamDispatcher.ts @@ -33,8 +33,9 @@ export class StreamDispatcher extends Dispatcher<{ guid: string, stream: stream. async read(params: channels.StreamReadParams): Promise { const stream = this._object.stream; - if (this._ended) + if (this._ended) { return { binary: Buffer.from('') }; + } if (!stream.readableLength) { const readyPromise = new ManualPromise(); const done = () => readyPromise.resolve(); diff --git a/packages/playwright-core/src/server/dispatchers/webSocketRouteDispatcher.ts b/packages/playwright-core/src/server/dispatchers/webSocketRouteDispatcher.ts index 87f469b7d0..48af5fe876 100644 --- a/packages/playwright-core/src/server/dispatchers/webSocketRouteDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/webSocketRouteDispatcher.ts @@ -43,12 +43,14 @@ export class WebSocketRouteDispatcher extends Dispatcher<{ guid: string }, chann // When the frame navigates or detaches, there will be no more communication // from the mock websocket, so pretend like it was closed. eventsHelper.addEventListener(frame._page, Page.Events.InternalFrameNavigatedToNewDocument, (frame: Frame) => { - if (frame === this._frame) + if (frame === this._frame) { this._executionContextGone(); + } }), eventsHelper.addEventListener(frame._page, Page.Events.FrameDetached, (frame: Frame) => { - if (frame === this._frame) + if (frame === this._frame) { this._executionContextGone(); + } }), eventsHelper.addEventListener(frame._page, Page.Events.Close, () => this._executionContextGone()), eventsHelper.addEventListener(frame._page, Page.Events.Crash, () => this._executionContextGone()), @@ -66,10 +68,11 @@ export class WebSocketRouteDispatcher extends Dispatcher<{ guid: string }, chann if (payload.type === 'onCreate') { const pageDispatcher = PageDispatcher.fromNullable(contextDispatcher, source.page); let scope: PageDispatcher | BrowserContextDispatcher | undefined; - if (pageDispatcher && matchesPattern(pageDispatcher, context._options.baseURL, payload.url)) + if (pageDispatcher && matchesPattern(pageDispatcher, context._options.baseURL, payload.url)) { scope = pageDispatcher; - else if (matchesPattern(contextDispatcher, context._options.baseURL, payload.url)) + } else if (matchesPattern(contextDispatcher, context._options.baseURL, payload.url)) { scope = contextDispatcher; + } if (scope) { new WebSocketRouteDispatcher(scope, payload.id, payload.url, source.frame); } else { @@ -80,14 +83,18 @@ export class WebSocketRouteDispatcher extends Dispatcher<{ guid: string }, chann } const dispatcher = WebSocketRouteDispatcher._idToDispatcher.get(payload.id); - if (payload.type === 'onMessageFromPage') + if (payload.type === 'onMessageFromPage') { dispatcher?._dispatchEvent('messageFromPage', { message: payload.data.data, isBase64: payload.data.isBase64 }); - if (payload.type === 'onMessageFromServer') + } + if (payload.type === 'onMessageFromServer') { dispatcher?._dispatchEvent('messageFromServer', { message: payload.data.data, isBase64: payload.data.isBase64 }); - if (payload.type === 'onClosePage') + } + if (payload.type === 'onClosePage') { dispatcher?._dispatchEvent('closePage', { code: payload.code, reason: payload.reason, wasClean: payload.wasClean }); - if (payload.type === 'onCloseServer') + } + if (payload.type === 'onCloseServer') { dispatcher?._dispatchEvent('closeServer', { code: payload.code, reason: payload.reason, wasClean: payload.wasClean }); + } }); } @@ -149,8 +156,9 @@ export class WebSocketRouteDispatcher extends Dispatcher<{ guid: string }, chann function matchesPattern(dispatcher: PageDispatcher | BrowserContextDispatcher, baseURL: string | undefined, url: string) { for (const pattern of dispatcher._webSocketInterceptionPatterns || []) { const urlMatch = pattern.regexSource ? new RegExp(pattern.regexSource, pattern.regexFlags) : pattern.glob; - if (urlMatches(baseURL, url, urlMatch)) + if (urlMatches(baseURL, url, urlMatch)) { return true; + } } return false; } diff --git a/packages/playwright-core/src/server/dispatchers/writableStreamDispatcher.ts b/packages/playwright-core/src/server/dispatchers/writableStreamDispatcher.ts index 3e54e644d5..9fef1e4ef2 100644 --- a/packages/playwright-core/src/server/dispatchers/writableStreamDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/writableStreamDispatcher.ts @@ -30,31 +30,36 @@ export class WritableStreamDispatcher extends Dispatcher<{ guid: string, streamO } async write(params: channels.WritableStreamWriteParams): Promise { - if (typeof this._object.streamOrDirectory === 'string') + if (typeof this._object.streamOrDirectory === 'string') { throw new Error('Cannot write to a directory'); + } const stream = this._object.streamOrDirectory; await new Promise((fulfill, reject) => { stream.write(params.binary, error => { - if (error) + if (error) { reject(error); - else + } else { fulfill(); + } }); }); } async close() { - if (typeof this._object.streamOrDirectory === 'string') + if (typeof this._object.streamOrDirectory === 'string') { throw new Error('Cannot close a directory'); + } const stream = this._object.streamOrDirectory; await new Promise(fulfill => stream.end(fulfill)); - if (this._lastModifiedMs) + if (this._lastModifiedMs) { await fs.promises.utimes(this.path(), new Date(this._lastModifiedMs), new Date(this._lastModifiedMs)); + } } path(): string { - if (typeof this._object.streamOrDirectory === 'string') + if (typeof this._object.streamOrDirectory === 'string') { return this._object.streamOrDirectory; + } return this._object.streamOrDirectory.path as string; } } diff --git a/packages/playwright-core/src/server/dom.ts b/packages/playwright-core/src/server/dom.ts index 8e65c7c67f..0f59c21d73 100644 --- a/packages/playwright-core/src/server/dom.ts +++ b/packages/playwright-core/src/server/dom.ts @@ -59,8 +59,9 @@ export class FrameExecutionContext extends js.ExecutionContext { } override adoptIfNeeded(handle: js.JSHandle): Promise | null { - if (handle instanceof ElementHandle && handle._context !== this) + if (handle instanceof ElementHandle && handle._context !== this) { return this.frame._page._delegate.adoptElementHandle(handle, this); + } return null; } @@ -81,8 +82,9 @@ export class FrameExecutionContext extends js.ExecutionContext { } override createHandle(remoteObject: js.RemoteObject): js.JSHandle { - if (this.frame._page._delegate.isElementHandle(remoteObject)) + if (this.frame._page._delegate.isElementHandle(remoteObject)) { return new ElementHandle(this, remoteObject.objectId!); + } return super.createHandle(remoteObject); } @@ -90,8 +92,9 @@ export class FrameExecutionContext extends js.ExecutionContext { if (!this._injectedScriptPromise) { const custom: string[] = []; const selectorsRegistry = this.frame._page.context().selectors(); - for (const [name, { source }] of selectorsRegistry._engines) + for (const [name, { source }] of selectorsRegistry._engines) { custom.push(`{ name: '${name}', engine: (${source}) }`); + } const sdkLanguage = this.frame.attribution.playwright.options.sdkLanguage; const source = ` (() => { @@ -142,8 +145,9 @@ export class ElementHandle extends js.JSHandle { const utility = await this._frame._utilityContext(); return await utility.evaluate(pageFunction, [await utility.injectedScript(), this, arg]); } catch (e) { - if (js.isJavaScriptErrorInEvaluate(e) || isSessionClosedError(e)) + if (js.isJavaScriptErrorInEvaluate(e) || isSessionClosedError(e)) { throw e; + } return 'error:notconnected'; } } @@ -153,23 +157,27 @@ export class ElementHandle extends js.JSHandle { const utility = await this._frame._utilityContext(); return await utility.evaluateHandle(pageFunction, [await utility.injectedScript(), this, arg]); } catch (e) { - if (js.isJavaScriptErrorInEvaluate(e) || isSessionClosedError(e)) + if (js.isJavaScriptErrorInEvaluate(e) || isSessionClosedError(e)) { throw e; + } return 'error:notconnected'; } } async ownerFrame(): Promise { const frameId = await this._page._delegate.getOwnerFrame(this); - if (!frameId) + if (!frameId) { return null; + } const frame = this._page._frameManager.frame(frameId); - if (frame) + if (frame) { return frame; + } for (const page of this._page._browserContext.pages()) { const frame = page._frameManager.frame(frameId); - if (frame) + if (frame) { return frame; + } } return null; } @@ -180,8 +188,9 @@ export class ElementHandle extends js.JSHandle { async contentFrame(): Promise { const isFrameElement = throwRetargetableDOMError(await this.isIframeElement()); - if (!isFrameElement) + if (!isFrameElement) { return null; + } return this._page._delegate.getContentFrame(this); } @@ -219,8 +228,9 @@ export class ElementHandle extends js.JSHandle { const waitResult = await this.evaluateInUtility(async ([injected, node, { waitForVisible }]) => { return await injected.checkElementStates(node, waitForVisible ? ['visible', 'stable'] : ['stable']); }, { waitForVisible }); - if (waitResult) + if (waitResult) { return waitResult; + } return await this._scrollRectIntoViewIfNeeded(); }, {}); assertDone(throwRetargetableDOMError(result)); @@ -257,15 +267,18 @@ export class ElementHandle extends js.JSHandle { this._page._delegate.getContentQuads(this), this._page.mainFrame()._utilityContext().then(utility => utility.evaluate(() => ({ width: innerWidth, height: innerHeight }))), ] as const); - if (quads === 'error:notconnected') + if (quads === 'error:notconnected') { return quads; - if (!quads || !quads.length) + } + if (!quads || !quads.length) { return 'error:notvisible'; + } // Allow 1x1 elements. Compensate for rounding errors by comparing with 0.99 instead. const filtered = quads.map(quad => intersectQuadWithViewport(quad)).filter(quad => computeQuadArea(quad) > 0.99); - if (!filtered.length) + if (!filtered.length) { return 'error:notinviewport'; + } if (this._page._browserContext._browser.options.name === 'firefox') { // Firefox internally uses integer coordinates, so 8.x is converted to 8 or 9 when clicking. // @@ -276,8 +289,9 @@ export class ElementHandle extends js.JSHandle { // Therefore, we try to find an integer point within a quad to make sure we click inside the element. for (const quad of filtered) { const integerPoint = findIntegerPointInsideQuad(quad); - if (integerPoint) + if (integerPoint) { return integerPoint; + } } } // Return the middle point of the first quad. @@ -289,10 +303,12 @@ export class ElementHandle extends js.JSHandle { this.boundingBox(), this.evaluateInUtility(([injected, node]) => injected.getElementBorderWidth(node), {}).catch(e => {}), ]); - if (!box || !border) + if (!box || !border) { return 'error:notvisible'; - if (border === 'error:notconnected') + } + if (border === 'error:notconnected') { return border; + } // Make point relative to the padding box to align with offsetX/offsetY. return { x: box.x + border.left + offset.x, @@ -312,25 +328,29 @@ export class ElementHandle extends js.JSHandle { if (timeout) { progress.log(` waiting ${timeout}ms`); const result = await this.evaluateInUtility(([injected, node, timeout]) => new Promise(f => setTimeout(f, timeout)), timeout); - if (result === 'error:notconnected') + if (result === 'error:notconnected') { return result; + } } } else { progress.log(`attempting ${actionName} action${options.trial ? ' (trial run)' : ''}`); } - if (!options.skipActionPreChecks && !options.force) + if (!options.skipActionPreChecks && !options.force) { await this._frame._page.performActionPreChecks(progress); + } const result = await action(retry); ++retry; if (result === 'error:notvisible') { - if (options.force) + if (options.force) { throw new NonRecoverableDOMError('Element is not visible'); + } progress.log(' element is not visible'); continue; } if (result === 'error:notinviewport') { - if (options.force) + if (options.force) { throw new NonRecoverableDOMError('Element is outside of the viewport'); + } progress.log(' element is outside of the viewport'); continue; } @@ -384,8 +404,9 @@ export class ElementHandle extends js.JSHandle { const doScrollIntoView = async () => { if (forceScrollOptions) { return await this.evaluateInUtility(([injected, node, options]) => { - if (node.nodeType === 1 /* Node.ELEMENT_NODE */) + if (node.nodeType === 1 /* Node.ELEMENT_NODE */) { (node as Node as Element).scrollIntoView(options); + } return 'done' as const; }, forceScrollOptions); } @@ -400,8 +421,9 @@ export class ElementHandle extends js.JSHandle { await doScrollIntoView().catch(() => {}); } - if ((options as any).__testHookBeforeStable) + if ((options as any).__testHookBeforeStable) { await (options as any).__testHookBeforeStable(); + } if (!force) { const elementStates: ElementState[] = waitForEnabled ? ['visible', 'enabled', 'stable'] : ['visible', 'stable']; @@ -409,24 +431,28 @@ export class ElementHandle extends js.JSHandle { const result = await this.evaluateInUtility(async ([injected, node, { elementStates }]) => { return await injected.checkElementStates(node, elementStates); }, { elementStates }); - if (result) + if (result) { return result; + } progress.log(` element is ${waitForEnabled ? 'visible, enabled and stable' : 'visible and stable'}`); } - if ((options as any).__testHookAfterStable) + if ((options as any).__testHookAfterStable) { await (options as any).__testHookAfterStable(); + } progress.log(' scrolling into view if needed'); progress.throwIfAborted(); // Avoid action that has side-effects. const scrolled = await doScrollIntoView(); - if (scrolled !== 'done') + if (scrolled !== 'done') { return scrolled; + } progress.log(' done scrolling'); const maybePoint = position ? await this._offsetPoint(position) : await this._clickablePoint(); - if (typeof maybePoint === 'string') + if (typeof maybePoint === 'string') { return maybePoint; + } const point = roundPoint(maybePoint); progress.metadata.point = point; await this.instrumentation.onBeforeInputAction(this, progress.metadata); @@ -435,21 +461,25 @@ export class ElementHandle extends js.JSHandle { if (force) { progress.log(` forcing action`); } else { - if ((options as any).__testHookBeforeHitTarget) + if ((options as any).__testHookBeforeHitTarget) { await (options as any).__testHookBeforeHitTarget(); + } const frameCheckResult = await this._checkFrameIsHitTarget(point); - if (frameCheckResult === 'error:notconnected' || ('hitTargetDescription' in frameCheckResult)) + if (frameCheckResult === 'error:notconnected' || ('hitTargetDescription' in frameCheckResult)) { return frameCheckResult; + } const hitPoint = frameCheckResult.framePoint; const actionType = actionName === 'move and up' ? 'drag' : ((actionName === 'hover' || actionName === 'tap') ? actionName : 'mouse'); const handle = await this.evaluateHandleInUtility(([injected, node, { actionType, hitPoint, trial }]) => injected.setupHitTargetInterceptor(node, actionType, hitPoint, trial), { actionType, hitPoint, trial: !!options.trial } as const); - if (handle === 'error:notconnected') + if (handle === 'error:notconnected') { return handle; + } if (!handle._objectId) { const error = handle.rawValue() as string; - if (error === 'error:notconnected') + if (error === 'error:notconnected') { return error; + } return { hitTargetDescription: error }; } hitTargetInterceptionHandle = handle as any; @@ -462,48 +492,56 @@ export class ElementHandle extends js.JSHandle { } const actionResult = await this._page._frameManager.waitForSignalsCreatedBy(progress, options.waitAfter === true, async () => { - if ((options as any).__testHookBeforePointerAction) + if ((options as any).__testHookBeforePointerAction) { await (options as any).__testHookBeforePointerAction(); + } progress.throwIfAborted(); // Avoid action that has side-effects. let restoreModifiers: types.KeyboardModifier[] | undefined; - if (options && options.modifiers) + if (options && options.modifiers) { restoreModifiers = await this._page.keyboard.ensureModifiers(options.modifiers); + } progress.log(` performing ${actionName} action`); await action(point); - if (restoreModifiers) + if (restoreModifiers) { await this._page.keyboard.ensureModifiers(restoreModifiers); + } if (hitTargetInterceptionHandle) { const stopHitTargetInterception = this._frame.raceAgainstEvaluationStallingEvents(() => { return hitTargetInterceptionHandle.evaluate(h => h.stop()); }).catch(e => 'done' as const).finally(() => { - hitTargetInterceptionHandle?.dispose(); + hitTargetInterceptionHandle.dispose(); }); if (options.waitAfter !== false) { // When noWaitAfter is passed, we do not want to accidentally stall on // non-committed navigation blocking the evaluate. const hitTargetResult = await stopHitTargetInterception; - if (hitTargetResult !== 'done') + if (hitTargetResult !== 'done') { return hitTargetResult; + } } } progress.log(` ${options.trial ? 'trial ' : ''}${actionName} action done`); progress.log(' waiting for scheduled navigations to finish'); - if ((options as any).__testHookAfterPointerAction) + if ((options as any).__testHookAfterPointerAction) { await (options as any).__testHookAfterPointerAction(); + } return 'done'; }); - if (actionResult !== 'done') + if (actionResult !== 'done') { return actionResult; + } progress.log(' navigations have finished'); return 'done'; } private async _markAsTargetElement(metadata: CallMetadata) { - if (!metadata.id) + if (!metadata.id) { return; + } await this.evaluateInUtility(([injected, node, callId]) => { - if (node.nodeType === 1 /* Node.ELEMENT_NODE */) + if (node.nodeType === 1 /* Node.ELEMENT_NODE */) { injected.markTargetElements(new Set([node as Node as Element]), callId); + } }, metadata.id); } @@ -572,14 +610,16 @@ export class ElementHandle extends js.JSHandle { let resultingOptions: string[] = []; await this._retryAction(progress, 'select option', async () => { await this.instrumentation.onBeforeInputAction(this, progress.metadata); - if (!options.force) + if (!options.force) { progress.log(` waiting for element to be visible and enabled`); + } const optionsToSelect = [...elements, ...values]; const result = await this.evaluateInUtility(async ([injected, node, { optionsToSelect, force }]) => { if (!force) { const checkResult = await injected.checkElementStates(node, ['visible', 'enabled']); - if (checkResult) + if (checkResult) { return checkResult; + } } return injected.selectOptions(node, optionsToSelect); }, { optionsToSelect, force: options.force }); @@ -606,22 +646,25 @@ export class ElementHandle extends js.JSHandle { progress.log(` fill("${value}")`); return await this._retryAction(progress, 'fill', async () => { await this.instrumentation.onBeforeInputAction(this, progress.metadata); - if (!options.force) + if (!options.force) { progress.log(' waiting for element to be visible, enabled and editable'); + } const result = await this.evaluateInUtility(async ([injected, node, { value, force }]) => { if (!force) { const checkResult = await injected.checkElementStates(node, ['visible', 'enabled', 'editable']); - if (checkResult) + if (checkResult) { return checkResult; + } } return injected.fill(node, value); }, { value, force: options.force }); progress.throwIfAborted(); // Avoid action that has side-effects. if (result === 'needsinput') { - if (value) + if (value) { await this._page.keyboard.insertText(value); - else + } else { await this._page.keyboard.press('Delete'); + } return 'done'; } else { return result; @@ -633,13 +676,15 @@ export class ElementHandle extends js.JSHandle { const controller = new ProgressController(metadata, this); return controller.run(async progress => { const result = await this._retryAction(progress, 'selectText', async () => { - if (!options.force) + if (!options.force) { progress.log(' waiting for element to be visible'); + } return await this.evaluateInUtility(async ([injected, node, { force }]) => { if (!force) { const checkResult = await injected.checkElementStates(node, ['visible']); - if (checkResult) + if (checkResult) { return checkResult; + } } return injected.selectText(node); }, { force: options.force }); @@ -663,21 +708,27 @@ export class ElementHandle extends js.JSHandle { const multiple = filePayloads && filePayloads.length > 1 || localPaths && localPaths.length > 1; const result = await this.evaluateHandleInUtility(([injected, node, { multiple, directoryUpload }]): Element | undefined => { const element = injected.retarget(node, 'follow-label'); - if (!element) + if (!element) { return; - if (element.tagName !== 'INPUT') + } + if (element.tagName !== 'INPUT') { throw injected.createStacklessError('Node is not an HTMLInputElement'); + } const inputElement = element as HTMLInputElement; - if (multiple && !inputElement.multiple && !inputElement.webkitdirectory) + if (multiple && !inputElement.multiple && !inputElement.webkitdirectory) { throw injected.createStacklessError('Non-multiple file input can only accept single file'); - if (directoryUpload && !inputElement.webkitdirectory) + } + if (directoryUpload && !inputElement.webkitdirectory) { throw injected.createStacklessError('File input does not support directories, pass individual files instead'); - if (!directoryUpload && inputElement.webkitdirectory) + } + if (!directoryUpload && inputElement.webkitdirectory) { throw injected.createStacklessError('[webkitdirectory] input requires passing a path to a directory'); + } return inputElement; }, { multiple, directoryUpload: !!localDirectory }); - if (result === 'error:notconnected' || !result.asElement()) + if (result === 'error:notconnected' || !result.asElement()) { return 'error:notconnected'; + } const retargeted = result.asElement() as ElementHandle; await this.instrumentation.onBeforeInputAction(this, progress.metadata); progress.throwIfAborted(); // Avoid action that has side-effects. @@ -730,8 +781,9 @@ export class ElementHandle extends js.JSHandle { progress.log(`elementHandle.type("${text}")`); await this.instrumentation.onBeforeInputAction(this, progress.metadata); const result = await this._focus(progress, true /* resetSelectionIfNotFocused */); - if (result !== 'done') + if (result !== 'done') { return result; + } progress.throwIfAborted(); // Avoid action that has side-effects. await this._page.keyboard.type(text, options); return 'done'; @@ -751,8 +803,9 @@ export class ElementHandle extends js.JSHandle { await this.instrumentation.onBeforeInputAction(this, progress.metadata); return this._page._frameManager.waitForSignalsCreatedBy(progress, !options.noWaitAfter, async () => { const result = await this._focus(progress, true /* resetSelectionIfNotFocused */); - if (result !== 'done') + if (result !== 'done') { return result; + } progress.throwIfAborted(); // Avoid action that has side-effects. await this._page.keyboard.press(key, options); return 'done'; @@ -781,15 +834,19 @@ export class ElementHandle extends js.JSHandle { return throwRetargetableDOMError(result); }; await this._markAsTargetElement(progress.metadata); - if (await isChecked() === state) + if (await isChecked() === state) { return 'done'; + } const result = await this._click(progress, { ...options, waitAfter: 'disabled' }); - if (result !== 'done') + if (result !== 'done') { return result; - if (options.trial) + } + if (options.trial) { return 'done'; - if (await isChecked() !== state) + } + if (await isChecked() !== state) { throw new NonRecoverableDOMError('Clicking the checkbox did not change its state'); + } return 'done'; } @@ -881,8 +938,9 @@ export class ElementHandle extends js.JSHandle { const frameElement = await frame.frameElement() as ElementHandle; const box = await frameElement.boundingBox(); const style = await frameElement.evaluateInUtility(([injected, iframe]) => injected.describeIFrameStyle(iframe), {}).catch(e => 'error:notconnected' as const); - if (!box || style === 'error:notconnected') + if (!box || style === 'error:notconnected') { return 'error:notconnected'; + } if (style === 'transformed') { // We cannot translate coordinates when iframe has any transform applied. // The best we can do right now is to skip the hitPoint check, @@ -904,16 +962,18 @@ export class ElementHandle extends js.JSHandle { const hitTargetResult = await element.evaluateInUtility(([injected, element, hitPoint]) => { return injected.expectHitTarget(hitPoint, element); }, point); - if (hitTargetResult !== 'done') + if (hitTargetResult !== 'done') { return hitTargetResult; + } } return { framePoint: data[0].pointInFrame }; } } export function throwRetargetableDOMError(result: T | 'error:notconnected'): T { - if (result === 'error:notconnected') + if (result === 'error:notconnected') { throw new Error('Element is not attached to the DOM'); + } return result; } @@ -945,8 +1005,9 @@ function isPointInsideQuad(point: types.Point, quad: types.Quad): boolean { const area1 = triangleArea(point, quad[0], quad[1]) + triangleArea(point, quad[1], quad[2]) + triangleArea(point, quad[2], quad[3]) + triangleArea(point, quad[3], quad[0]); const area2 = triangleArea(quad[0], quad[1], quad[2]) + triangleArea(quad[1], quad[2], quad[3]); // Check that point is inside the quad. - if (Math.abs(area1 - area2) > 0.1) + if (Math.abs(area1 - area2) > 0.1) { return false; + } // Check that point is not on the right/bottom edge, because clicking // there does not actually click the element. return point.x < Math.max(quad[0].x, quad[1].x, quad[2].x, quad[3].x) && @@ -958,17 +1019,21 @@ function findIntegerPointInsideQuad(quad: types.Quad): types.Point | undefined { const point = quadMiddlePoint(quad); point.x = Math.floor(point.x); point.y = Math.floor(point.y); - if (isPointInsideQuad(point, quad)) + if (isPointInsideQuad(point, quad)) { return point; + } point.x += 1; - if (isPointInsideQuad(point, quad)) + if (isPointInsideQuad(point, quad)) { return point; + } point.y += 1; - if (isPointInsideQuad(point, quad)) + if (isPointInsideQuad(point, quad)) { return point; + } point.x -= 1; - if (isPointInsideQuad(point, quad)) + if (isPointInsideQuad(point, quad)) { return point; + } } export const kUnableToAdoptErrorMessage = 'Unable to adopt element handle from a different document'; diff --git a/packages/playwright-core/src/server/download.ts b/packages/playwright-core/src/server/download.ts index 78a9c015dc..32eec4bd95 100644 --- a/packages/playwright-core/src/server/download.ts +++ b/packages/playwright-core/src/server/download.ts @@ -34,8 +34,9 @@ export class Download { this.url = url; this._suggestedFilename = suggestedFilename; page._browserContext._downloads.add(this); - if (suggestedFilename !== undefined) + if (suggestedFilename !== undefined) { this._fireDownloadEvent(); + } } page(): Page { diff --git a/packages/playwright-core/src/server/electron/electron.ts b/packages/playwright-core/src/server/electron/electron.ts index 1606c407d5..1729eafb56 100644 --- a/packages/playwright-core/src/server/electron/electron.ts +++ b/packages/playwright-core/src/server/electron/electron.ts @@ -71,8 +71,9 @@ export class ElectronApplication extends SdkObject { this._nodeConnection = nodeConnection; this._nodeSession = nodeConnection.rootSession; this._nodeSession.on('Runtime.executionContextCreated', async (event: Protocol.Runtime.executionContextCreatedPayload) => { - if (!event.context.auxData || !event.context.auxData.isDefault) + if (!event.context.auxData || !event.context.auxData.isDefault) { return; + } const crExecutionContext = new CRExecutionContext(this._nodeSession, event.context); this._nodeExecutionContext = new js.ExecutionContext(this, crExecutionContext, 'electron'); const { result: remoteObject } = await crExecutionContext._client.send('Runtime.evaluate', { @@ -111,8 +112,9 @@ export class ElectronApplication extends SdkObject { // @see https://github.com/GoogleChrome/puppeteer/issues/3865 return; } - if (!this._nodeExecutionContext) + if (!this._nodeExecutionContext) { return; + } const args = event.args.map(arg => this._nodeExecutionContext!.createHandle(arg)); const message = new ConsoleMessage(null, event.type, undefined, args, toConsoleMessageLocation(event.stackTrace)); this.emit(ElectronApplication.Events.Console, message); @@ -166,8 +168,9 @@ export class Electron extends SdkObject { if (os.platform() === 'linux') { const runningAsRoot = process.geteuid && process.geteuid() === 0; - if (runningAsRoot && electronArguments.indexOf('--no-sandbox') === -1) + if (runningAsRoot && electronArguments.indexOf('--no-sandbox') === -1) { electronArguments.unshift('--no-sandbox'); + } } const artifactsDir = await fs.promises.mkdtemp(ARTIFACTS_FOLDER); @@ -184,7 +187,7 @@ export class Electron extends SdkObject { // 'electron/index.js' resolves to the actual Electron App. command = require('electron/index.js'); } catch (error: any) { - if ((error as NodeJS.ErrnoException)?.code === 'MODULE_NOT_FOUND') { + if ((error as NodeJS.ErrnoException).code === 'MODULE_NOT_FOUND') { throw new Error('\n' + wrapInASCIIBox([ 'Electron executablePath not found!', 'Please install it using `npm install -D electron` or set the executablePath to your Electron executable.', @@ -308,8 +311,9 @@ function waitForLine(progress: Progress, process: childProcess.ChildProcess, reg function onLine(line: string) { const match = line.match(regex); - if (!match) + if (!match) { return; + } cleanup(); resolve(match); } diff --git a/packages/playwright-core/src/server/errors.ts b/packages/playwright-core/src/server/errors.ts index c3a63cb033..0764670ad8 100644 --- a/packages/playwright-core/src/server/errors.ts +++ b/packages/playwright-core/src/server/errors.ts @@ -38,15 +38,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); } const e = new Error(error.error.message); diff --git a/packages/playwright-core/src/server/fetch.ts b/packages/playwright-core/src/server/fetch.ts index f231c907c0..a8aa2adbef 100644 --- a/packages/playwright-core/src/server/fetch.ts +++ b/packages/playwright-core/src/server/fetch.ts @@ -102,8 +102,9 @@ export abstract class APIRequestContext extends SdkObject { static findResponseBody(guid: string): Buffer | undefined { for (const request of APIRequestContext.allInstances) { const body = request.fetchResponses.get(guid); - if (body) + if (body) { return body; + } } return undefined; } @@ -149,34 +150,39 @@ export abstract class APIRequestContext extends SdkObject { }; if (defaults.extraHTTPHeaders) { - for (const { name, value } of defaults.extraHTTPHeaders) + for (const { name, value } of defaults.extraHTTPHeaders) { setHeader(headers, name, value); + } } if (params.headers) { - for (const { name, value } of params.headers) + for (const { name, value } of params.headers) { setHeader(headers, name, value); + } } const requestUrl = new URL(constructURLBasedOnBaseURL(defaults.baseURL, params.url)); if (params.encodedParams) { requestUrl.search = params.encodedParams; } else if (params.params) { - for (const { name, value } of params.params) + for (const { name, value } of params.params) { requestUrl.searchParams.append(name, value); + } } const credentials = this._getHttpCredentials(requestUrl); - if (credentials?.send === 'always') + if (credentials?.send === 'always') { setBasicAuthorizationHeader(headers, credentials); + } const method = params.method?.toUpperCase() || 'GET'; const proxy = defaults.proxy; let agent; // We skip 'per-context' in order to not break existing users. 'per-context' was previously used to // workaround an upstream Chromium bug. Can be removed in the future. - if (proxy && proxy.server !== 'per-context' && !shouldBypassProxy(requestUrl, proxy.bypass)) + if (proxy && proxy.server !== 'per-context' && !shouldBypassProxy(requestUrl, proxy.bypass)) { agent = createProxyAgent(proxy); + } const timeout = defaults.timeoutSettings.timeout(params); @@ -193,12 +199,14 @@ export abstract class APIRequestContext extends SdkObject { __testHookLookup: (params as any).__testHookLookup, }; // rejectUnauthorized = undefined is treated as true in Node.js 12. - if (params.ignoreHTTPSErrors || defaults.ignoreHTTPSErrors) + if (params.ignoreHTTPSErrors || defaults.ignoreHTTPSErrors) { options.rejectUnauthorized = false; + } const postData = serializePostData(params, headers); - if (postData) + if (postData) { setHeader(headers, 'content-length', String(postData.byteLength)); + } const controller = new ProgressController(metadata, this); const fetchResponse = await controller.run(progress => { return this._sendRequestWithRetries(progress, requestUrl, options, postData, params.maxRetries); @@ -209,8 +217,9 @@ export abstract class APIRequestContext extends SdkObject { let responseText = ''; if (fetchResponse.body.byteLength) { let text = fetchResponse.body.toString('utf8'); - if (text.length > 1000) + if (text.length > 1000) { text = text.substring(0, 997) + '...'; + } responseText = `\nResponse text:\n${text}`; } throw new Error(`${fetchResponse.status} ${fetchResponse.statusText}${responseText}`); @@ -219,8 +228,9 @@ export abstract class APIRequestContext extends SdkObject { } private _parseSetCookieHeader(responseUrl: string, setCookie: string[] | undefined): channels.NetworkCookie[] { - if (!setCookie) + if (!setCookie) { return []; + } const url = new URL(responseUrl); // https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.4 const defaultPath = '/' + url.pathname.substr(1).split('/').slice(0, -1).join('/'); @@ -228,26 +238,31 @@ export abstract class APIRequestContext extends SdkObject { for (const header of setCookie) { // Decode cookie value? const cookie: channels.NetworkCookie | null = parseCookie(header); - if (!cookie) + if (!cookie) { continue; + } // https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.3 - if (!cookie.domain) + if (!cookie.domain) { cookie.domain = url.hostname; - else + } else { assert(cookie.domain.startsWith('.') || !cookie.domain.includes('.')); - if (!domainMatches(url.hostname, cookie.domain!)) + } + if (!domainMatches(url.hostname, cookie.domain!)) { continue; + } // https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.4 - if (!cookie.path || !cookie.path.startsWith('/')) + if (!cookie.path || !cookie.path.startsWith('/')) { cookie.path = defaultPath; + } cookies.push(cookie); } return cookies; } private async _updateRequestCookieHeader(url: URL, headers: HeadersObject) { - if (getHeader(headers, 'cookie') !== undefined) + if (getHeader(headers, 'cookie') !== undefined) { return; + } const cookies = await this._cookies(url); if (cookies.length) { const valueArray = cookies.map(c => `${c.name}=${c.value}`); @@ -263,13 +278,16 @@ export abstract class APIRequestContext extends SdkObject { return await this._sendRequest(progress, url, options, postData); } catch (e) { e = rewriteOpenSSLErrorIfNeeded(e); - if (maxRetries === 0) + if (maxRetries === 0) { throw e; - if (i === maxRetries || (options.deadline && monotonicTime() + backoff > options.deadline)) + } + if (i === maxRetries || (options.deadline && monotonicTime() + backoff > options.deadline)) { throw new Error(`Failed after ${i + 1} attempt(s): ${e}`); + } // Retry on connection reset only. - if (e.code !== 'ECONNRESET') + if (e.code !== 'ECONNRESET') { throw e; + } progress.log(` Received ECONNRESET, will retry after ${backoff}ms.`); await new Promise(f => setTimeout(f, backoff)); backoff *= 2; @@ -348,8 +366,9 @@ export abstract class APIRequestContext extends SdkObject { this.emit(APIRequestContext.Events.RequestFinished, requestFinishedEvent); }; progress.log(`← ${response.statusCode} ${response.statusMessage}`); - for (const [name, value] of Object.entries(response.headers)) + for (const [name, value] of Object.entries(response.headers)) { progress.log(` ${name}: ${value}`); + } const cookies = this._parseSetCookieHeader(response.url || url.toString(), response.headers['set-cookie']) ; if (cookies.length) { @@ -397,8 +416,9 @@ export abstract class APIRequestContext extends SdkObject { __testHookLookup: options.__testHookLookup, }; // rejectUnauthorized = undefined is treated as true in node 12. - if (options.rejectUnauthorized === false) + if (options.rejectUnauthorized === false) { redirectOptions.rejectUnauthorized = false; + } // HTTP-redirect fetch step 4: If locationURL is null, then return response. // Best-effort UTF-8 decoding, per spec it's US-ASCII only, but browsers are more lenient. @@ -414,8 +434,9 @@ export abstract class APIRequestContext extends SdkObject { return; } - if (headers['host']) + if (headers['host']) { headers['host'] = locationURL.host; + } notifyRequestFinished(); fulfill(this._sendRequest(progress, locationURL, redirectOptions, postData)); @@ -469,8 +490,9 @@ export abstract class APIRequestContext extends SdkObject { // Brotli and deflate decompressors throw if the input stream is empty. const emptyStreamTransform = new SafeEmptyStreamTransform(notifyBodyFinished); body = pipeline(response, emptyStreamTransform, transform, e => { - if (e) + if (e) { reject(new Error(`failed to decompress '${encoding}' encoding: ${e.message}`)); + } }); body.on('error', e => reject(new Error(`failed to decompress '${encoding}' encoding: ${e}`))); } else { @@ -503,8 +525,12 @@ export abstract class APIRequestContext extends SdkObject { // non-happy-eyeballs sockets listeners.push( - eventsHelper.addEventListener(socket, 'lookup', () => { dnsLookupAt = monotonicTime(); }), - eventsHelper.addEventListener(socket, 'connect', () => { tcpConnectionAt ??= monotonicTime(); }), + eventsHelper.addEventListener(socket, 'lookup', () => { + dnsLookupAt = monotonicTime(); + }), + eventsHelper.addEventListener(socket, 'connect', () => { + tcpConnectionAt ??= monotonicTime(); + }), eventsHelper.addEventListener(socket, 'secureConnect', () => { tlsHandshakeAt = monotonicTime(); @@ -522,13 +548,16 @@ export abstract class APIRequestContext extends SdkObject { ); // when using socks proxy, having the socket means the connection got established - if (agent instanceof SocksProxyAgent) + if (agent instanceof SocksProxyAgent) { tcpConnectionAt ??= monotonicTime(); + } serverIPAddress = socket.remoteAddress; serverPort = socket.remotePort; }); - request.on('finish', () => { requestFinishAt = monotonicTime(); }); + request.on('finish', () => { + requestFinishAt = monotonicTime(); + }); // http proxy request.on('proxyConnect', () => { @@ -538,8 +567,9 @@ export abstract class APIRequestContext extends SdkObject { progress.log(`→ ${options.method} ${url.toString()}`); if (options.headers) { - for (const [name, value] of Object.entries(options.headers)) + for (const [name, value] of Object.entries(options.headers)) { progress.log(` ${name}: ${value}`); + } } if (options.deadline) { @@ -555,15 +585,17 @@ export abstract class APIRequestContext extends SdkObject { request.setTimeout(remaining, rejectOnTimeout); } - if (postData) + if (postData) { request.write(postData); + } request.end(); }); } private _getHttpCredentials(url: URL) { - if (!this._defaultOptions().httpCredentials?.origin || url.origin.toLowerCase() === this._defaultOptions().httpCredentials?.origin?.toLowerCase()) + if (!this._defaultOptions().httpCredentials?.origin || url.origin.toLowerCase() === this._defaultOptions().httpCredentials?.origin?.toLowerCase()) { return this._defaultOptions().httpCredentials; + } return undefined; } } @@ -581,10 +613,11 @@ class SafeEmptyStreamTransform extends Transform { callback(null, chunk); } override _flush(callback: TransformCallback): void { - if (this._receivedSomeData) + if (this._receivedSomeData) { callback(null); - else + } else { this._onEmptyStreamCallback(); + } } } @@ -643,13 +676,15 @@ export class GlobalAPIRequestContext extends APIRequestContext { super(playwright); this.attribution.context = this; const timeoutSettings = new TimeoutSettings(); - if (options.timeout !== undefined) + if (options.timeout !== undefined) { timeoutSettings.setDefaultTimeout(options.timeout); + } const proxy = options.proxy; if (proxy?.server) { - let url = proxy?.server.trim(); - if (!/^\w+:\/\//.test(url)) + let url = proxy.server.trim(); + if (!/^\w+:\/\//.test(url)) { url = 'http://' + url; + } proxy.server = url; } if (options.storageState) { @@ -703,21 +738,25 @@ export class GlobalAPIRequestContext extends APIRequestContext { export function createProxyAgent(proxy: types.ProxySettings) { const proxyURL = new URL(proxy.server); - if (proxyURL.protocol?.startsWith('socks')) + if (proxyURL.protocol.startsWith('socks')) { return new SocksProxyAgent(proxyURL); + } - if (proxy.username) + if (proxy.username) { proxyURL.username = proxy.username; - if (proxy.password) + } + if (proxy.password) { proxyURL.password = proxy.password; + } // TODO: We should use HttpProxyAgent conditional on proxyURL.protocol instead of always using CONNECT method. return new HttpsProxyAgent(proxyURL); } function toHeadersArray(rawHeaders: string[]): types.HeadersArray { const result: types.HeadersArray = []; - for (let i = 0; i < rawHeaders.length; i += 2) + for (let i = 0; i < rawHeaders.length; i += 2) { result.push({ name: rawHeaders[i], value: rawHeaders[i + 1] }); + } return result; } @@ -725,8 +764,9 @@ const redirectStatus = [301, 302, 303, 307, 308]; function parseCookie(header: string): channels.NetworkCookie | null { const raw = parseRawCookie(header); - if (!raw) + if (!raw) { return null; + } const cookie: channels.NetworkCookie = { domain: '', path: '', @@ -748,17 +788,19 @@ function serializePostData(params: channels.APIRequestContextFetchParams, header return Buffer.from(params.jsonData, 'utf8'); } else if (params.formData) { const searchParams = new URLSearchParams(); - for (const { name, value } of params.formData) + for (const { name, value } of params.formData) { searchParams.append(name, value); + } setHeader(headers, 'content-type', 'application/x-www-form-urlencoded', true); return Buffer.from(searchParams.toString(), 'utf8'); } else if (params.multipartData) { const formData = new MultipartFormData(); for (const field of params.multipartData) { - if (field.file) + if (field.file) { formData.addFileField(field.name, field.file); - else if (field.value) + } else if (field.value) { formData.addField(field.name, field.value); + } } setHeader(headers, 'content-type', formData.contentTypeHeader(), true); return formData.finish(); @@ -771,10 +813,11 @@ function serializePostData(params: channels.APIRequestContextFetchParams, header function setHeader(headers: { [name: string]: string }, name: string, value: string, keepExisting = false) { const existing = Object.entries(headers).find(pair => pair[0].toLowerCase() === name.toLowerCase()); - if (!existing) + if (!existing) { headers[name] = value; - else if (!keepExisting) + } else if (!keepExisting) { headers[existing[0]] = value; + } } function getHeader(headers: HeadersObject, name: string) { @@ -787,12 +830,14 @@ function removeHeader(headers: { [name: string]: string }, name: string) { } function shouldBypassProxy(url: URL, bypass?: string): boolean { - if (!bypass) + if (!bypass) { return false; + } const domains = bypass.split(',').map(s => { s = s.trim(); - if (!s.startsWith('.')) + if (!s.startsWith('.')) { s = '.' + s; + } return s; }); const domain = '.' + url.hostname; diff --git a/packages/playwright-core/src/server/fileUploadUtils.ts b/packages/playwright-core/src/server/fileUploadUtils.ts index 22ac13b127..5c1de94d52 100644 --- a/packages/playwright-core/src/server/fileUploadUtils.ts +++ b/packages/playwright-core/src/server/fileUploadUtils.ts @@ -33,17 +33,21 @@ export async function prepareFilesForUpload(frame: Frame, params: channels.Eleme const { payloads, streams, directoryStream } = params; let { localPaths, localDirectory } = params; - if ([payloads, localPaths, localDirectory, streams, directoryStream].filter(Boolean).length !== 1) + if ([payloads, localPaths, localDirectory, streams, directoryStream].filter(Boolean).length !== 1) { throw new Error('Exactly one of payloads, localPaths and streams must be provided'); + } - if (streams) + if (streams) { localPaths = streams.map(c => (c as WritableStreamDispatcher).path()); - if (directoryStream) + } + if (directoryStream) { localDirectory = (directoryStream as WritableStreamDispatcher).path(); + } if (localPaths) { - for (const p of localPaths) + for (const p of localPaths) { assert(path.isAbsolute(p) && path.resolve(p) === p, 'Paths provided to localPaths must be absolute and fully resolved.'); + } } let fileBuffers: { @@ -56,8 +60,9 @@ export async function prepareFilesForUpload(frame: Frame, params: channels.Eleme if (!frame._page._browserContext._browser._isCollocatedWithServer) { // If the browser is on a different machine read files into buffers. if (localPaths) { - if (await filesExceedUploadLimit(localPaths)) + if (await filesExceedUploadLimit(localPaths)) { throw new Error('Cannot transfer files larger than 50Mb to a browser not co-located with the server'); + } fileBuffers = await Promise.all(localPaths.map(async item => { return { name: path.basename(item), diff --git a/packages/playwright-core/src/server/firefox/ffAccessibility.ts b/packages/playwright-core/src/server/firefox/ffAccessibility.ts index d8d56b58de..22d22e65a5 100644 --- a/packages/playwright-core/src/server/firefox/ffAccessibility.ts +++ b/packages/playwright-core/src/server/firefox/ffAccessibility.ts @@ -81,10 +81,12 @@ class FFAXNode implements accessibility.AXNode { } _isPlainTextField(): boolean { - if (this._richlyEditable) + if (this._richlyEditable) { return false; - if (this._editable) + } + if (this._editable) { return true; + } return this._role === 'entry'; } @@ -111,29 +113,33 @@ class FFAXNode implements accessibility.AXNode { } _findNeedle(): FFAXNode | null { - if (this._payload.foundObject) + if (this._payload.foundObject) { return this; + } for (const child of this._children) { const found = child._findNeedle(); - if (found) + if (found) { return found; + } } 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()) + } + // 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()) { return true; - // Roles whose children are only presentational according to the ARIA and - // HTML5 Specs should be hidden from screen readers. - // (Note that whilst ARIA buttons can have only presentational children, HTML5 - // buttons are allowed to have content.) + } + // Roles whose children are only presentational according to the ARIA and + // HTML5 Specs should be hidden from screen readers. + // (Note that whilst ARIA buttons can have only presentational children, HTML5 + // buttons are allowed to have content.) switch (this._role) { case 'graphic': case 'scrollbar': @@ -145,12 +151,15 @@ class FFAXNode implements accessibility.AXNode { break; } // Here and below: Android heuristics - if (this._hasFocusableChild()) + if (this._hasFocusableChild()) { return false; - if (this._focusable && this._role !== 'document' && this._name) + } + if (this._focusable && this._role !== 'document' && this._name) { return true; - if (this._role === 'heading' && this._name) + } + if (this._role === 'heading' && this._name) { return true; + } return false; } @@ -187,14 +196,17 @@ class FFAXNode implements accessibility.AXNode { } isInteresting(insideControl: boolean): boolean { - 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 it's not focusable but has a control role, then it's interesting. + if (this.isControl()) { return true; - // A non focusable child of a control is not interesting - if (insideControl) + } + // A non focusable child of a control is not interesting + if (insideControl) { return false; + } return this.isLeafNode() && !!this._name.trim(); } @@ -211,8 +223,9 @@ class FFAXNode implements accessibility.AXNode { 'keyshortcuts', ]; for (const userStringProperty of userStringProperties) { - if (!(userStringProperty in this._payload)) + if (!(userStringProperty in this._payload)) { continue; + } node[userStringProperty] = this._payload[userStringProperty]; } const booleanProperties: Array = [ @@ -227,19 +240,22 @@ class FFAXNode implements accessibility.AXNode { 'selected', ]; for (const booleanProperty of booleanProperties) { - if (this._role === 'document' && booleanProperty === 'focused') - continue; // document focusing is strange - const value = this._payload[booleanProperty]; - if (!value) + if (this._role === 'document' && booleanProperty === 'focused') { continue; + } // document focusing is strange + const value = this._payload[booleanProperty]; + if (!value) { + continue; + } node[booleanProperty] = value; } const numericalProperties: Array = [ 'level' ]; for (const numericalProperty of numericalProperties) { - if (!(numericalProperty in this._payload)) + if (!(numericalProperty in this._payload)) { continue; + } node[numericalProperty] = this._payload[numericalProperty]; } const tokenProperties: Array = [ @@ -249,19 +265,23 @@ class FFAXNode implements accessibility.AXNode { ]; for (const tokenProperty of tokenProperties) { const value = this._payload[tokenProperty]; - if (!value || value === 'false') + if (!value || value === 'false') { continue; + } node[tokenProperty] = value; } const axNode = node as channels.AXNode; axNode.valueString = this._payload.value; - if ('checked' in this._payload) + if ('checked' in this._payload) { axNode.checked = this._payload.checked === true ? 'checked' : this._payload.checked === 'mixed' ? 'mixed' : 'unchecked'; - if ('pressed' in this._payload) + } + if ('pressed' in this._payload) { axNode.pressed = this._payload.pressed === true ? 'pressed' : 'released'; - if ('invalid' in this._payload) + } + if ('invalid' in this._payload) { axNode.invalid = this._payload.invalid === true ? 'true' : 'false'; + } return axNode; } } diff --git a/packages/playwright-core/src/server/firefox/ffBrowser.ts b/packages/playwright-core/src/server/firefox/ffBrowser.ts index 92998a7946..0da66ea2da 100644 --- a/packages/playwright-core/src/server/firefox/ffBrowser.ts +++ b/packages/playwright-core/src/server/firefox/ffBrowser.ts @@ -42,11 +42,13 @@ export class FFBrowser extends Browser { static async connect(parent: SdkObject, transport: ConnectionTransport, options: BrowserOptions): Promise { const connection = new FFConnection(transport, options.protocolLogger, options.browserLogsCollector); const browser = new FFBrowser(parent, connection, options); - if ((options as any).__testHookOnConnectToBrowser) + if ((options as any).__testHookOnConnectToBrowser) { await (options as any).__testHookOnConnectToBrowser(); + } let firefoxUserPrefs = options.originalLaunchOptions.firefoxUserPrefs ?? {}; - if (Object.keys(kBandaidFirefoxUserPrefs).length) + if (Object.keys(kBandaidFirefoxUserPrefs).length) { firefoxUserPrefs = { ...kBandaidFirefoxUserPrefs, ...firefoxUserPrefs }; + } const promises: Promise[] = [ browser.session.send('Browser.enable', { attachToDefaultContext: !!options.persistent, @@ -59,8 +61,9 @@ export class FFBrowser extends Browser { promises.push((browser._defaultContext as FFBrowserContext)._initialize()); } const proxy = options.originalLaunchOptions.proxyOverride || options.proxy; - if (proxy) + if (proxy) { promises.push(browser.session.send('Browser.setBrowserProxy', toJugglerProxyOptions(proxy))); + } await Promise.all(promises); return browser; } @@ -90,8 +93,9 @@ export class FFBrowser extends Browser { } async doCreateNewContext(options: types.BrowserContextOptions): Promise { - if (options.isMobile) + if (options.isMobile) { throw new Error('options.isMobile is not supported in Firefox'); + } const { browserContextId } = await this.session.send('Browser.createBrowserContext', { removeOnDetach: true }); const context = new FFBrowserContext(this, browserContextId, options); await context._initialize(); @@ -130,8 +134,9 @@ export class FFBrowser extends Browser { _onDownloadCreated(payload: Protocol.Browser.downloadCreatedPayload) { const ffPage = this._ffPages.get(payload.pageTargetId); - if (!ffPage) + if (!ffPage) { return; + } // Abort the navigation that turned into download. ffPage._page._frameManager.frameAbortedNavigation(payload.frameId, 'Download is starting'); @@ -142,11 +147,13 @@ export class FFBrowser extends Browser { // Resume the page creation with an error. The page will automatically close right // after the download begins. ffPage._markAsError(new Error('Starting new page download')); - if (ffPage._opener) + if (ffPage._opener) { originPage = ffPage._opener._page.initializedOrUndefined(); + } } - if (!originPage) + if (!originPage) { return; + } this._downloadCreated(originPage, payload.uuid, payload.url, payload.suggestedFileName); } @@ -160,11 +167,13 @@ export class FFBrowser extends Browser { } _onDisconnect() { - for (const video of this._idToVideo.values()) + for (const video of this._idToVideo.values()) { video.artifact.reportFinished(new TargetClosedError()); + } this._idToVideo.clear(); - for (const ffPage of this._ffPages.values()) + for (const ffPage of this._ffPages.values()) { ffPage.didClose(); + } this._ffPages.clear(); this._didClose(); } @@ -200,28 +209,39 @@ export class FFBrowserContext extends BrowserContext { }; promises.push(this._browser.session.send('Browser.setDefaultViewport', { browserContextId, viewport })); } - if (this._options.hasTouch) + if (this._options.hasTouch) { promises.push(this._browser.session.send('Browser.setTouchOverride', { browserContextId, hasTouch: true })); - if (this._options.userAgent) + } + if (this._options.userAgent) { promises.push(this._browser.session.send('Browser.setUserAgentOverride', { browserContextId, userAgent: this._options.userAgent })); - if (this._options.bypassCSP) + } + if (this._options.bypassCSP) { promises.push(this._browser.session.send('Browser.setBypassCSP', { browserContextId, bypassCSP: true })); - if (this._options.ignoreHTTPSErrors || this._options.internalIgnoreHTTPSErrors) + } + if (this._options.ignoreHTTPSErrors || this._options.internalIgnoreHTTPSErrors) { promises.push(this._browser.session.send('Browser.setIgnoreHTTPSErrors', { browserContextId, ignoreHTTPSErrors: true })); - if (this._options.javaScriptEnabled === false) + } + if (this._options.javaScriptEnabled === false) { promises.push(this._browser.session.send('Browser.setJavaScriptDisabled', { browserContextId, javaScriptDisabled: true })); - if (this._options.locale) + } + if (this._options.locale) { promises.push(this._browser.session.send('Browser.setLocaleOverride', { browserContextId, locale: this._options.locale })); - if (this._options.timezoneId) + } + if (this._options.timezoneId) { promises.push(this._browser.session.send('Browser.setTimezoneOverride', { browserContextId, timezoneId: this._options.timezoneId })); - if (this._options.extraHTTPHeaders || this._options.locale) + } + if (this._options.extraHTTPHeaders || this._options.locale) { promises.push(this.setExtraHTTPHeaders(this._options.extraHTTPHeaders || [])); - if (this._options.httpCredentials) + } + if (this._options.httpCredentials) { promises.push(this.setHTTPCredentials(this._options.httpCredentials)); - if (this._options.geolocation) + } + if (this._options.geolocation) { promises.push(this.setGeolocation(this._options.geolocation)); - if (this._options.offline) + } + if (this._options.offline) { promises.push(this.setOffline(this._options.offline)); + } if (this._options.colorScheme !== 'no-override') { promises.push(this._browser.session.send('Browser.setColorScheme', { browserContextId, @@ -276,8 +296,9 @@ export class FFBrowserContext extends BrowserContext { const { targetId } = await this._browser.session.send('Browser.newPage', { browserContextId: this._browserContextId }).catch(e => { - if (e.message.includes('Failed to override timezone')) + if (e.message.includes('Failed to override timezone')) { throw new Error(`Invalid timezone ID: ${this._options.timezoneId}`); + } throw e; }); return this._browser._ffPages.get(targetId)!._page; @@ -314,8 +335,9 @@ export class FFBrowserContext 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, browserContextId: this._browserContextId, permissions: filtered }); @@ -334,8 +356,9 @@ export class FFBrowserContext extends BrowserContext { async setExtraHTTPHeaders(headers: types.HeadersArray): Promise { this._options.extraHTTPHeaders = headers; let allHeaders = this._options.extraHTTPHeaders; - if (this._options.locale) + if (this._options.locale) { allHeaders = network.mergeHeaders([allHeaders, network.singleHeader('Accept-Language', this._options.locale)]); + } await this._browser.session.send('Browser.setExtraHTTPHeaders', { browserContextId: this._browserContextId, headers: allHeaders }); } @@ -411,17 +434,19 @@ function toJugglerProxyOptions(proxy: types.ProxySettings) { const proxyServer = new URL(proxy.server); let port = parseInt(proxyServer.port, 10); let type: 'http' | 'https' | 'socks' | 'socks4' = 'http'; - if (proxyServer.protocol === 'socks5:') + if (proxyServer.protocol === 'socks5:') { type = 'socks'; - else if (proxyServer.protocol === 'socks4:') + } else if (proxyServer.protocol === 'socks4:') { type = 'socks4'; - else if (proxyServer.protocol === 'https:') + } else if (proxyServer.protocol === 'https:') { type = 'https'; + } if (proxyServer.port === '') { - if (proxyServer.protocol === 'http:') + if (proxyServer.protocol === 'http:') { port = 80; - else if (proxyServer.protocol === 'https:') + } else if (proxyServer.protocol === 'https:') { port = 443; + } } return { type, diff --git a/packages/playwright-core/src/server/firefox/ffConnection.ts b/packages/playwright-core/src/server/firefox/ffConnection.ts index 1a24e1dbf0..befe9bc3cb 100644 --- a/packages/playwright-core/src/server/firefox/ffConnection.ts +++ b/packages/playwright-core/src/server/firefox/ffConnection.ts @@ -70,11 +70,13 @@ export class FFConnection 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.dispatchMessage(message); + } } _onClose(reason?: string) { @@ -87,8 +89,9 @@ export class FFConnection extends EventEmitter { } close() { - if (!this._closed) + if (!this._closed) { this._transport.close(); + } } createSession(sessionId: string): FFSession { @@ -134,8 +137,9 @@ export class FFSession extends EventEmitter { method: T, params?: Protocol.CommandParameters[T] ): Promise { - if (this._crashed || this._disposed || this._connection._closed || this._connection._browserDisconnectedLogs) + if (this._crashed || this._disposed || this._connection._closed || this._connection._browserDisconnectedLogs) { throw new ProtocolError(this._crashed ? 'crashed' : 'closed', undefined, this._connection._browserDisconnectedLogs); + } const id = this._connection.nextMessageId(); this._rawSend({ method, params, id }); return new Promise((resolve, reject) => { diff --git a/packages/playwright-core/src/server/firefox/ffExecutionContext.ts b/packages/playwright-core/src/server/firefox/ffExecutionContext.ts index c7a3f106f8..ee928b69c4 100644 --- a/packages/playwright-core/src/server/firefox/ffExecutionContext.ts +++ b/packages/playwright-core/src/server/firefox/ffExecutionContext.ts @@ -63,8 +63,9 @@ export class FFExecutionContext implements js.ExecutionContextDelegate { executionContextId: this._executionContextId }).catch(rewriteError); checkException(payload.exceptionDetails); - if (returnByValue) + if (returnByValue) { return parseEvaluationResultValue(payload.result!.value); + } return utilityScript._context.createHandle(payload.result!); } @@ -74,8 +75,9 @@ export class FFExecutionContext implements js.ExecutionContextDelegate { objectId, }); const result = new Map(); - for (const property of response.properties) + for (const property of response.properties) { result.set(property.name, context.createHandle(property.value)); + } return result; } @@ -92,21 +94,26 @@ export class FFExecutionContext implements js.ExecutionContextDelegate { } function checkException(exceptionDetails?: Protocol.Runtime.ExceptionDetails) { - if (!exceptionDetails) + if (!exceptionDetails) { return; - if (exceptionDetails.value) + } + if (exceptionDetails.value) { throw new js.JavaScriptErrorInEvaluate(JSON.stringify(exceptionDetails.value)); - else + } else { throw new js.JavaScriptErrorInEvaluate(exceptionDetails.text + (exceptionDetails.stack ? '\n' + exceptionDetails.stack : '')); + } } function rewriteError(error: Error): (Protocol.Runtime.evaluateReturnValue | Protocol.Runtime.callFunctionReturnValue) { - if (error.message.includes('cyclic object value') || error.message.includes('Object is not serializable')) + if (error.message.includes('cyclic object value') || error.message.includes('Object is not serializable')) { return { result: { type: 'undefined', value: 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; } @@ -117,20 +124,28 @@ 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 (object.unserializableValue) + } + if (object.unserializableValue) { return String(object.unserializableValue); - if (object.type === 'symbol') + } + if (object.type === 'symbol') { return 'Symbol()'; - if (object.subtype === 'regexp') + } + if (object.subtype === 'regexp') { return 'RegExp'; - if (object.subtype === 'weakmap') + } + if (object.subtype === 'weakmap') { return 'WeakMap'; - if (object.subtype === 'weakset') + } + if (object.subtype === 'weakset') { return 'WeakSet'; - if (object.subtype) + } + if (object.subtype) { return object.subtype[0].toUpperCase() + object.subtype.slice(1); - if ('value' in object) + } + if ('value' in object) { return String(object.value); + } } diff --git a/packages/playwright-core/src/server/firefox/ffInput.ts b/packages/playwright-core/src/server/firefox/ffInput.ts index 66f35399a5..05168ecfdb 100644 --- a/packages/playwright-core/src/server/firefox/ffInput.ts +++ b/packages/playwright-core/src/server/firefox/ffInput.ts @@ -22,35 +22,45 @@ import type { FFSession } from './ffConnection'; function toModifiersMask(modifiers: Set): 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('Shift')) + } + if (modifiers.has('Shift')) { mask |= 4; - if (modifiers.has('Meta')) + } + if (modifiers.has('Meta')) { mask |= 8; + } return mask; } function toButtonNumber(button: types.MouseButton): number { - if (button === 'left') + if (button === 'left') { return 0; - if (button === 'middle') + } + if (button === 'middle') { return 1; - if (button === 'right') + } + if (button === 'right') { return 2; + } return 0; } function toButtonsMask(buttons: Set): 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; } @@ -63,8 +73,9 @@ export class RawKeyboardImpl implements input.RawKeyboard { async keydown(modifiers: Set, code: string, keyCode: number, keyCodeWithoutLocation: number, key: string, location: number, autoRepeat: boolean, text: string | undefined): Promise { // Firefox will figure out Enter by itself - if (text === '\r') + if (text === '\r') { text = ''; + } await this._client.send('Page.dispatchKeyEvent', { type: 'keydown', keyCode: keyCodeWithoutLocation, diff --git a/packages/playwright-core/src/server/firefox/ffNetworkManager.ts b/packages/playwright-core/src/server/firefox/ffNetworkManager.ts index 73b8e3589f..50bc2f45f2 100644 --- a/packages/playwright-core/src/server/firefox/ffNetworkManager.ts +++ b/packages/playwright-core/src/server/firefox/ffNetworkManager.ts @@ -59,35 +59,41 @@ export class FFNetworkManager { _onRequestWillBeSent(event: Protocol.Network.requestWillBeSentPayload) { const redirectedFrom = event.redirectedFrom ? (this._requests.get(event.redirectedFrom) || null) : null; const frame = redirectedFrom ? redirectedFrom.request.frame() : (event.frameId ? this._page._frameManager.frame(event.frameId) : null); - if (!frame) + if (!frame) { return; - if (redirectedFrom) + } + if (redirectedFrom) { this._requests.delete(redirectedFrom._id); + } const request = new InterceptableRequest(frame, redirectedFrom, event); let route; - if (event.isIntercepted) + if (event.isIntercepted) { route = new FFRouteImpl(this._session, request); + } this._requests.set(request._id, request); this._page._frameManager.requestStarted(request.request, route); } _onResponseReceived(event: Protocol.Network.responseReceivedPayload) { const request = this._requests.get(event.requestId); - if (!request) + if (!request) { return; + } const getResponseBody = async () => { const response = await this._session.send('Network.getResponseBody', { requestId: request._id }); - if (response.evicted) + if (response.evicted) { throw new Error(`Response body for ${request.request.method()} ${request.request.url()} was evicted!`); + } return Buffer.from(response.base64body, 'base64'); }; const startTime = event.timing.startTime; function relativeToStart(time: number): number { - if (!time) + if (!time) { return -1; + } return (time - startTime) / 1000; } const timing = { @@ -101,7 +107,7 @@ export class FFNetworkManager { responseStart: relativeToStart(event.timing.responseStart), }; const response = new network.Response(request.request, event.status, event.statusText, parseMultivalueHeaders(event.headers), timing, getResponseBody, event.fromServiceWorker); - if (event?.remoteIPAddress && typeof event?.remotePort === 'number') { + if (event.remoteIPAddress && typeof event.remotePort === 'number') { response._serverAddrFinished({ ipAddress: event.remoteIPAddress, port: event.remotePort, @@ -110,11 +116,11 @@ export class FFNetworkManager { response._serverAddrFinished(); } response._securityDetailsFinished({ - protocol: event?.securityDetails?.protocol, - subjectName: event?.securityDetails?.subjectName, - issuer: event?.securityDetails?.issuer, - validFrom: event?.securityDetails?.validFrom, - validTo: event?.securityDetails?.validTo, + protocol: event.securityDetails?.protocol, + subjectName: event.securityDetails?.subjectName, + issuer: event.securityDetails?.issuer, + validFrom: event.securityDetails?.validFrom, + validTo: event.securityDetails?.validTo, }); // "raw" headers are the same as "provisional" headers in Firefox. response.setRawResponseHeaders(null); @@ -125,8 +131,9 @@ export class FFNetworkManager { _onRequestFinished(event: Protocol.Network.requestFinishedPayload) { const request = this._requests.get(event.requestId); - if (!request) + if (!request) { return; + } const response = request.request._existingResponse()!; response.setTransferSize(event.transferSize); response.setEncodedBodySize(event.encodedBodySize); @@ -140,15 +147,17 @@ export class FFNetworkManager { this._requests.delete(request._id); response._requestFinished(responseEndTime); } - if (event.protocolVersion) + if (event.protocolVersion) { response._setHttpVersion(event.protocolVersion); + } this._page._frameManager.reportRequestFinished(request.request, response); } _onRequestFailed(event: Protocol.Network.requestFailedPayload) { const request = this._requests.get(event.requestId); - if (!request) + if (!request) { return; + } this._requests.delete(request._id); const response = request.request._existingResponse(); if (response) { @@ -198,11 +207,13 @@ class InterceptableRequest { constructor(frame: frames.Frame, redirectedFrom: InterceptableRequest | null, payload: Protocol.Network.requestWillBeSentPayload) { this._id = payload.requestId; - if (redirectedFrom) + if (redirectedFrom) { redirectedFrom._redirectedTo = this; + } let postDataBuffer = null; - if (payload.postData) + if (payload.postData) { postDataBuffer = Buffer.from(payload.postData, 'base64'); + } this.request = new network.Request(frame._page._browserContext, frame, null, redirectedFrom ? redirectedFrom.request : null, payload.navigationId, payload.url, internalCauseToResourceType[payload.internalCause] || causeToResourceType[payload.cause] || 'other', payload.method, postDataBuffer, payload.headers); // "raw" headers are the same as "provisional" headers in Firefox. @@ -211,8 +222,9 @@ class InterceptableRequest { _finalRequest(): InterceptableRequest { let request: InterceptableRequest = this; - while (request._redirectedTo) + while (request._redirectedTo) { request = request._redirectedTo; + } return request; } } @@ -261,8 +273,9 @@ function parseMultivalueHeaders(headers: HeadersArray) { for (const header of headers) { const separator = header.name.toLowerCase() === 'set-cookie' ? '\n' : ','; const tokens = header.value.split(separator).map(s => s.trim()); - for (const token of tokens) + for (const token of tokens) { result.push({ name: header.name, value: token }); + } } return result; } diff --git a/packages/playwright-core/src/server/firefox/ffPage.ts b/packages/playwright-core/src/server/firefox/ffPage.ts index 68559d7c7e..837b6c8e09 100644 --- a/packages/playwright-core/src/server/firefox/ffPage.ts +++ b/packages/playwright-core/src/server/firefox/ffPage.ts @@ -100,8 +100,9 @@ export class FFPage implements PageDelegate { ]; this._session.once('Page.ready', () => { - if (this._reportedAsNew) + if (this._reportedAsNew) { return; + } this._reportedAsNew = true; this._page.reportAsNew(this._opener?._page); }); @@ -112,8 +113,9 @@ export class FFPage implements PageDelegate { async _markAsError(error: Error) { // Same error may be reported twice: channel disconnected and session.send fails. - if (this._reportedAsNew) + if (this._reportedAsNew) { return; + } this._reportedAsNew = true; this._page.reportAsNew(this._opener?._page, error); } @@ -124,8 +126,9 @@ export class FFPage implements PageDelegate { } _onWebSocketClosed(event: Protocol.Page.webSocketClosedPayload) { - if (event.error) + if (event.error) { this._page._frameManager.webSocketError(webSocketId(event.frameId, event.wsid), event.error); + } this._page._frameManager.webSocketClosed(webSocketId(event.frameId, event.wsid)); } @@ -140,47 +143,54 @@ export class FFPage implements PageDelegate { _onExecutionContextCreated(payload: Protocol.Runtime.executionContextCreatedPayload) { const { executionContextId, auxData } = payload; const frame = this._page._frameManager.frame(auxData.frameId!); - if (!frame) + if (!frame) { return; + } const delegate = new FFExecutionContext(this._session, executionContextId); let worldName: types.World|null = null; - if (auxData.name === UTILITY_WORLD_NAME) + if (auxData.name === UTILITY_WORLD_NAME) { worldName = 'utility'; - else if (!auxData.name) + } else if (!auxData.name) { worldName = 'main'; + } const context = new dom.FrameExecutionContext(delegate, frame, worldName); (context as any)[contextDelegateSymbol] = delegate; - if (worldName) + if (worldName) { frame._contextCreated(worldName, context); + } this._contextIdToContext.set(executionContextId, context); } _onExecutionContextDestroyed(payload: Protocol.Runtime.executionContextDestroyedPayload) { const { executionContextId } = payload; const context = this._contextIdToContext.get(executionContextId); - if (!context) + if (!context) { return; + } this._contextIdToContext.delete(executionContextId); context.frame._contextDestroyed(context); } _onExecutionContextsCleared() { - for (const executionContextId of Array.from(this._contextIdToContext.keys())) + for (const executionContextId of Array.from(this._contextIdToContext.keys())) { this._onExecutionContextDestroyed({ executionContextId }); + } } private _removeContextsForFrame(frame: frames.Frame) { for (const [contextId, context] of this._contextIdToContext) { - if (context.frame === frame) + if (context.frame === frame) { this._contextIdToContext.delete(contextId); + } } } _onLinkClicked(phase: 'before' | 'after') { - if (phase === 'before') + if (phase === 'before') { this._page._frameManager.frameWillPotentiallyRequestNavigation(); - else + } else { this._page._frameManager.frameDidPotentiallyRequestNavigation(); + } } _onNavigationStarted(params: Protocol.Page.navigationStartedPayload) { @@ -193,8 +203,9 @@ export class FFPage implements PageDelegate { _onNavigationCommitted(params: Protocol.Page.navigationCommittedPayload) { for (const [workerId, worker] of this._workers) { - if (worker.frameId === params.frameId) + if (worker.frameId === params.frameId) { this._onWorkerDestroyed({ workerId }); + } } this._page._frameManager.frameCommittedNewDocumentNavigation(params.frameId, params.url, params.name || '', params.navigationId || '', false); } @@ -213,10 +224,12 @@ export class FFPage implements PageDelegate { _onEventFired(payload: Protocol.Page.eventFiredPayload) { const { frameId, name } = payload; - if (name === 'load') + if (name === 'load') { this._page._frameManager.frameLifecycleEvent(frameId, 'load'); - if (name === 'DOMContentLoaded') + } + if (name === 'DOMContentLoaded') { this._page._frameManager.frameLifecycleEvent(frameId, 'domcontentloaded'); + } } _onUncaughtError(params: Protocol.Page.uncaughtErrorPayload) { @@ -230,8 +243,9 @@ export class FFPage implements PageDelegate { _onConsole(payload: Protocol.Runtime.consolePayload) { const { type, args, executionContextId, location } = payload; const context = this._contextIdToContext.get(executionContextId); - if (!context) + if (!context) { return; + } // Juggler reports 'warn' for some internal messages generated by the browser. this._page._addConsoleMessage(type === 'warn' ? 'warning' : type, args.map(arg => context.createHandle(arg)), location); } @@ -251,16 +265,18 @@ export class FFPage implements PageDelegate { const pageOrError = await this._page.waitForInitializedOrError(); if (!(pageOrError instanceof Error)) { const context = this._contextIdToContext.get(event.executionContextId); - if (context) + if (context) { await this._page._onBindingCalled(event.payload, context); + } } } async _onFileChooserOpened(payload: Protocol.Page.fileChooserOpenedPayload) { const { executionContextId, element } = payload; const context = this._contextIdToContext.get(executionContextId); - if (!context) + if (!context) { return; + } const handle = context.createHandle(element).asElement()!; await this._page._onFileChooserOpened(handle); } @@ -293,8 +309,9 @@ export class FFPage implements PageDelegate { _onWorkerDestroyed(event: Protocol.Page.workerDestroyedPayload) { const workerId = event.workerId; const worker = this._workers.get(workerId); - if (!worker) + if (!worker) { return; + } worker.session.dispose(); this._workers.delete(workerId); this._page._removeWorker(workerId); @@ -302,8 +319,9 @@ export class FFPage implements PageDelegate { async _onDispatchMessageFromWorker(event: Protocol.Page.dispatchMessageFromWorkerPayload) { const worker = this._workers.get(event.workerId); - if (!worker) + if (!worker) { return; + } worker.session.dispatchMessage(JSON.parse(event.message)); } @@ -398,8 +416,9 @@ export class FFPage implements PageDelegate { } async setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise { - if (color) + if (color) { throw new Error('Not implemented'); + } } async takeScreenshot(progress: Progress, format: 'png' | 'jpeg', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined, fitsViewport: boolean, scale: 'css' | 'device'): Promise { @@ -427,8 +446,9 @@ export class FFPage implements PageDelegate { frameId: handle._context.frame._id, objectId: handle._objectId, }); - if (!contentFrameId) + if (!contentFrameId) { return null; + } return this._page._frameManager.frame(contentFrameId); } @@ -446,8 +466,9 @@ export class FFPage implements PageDelegate { async getBoundingBox(handle: dom.ElementHandle): Promise { const quads = await this.getContentQuads(handle); - if (!quads || !quads.length) + if (!quads || !quads.length) { return null; + } let minX = Infinity; let maxX = -Infinity; let minY = Infinity; @@ -469,10 +490,12 @@ export class FFPage implements PageDelegate { objectId: handle._objectId, rect, }).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; }); } @@ -487,8 +510,9 @@ export class FFPage implements PageDelegate { } private _onScreencastFrame(event: Protocol.Page.screencastFramePayload) { - if (!this._screencastId) + if (!this._screencastId) { return; + } const screencastId = this._screencastId; this._page.throttleScreencastFrameAck(() => { this._session.send('Page.screencastFrameAck', { screencastId }).catch(e => debugLogger.log('error', e)); @@ -511,8 +535,9 @@ export class FFPage implements PageDelegate { frameId: handle._context.frame._id, objectId: handle._objectId, }); - if (!result) + if (!result) { return null; + } return result.quads.map(quad => [quad.p1, quad.p2, quad.p3, quad.p4]); } @@ -535,8 +560,9 @@ export class FFPage implements PageDelegate { objectId: handle._objectId, executionContextId: ((to as any)[contextDelegateSymbol] as FFExecutionContext)._executionContextId }); - if (!result.remoteObject) + if (!result.remoteObject) { throw new Error(dom.kUnableToAdoptErrorMessage); + } return to.createHandle(result.remoteObject) as dom.ElementHandle; } @@ -557,15 +583,17 @@ export class FFPage implements PageDelegate { async getFrameElement(frame: frames.Frame): Promise { const parent = frame.parentFrame(); - if (!parent) + if (!parent) { throw new Error('Frame has been detached.'); + } const context = await parent._mainContext(); const result = await this._session.send('Page.adoptNode', { frameId: frame._id, executionContextId: ((context as any)[contextDelegateSymbol] as FFExecutionContext)._executionContextId }); - if (!result.remoteObject) + if (!result.remoteObject) { throw new Error('Frame has been detached.'); + } return context.createHandle(result.remoteObject) as dom.ElementHandle; } diff --git a/packages/playwright-core/src/server/firefox/firefox.ts b/packages/playwright-core/src/server/firefox/firefox.ts index 9fbc409a56..ceaa8d0dd6 100644 --- a/packages/playwright-core/src/server/firefox/firefox.ts +++ b/packages/playwright-core/src/server/firefox/firefox.ts @@ -39,19 +39,23 @@ export class Firefox 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?`); + } if (os.platform() === 'linux') { // Always remove SNAP_NAME and SNAP_INSTANCE_NAME env variables since they // confuse Firefox: in our case, builds never come from SNAP. @@ -69,10 +73,12 @@ export class Firefox 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'); - if (args.find(arg => arg.startsWith('-juggler'))) + } + if (args.find(arg => arg.startsWith('-juggler'))) { throw new Error('Use the port parameter instead of -juggler argument'); + } const firefoxArguments = ['-no-remote']; if (headless) { firefoxArguments.push('-headless'); @@ -83,10 +89,11 @@ export class Firefox extends BrowserType { firefoxArguments.push(`-profile`, userDataDir); firefoxArguments.push('-juggler-pipe'); firefoxArguments.push(...args); - if (isPersistent) + if (isPersistent) { firefoxArguments.push('about:blank'); - else + } else { firefoxArguments.push('-silent'); + } return firefoxArguments; } @@ -97,8 +104,9 @@ export class Firefox extends BrowserType { class JugglerReadyState extends BrowserReadyState { override onBrowserOutput(message: string): void { - if (message.includes('Juggler listening to the pipe')) + if (message.includes('Juggler listening to the pipe')) { this._wsEndpoint.resolve(undefined); + } } } diff --git a/packages/playwright-core/src/server/formData.ts b/packages/playwright-core/src/server/formData.ts index bc7f1e5bfe..c23519e3a5 100644 --- a/packages/playwright-core/src/server/formData.ts +++ b/packages/playwright-core/src/server/formData.ts @@ -65,8 +65,9 @@ export class MultipartFormData { private _addBoundary(isLastBoundary?: boolean) { this._chunks.push(Buffer.from('--' + this._boundary)); - if (isLastBoundary) + if (isLastBoundary) { this._chunks.push(Buffer.from('--')); + } this._chunks.push(Buffer.from('\r\n')); } } @@ -85,7 +86,8 @@ const alphaNumericEncodingMap = [ // See generateUniqueBoundaryString() in WebKit function generateUniqueBoundaryString(): string { const charCodes = []; - for (let i = 0; i < 16; i++) + for (let i = 0; i < 16; i++) { charCodes.push(alphaNumericEncodingMap[Math.floor(Math.random() * alphaNumericEncodingMap.length)]); + } return '----WebKitFormBoundary' + String.fromCharCode(...charCodes); } diff --git a/packages/playwright-core/src/server/frameSelectors.ts b/packages/playwright-core/src/server/frameSelectors.ts index 4be2a9c285..3497d4a7ca 100644 --- a/packages/playwright-core/src/server/frameSelectors.ts +++ b/packages/playwright-core/src/server/frameSelectors.ts @@ -49,8 +49,9 @@ export class FrameSelectors { async query(selector: string, options?: types.StrictOptions, scope?: ElementHandle): Promise | null> { const resolved = await this.resolveInjectedForSelector(selector, options, scope); // Be careful, |this.frame| can be different from |resolved.frame|. - if (!resolved) + if (!resolved) { return null; + } const handle = await resolved.injected.evaluateHandle((injected, { info, scope }) => { return injected.querySelector(info.parsed, scope || document, info.strict); }, { info: resolved.info, scope: resolved.scope }); @@ -65,8 +66,9 @@ export class FrameSelectors { async queryArrayInMainWorld(selector: string, scope?: ElementHandle): Promise> { const resolved = await this.resolveInjectedForSelector(selector, { mainWorld: true }, scope); // Be careful, |this.frame| can be different from |resolved.frame|. - if (!resolved) + if (!resolved) { throw new Error(`Failed to find frame for selector "${selector}"`); + } return await resolved.injected.evaluateHandle((injected, { info, scope }) => { return injected.querySelectorAll(info.parsed, scope || document); }, { info: resolved.info, scope: resolved.scope }); @@ -75,8 +77,9 @@ export class FrameSelectors { async queryCount(selector: string): Promise { const resolved = await this.resolveInjectedForSelector(selector); // Be careful, |this.frame| can be different from |resolved.frame|. - if (!resolved) + if (!resolved) { throw new Error(`Failed to find frame for selector "${selector}"`); + } return await resolved.injected.evaluate((injected, { info }) => { return injected.querySelectorAll(info.parsed, document).length; }, { info: resolved.info }); @@ -85,8 +88,9 @@ export class FrameSelectors { async queryAll(selector: string, scope?: ElementHandle): Promise[]> { const resolved = await this.resolveInjectedForSelector(selector, {}, scope); // Be careful, |this.frame| can be different from |resolved.frame|. - if (!resolved) + if (!resolved) { return []; + } const arrayHandle = await resolved.injected.evaluateHandle((injected, { info, scope }) => { return injected.querySelectorAll(info.parsed, scope || document); }, { info: resolved.info, scope: resolved.scope }); @@ -100,10 +104,11 @@ export class FrameSelectors { const result: Promise>[] = []; for (const property of properties.values()) { const elementHandle = property.asElement() as ElementHandle; - if (elementHandle) + if (elementHandle) { result.push(adoptIfNeeded(elementHandle, targetContext)); - else + } else { property.dispose(); + } } return Promise.all(result); } @@ -127,30 +132,35 @@ export class FrameSelectors { const injectedScript = await context.injectedScript(); const handle = await injectedScript.evaluateHandle((injected, { info, scope, selectorString }) => { const element = injected.querySelector(info.parsed, scope || document, info.strict); - if (element && element.nodeName !== 'IFRAME' && element.nodeName !== 'FRAME') + if (element && element.nodeName !== 'IFRAME' && element.nodeName !== 'FRAME') { throw injected.createStacklessError(`Selector "${selectorString}" resolved to ${injected.previewNode(element)}, ') + if (text === '') { break; + } await page.waitForTimeout(250); } }); @@ -209,8 +219,9 @@ it.describe('snapshots', () => { for (let counter = 0; ; ++counter) { const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'call@' + counter, 'snapshot@call@' + counter); const text = distillSnapshot(snapshot).replace(/frame@[^"]+["]/, '"'); - if (text === '') + if (text === '') { break; + } await page.waitForTimeout(250); } }); @@ -270,10 +281,12 @@ it.describe('snapshots', () => { function distillSnapshot(snapshot, options: { distillTarget: boolean, distillBoundingRect: boolean } = { distillTarget: true, distillBoundingRect: true }) { let { html } = snapshot.render(); - if (options.distillTarget) + if (options.distillTarget) { html = html.replace(/\s__playwright_target__="[^"]+"/g, ''); - if (options.distillBoundingRect) + } + if (options.distillBoundingRect) { html = html.replace(/\s__playwright_bounding_rect__="[^"]+"/g, ''); + } return html .replace(/
`); const exception = await page.screenshot({ fullPage: true }).catch(e => e); - if (browserName === 'firefox' || (browserName === 'webkit' && !isMac)) + if (browserName === 'firefox' || (browserName === 'webkit' && !isMac)) { expect(exception.message).toContain('Cannot take screenshot larger than 32767'); + } } }); diff --git a/tests/page/page-set-content.spec.ts b/tests/page/page-set-content.spec.ts index 4f0c903bfc..6efc6d0d74 100644 --- a/tests/page/page-set-content.spec.ts +++ b/tests/page/page-set-content.spec.ts @@ -84,8 +84,9 @@ it('should await resources to load', async ({ page, server }) => { }); it('should work fast enough', async ({ page, server }) => { - for (let i = 0; i < 20; ++i) + for (let i = 0; i < 20; ++i) { await page.setContent('
yo
'); + } }); it('should work with tricky content', async ({ page, server }) => { @@ -118,8 +119,9 @@ it('content() should throw nice error during navigation', async ({ page, server promise, ]); const emptyOutput = ''; - if (contentOrError !== expectedOutput && contentOrError !== emptyOutput) + if (contentOrError !== expectedOutput && contentOrError !== emptyOutput) { expect(contentOrError?.message).toContain('Unable to retrieve content because the page is navigating and changing the content.'); + } } }); diff --git a/tests/page/page-set-input-files.spec.ts b/tests/page/page-set-input-files.spec.ts index eaf1316f5c..480c6c6d9e 100644 --- a/tests/page/page-set-input-files.spec.ts +++ b/tests/page/page-set-input-files.spec.ts @@ -157,10 +157,11 @@ it('should upload large file', async ({ page, server, isAndroid, isWebView2, mod for (let i = 0; i < 50 * 1024; i++) { await new Promise((fulfill, reject) => { stream.write(str, err => { - if (err) + if (err) { reject(err); - else + } else { fulfill(); + } }); }); } @@ -217,10 +218,11 @@ it('should upload multiple large files', async ({ page, server, isAndroid, isWeb for (let i = 0; i < 49 * 1024; i++) { await new Promise((fulfill, reject) => { stream.write(str, err => { - if (err) + if (err) { reject(err); - else + } else { fulfill(); + } }); }); } @@ -255,10 +257,11 @@ it('should upload large file with relative path', async ({ page, server, isAndro for (let i = 0; i < 50 * 1024; i++) { await new Promise((fulfill, reject) => { stream.write(str, err => { - if (err) + if (err) { reject(err); - else + } else { fulfill(); + } }); }); } @@ -775,6 +778,7 @@ it('should preserve lastModified timestamp', async ({ page, asset }) => { const expectedTimestamps = files.map(file => Math.round(fs.statSync(asset(file)).mtimeMs)); // On Linux browser sometimes reduces the timestamp by 1ms: 1696272058110.0715 -> 1696272058109 or even // rounds it to seconds in WebKit: 1696272058110 -> 1696272058000. - for (let i = 0; i < timestamps.length; i++) + for (let i = 0; i < timestamps.length; i++) { expect(Math.abs(timestamps[i] - expectedTimestamps[i]), `expected: ${expectedTimestamps}; actual: ${timestamps}`).toBeLessThan(1000); + } }); diff --git a/tests/page/page-wait-for-function.spec.ts b/tests/page/page-wait-for-function.spec.ts index 88b6d7cb14..d778ea4894 100644 --- a/tests/page/page-wait-for-function.spec.ts +++ b/tests/page/page-wait-for-function.spec.ts @@ -34,8 +34,9 @@ it('should accept a string', async ({ page }) => { it('should work when resolved right before execution context disposal', async ({ page }) => { await page.addInitScript(() => window['__RELOADED'] = true); await page.waitForFunction(() => { - if (!window['__RELOADED']) + if (!window['__RELOADED']) { window.location.reload(); + } return true; }); }); @@ -81,15 +82,18 @@ it('should poll on raf', async ({ page }) => { }); it('should fail with predicate throwing on first call', async ({ page }) => { - const error = await page.waitForFunction(() => { throw new Error('oh my'); }).catch(e => e); + const error = await page.waitForFunction(() => { + throw new Error('oh my'); + }).catch(e => e); expect(error.message).toContain('oh my'); }); it('should fail with predicate throwing sometimes', async ({ page }) => { const error = await page.waitForFunction(() => { window['counter'] = (window['counter'] || 0) + 1; - if (window['counter'] === 3) + if (window['counter'] === 3) { throw new Error('Bad counter!'); + } return window['counter'] === 5 ? 'result' : false; }).catch(e => e); expect(error.message).toContain('Bad counter!'); @@ -217,8 +221,9 @@ it('should not be called after finishing successfully', async ({ page, server }) const messages = []; page.on('console', msg => { - if (msg.text().startsWith('waitForFunction')) + if (msg.text().startsWith('waitForFunction')) { messages.push(msg.text()); + } }); await page.waitForFunction(() => { @@ -244,8 +249,9 @@ it('should not be called after finishing unsuccessfully', async ({ page, server const messages = []; page.on('console', msg => { - if (msg.text().startsWith('waitForFunction')) + if (msg.text().startsWith('waitForFunction')) { messages.push(msg.text()); + } }); await page.waitForFunction(() => { diff --git a/tests/page/page-wait-for-load-state.spec.ts b/tests/page/page-wait-for-load-state.spec.ts index 10601b7cef..aac56a267a 100644 --- a/tests/page/page-wait-for-load-state.spec.ts +++ b/tests/page/page-wait-for-load-state.spec.ts @@ -156,8 +156,9 @@ it('should resolve after popup load', async ({ page, server }) => { let resolved = false; const loadSatePromise = popup.waitForLoadState().then(() => resolved = true); // Round trips! - for (let i = 0; i < 5; i++) + for (let i = 0; i < 5; i++) { await page.evaluate('window'); + } expect(resolved).toBe(false); cssResponse.end(''); await loadSatePromise; diff --git a/tests/page/page-wait-for-navigation.spec.ts b/tests/page/page-wait-for-navigation.spec.ts index d34251e977..a96afdc29a 100644 --- a/tests/page/page-wait-for-navigation.spec.ts +++ b/tests/page/page-wait-for-navigation.spec.ts @@ -159,8 +159,9 @@ it('should work when subframe issues window.stop()', async ({ browserName, page, page.goto(server.PREFIX + '/frames/one-frame.html').then(() => done = true).catch(() => {}); const frame = await new Promise(f => page.once('frameattached', f)); await new Promise(fulfill => page.on('framenavigated', f => { - if (f === frame) + if (f === frame) { fulfill(); + } })); await frame.evaluate(() => window.stop()); expect(done).toBe(true); @@ -253,7 +254,9 @@ it('should fail when frame detaches', async ({ page, server }) => { server.setRoute('/one-style.css', () => {}); const [error] = await Promise.all([ frame.waitForNavigation().catch(e => e), - page.$eval('iframe', frame => { frame.contentWindow.location.href = '/one-style.html'; }), + page.$eval('iframe', frame => { + frame.contentWindow.location.href = '/one-style.html'; + }), // Make sure policy checks pass and navigation actually begins before removing the frame to avoid other errors server.waitForRequest('/one-style.css').then(() => page.$eval('iframe', frame => window.builtinSetTimeout(() => frame.remove(), 0))) ]); diff --git a/tests/page/page-wait-for-selector-1.spec.ts b/tests/page/page-wait-for-selector-1.spec.ts index 6ee01e29e8..f9fbfaeff4 100644 --- a/tests/page/page-wait-for-selector-1.spec.ts +++ b/tests/page/page-wait-for-selector-1.spec.ts @@ -77,8 +77,9 @@ it('elementHandle.waitForSelector should throw on navigation', async ({ page, se const div = (await page.$('div'))!; const promise = div.waitForSelector('span').catch(e => e); // Give it some time before navigating. - for (let i = 0; i < 10; i++) + for (let i = 0; i < 10; i++) { await page.evaluate(() => 1); + } await page.goto(server.EMPTY_PAGE); const error = await promise; expect(error.message).toContain(`waiting for locator('span') to be visible`); diff --git a/tests/page/pageTest.ts b/tests/page/pageTest.ts index a28984e55a..bfb78a083e 100644 --- a/tests/page/pageTest.ts +++ b/tests/page/pageTest.ts @@ -28,12 +28,15 @@ export { expect } from '@playwright/test'; let impl: TestType = browserTest; export type BoundingBox = Awaited>; -if (process.env.PWPAGE_IMPL === 'android') +if (process.env.PWPAGE_IMPL === 'android') { impl = androidTest; -if (process.env.PWPAGE_IMPL === 'electron') +} +if (process.env.PWPAGE_IMPL === 'electron') { impl = electronTest; -if (process.env.PWPAGE_IMPL === 'webview2') +} +if (process.env.PWPAGE_IMPL === 'webview2') { impl = webView2Test; +} export const test = impl; diff --git a/tests/page/selectors-css.spec.ts b/tests/page/selectors-css.spec.ts index 3c34bd5c55..fb0e98a9c2 100644 --- a/tests/page/selectors-css.spec.ts +++ b/tests/page/selectors-css.spec.ts @@ -58,17 +58,21 @@ it('should work with large DOM @smoke', async ({ page, server }) => { for (const selector of selectors) { const counts1 = []; const time1 = Date.now(); - for (let i = 0; i < (measure ? 10 : 1); i++) + for (let i = 0; i < (measure ? 10 : 1); i++) { counts1.push(await page.$$eval(selector, els => els.length)); - if (measure) + } + if (measure) { console.log('pw: ' + (Date.now() - time1)); + } const time2 = Date.now(); const counts2 = []; - for (let i = 0; i < (measure ? 10 : 1); i++) + for (let i = 0; i < (measure ? 10 : 1); i++) { counts2.push(await page.evaluate(selector => document.querySelectorAll(selector).length, selector)); - if (measure) + } + if (measure) { console.log('qs: ' + (Date.now() - time2)); + } expect(counts1).toEqual(counts2); } @@ -194,8 +198,9 @@ it('should work with attribute selectors', async ({ page }) => { `[attr2 = "hello-''>>foo=bar[]"]`, `[attr2 $="foo=bar[]"]`, ]; - for (const selector of selectors) + for (const selector of selectors) { expect(await page.$eval(selector, e => e === (window as any)['div'])).toBe(true); + } expect(await page.$eval(`[attr*=hello] span`, e => e.parentNode === (window as any)['div'])).toBe(true); expect(await page.$eval(`[attr*=hello] >> span`, e => e.parentNode === (window as any)['div'])).toBe(true); expect(await page.$eval(`[attr3="] span"] >> span`, e => e.parentNode === (window as any)['div'])).toBe(true); diff --git a/tests/page/selectors-register.spec.ts b/tests/page/selectors-register.spec.ts index 354138a134..3c88934a86 100644 --- a/tests/page/selectors-register.spec.ts +++ b/tests/page/selectors-register.spec.ts @@ -23,14 +23,16 @@ it('textContent should be atomic', async ({ playwright, page }) => { const createDummySelector = () => ({ query(root, selector) { const result = root.querySelector(selector); - if (result) + if (result) { void Promise.resolve().then(() => result.textContent = 'modified'); + } return result; }, queryAll(root: HTMLElement, selector: string) { const result = Array.from(root.querySelectorAll(selector)); - for (const e of result) + for (const e of result) { void Promise.resolve().then(() => e.textContent = 'modified'); + } return result; } }); @@ -45,14 +47,16 @@ it('innerText should be atomic', async ({ playwright, page }) => { const createDummySelector = () => ({ query(root: HTMLElement, selector: string) { const result = root.querySelector(selector); - if (result) + if (result) { void Promise.resolve().then(() => result.textContent = 'modified'); + } return result; }, queryAll(root: HTMLElement, selector: string) { const result = Array.from(root.querySelectorAll(selector)); - for (const e of result) + for (const e of result) { void Promise.resolve().then(() => e.textContent = 'modified'); + } return result; } }); @@ -67,14 +71,16 @@ it('innerHTML should be atomic', async ({ playwright, page }) => { const createDummySelector = () => ({ query(root, selector) { const result = root.querySelector(selector); - if (result) + if (result) { void Promise.resolve().then(() => result.textContent = 'modified'); + } return result; }, queryAll(root: HTMLElement, selector: string) { const result = Array.from(root.querySelectorAll(selector)); - for (const e of result) + for (const e of result) { void Promise.resolve().then(() => e.textContent = 'modified'); + } return result; } }); @@ -89,14 +95,16 @@ it('getAttribute should be atomic', async ({ playwright, page }) => { const createDummySelector = () => ({ query(root: HTMLElement, selector: string) { const result = root.querySelector(selector); - if (result) + if (result) { void Promise.resolve().then(() => result.setAttribute('foo', 'modified')); + } return result; }, queryAll(root: HTMLElement, selector: string) { const result = Array.from(root.querySelectorAll(selector)); - for (const e of result) + for (const e of result) { void Promise.resolve().then(() => (e as HTMLElement).setAttribute('foo', 'modified')); + } return result; } }); @@ -111,14 +119,16 @@ it('isVisible should be atomic', async ({ playwright, page }) => { const createDummySelector = () => ({ query(root, selector) { const result = root.querySelector(selector); - if (result) + if (result) { void Promise.resolve().then(() => result.style.display = 'none'); + } return result; }, queryAll(root: HTMLElement, selector: string) { const result = Array.from(root.querySelectorAll(selector)); - for (const e of result) + for (const e of result) { void Promise.resolve().then(() => (e as HTMLElement).style.display = 'none'); + } return result; } }); diff --git a/tests/page/selectors-text.spec.ts b/tests/page/selectors-text.spec.ts index 3793cdac5c..e2e52e870f 100644 --- a/tests/page/selectors-text.spec.ts +++ b/tests/page/selectors-text.spec.ts @@ -309,15 +309,18 @@ it('should work with large DOM', async ({ page }) => { const measure = false; for (const selector of selectors) { const time1 = Date.now(); - for (let i = 0; i < (measure ? 10 : 1); i++) + for (let i = 0; i < (measure ? 10 : 1); i++) { await page.$$eval(selector, els => els.length); - if (measure) + } + if (measure) { console.log(`pw("${selector}"): ` + (Date.now() - time1)); + } if (measure && !selector.includes('text')) { const time2 = Date.now(); - for (let i = 0; i < (measure ? 10 : 1); i++) + for (let i = 0; i < (measure ? 10 : 1); i++) { await page.evaluate(selector => document.querySelectorAll(selector).length, selector); + } console.log(`qs("${selector}"): ` + (Date.now() - time2)); } } diff --git a/tests/page/workers.spec.ts b/tests/page/workers.spec.ts index 3ca56a6866..cf248aff70 100644 --- a/tests/page/workers.spec.ts +++ b/tests/page/workers.spec.ts @@ -75,10 +75,11 @@ it('should have JSHandles for console logs', async function({ page, browserName const logPromise = new Promise(x => page.on('console', x)); await page.evaluate(() => new Worker(URL.createObjectURL(new Blob(['console.log(1,2,3,this)'], { type: 'application/javascript' })))); const log = await logPromise; - if (browserName !== 'firefox') + if (browserName !== 'firefox') { expect(log.text()).toBe('1 2 3 DedicatedWorkerGlobalScope'); - else + } else { expect(log.text()).toBe('1 2 3 JSHandle@object'); + } expect(log.args().length).toBe(4); expect(await (await log.args()[3].getProperty('origin')).jsonValue()).toBe('null'); }); diff --git a/tests/playwright-test/golden.spec.ts b/tests/playwright-test/golden.spec.ts index bf3dbb8880..8e7db176d4 100644 --- a/tests/playwright-test/golden.spec.ts +++ b/tests/playwright-test/golden.spec.ts @@ -1007,8 +1007,9 @@ test('should attach expected/actual/diff', async ({ runInlineTest }, testInfo) = const outputText = result.output; const attachments = outputText.split('\n').filter(l => l.startsWith('## ')).map(l => l.substring(3)).map(l => JSON.parse(l))[0]; - for (const attachment of attachments) + for (const attachment of attachments) { attachment.path = attachment.path.replace(/\\/g, '/').replace(/.*test-results\//, ''); + } expect(attachments).toEqual([ { name: 'snapshot-expected.png', @@ -1048,8 +1049,9 @@ test('should attach expected/actual/diff for different sizes', async ({ runInlin expect(outputText).toContain('Expected an image 2px by 2px, received 1px by 1px.'); expect(outputText).toContain('4 pixels (ratio 1.00 of all image pixels) are different.'); const attachments = outputText.split('\n').filter(l => l.startsWith('## ')).map(l => l.substring(3)).map(l => JSON.parse(l))[0]; - for (const attachment of attachments) + for (const attachment of attachments) { attachment.path = attachment.path.replace(testInfo.outputDir, '').substring(1).replace(/\\/g, '/'); + } expect(attachments).toEqual([ { name: 'snapshot-expected.png', diff --git a/tests/playwright-test/playwright-test-fixtures.ts b/tests/playwright-test/playwright-test-fixtures.ts index 23c26a3e3c..63293a83f5 100644 --- a/tests/playwright-test/playwright-test-fixtures.ts +++ b/tests/playwright-test/playwright-test-fixtures.ts @@ -78,8 +78,9 @@ export async function writeFiles(testInfo: TestInfo, files: Files, initial: bool await Promise.all(Object.keys(files).map(async name => { const fullName = path.join(baseDir, name); - if (files[name] === undefined) + if (files[name] === undefined) { return; + } await fs.promises.mkdir(path.dirname(fullName), { recursive: true }); await fs.promises.writeFile(fullName, files[name]); })); @@ -92,16 +93,18 @@ export const cliEntrypoint = path.join(__dirname, '../../packages/playwright-tes const configFile = (baseDir: string, files: Files): string | undefined => { for (const [name, content] of Object.entries(files)) { if (name.includes('playwright.config')) { - if (content.includes('reporter:') || content.includes('reportSlowTests:')) + if (content.includes('reporter:') || content.includes('reportSlowTests:')) { return path.resolve(baseDir, name); + } } } return undefined; }; function findPackageJSONDir(files: Files, dir: string) { - while (dir && !files[dir + '/package.json']) + while (dir && !files[dir + '/package.json']) { dir = path.dirname(dir); + } return dir; } @@ -123,8 +126,9 @@ function startPlaywrightTest(childProcess: CommonFixtures['childProcess'], baseD '--workers=2', ...paramList ); - if (options.additionalArgs) + if (options.additionalArgs) { args.push(...options.additionalArgs); + } return startPlaywrightChildProcess(childProcess, baseDir, args, env, options); } @@ -155,11 +159,13 @@ async function runPlaywrightTest(childProcess: CommonFixtures['childProcess'], b if (useIntermediateMergeReport) { const additionalArgs = []; - if (reporter) + if (reporter) { additionalArgs.push('--reporter', reporter); + } const config = configFile(baseDir, files); - if (config) + if (config) { additionalArgs.push('--config', config); + } const cwd = options.cwd ? path.resolve(baseDir, options.cwd) : baseDir; const packageRoot = path.resolve(baseDir, findPackageJSONDir(files, options.cwd ?? '')); const relativeBlobReportPath = path.relative(cwd, path.join(packageRoot, 'blob-report')); @@ -179,18 +185,21 @@ async function runPlaywrightTest(childProcess: CommonFixtures['childProcess'], b const results: JSONReportTestResult[] = []; function visitSuites(suites?: JSONReportSuite[]) { - if (!suites) + if (!suites) { return; + } for (const suite of suites) { for (const spec of suite.specs) { - for (const test of spec.tests) + for (const test of spec.tests) { results.push(...test.results); + } } visitSuites(suite.suites); } } - if (report) + if (report) { visitSuites(report.suites); + } return { ...parsed, @@ -335,8 +344,9 @@ export const test = base mergeReports: async ({ childProcess }, use) => { await use(async (reportFolder: string, env: NodeJS.ProcessEnv = {}, options: RunOptions = {}) => { const command = ['node', cliEntrypoint, 'merge-reports', reportFolder]; - if (options.additionalArgs) + if (options.additionalArgs) { command.push(...options.additionalArgs); + } const cwd = options.cwd ? path.resolve(test.info().outputDir, options.cwd) : test.info().outputDir; const testProcess = childProcess({ @@ -408,8 +418,9 @@ export function createWhiteImage(width: number, height: number) { export function paintBlackPixels(image: Buffer, blackPixelsCount: number): Buffer { const png = PNG.sync.read(image); for (let i = 0; i < blackPixelsCount; ++i) { - for (let j = 0; j < 3; ++j) + for (let j = 0; j < 3; ++j) { png.data[i * 4 + j] = 0; + } } return PNG.sync.write(png); } @@ -417,8 +428,9 @@ export function paintBlackPixels(image: Buffer, blackPixelsCount: number): Buffe function filterTests(result: RunResult, filter: (spec: JSONReportSpec) => boolean) { const tests: JSONReportTest[] = []; const visit = (suite: JSONReportSuite) => { - for (const spec of suite.specs) + for (const spec of suite.specs) { spec.tests.forEach(t => filter(spec) && tests.push(t)); + } suite.suites?.forEach(s => visit(s)); }; visit(result.report.suites[0]); diff --git a/tests/playwright-test/playwright.artifacts.spec.ts b/tests/playwright-test/playwright.artifacts.spec.ts index 2e3d99766b..4f6616b217 100644 --- a/tests/playwright-test/playwright.artifacts.spec.ts +++ b/tests/playwright-test/playwright.artifacts.spec.ts @@ -23,8 +23,9 @@ function listFiles(dir: string): string[] { const entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name)); for (const entry of entries) { result.push(entry.name); - if (entry.isDirectory()) + if (entry.isDirectory()) { result.push(...listFiles(path.join(dir, entry.name)).map(x => ' ' + x)); + } } return result; } diff --git a/tests/playwright-test/playwright.ct-build.spec.ts b/tests/playwright-test/playwright.ct-build.spec.ts index a2a021b642..b9459b2535 100644 --- a/tests/playwright-test/playwright.ct-build.spec.ts +++ b/tests/playwright-test/playwright.ct-build.spec.ts @@ -191,8 +191,9 @@ test('should extract component list', async ({ runInlineTest }, testInfo) => { filename: expect.stringContaining(`two${path.sep}two.spec.tsx`), }]); - for (const [, value] of Object.entries(metainfo.deps)) + for (const [, value] of Object.entries(metainfo.deps)) { (value as string[]).sort(); + } expect(Object.entries(metainfo.deps)).toEqual([ [expect.stringContaining('clashingNames1.tsx'), [ @@ -496,8 +497,9 @@ test('should retain deps when test changes', async ({ runInlineTest }, testInfo) filename: expect.stringContaining('button.test.tsx'), }]); - for (const [, value] of Object.entries(metainfo.deps)) + for (const [, value] of Object.entries(metainfo.deps)) { (value as string[]).sort(); + } expect(Object.entries(metainfo.deps)).toEqual([ [ diff --git a/tests/playwright-test/reporter-blob.spec.ts b/tests/playwright-test/reporter-blob.spec.ts index 91b32e76a2..37d87823c4 100644 --- a/tests/playwright-test/reporter-blob.spec.ts +++ b/tests/playwright-test/reporter-blob.spec.ts @@ -204,8 +204,9 @@ test('should merge into html with dependencies', async ({ runInlineTest, mergeRe ` }; const totalShards = 3; - for (let i = 0; i < totalShards; i++) + for (let i = 0; i < totalShards; i++) { await runInlineTest(files, { shard: `${i + 1}/${totalShards}` }, { PWTEST_BLOB_DO_NOT_REMOVE: '1' }); + } const reportFiles = await fs.promises.readdir(reportDir); reportFiles.sort(); expect(reportFiles).toEqual([expect.stringMatching(/report-.*.zip/), expect.stringMatching(/report-.*.zip/), expect.stringMatching(/report-.*.zip/)]); @@ -468,8 +469,9 @@ test('merge into list report by default', async ({ runInlineTest, mergeReports } }; const totalShards = 3; - for (let i = 0; i < totalShards; i++) + for (let i = 0; i < totalShards; i++) { await runInlineTest(files, { shard: `${i + 1}/${totalShards}` }, { PWTEST_BLOB_DO_NOT_REMOVE: '1' }); + } const reportFiles = await fs.promises.readdir(reportDir); reportFiles.sort(); expect(reportFiles).toEqual(['report-1.zip', 'report-2.zip', 'report-3.zip']); @@ -678,8 +680,9 @@ test('generate html with attachment urls', async ({ runInlineTest, mergeReports, expect(exitCode).toBe(0); const htmlReportDir = test.info().outputPath('playwright-report'); - for (const entry of await fs.promises.readdir(htmlReportDir)) + for (const entry of await fs.promises.readdir(htmlReportDir)) { await fs.promises.cp(path.join(htmlReportDir, entry), path.join(reportDir, entry), { recursive: true }); + } const oldServeFile = server.serveFile; server.serveFile = async (req, res) => { @@ -1695,11 +1698,13 @@ function patchPathSeparators(json: any) { const to = (path.sep === '/') ? '\\' : '/'; function patchPathSeparatorsRecursive(obj: any) { - if (typeof obj !== 'object') + if (typeof obj !== 'object') { return; + } for (const key in obj) { - if (/file|dir|path|^title$/i.test(key) && typeof obj[key] === 'string') + if (/file|dir|path|^title$/i.test(key) && typeof obj[key] === 'string') { obj[key] = obj[key].replace(from, to); + } patchPathSeparatorsRecursive(obj[key]); } } diff --git a/tests/playwright-test/reporter-github.spec.ts b/tests/playwright-test/reporter-github.spec.ts index 100feb157f..9d6f868801 100644 --- a/tests/playwright-test/reporter-github.spec.ts +++ b/tests/playwright-test/reporter-github.spec.ts @@ -18,8 +18,9 @@ import { test, expect } from './playwright-test-fixtures'; import path from 'path'; function relativeFilePath(file: string): string { - if (!path.isAbsolute(file)) + if (!path.isAbsolute(file)) { return file; + } return path.relative(process.cwd(), file); } diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index 5568455d63..e19348cfbf 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -1104,8 +1104,9 @@ for (const useIntermediateMergeReport of [true, false] as const) { const execGit = async (args: string[]) => { const { code, stdout, stderr } = await spawnAsync('git', args, { stdio: 'pipe', cwd: baseDir }); - if (!!code) + if (!!code) { throw new Error(`Non-zero exit of:\n$ git ${args.join(' ')}\nConsole:\nstdout:\n${stdout}\n\nstderr:\n${stderr}\n\n`); + } return; }; @@ -1746,11 +1747,13 @@ for (const useIntermediateMergeReport of [true, false] as const) { let total = 0; for (const suite of result.report.suites) { for (const spec of suite.specs) { - if (!testNames.includes(spec.title)) + if (!testNames.includes(spec.title)) { continue; + } for (const test of spec.tests) { - for (const result of test.results) + for (const result of test.results) { total += result.duration; + } } } } diff --git a/tests/playwright-test/reporter-list.spec.ts b/tests/playwright-test/reporter-list.spec.ts index 752d29c649..107cb9226c 100644 --- a/tests/playwright-test/reporter-list.spec.ts +++ b/tests/playwright-test/reporter-list.spec.ts @@ -167,10 +167,11 @@ for (const useIntermediateMergeReport of [false, true] as const) { }, { reporter: 'list' }, { PLAYWRIGHT_FORCE_TTY: TTY_WIDTH + '' }); const renderedText = simpleAnsiRenderer(result.rawOutput, TTY_WIDTH); - if (process.platform === 'win32') + if (process.platform === 'win32') { expect(renderedText).toContain(' ok 1 a.test.ts:3:15 › passes'); - else + } else { expect(renderedText).toContain(' ✓ 1 a.test.ts:3:15 › passes'); + } expect(renderedText).not.toContain(' 1 a.test.ts:3:15 › passes'); expect(renderedText).toContain('a'.repeat(80) + '\n' + 'b'.repeat(20)); }); @@ -266,10 +267,12 @@ function simpleAnsiRenderer(text, ttyWidth) { let columnNumber = 0; const screenLines: string[][] = []; const ensureScreenSize = () => { - if (lineNumber < 0) + if (lineNumber < 0) { throw new Error('Bad terminal navigation!'); - while (lineNumber >= screenLines.length) + } + while (lineNumber >= screenLines.length) { screenLines.push(new Array(ttyWidth).fill('')); + } }; const print = ch => { ensureScreenSize(); @@ -292,8 +295,9 @@ function simpleAnsiRenderer(text, ttyWidth) { for (const ansiCode of ansiCodes) { const [matchText, codeValue, codeType] = ansiCode; const code = (codeValue + codeType).toUpperCase(); - while (index < ansiCode.index) + while (index < ansiCode.index) { print(text[index++]); + } if (codeType.toUpperCase() === 'E') { // Go X lines down lineNumber += +codeValue; @@ -314,8 +318,9 @@ function simpleAnsiRenderer(text, ttyWidth) { } index += matchText.length; } - while (index < text.length) + while (index < text.length) { print(text[index++]); + } return screenLines.map(line => line.join('')).join('\n'); } diff --git a/tests/playwright-test/runner.spec.ts b/tests/playwright-test/runner.spec.ts index dc88187229..8be30c2e12 100644 --- a/tests/playwright-test/runner.spec.ts +++ b/tests/playwright-test/runner.spec.ts @@ -140,8 +140,9 @@ test('should ignore subprocess creation error because of SIGINT', async ({ inter ` }); - while (!fs.existsSync(readyFile)) + while (!fs.existsSync(readyFile)) { await new Promise(f => setTimeout(f, 100)); + } process.kill(-testProcess.process.pid!, 'SIGINT'); const { exitCode } = await testProcess.exited; diff --git a/tests/playwright-test/snapshot-path-template.spec.ts b/tests/playwright-test/snapshot-path-template.spec.ts index 4f260469b7..b7e83a89c7 100644 --- a/tests/playwright-test/snapshot-path-template.spec.ts +++ b/tests/playwright-test/snapshot-path-template.spec.ts @@ -42,8 +42,9 @@ async function getSnapshotPaths(runInlineTest, testInfo, playwrightConfig, pathA expect(result.exitCode).toBe(0); const allSegments = result.output.split(SEPARATOR); const projToSnapshot = {}; - for (let i = 1; i < allSegments.length; i += 3) + for (let i = 1; i < allSegments.length; i += 3) { projToSnapshot[allSegments[i]] = path.relative(testInfo.outputDir, allSegments[i + 1]); + } return projToSnapshot; } diff --git a/tests/playwright-test/to-have-screenshot.spec.ts b/tests/playwright-test/to-have-screenshot.spec.ts index 83642bd19e..3c914deaa1 100644 --- a/tests/playwright-test/to-have-screenshot.spec.ts +++ b/tests/playwright-test/to-have-screenshot.spec.ts @@ -655,8 +655,9 @@ test('should write missing expectations locally twice and attach them', async ({ expect(stackLines.length).toBe(0); const attachments = result.outputLines.map(l => JSON.parse(l))[0]; - for (const attachment of attachments) + for (const attachment of attachments) { attachment.path = attachment.path.replace(/\\/g, '/').replace(/.*test-results\//, '').replace(/.*__screenshots__/, '__screenshots__'); + } expect(attachments).toEqual([ { name: 'snapshot-expected.png', @@ -1097,8 +1098,9 @@ test('should attach expected/actual/diff when sizes are different', async ({ run expect(outputText).toContain('Expected an image 2px by 2px, received 1280px by 720px.'); expect(outputText).toContain('4 pixels (ratio 0.01 of all image pixels) are different.'); const attachments = outputText.split('\n').filter(l => l.startsWith('## ')).map(l => l.substring(3)).map(l => JSON.parse(l))[0]; - for (const attachment of attachments) + for (const attachment of attachments) { attachment.path = attachment.path.replace(testInfo.outputDir, '').substring(1).replace(/\\/g, '/'); + } expect(attachments).toEqual([ { name: 'snapshot-expected.png', diff --git a/tests/playwright-test/ui-mode-fixtures.ts b/tests/playwright-test/ui-mode-fixtures.ts index 43089aa1d0..00ada214e4 100644 --- a/tests/playwright-test/ui-mode-fixtures.ts +++ b/tests/playwright-test/ui-mode-fixtures.ts @@ -43,26 +43,36 @@ export function dumpTestTree(page: Page, options: { time?: boolean } = {}): () = return () => page.getByTestId('test-tree').evaluate(async (treeElement, options) => { function iconName(iconElement: Element): string { const icon = iconElement.className.replace('codicon codicon-', ''); - if (icon === 'chevron-right') + if (icon === 'chevron-right') { return '►'; - if (icon === 'chevron-down') + } + if (icon === 'chevron-down') { return '▼'; - if (icon === 'blank') + } + if (icon === 'blank') { return ' '; - if (icon === 'circle-outline') + } + if (icon === 'circle-outline') { return '◯'; - if (icon === 'circle-slash') + } + if (icon === 'circle-slash') { return '⊘'; - if (icon === 'check') + } + if (icon === 'check') { return '✅'; - if (icon === 'error') + } + if (icon === 'error') { return '❌'; - if (icon === 'eye') + } + if (icon === 'eye') { return '👁'; - if (icon === 'loading') + } + if (icon === 'loading') { return '↻'; - if (icon === 'clock') + } + if (icon === 'clock') { return '🕦'; + } return icon; } @@ -88,8 +98,9 @@ export function dumpTestTree(page: Page, options: { time?: boolean } = {}): () = export const test = base .extend({ runUITest: async ({ childProcess, headless }, use, testInfo: TestInfo) => { - if (process.env.CI) + if (process.env.CI) { testInfo.slow(); + } const cacheDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'playwright-test-cache-')); let testProcess: TestChildProcess | undefined; let browser: Browser | undefined; @@ -148,6 +159,7 @@ export const retries = process.env.CI ? 3 : 0; async function waitForLatch(latchFile: string) { const fs = require('fs'); - while (!fs.existsSync(latchFile)) + while (!fs.existsSync(latchFile)) { await new Promise(f => setTimeout(f, 250)); + } } diff --git a/tests/playwright-test/web-server.spec.ts b/tests/playwright-test/web-server.spec.ts index 04e3c1d328..9b02ca2f41 100644 --- a/tests/playwright-test/web-server.spec.ts +++ b/tests/playwright-test/web-server.spec.ts @@ -438,8 +438,9 @@ test(`should support self signed certificate`, async ({ runInlineTest, httpsServ test('should send Accept header', async ({ runInlineTest, server }) => { let acceptHeader: string | undefined | null = null; server.setRoute('/hello', (req, res) => { - if (acceptHeader === null) + if (acceptHeader === null) { acceptHeader = req.headers.accept; + } res.end('hello'); }); const result = await runInlineTest({ diff --git a/tests/stress/frames.spec.ts b/tests/stress/frames.spec.ts index a438ec689f..37fec22a42 100644 --- a/tests/stress/frames.spec.ts +++ b/tests/stress/frames.spec.ts @@ -30,8 +30,9 @@ test('cycle frames', async ({ page, server }) => { page.on('frameattached', async () => { // Make sure we can access page. await page.title(); - if (++counter === kFrameCount) + if (++counter === kFrameCount) { cb(); + } }); page.evaluate(async ({ url, count }) => { diff --git a/tests/stress/heap.spec.ts b/tests/stress/heap.spec.ts index 66bedfe9ff..535326ef26 100644 --- a/tests/stress/heap.spec.ts +++ b/tests/stress/heap.spec.ts @@ -68,8 +68,9 @@ test('should not leak dispatchers after closing page', async ({ context, server expect(await queryObjectCount(require('../../packages/playwright-core/lib/server/dispatchers/networkDispatchers').ResponseDispatcher)).toBe(COUNT); expect(await queryObjectCount(require('../../packages/playwright-core/lib/server/console').ConsoleMessage)).toBe(0); - for (const page of pages) + for (const page of pages) { await page.close(); + } pages.length = 0; expect(await queryObjectCount(require('../../packages/playwright-core/lib/server/page').Page)).toBe(0); diff --git a/tests/webview2/globalSetup.ts b/tests/webview2/globalSetup.ts index 030a10401e..feec9e7cbe 100644 --- a/tests/webview2/globalSetup.ts +++ b/tests/webview2/globalSetup.ts @@ -27,8 +27,9 @@ export default async () => { } }); await new Promise(resolve => spawnedProcess.stdout.on('data', (data: Buffer): void => { - if (data.toString().includes('WebView2 initialized')) + if (data.toString().includes('WebView2 initialized')) { resolve(); + } })); const browser = await playwright.chromium.connectOverCDP(`http://127.0.0.1:${cdpPort}`); console.log(`Using version ${browser.version()} WebView2 runtime`); diff --git a/tests/webview2/webView2Test.ts b/tests/webview2/webView2Test.ts index a72fc9f2b6..0ba0e3559a 100644 --- a/tests/webview2/webView2Test.ts +++ b/tests/webview2/webView2Test.ts @@ -46,8 +46,9 @@ export const webView2Test = baseTest.extend(traceViewerFixt } }); await new Promise(resolve => spawnedProcess.process.stdout.on('data', data => { - if (data.toString().includes('WebView2 initialized')) + if (data.toString().includes('WebView2 initialized')) { resolve(); + } })); const browser = await playwright.chromium.connectOverCDP(`http://127.0.0.1:${cdpPort}`); await use(browser);