Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions packages/html-reporter/src/testResultView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -164,12 +164,12 @@ export const TestResultView: React.FC<{
</AutoChip>}

{!!traces.length && <Anchor id='attachment-trace'><AutoChip header='Traces' revealOnAnchorId='attachment-trace'>
{<div>
<a href={formatUrl(generateTraceUrl(traces))}>
{traces.map((a, i) => <div key={`trace-${i}`}>
<a href={formatUrl(generateTraceUrl([a]))}>
<img className='screenshot' src={traceImage} style={{ width: 192, height: 117, marginLeft: 20 }} />
</a>
{traces.map((a, i) => <AttachmentLink key={`trace-${i}`} attachment={a} result={result} linkName={traces.length === 1 ? 'trace' : `trace-${i + 1}`}></AttachmentLink>)}
</div>}
<AttachmentLink attachment={a} result={result} linkName={traces.length === 1 ? 'trace' : `trace-${i + 1}`}></AttachmentLink>
</div>)}
</AutoChip></Anchor>}

{!!videos.length && <Anchor id='attachment-video'><AutoChip header='Videos' revealOnAnchorId='attachment-video'>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
40 changes: 25 additions & 15 deletions packages/playwright-core/src/server/dispatchers/dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,8 @@ export class Dispatcher<Type extends SdkObject, ChannelType, ParentScopeType ext
this.connection.sendAdopt(this, child);
}

createProgressController(callMetadata: CallMetadata): ProgressController {
return ProgressController.createForSdkObject(this._object, callMetadata);
createProgressController(callMetadata: CallMetadata, pendingAbortError?: Error): ProgressController {
return ProgressController.createForSdkObject(this._object, callMetadata, pendingAbortError);
}

_dispatchEvent<T extends keyof channels.EventsTraits<ChannelType>>(method: T, params?: channels.EventsTraits<ChannelType>[T]) {
Expand Down Expand Up @@ -188,7 +188,7 @@ export class DispatcherConnection {
readonly _dispatchersByBucket = new Map<string, Set<string>>();
onmessage = (message: object) => {};
private _waitOperations = new Map<string, CallMetadata>();
private _activeProgressControllers = new Map<string, ProgressController>();
private _activeProgressControllers = new Map<string, { controller?: ProgressController, abortError?: Error }>();
private _isInProcess: boolean;

constructor(isInProcess?: boolean) {
Expand All @@ -197,9 +197,11 @@ export class DispatcherConnection {

async abortControllersForGuids(guids: Set<string>, 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)));
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand All @@ -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)
Expand Down
7 changes: 4 additions & 3 deletions packages/playwright-core/src/server/progress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,22 +44,23 @@ 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!
if (logName === 'api' && sdkObject.attribution.playwright?.options.isInternalPlaywright)
return;
debugLogger.log(logName, message);
sdkObject.instrumentation.onCallLog(sdkObject, callMetadata, logName, message);
});
}, pendingAbortError);
}

async abort(error: Error) {
Expand Down
4 changes: 3 additions & 1 deletion tests/library/inspector/cli-codegen-1.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(`<input type="range" min="0" max="10" value="5">`);
Expand Down
39 changes: 39 additions & 0 deletions tests/playwright-test/reporter-html.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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': `
Expand Down
Loading