diff --git a/packages/html-reporter/src/testResultView.tsx b/packages/html-reporter/src/testResultView.tsx
index c91027147e081..9f8649e52531c 100644
--- a/packages/html-reporter/src/testResultView.tsx
+++ b/packages/html-reporter/src/testResultView.tsx
@@ -164,12 +164,12 @@ export const TestResultView: React.FC<{
}
{!!traces.length &&
- {)}
}
{!!videos.length &&
diff --git a/packages/playwright-core/src/server/chromium/chromiumSwitches.ts b/packages/playwright-core/src/server/chromium/chromiumSwitches.ts
index 7543642c624c9..da8d185fa8177 100644
--- a/packages/playwright-core/src/server/chromium/chromiumSwitches.ts
+++ b/packages/playwright-core/src/server/chromium/chromiumSwitches.ts
@@ -20,8 +20,6 @@
const disabledFeatures = [
// See https://github.com/microsoft/playwright/issues/14047
'AvoidUnnecessaryBeforeUnloadCheckSync',
- // See https://github.com/microsoft/playwright/issues/38568
- 'BoundaryEventDispatchTracksNodeRemoval',
'DestroyProfileOnBrowserClose',
// See https://github.com/microsoft/playwright/pull/13854
'DialMediaRouteProvider',
diff --git a/packages/playwright-core/src/server/dispatchers/dispatcher.ts b/packages/playwright-core/src/server/dispatchers/dispatcher.ts
index 07fda756a7ac5..3a164f9f31809 100644
--- a/packages/playwright-core/src/server/dispatchers/dispatcher.ts
+++ b/packages/playwright-core/src/server/dispatchers/dispatcher.ts
@@ -102,8 +102,8 @@ export class Dispatcher>(method: T, params?: channels.EventsTraits[T]) {
@@ -188,7 +188,7 @@ export class DispatcherConnection {
readonly _dispatchersByBucket = new Map>();
onmessage = (message: object) => {};
private _waitOperations = new Map();
- private _activeProgressControllers = new Map();
+ private _activeProgressControllers = new Map();
private _isInProcess: boolean;
constructor(isInProcess?: boolean) {
@@ -197,9 +197,11 @@ export class DispatcherConnection {
async abortControllersForGuids(guids: Set, error: Error) {
const controllers: ProgressController[] = [];
- for (const controller of this._activeProgressControllers.values()) {
- if (controller.metadata.objectId && guids.has(controller.metadata.objectId))
- controllers.push(controller);
+ for (const entry of this._activeProgressControllers.values()) {
+ if (entry.controller?.metadata.objectId && guids.has(entry.controller.metadata.objectId)) {
+ entry.abortError = error;
+ controllers.push(entry.controller);
+ }
}
await Promise.all(controllers.map(controller => controller.abort(error)));
}
@@ -304,7 +306,12 @@ export class DispatcherConnection {
return;
}
if (method === '__abort__') {
- await this._activeProgressControllers.get(`call@${params.id}`)?.abort(new AbortError(params.reason));
+ const entry = this._activeProgressControllers.get(`call@${params.id}`);
+ if (!entry)
+ return;
+ entry.abortError = new AbortError(params.reason);
+ const controller = entry.controller;
+ await controller?.abort(entry.abortError);
return;
}
if (!dispatcher) {
@@ -352,21 +359,25 @@ export class DispatcherConnection {
log: [],
};
- const beforeController = dispatcher.createProgressController(callMetadata);
- this._activeProgressControllers.set(callMetadata.id, beforeController);
+ const abortControllerEntry: { controller?: ProgressController, abortError?: Error } = {};
+ this._activeProgressControllers.set(callMetadata.id, abortControllerEntry);
+ const swapProgressController = () => {
+ const controller = dispatcher.createProgressController(callMetadata, abortControllerEntry.abortError);
+ abortControllerEntry.controller = controller;
+ return controller;
+ };
+
+ const beforeController = swapProgressController();
// Be generous with the tracing timeout in case it wants to capture a screenshot, fail silently.
await beforeController.run(progress => sdkObject.instrumentation.onBeforeCall(progress, sdkObject), 3000).catch(() => {});
- this._activeProgressControllers.delete(callMetadata.id);
const response: any = { id };
try {
// If the dispatcher has been disposed while running the instrumentation call, error out.
if (this._dispatcherByGuid.get(guid) !== dispatcher)
throw new TargetClosedError(sdkObject.closeReason());
- const controller = dispatcher.createProgressController(callMetadata);
- this._activeProgressControllers.set(callMetadata.id, controller);
+ const controller = swapProgressController();
const result = await controller.run(progress => (dispatcher as any)[method](validParams, progress), validMetadata.timeout);
- this._activeProgressControllers.delete(callMetadata.id);
const validator = findValidator(dispatcher._type, method, 'Result');
response.result = validator(result, '', this._validatorToWireContext());
callMetadata.result = result;
@@ -389,8 +400,7 @@ export class DispatcherConnection {
callMetadata.error = response.error;
} finally {
callMetadata.endTime = monotonicTime();
- const afterController = dispatcher.createProgressController(callMetadata);
- this._activeProgressControllers.set(callMetadata.id, afterController);
+ const afterController = swapProgressController();
// Be generous with the tracing timeout in case it wants to capture a screenshot, fail silently.
await afterController.run(progress => sdkObject.instrumentation.onAfterCall(progress, sdkObject), 3000).catch(() => {});
if (metainfo?.slowMo)
diff --git a/packages/playwright-core/src/server/progress.ts b/packages/playwright-core/src/server/progress.ts
index 78d3298257238..43b0137e7b57d 100644
--- a/packages/playwright-core/src/server/progress.ts
+++ b/packages/playwright-core/src/server/progress.ts
@@ -44,14 +44,15 @@ export class ProgressController {
readonly metadata: CallMetadata;
private _controller: AbortController;
- constructor(metadata?: CallMetadata, onCallLog?: (message: string) => void) {
+ constructor(metadata?: CallMetadata, onCallLog?: (message: string) => void, pendingAbortError?: Error) {
this.metadata = metadata || { id: '', startTime: 0, endTime: 0, type: 'Internal', method: '', params: {}, log: [], internal: true };
this._onCallLog = onCallLog;
+ this._pendingAbortError = pendingAbortError;
this._forceAbortPromise.catch(e => null); // Prevent unhandled promise rejection.
this._controller = new AbortController();
}
- static createForSdkObject(sdkObject: SdkObject, callMetadata: CallMetadata) {
+ static createForSdkObject(sdkObject: SdkObject, callMetadata: CallMetadata, pendingAbortError?: Error) {
const logName = sdkObject.logName || 'api';
return new ProgressController(callMetadata, message => {
// Note: "attribution.playwright" is undefined in DebugController. Unfortunate!
@@ -59,7 +60,7 @@ export class ProgressController {
return;
debugLogger.log(logName, message);
sdkObject.instrumentation.onCallLog(sdkObject, callMetadata, logName, message);
- });
+ }, pendingAbortError);
}
async abort(error: Error) {
diff --git a/tests/library/inspector/cli-codegen-1.spec.ts b/tests/library/inspector/cli-codegen-1.spec.ts
index 9f42cf44febfe..afb0140448549 100644
--- a/tests/library/inspector/cli-codegen-1.spec.ts
+++ b/tests/library/inspector/cli-codegen-1.spec.ts
@@ -1025,7 +1025,9 @@ await page.GetByText("Click me").ClickAsync(new()
});`);
});
- test('should record slider', async ({ openRecorder, browserName, headless }) => {
+ test('should record slider', async ({ openRecorder, browserName, isLinux, headless }) => {
+ test.fixme(browserName === 'chromium' && isLinux && headless, 'https://github.com/microsoft/playwright/issues/38568');
+
const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(``);
diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts
index 63cbacea821cf..fc14d4d7f80e0 100644
--- a/tests/playwright-test/reporter-html.spec.ts
+++ b/tests/playwright-test/reporter-html.spec.ts
@@ -714,6 +714,45 @@ for (const useIntermediateMergeReport of [true, false] as const) {
await expect(page.locator('.source-line-running')).toContainText('request.get');
});
+ test('should show a thumbnail for every trace attachment', async ({ runInlineTest, page, server, showReport }) => {
+ const result = await runInlineTest({
+ 'a.test.js': `
+ import { test, expect } from '@playwright/test';
+ test('passes', async ({ browser }, testInfo) => {
+ for (const index of [1, 2]) {
+ const context = await browser.newContext();
+ await context.tracing.start({ screenshots: true, snapshots: true });
+ const page = await context.newPage();
+ await page.goto('${server.EMPTY_PAGE}');
+ const tracePath = testInfo.outputPath('trace' + index + '.zip');
+ await context.tracing.stop({ path: tracePath });
+ await testInfo.attach('trace', { path: tracePath, contentType: 'application/zip' });
+ await context.close();
+ }
+ });
+ `,
+ }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' });
+ expect(result.exitCode).toBe(0);
+ expect(result.passed).toBe(1);
+
+ await showReport();
+ await page.getByRole('link', { name: 'passes' }).click();
+
+ const traces = page.locator('.chip').filter({ hasText: 'Traces' });
+ await expect(traces.locator('img')).toHaveCount(2);
+ await expect(traces.getByRole('link', { name: 'trace-1', exact: true })).toBeVisible();
+ await expect(traces.getByRole('link', { name: 'trace-2', exact: true })).toBeVisible();
+
+ const hrefs = await traces.locator('a').filter({ has: page.locator('img') }).evaluateAll(links => links.map(link => link.getAttribute('href')));
+ expect(hrefs).toHaveLength(2);
+ for (const href of hrefs)
+ expect(href!.match(/trace=/g)).toHaveLength(1);
+ expect(hrefs[0]).not.toBe(hrefs[1]);
+
+ await traces.locator('img').first().click();
+ await expect(page.locator('.action-title').first()).toBeVisible();
+ });
+
test('trace should not hang when showing parallel api requests', async ({ runInlineTest, page, server, showReport }) => {
const result = await runInlineTest({
'playwright.config.js': `