diff --git a/README.md b/README.md index 8f7324fd028ac..10185f2729996 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 🎭 Playwright -[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) [![Chromium version](https://img.shields.io/badge/chromium-139.0.7258.42-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-140.0.2-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-26.0-blue.svg?logo=safari)](https://webkit.org/) [![Join Discord](https://img.shields.io/badge/join-discord-informational)](https://aka.ms/playwright/discord) +[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) [![Chromium version](https://img.shields.io/badge/chromium-139.0.7258.42-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-141.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-26.0-blue.svg?logo=safari)](https://webkit.org/) [![Join Discord](https://img.shields.io/badge/join-discord-informational)](https://aka.ms/playwright/discord) ## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright) @@ -10,7 +10,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr | :--- | :---: | :---: | :---: | | Chromium 139.0.7258.42 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | WebKit 26.0 | :white_check_mark: | :white_check_mark: | :white_check_mark: | -| Firefox 140.0.2 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Firefox 141.0 | :white_check_mark: | :white_check_mark: | :white_check_mark: | Headless execution is supported for all browsers on all platforms. Check out [system requirements](https://playwright.dev/docs/intro#system-requirements) for details. diff --git a/docs/src/browsers.md b/docs/src/browsers.md index c2c8449171cf0..aafa274b5b725 100644 --- a/docs/src/browsers.md +++ b/docs/src/browsers.md @@ -232,7 +232,7 @@ Running 1 test using 1 worker With the VS Code extension you can run your tests on different browsers by checking the checkbox next to the browser name in the Playwright sidebar. These names are defined in your Playwright config file under the projects section. The default config when installing Playwright gives you 3 projects, Chromium, Firefox and WebKit. The first project is selected by default. -![Projects section in VS Code extension](https://github.com/microsoft/playwright/assets/13063165/58fedea6-a2b9-4942-b2c7-2f3d482210cf) +![Projects section in VS Code extension](./images/vscode-projects-section.png) To run tests on multiple projects(browsers), select each project by checking the checkboxes next to the project name. diff --git a/docs/src/images/vscode-projects-section.png b/docs/src/images/vscode-projects-section.png new file mode 100644 index 0000000000000..d304dd04f9d3c Binary files /dev/null and b/docs/src/images/vscode-projects-section.png differ diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index ec4ccb5ab2439..d04f9eb2b625d 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -15,27 +15,27 @@ }, { "name": "chromium-tip-of-tree", - "revision": "1353", + "revision": "1354", "installByDefault": false, - "browserVersion": "140.0.7315.0" + "browserVersion": "140.0.7325.0" }, { "name": "chromium-tip-of-tree-headless-shell", - "revision": "1353", + "revision": "1354", "installByDefault": false, - "browserVersion": "140.0.7315.0" + "browserVersion": "140.0.7325.0" }, { "name": "firefox", - "revision": "1489", + "revision": "1490", "installByDefault": true, - "browserVersion": "140.0.2" + "browserVersion": "141.0" }, { "name": "firefox-beta", - "revision": "1484", + "revision": "1485", "installByDefault": false, - "browserVersion": "140.0b7" + "browserVersion": "142.0b4" }, { "name": "webkit", diff --git a/packages/playwright-core/src/server/chromium/crDragDrop.ts b/packages/playwright-core/src/server/chromium/crDragDrop.ts index b54fe50f3f707..893968917a4c0 100644 --- a/packages/playwright-core/src/server/chromium/crDragDrop.ts +++ b/packages/playwright-core/src/server/chromium/crDragDrop.ts @@ -91,26 +91,26 @@ export class DragManager { }; } - await this._crPage._page.safeNonStallingEvaluateInAllFrames(`(${setupDragListeners.toString()})()`, 'utility'); - progress.cleanupWhenAborted(() => this._crPage._page.safeNonStallingEvaluateInAllFrames('window.__cleanupDrag && window.__cleanupDrag()', 'utility')); - - client.on('Input.dragIntercepted', onDragIntercepted!); try { + let expectingDrag = false; + await progress.race(this._crPage._page.safeNonStallingEvaluateInAllFrames(`(${setupDragListeners.toString()})()`, 'utility')); + client.on('Input.dragIntercepted', onDragIntercepted!); await client.send('Input.setInterceptDrags', { enabled: true }); - } catch { - // If Input.setInterceptDrags is not supported, just do a regular move. - // This can be removed once we stop supporting old Electron. - client.off('Input.dragIntercepted', onDragIntercepted!); - return moveCallback(); + try { + await progress.race(moveCallback()); + expectingDrag = (await Promise.all(this._crPage._page.frames().map(async frame => { + return frame.nonStallingEvaluateInExistingContext('window.__cleanupDrag?.()', 'utility').catch(() => false); + }))).some(x => x); + } finally { + client.off('Input.dragIntercepted', onDragIntercepted!); + await client.send('Input.setInterceptDrags', { enabled: false }); + } + this._dragState = expectingDrag ? (await dragInterceptedPromise).data : null; + } catch (error) { + // Cleanup without blocking, it will be done before the next playwright action. + this._crPage._page.safeNonStallingEvaluateInAllFrames('window.__cleanupDrag?.()', 'utility').catch(() => {}); + throw error; } - await moveCallback(); - - const expectingDrag = (await Promise.all(this._crPage._page.frames().map(async frame => { - return frame.nonStallingEvaluateInExistingContext('window.__cleanupDrag && window.__cleanupDrag()', 'utility').catch(() => false); - }))).some(x => x); - this._dragState = expectingDrag ? (await dragInterceptedPromise).data : null; - client.off('Input.dragIntercepted', onDragIntercepted!); - await progress.race(client.send('Input.setInterceptDrags', { enabled: false })); if (this._dragState) { await progress.race(this._crPage._mainFrameSession._client.send('Input.dispatchDragEvent', { diff --git a/packages/playwright-core/src/server/deviceDescriptorsSource.json b/packages/playwright-core/src/server/deviceDescriptorsSource.json index 358e7727658b0..eacfd364f4eaa 100644 --- a/packages/playwright-core/src/server/deviceDescriptorsSource.json +++ b/packages/playwright-core/src/server/deviceDescriptorsSource.json @@ -1702,7 +1702,7 @@ "defaultBrowserType": "chromium" }, "Desktop Firefox HiDPI": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0.2) Gecko/20100101 Firefox/140.0.2", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:141.0) Gecko/20100101 Firefox/141.0", "screen": { "width": 1792, "height": 1120 @@ -1762,7 +1762,7 @@ "defaultBrowserType": "chromium" }, "Desktop Firefox": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0.2) Gecko/20100101 Firefox/140.0.2", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:141.0) Gecko/20100101 Firefox/141.0", "screen": { "width": 1920, "height": 1080 diff --git a/packages/playwright-core/src/server/electron/electron.ts b/packages/playwright-core/src/server/electron/electron.ts index d88d61645c14e..efe830935c5a4 100644 --- a/packages/playwright-core/src/server/electron/electron.ts +++ b/packages/playwright-core/src/server/electron/electron.ts @@ -19,7 +19,7 @@ import os from 'os'; import path from 'path'; import * as readline from 'readline'; -import { ManualPromise, removeFolders } from '../../utils'; +import { ManualPromise } from '../../utils'; import { wrapInASCIIBox } from '../utils/ascii'; import { RecentLogsCollector } from '../utils/debugLogger'; import { eventsHelper } from '../utils/eventsHelper'; @@ -166,8 +166,6 @@ export class Electron extends SdkObject { } const artifactsDir = await progress.race(fs.promises.mkdtemp(ARTIFACTS_FOLDER)); - progress.cleanupWhenAborted(() => removeFolders([artifactsDir])); - const browserLogsCollector = new RecentLogsCollector(); const env = options.env ? envArrayToObject(options.env) : process.env; @@ -243,76 +241,76 @@ export class Electron extends SdkObject { const chromeMatchPromise = waitForLine(progress, launchedProcess, /^DevTools listening on (ws:\/\/.*)$/); const debuggerDisconnectPromise = waitForLine(progress, launchedProcess, /Waiting for the debugger to disconnect\.\.\./); - const nodeMatch = await nodeMatchPromise; - const nodeTransport = await WebSocketTransport.connect(progress, nodeMatch[1]); - progress.cleanupWhenAborted(() => nodeTransport.close()); - const nodeConnection = new CRConnection(this, nodeTransport, helper.debugProtocolLogger(), browserLogsCollector); + try { + const nodeMatch = await nodeMatchPromise; + const nodeTransport = await WebSocketTransport.connect(progress, nodeMatch[1]); + const nodeConnection = new CRConnection(this, nodeTransport, helper.debugProtocolLogger(), browserLogsCollector); + // Immediately release exiting process under debug. + debuggerDisconnectPromise.then(() => { + nodeTransport.close(); + }).catch(() => {}); - // Immediately release exiting process under debug. - debuggerDisconnectPromise.then(() => { - nodeTransport.close(); - }).catch(() => {}); - const chromeMatch = await Promise.race([ - chromeMatchPromise, - waitForXserverError, - ]) as RegExpMatchArray; - const chromeTransport = await WebSocketTransport.connect(progress, chromeMatch[1]); - progress.cleanupWhenAborted(() => chromeTransport.close()); - const browserProcess: BrowserProcess = { - onclose: undefined, - process: launchedProcess, - close: gracefullyClose, - kill - }; - const contextOptions: types.BrowserContextOptions = { - ...options, - noDefaultViewport: true, - }; - const browserOptions: BrowserOptions = { - name: 'electron', - isChromium: true, - headful: true, - persistent: contextOptions, - browserProcess, - protocolLogger: helper.debugProtocolLogger(), - browserLogsCollector, - artifactsDir, - downloadsPath: artifactsDir, - tracesDir: options.tracesDir || artifactsDir, - originalLaunchOptions: {}, - }; - validateBrowserContextOptions(contextOptions, browserOptions); - const browser = await progress.race(CRBrowser.connect(this.attribution.playwright, chromeTransport, browserOptions)); - app = new ElectronApplication(this, browser, nodeConnection, launchedProcess); - await progress.race(app.initialize()); - return app; + const chromeMatch = await Promise.race([ + chromeMatchPromise, + waitForXserverError, + ]); + const chromeTransport = await WebSocketTransport.connect(progress, chromeMatch[1]); + const browserProcess: BrowserProcess = { + onclose: undefined, + process: launchedProcess, + close: gracefullyClose, + kill + }; + const contextOptions: types.BrowserContextOptions = { + ...options, + noDefaultViewport: true, + }; + const browserOptions: BrowserOptions = { + name: 'electron', + isChromium: true, + headful: true, + persistent: contextOptions, + browserProcess, + protocolLogger: helper.debugProtocolLogger(), + browserLogsCollector, + artifactsDir, + downloadsPath: artifactsDir, + tracesDir: options.tracesDir || artifactsDir, + originalLaunchOptions: {}, + }; + validateBrowserContextOptions(contextOptions, browserOptions); + const browser = await progress.race(CRBrowser.connect(this.attribution.playwright, chromeTransport, browserOptions)); + app = new ElectronApplication(this, browser, nodeConnection, launchedProcess); + await progress.race(app.initialize()); + return app; + } catch (error) { + await kill(); + throw error; + } } } -function waitForLine(progress: Progress, process: childProcess.ChildProcess, regex: RegExp): Promise { - return progress.race(new Promise((resolve, reject) => { - const rl = readline.createInterface({ input: process.stderr! }); - const failError = new Error('Process failed to launch!'); - const listeners = [ - eventsHelper.addEventListener(rl, 'line', onLine), - eventsHelper.addEventListener(rl, 'close', reject.bind(null, failError)), - eventsHelper.addEventListener(process, 'exit', reject.bind(null, failError)), - // It is Ok to remove error handler because we did not create process and there is another listener. - eventsHelper.addEventListener(process, 'error', reject.bind(null, failError)) - ]; +async function waitForLine(progress: Progress, process: childProcess.ChildProcess, regex: RegExp) { + const promise = new ManualPromise(); + const rl = readline.createInterface({ input: process.stderr! }); + const failError = new Error('Process failed to launch!'); + const listeners = [ + eventsHelper.addEventListener(rl, 'line', onLine), + eventsHelper.addEventListener(rl, 'close', () => promise.reject(failError)), + eventsHelper.addEventListener(process, 'exit', () => promise.reject(failError)), + // It is Ok to remove error handler because we did not create process and there is another listener. + eventsHelper.addEventListener(process, 'error', () => promise.reject(failError)), + ]; - progress.cleanupWhenAborted(cleanup); - - function onLine(line: string) { - const match = line.match(regex); - if (!match) - return; - cleanup(); - resolve(match); - } + function onLine(line: string) { + const match = line.match(regex); + if (match) + promise.resolve(match); + } - function cleanup() { - eventsHelper.removeEventListeners(listeners); - } - })); + try { + return await progress.race(promise); + } finally { + eventsHelper.removeEventListeners(listeners); + } } diff --git a/packages/playwright-core/src/server/fetch.ts b/packages/playwright-core/src/server/fetch.ts index af6f4db0dbdf8..b1ff775a1fdfc 100644 --- a/packages/playwright-core/src/server/fetch.ts +++ b/packages/playwright-core/src/server/fetch.ts @@ -298,6 +298,7 @@ export abstract class APIRequestContext extends SdkObject { }; this.emit(APIRequestContext.Events.Request, requestEvent); + let destroyRequest: (() => void) | undefined; const resultPromise = new Promise((fulfill, reject) => { const requestConstructor: ((url: URL, options: http.RequestOptions, callback?: (res: http.IncomingMessage) => void) => http.ClientRequest) = (url.protocol === 'https:' ? https : http).request; @@ -483,7 +484,7 @@ export abstract class APIRequestContext extends SdkObject { body.on('end', notifyBodyFinished); }); request.on('error', reject); - progress.cleanupWhenAborted(() => request.destroy()); + destroyRequest = () => request.destroy(); listeners.push( eventsHelper.addEventListener(this, APIRequestContext.Events.Dispose, () => { @@ -539,7 +540,11 @@ export abstract class APIRequestContext extends SdkObject { request.write(postData); request.end(); }); - return progress.race(resultPromise); + + return progress.race(resultPromise).catch(error => { + destroyRequest?.(); + throw error; + }); } private _getHttpCredentials(url: URL) { diff --git a/packages/playwright-core/src/server/helper.ts b/packages/playwright-core/src/server/helper.ts index fb6622846ca99..e1e9e146815f7 100644 --- a/packages/playwright-core/src/server/helper.ts +++ b/packages/playwright-core/src/server/helper.ts @@ -57,22 +57,19 @@ class Helper { static waitForEvent(progress: Progress, emitter: EventEmitter, event: string | symbol, predicate?: Function): { promise: Promise, dispose: () => void } { const listeners: RegisteredListener[] = []; - const promise = new Promise((resolve, reject) => { + const dispose = () => eventsHelper.removeEventListeners(listeners); + const promise = progress.race(new Promise((resolve, reject) => { listeners.push(eventsHelper.addEventListener(emitter, event, eventArg => { try { if (predicate && !predicate(eventArg)) return; - eventsHelper.removeEventListeners(listeners); resolve(eventArg); } catch (e) { - eventsHelper.removeEventListeners(listeners); reject(e); } })); - }); - const dispose = () => eventsHelper.removeEventListeners(listeners); - progress.cleanupWhenAborted(dispose); - return { promise: progress.race(promise), dispose }; + })).finally(() => dispose()); + return { promise, dispose }; } static secondsToRoundishMillis(value: number): number { diff --git a/packages/playwright-core/src/server/localUtils.ts b/packages/playwright-core/src/server/localUtils.ts index 47c93362b63a6..5fb9b57d41fb0 100644 --- a/packages/playwright-core/src/server/localUtils.ts +++ b/packages/playwright-core/src/server/localUtils.ts @@ -179,16 +179,18 @@ export function harClose(harBackends: Map, params: channels. export async function harUnzip(progress: Progress, params: channels.LocalUtilsHarUnzipParams): Promise { const dir = path.dirname(params.zipFile); const zipFile = new ZipFile(params.zipFile); - progress.cleanupWhenAborted(() => zipFile.close()); - for (const entry of await progress.race(zipFile.entries())) { - const buffer = await progress.race(zipFile.read(entry)); - if (entry === 'har.har') - await progress.race(fs.promises.writeFile(params.harFile, buffer)); - else - await progress.race(fs.promises.writeFile(path.join(dir, entry), buffer)); + try { + for (const entry of await progress.race(zipFile.entries())) { + const buffer = await progress.race(zipFile.read(entry)); + if (entry === 'har.har') + await progress.race(fs.promises.writeFile(params.harFile, buffer)); + else + await progress.race(fs.promises.writeFile(path.join(dir, entry), buffer)); + } + await progress.race(fs.promises.unlink(params.zipFile)); + } finally { + zipFile.close(); } - await progress.race(fs.promises.unlink(params.zipFile)); - zipFile.close(); } export async function tracingStarted(progress: Progress, stackSessions: Map, params: channels.LocalUtilsTracingStartedParams): Promise { diff --git a/packages/playwright-core/src/server/screenshotter.ts b/packages/playwright-core/src/server/screenshotter.ts index ea8b09e4864a1..93765cc3372f5 100644 --- a/packages/playwright-core/src/server/screenshotter.ts +++ b/packages/playwright-core/src/server/screenshotter.ts @@ -206,22 +206,20 @@ export class Screenshotter { progress.log('taking page screenshot'); const viewportSize = await this._originalViewportSize(progress); await this._preparePageForScreenshot(progress, this._page.mainFrame(), options.style, options.caret !== 'initial', options.animations === 'disabled'); - - if (options.fullPage) { - const fullPageSize = await this._fullPageSize(progress); - let documentRect = { x: 0, y: 0, width: fullPageSize.width, height: fullPageSize.height }; - const fitsViewport = fullPageSize.width <= viewportSize.width && fullPageSize.height <= viewportSize.height; - if (options.clip) - documentRect = trimClipToSize(options.clip, documentRect); - const buffer = await this._screenshot(progress, format, documentRect, undefined, fitsViewport, options); + try { + if (options.fullPage) { + const fullPageSize = await this._fullPageSize(progress); + let documentRect = { x: 0, y: 0, width: fullPageSize.width, height: fullPageSize.height }; + const fitsViewport = fullPageSize.width <= viewportSize.width && fullPageSize.height <= viewportSize.height; + if (options.clip) + documentRect = trimClipToSize(options.clip, documentRect); + return await this._screenshot(progress, format, documentRect, undefined, fitsViewport, options); + } + const viewportRect = options.clip ? trimClipToSize(options.clip, viewportSize) : { x: 0, y: 0, ...viewportSize }; + return await this._screenshot(progress, format, undefined, viewportRect, true, options); + } finally { await this._restorePageAfterScreenshot(); - return buffer; } - - const viewportRect = options.clip ? trimClipToSize(options.clip, viewportSize) : { x: 0, y: 0, ...viewportSize }; - const buffer = await this._screenshot(progress, format, undefined, viewportRect, true, options); - await this._restorePageAfterScreenshot(); - return buffer; }); } @@ -232,21 +230,23 @@ export class Screenshotter { const viewportSize = await this._originalViewportSize(progress); await this._preparePageForScreenshot(progress, handle._frame, options.style, options.caret !== 'initial', options.animations === 'disabled'); - await handle._waitAndScrollIntoViewIfNeeded(progress, true /* waitForVisible */); - - const boundingBox = await progress.race(handle.boundingBox()); - assert(boundingBox, 'Node is either not visible or not an HTMLElement'); - assert(boundingBox.width !== 0, 'Node has 0 width.'); - assert(boundingBox.height !== 0, 'Node has 0 height.'); - - const fitsViewport = boundingBox.width <= viewportSize.width && boundingBox.height <= viewportSize.height; - const scrollOffset = await this._page.mainFrame().waitForFunctionValueInUtility(progress, () => ({ x: window.scrollX, y: window.scrollY })); - const documentRect = { ...boundingBox }; - documentRect.x += scrollOffset.x; - documentRect.y += scrollOffset.y; - const buffer = await this._screenshot(progress, format, helper.enclosingIntRect(documentRect), undefined, fitsViewport, options); - await this._restorePageAfterScreenshot(); - return buffer; + try { + await handle._waitAndScrollIntoViewIfNeeded(progress, true /* waitForVisible */); + + const boundingBox = await progress.race(handle.boundingBox()); + assert(boundingBox, 'Node is either not visible or not an HTMLElement'); + assert(boundingBox.width !== 0, 'Node has 0 width.'); + assert(boundingBox.height !== 0, 'Node has 0 height.'); + + const fitsViewport = boundingBox.width <= viewportSize.width && boundingBox.height <= viewportSize.height; + const scrollOffset = await this._page.mainFrame().waitForFunctionValueInUtility(progress, () => ({ x: window.scrollX, y: window.scrollY })); + const documentRect = { ...boundingBox }; + documentRect.x += scrollOffset.x; + documentRect.y += scrollOffset.y; + return await this._screenshot(progress, format, helper.enclosingIntRect(documentRect), undefined, fitsViewport, options); + } finally { + await this._restorePageAfterScreenshot(); + } }); } @@ -254,12 +254,16 @@ export class Screenshotter { if (disableAnimations) progress.log(' disabled all CSS animations'); const syncAnimations = this._page.delegate.shouldToggleStyleSheetToSyncAnimations(); - progress.cleanupWhenAborted(() => this._restorePageAfterScreenshot()); await progress.race(this._page.safeNonStallingEvaluateInAllFrames('(' + inPagePrepareForScreenshots.toString() + `)(${JSON.stringify(screenshotStyle)}, ${hideCaret}, ${disableAnimations}, ${syncAnimations})`, 'utility')); - if (!process.env.PW_TEST_SCREENSHOT_NO_FONTS_READY) { - progress.log('waiting for fonts to load...'); - await progress.race(frame.nonStallingEvaluateInExistingContext('document.fonts.ready', 'utility').catch(() => {})); - progress.log('fonts loaded'); + try { + if (!process.env.PW_TEST_SCREENSHOT_NO_FONTS_READY) { + progress.log('waiting for fonts to load...'); + await progress.race(frame.nonStallingEvaluateInExistingContext('document.fonts.ready', 'utility').catch(() => {})); + progress.log('fonts loaded'); + } + } catch (error) { + await this._restorePageAfterScreenshot(); + throw error; } } @@ -272,45 +276,52 @@ export class Screenshotter { return () => Promise.resolve(); const framesToParsedSelectors: MultiMap = new MultiMap(); - - const cleanup = async () => { - await Promise.all([...framesToParsedSelectors.keys()].map(async frame => { - await frame.hideHighlight(); - })); - }; - progress.cleanupWhenAborted(cleanup); - await progress.race(Promise.all((options.mask || []).map(async ({ frame, selector }) => { const pair = await frame.selectors.resolveFrameForSelector(selector); if (pair) framesToParsedSelectors.set(pair.frame, pair.info.parsed); }))); - await progress.race(Promise.all([...framesToParsedSelectors.keys()].map(async frame => { - await frame.maskSelectors(framesToParsedSelectors.get(frame), options.maskColor || '#F0F'); - }))); - return cleanup; + const frames = [...framesToParsedSelectors.keys()]; + const cleanup = async () => { + await Promise.all(frames.map(frame => frame.hideHighlight())); + }; + + try { + const promises = frames.map(frame => frame.maskSelectors(framesToParsedSelectors.get(frame), options.maskColor || '#F0F')); + await progress.race(Promise.all(promises)); + return cleanup; + } catch (error) { + cleanup().catch(() => {}); + throw error; + } } private async _screenshot(progress: Progress, format: 'png' | 'jpeg', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, fitsViewport: boolean, options: ScreenshotOptions): Promise { if ((options as any).__testHookBeforeScreenshot) await progress.race((options as any).__testHookBeforeScreenshot()); + const shouldSetDefaultBackground = options.omitBackground && format === 'png'; - if (shouldSetDefaultBackground) { - progress.cleanupWhenAborted(() => this._page.delegate.setBackgroundColor()); + if (shouldSetDefaultBackground) await progress.race(this._page.delegate.setBackgroundColor({ r: 0, g: 0, b: 0, a: 0 })); - } - const cleanupHighlight = await this._maskElements(progress, options); - const quality = format === 'jpeg' ? options.quality ?? 80 : undefined; - const buffer = await this._page.delegate.takeScreenshot(progress, format, documentRect, viewportRect, quality, fitsViewport, options.scale || 'device'); - await cleanupHighlight(); - if (shouldSetDefaultBackground) - await progress.race(this._page.delegate.setBackgroundColor()); - if ((options as any).__testHookAfterScreenshot) - await progress.race((options as any).__testHookAfterScreenshot()); - return buffer; + try { + const quality = format === 'jpeg' ? options.quality ?? 80 : undefined; + const buffer = await this._page.delegate.takeScreenshot(progress, format, documentRect, viewportRect, quality, fitsViewport, options.scale || 'device'); + await cleanupHighlight(); + if (shouldSetDefaultBackground) + await this._page.delegate.setBackgroundColor(); + if ((options as any).__testHookAfterScreenshot) + await progress.race((options as any).__testHookAfterScreenshot()); + return buffer; + } catch (error) { + // Cleanup without blocking, it will be done before the next playwright action. + cleanupHighlight().catch(() => {}); + if (shouldSetDefaultBackground) + this._page.delegate.setBackgroundColor().catch(() => {}); + throw error; + } } } diff --git a/packages/playwright-core/src/server/transport.ts b/packages/playwright-core/src/server/transport.ts index c2fefad6d2070..c8b63fe7acde8 100644 --- a/packages/playwright-core/src/server/transport.ts +++ b/packages/playwright-core/src/server/transport.ts @@ -84,11 +84,10 @@ export class WebSocketTransport implements ConnectionTransport { const logUrl = stripQueryParams(url); progress?.log(` ${logUrl}`); const transport = new WebSocketTransport(progress, url, logUrl, { ...options, followRedirects: !!options.followRedirects && hadRedirects }); - progress?.cleanupWhenAborted(() => transport.closeAndWait()); - const resultPromise = new Promise<{ transport?: WebSocketTransport, redirect?: IncomingMessage }>((fulfill, reject) => { + const resultPromise = new Promise<{ redirect?: IncomingMessage }>((fulfill, reject) => { transport._ws.on('open', async () => { progress?.log(` ${logUrl}`); - fulfill({ transport }); + fulfill({}); }); transport._ws.on('error', event => { progress?.log(` ${logUrl} ${event.message}`); @@ -116,16 +115,20 @@ export class WebSocketTransport implements ConnectionTransport { }); }); }); - const result = progress ? await progress.race(resultPromise) : await resultPromise; - - if (result.redirect) { - // Strip authorization headers from the redirected request. - const newHeaders = Object.fromEntries(Object.entries(options.headers || {}).filter(([name]) => { - return !name.includes('access-key') && name.toLowerCase() !== 'authorization'; - })); - return WebSocketTransport._connect(progress, result.redirect.headers.location!, { ...options, headers: newHeaders }, true /* hadRedirects */); + try { + const result = progress ? await progress.race(resultPromise) : await resultPromise; + if (result.redirect) { + // Strip authorization headers from the redirected request. + const newHeaders = Object.fromEntries(Object.entries(options.headers || {}).filter(([name]) => { + return !name.includes('access-key') && name.toLowerCase() !== 'authorization'; + })); + return WebSocketTransport._connect(progress, result.redirect.headers.location!, { ...options, headers: newHeaders }, true /* hadRedirects */); + } + return transport; + } catch (error) { + await transport.closeAndWait(); + throw error; } - return transport; } constructor(progress: Progress|undefined, url: string, logUrl: string, options: WebSocketTransportOptions) { diff --git a/packages/playwright-core/src/server/utils/network.ts b/packages/playwright-core/src/server/utils/network.ts index d12b72a1c389e..157a6f70862b7 100644 --- a/packages/playwright-core/src/server/utils/network.ts +++ b/packages/playwright-core/src/server/utils/network.ts @@ -22,6 +22,7 @@ import url from 'url'; import { HttpsProxyAgent, SocksProxyAgent, getProxyForUrl } from '../../utilsBundle'; import { httpHappyEyeballsAgent, httpsHappyEyeballsAgent } from './happyEyeballs'; +import { ManualPromise } from '../../utils/isomorphic/manualPromise'; import type net from 'net'; import type { ProxySettings } from '../types'; @@ -90,27 +91,37 @@ export function httpRequest(params: HTTPRequestParams, onResponse: (r: http.Inco request.abort(); }); } - cancelRequest = e => request.destroy(e); + cancelRequest = e => { + try { + request.destroy(e); + } catch { + } + }; request.end(params.data); return { cancel: e => cancelRequest(e) }; } -export function fetchData(progress: Progress | undefined, params: HTTPRequestParams, onError?: (params: HTTPRequestParams, response: http.IncomingMessage) => Promise): Promise { - const promise = new Promise((resolve, reject) => { - const { cancel } = httpRequest(params, async response => { - if (response.statusCode !== 200) { - const error = onError ? await onError(params, response) : new Error(`fetch failed: server returned code ${response.statusCode}. URL: ${params.url}`); - reject(error); - return; - } - let body = ''; - response.on('data', (chunk: string) => body += chunk); - response.on('error', (error: any) => reject(error)); - response.on('end', () => resolve(body)); - }, reject); - progress?.cleanupWhenAborted(cancel); - }); - return progress ? progress.race(promise) : promise; +export async function fetchData(progress: Progress | undefined, params: HTTPRequestParams, onError?: (params: HTTPRequestParams, response: http.IncomingMessage) => Promise): Promise { + const promise = new ManualPromise(); + const { cancel } = httpRequest(params, async response => { + if (response.statusCode !== 200) { + const error = onError ? await onError(params, response) : new Error(`fetch failed: server returned code ${response.statusCode}. URL: ${params.url}`); + promise.reject(error); + return; + } + let body = ''; + response.on('data', (chunk: string) => body += chunk); + response.on('error', (error: any) => promise.reject(error)); + response.on('end', () => promise.resolve(body)); + }, error => promise.reject(error)); + if (!progress) + return promise; + try { + return await progress.race(promise); + } catch (error) { + cancel(error); + throw error; + } } function shouldBypassProxy(url: URL, bypass?: string): boolean { diff --git a/packages/playwright/ThirdPartyNotices.txt b/packages/playwright/ThirdPartyNotices.txt index 14b1e01548a6e..b860821def6f8 100644 --- a/packages/playwright/ThirdPartyNotices.txt +++ b/packages/playwright/ThirdPartyNotices.txt @@ -29,7 +29,6 @@ This project incorporates components from the projects listed below. The origina - @babel/highlight@7.24.7 (https://github.com/babel/babel) - @babel/parser@7.28.0 (https://github.com/babel/babel) - @babel/plugin-proposal-decorators@7.28.0 (https://github.com/babel/babel) -- @babel/plugin-proposal-explicit-resource-management@7.27.4 (https://github.com/babel/babel) - @babel/plugin-syntax-async-generators@7.8.4 (https://github.com/babel/babel/tree/master/packages/babel-plugin-syntax-async-generators) - @babel/plugin-syntax-decorators@7.27.1 (https://github.com/babel/babel) - @babel/plugin-syntax-import-attributes@7.27.1 (https://github.com/babel/babel) @@ -41,6 +40,7 @@ This project incorporates components from the projects listed below. The origina - @babel/plugin-transform-class-properties@7.27.1 (https://github.com/babel/babel) - @babel/plugin-transform-class-static-block@7.27.1 (https://github.com/babel/babel) - @babel/plugin-transform-destructuring@7.28.0 (https://github.com/babel/babel) +- @babel/plugin-transform-explicit-resource-management@7.28.0 (https://github.com/babel/babel) - @babel/plugin-transform-export-namespace-from@7.27.1 (https://github.com/babel/babel) - @babel/plugin-transform-logical-assignment-operators@7.27.1 (https://github.com/babel/babel) - @babel/plugin-transform-modules-commonjs@7.27.1 (https://github.com/babel/babel) @@ -993,7 +993,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= END OF @babel/plugin-proposal-decorators@7.28.0 AND INFORMATION -%% @babel/plugin-proposal-explicit-resource-management@7.27.4 NOTICES AND INFORMATION BEGIN HERE +%% @babel/plugin-syntax-async-generators@7.8.4 NOTICES AND INFORMATION BEGIN HERE ========================================= MIT License @@ -1018,9 +1018,9 @@ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= -END OF @babel/plugin-proposal-explicit-resource-management@7.27.4 AND INFORMATION +END OF @babel/plugin-syntax-async-generators@7.8.4 AND INFORMATION -%% @babel/plugin-syntax-async-generators@7.8.4 NOTICES AND INFORMATION BEGIN HERE +%% @babel/plugin-syntax-decorators@7.27.1 NOTICES AND INFORMATION BEGIN HERE ========================================= MIT License @@ -1045,9 +1045,9 @@ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= -END OF @babel/plugin-syntax-async-generators@7.8.4 AND INFORMATION +END OF @babel/plugin-syntax-decorators@7.27.1 AND INFORMATION -%% @babel/plugin-syntax-decorators@7.27.1 NOTICES AND INFORMATION BEGIN HERE +%% @babel/plugin-syntax-import-attributes@7.27.1 NOTICES AND INFORMATION BEGIN HERE ========================================= MIT License @@ -1072,9 +1072,9 @@ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= -END OF @babel/plugin-syntax-decorators@7.27.1 AND INFORMATION +END OF @babel/plugin-syntax-import-attributes@7.27.1 AND INFORMATION -%% @babel/plugin-syntax-import-attributes@7.27.1 NOTICES AND INFORMATION BEGIN HERE +%% @babel/plugin-syntax-json-strings@7.8.3 NOTICES AND INFORMATION BEGIN HERE ========================================= MIT License @@ -1099,9 +1099,9 @@ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= -END OF @babel/plugin-syntax-import-attributes@7.27.1 AND INFORMATION +END OF @babel/plugin-syntax-json-strings@7.8.3 AND INFORMATION -%% @babel/plugin-syntax-json-strings@7.8.3 NOTICES AND INFORMATION BEGIN HERE +%% @babel/plugin-syntax-jsx@7.27.1 NOTICES AND INFORMATION BEGIN HERE ========================================= MIT License @@ -1126,9 +1126,9 @@ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= -END OF @babel/plugin-syntax-json-strings@7.8.3 AND INFORMATION +END OF @babel/plugin-syntax-jsx@7.27.1 AND INFORMATION -%% @babel/plugin-syntax-jsx@7.27.1 NOTICES AND INFORMATION BEGIN HERE +%% @babel/plugin-syntax-object-rest-spread@7.8.3 NOTICES AND INFORMATION BEGIN HERE ========================================= MIT License @@ -1153,9 +1153,9 @@ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= -END OF @babel/plugin-syntax-jsx@7.27.1 AND INFORMATION +END OF @babel/plugin-syntax-object-rest-spread@7.8.3 AND INFORMATION -%% @babel/plugin-syntax-object-rest-spread@7.8.3 NOTICES AND INFORMATION BEGIN HERE +%% @babel/plugin-syntax-optional-catch-binding@7.8.3 NOTICES AND INFORMATION BEGIN HERE ========================================= MIT License @@ -1180,9 +1180,9 @@ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= -END OF @babel/plugin-syntax-object-rest-spread@7.8.3 AND INFORMATION +END OF @babel/plugin-syntax-optional-catch-binding@7.8.3 AND INFORMATION -%% @babel/plugin-syntax-optional-catch-binding@7.8.3 NOTICES AND INFORMATION BEGIN HERE +%% @babel/plugin-syntax-typescript@7.27.1 NOTICES AND INFORMATION BEGIN HERE ========================================= MIT License @@ -1207,9 +1207,9 @@ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= -END OF @babel/plugin-syntax-optional-catch-binding@7.8.3 AND INFORMATION +END OF @babel/plugin-syntax-typescript@7.27.1 AND INFORMATION -%% @babel/plugin-syntax-typescript@7.27.1 NOTICES AND INFORMATION BEGIN HERE +%% @babel/plugin-transform-class-properties@7.27.1 NOTICES AND INFORMATION BEGIN HERE ========================================= MIT License @@ -1234,9 +1234,9 @@ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= -END OF @babel/plugin-syntax-typescript@7.27.1 AND INFORMATION +END OF @babel/plugin-transform-class-properties@7.27.1 AND INFORMATION -%% @babel/plugin-transform-class-properties@7.27.1 NOTICES AND INFORMATION BEGIN HERE +%% @babel/plugin-transform-class-static-block@7.27.1 NOTICES AND INFORMATION BEGIN HERE ========================================= MIT License @@ -1261,9 +1261,9 @@ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= -END OF @babel/plugin-transform-class-properties@7.27.1 AND INFORMATION +END OF @babel/plugin-transform-class-static-block@7.27.1 AND INFORMATION -%% @babel/plugin-transform-class-static-block@7.27.1 NOTICES AND INFORMATION BEGIN HERE +%% @babel/plugin-transform-destructuring@7.28.0 NOTICES AND INFORMATION BEGIN HERE ========================================= MIT License @@ -1288,9 +1288,9 @@ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= -END OF @babel/plugin-transform-class-static-block@7.27.1 AND INFORMATION +END OF @babel/plugin-transform-destructuring@7.28.0 AND INFORMATION -%% @babel/plugin-transform-destructuring@7.28.0 NOTICES AND INFORMATION BEGIN HERE +%% @babel/plugin-transform-explicit-resource-management@7.28.0 NOTICES AND INFORMATION BEGIN HERE ========================================= MIT License @@ -1315,7 +1315,7 @@ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= -END OF @babel/plugin-transform-destructuring@7.28.0 AND INFORMATION +END OF @babel/plugin-transform-explicit-resource-management@7.28.0 AND INFORMATION %% @babel/plugin-transform-export-namespace-from@7.27.1 NOTICES AND INFORMATION BEGIN HERE ========================================= diff --git a/packages/playwright/bundles/babel/package-lock.json b/packages/playwright/bundles/babel/package-lock.json index 553154a236469..eeb420a495b60 100644 --- a/packages/playwright/bundles/babel/package-lock.json +++ b/packages/playwright/bundles/babel/package-lock.json @@ -13,7 +13,6 @@ "@babel/helper-plugin-utils": "^7.27.1", "@babel/parser": "^7.28.0", "@babel/plugin-proposal-decorators": "^7.28.0", - "@babel/plugin-proposal-explicit-resource-management": "^7.27.4", "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-import-attributes": "^7.27.1", "@babel/plugin-syntax-json-strings": "^7.8.3", @@ -21,6 +20,7 @@ "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-transform-class-properties": "^7.27.1", "@babel/plugin-transform-class-static-block": "^7.27.1", + "@babel/plugin-transform-explicit-resource-management": "^7.28.0", "@babel/plugin-transform-export-namespace-from": "^7.27.1", "@babel/plugin-transform-logical-assignment-operators": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", @@ -344,23 +344,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-proposal-explicit-resource-management": { - "version": "7.27.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-explicit-resource-management/-/plugin-proposal-explicit-resource-management-7.27.4.tgz", - "integrity": "sha512-1SwtCDdZWQvUU1i7wt/ihP7W38WjC3CSTOHAl+Xnbze8+bbMNjRvRQydnj0k9J1jPqCAZctBFp6NHJXkrVVmEA==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-explicit-resource-management instead.", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.27.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-async-generators": { "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", @@ -517,6 +500,22 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz", + "integrity": "sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-export-namespace-from": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", diff --git a/packages/playwright/bundles/babel/package.json b/packages/playwright/bundles/babel/package.json index d2a8ae4413c02..379469c6e242d 100644 --- a/packages/playwright/bundles/babel/package.json +++ b/packages/playwright/bundles/babel/package.json @@ -8,7 +8,6 @@ "@babel/helper-plugin-utils": "^7.27.1", "@babel/parser": "^7.28.0", "@babel/plugin-proposal-decorators": "^7.28.0", - "@babel/plugin-proposal-explicit-resource-management": "^7.27.4", "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-import-attributes": "^7.27.1", "@babel/plugin-syntax-json-strings": "^7.8.3", @@ -16,6 +15,7 @@ "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-transform-class-properties": "^7.27.1", "@babel/plugin-transform-class-static-block": "^7.27.1", + "@babel/plugin-transform-explicit-resource-management": "^7.28.0", "@babel/plugin-transform-export-namespace-from": "^7.27.1", "@babel/plugin-transform-logical-assignment-operators": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", diff --git a/packages/playwright/bundles/babel/src/babelBundleImpl.ts b/packages/playwright/bundles/babel/src/babelBundleImpl.ts index 2810a1bd5cad3..27d94349b6a8a 100644 --- a/packages/playwright/bundles/babel/src/babelBundleImpl.ts +++ b/packages/playwright/bundles/babel/src/babelBundleImpl.ts @@ -36,7 +36,7 @@ function babelTransformOptions(isTypeScript: boolean, isModule: boolean, plugins if (isTypeScript) { plugins.push( [require('@babel/plugin-proposal-decorators'), { version: '2023-05' }], - [require('@babel/plugin-proposal-explicit-resource-management')], + [require('@babel/plugin-transform-explicit-resource-management')], [require('@babel/plugin-transform-class-properties')], [require('@babel/plugin-transform-class-static-block')], [require('@babel/plugin-transform-numeric-separator')], diff --git a/utils/doclint/cli.js b/utils/doclint/cli.js index a388d860d07f0..a694cd07497ef 100755 --- a/utils/doclint/cli.js +++ b/utils/doclint/cli.js @@ -247,6 +247,8 @@ async function run() { const badLinks = []; for (const { filePath, linkTarget, name } of mdLinks) { + if (linkTarget.startsWith(path.join(documentationRoot, 'images'))) + continue; if (!mdSections.has(linkTarget)) badLinks.push(`${path.relative(PROJECT_DIR, filePath)} references to '${linkTarget}' as '${name}' which does not exist.`); }