Skip to content

Commit

Permalink
feat(connect): support special headers for debug/attachments (#22106)
Browse files Browse the repository at this point in the history
`x-playwright-debug-log: value` headers are printed to `pw:browser`
debug log.
`x-playwright-attachment: name=value` headers are attached to each test.

Fixes #21619.
  • Loading branch information
dgozman committed Mar 31, 2023
1 parent a567cf0 commit 37d1659
Show file tree
Hide file tree
Showing 10 changed files with 67 additions and 4 deletions.
5 changes: 4 additions & 1 deletion packages/playwright-core/src/client/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { BrowserContext, prepareBrowserContextParams } from './browserContext';
import type { Page } from './page';
import { ChannelOwner } from './channelOwner';
import { Events } from './events';
import type { LaunchOptions, BrowserContextOptions } from './types';
import type { LaunchOptions, BrowserContextOptions, HeadersArray } from './types';
import { isSafeCloseError, kBrowserClosedError } from '../common/errors';
import type * as api from '../../types/types';
import { CDPSession } from './cdpSession';
Expand All @@ -34,6 +34,9 @@ export class Browser extends ChannelOwner<channels.BrowserChannel> implements ap
_options: LaunchOptions = {};
readonly _name: string;

// Used from @playwright/test fixtures.
_connectHeaders?: HeadersArray;

static from(browser: channels.BrowserChannel): Browser {
return (browser as any)._object;
}
Expand Down
8 changes: 7 additions & 1 deletion packages/playwright-core/src/client/browserType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import type * as api from '../../types/types';
import { kBrowserClosedError } from '../common/errors';
import { raceAgainstTimeout } from '../utils/timeoutRunner';
import type { Playwright } from './playwright';
import { debugLogger } from '../common/debugLogger';

export interface BrowserServerLauncher {
launchServer(options?: LaunchServerOptions): Promise<api.BrowserServer>;
Expand Down Expand Up @@ -154,7 +155,7 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
};
if ((params as any).__testHookRedirectPortForwarding)
connectParams.socksProxyRedirectPortForTest = (params as any).__testHookRedirectPortForwarding;
const { pipe } = await localUtils._channel.connect(connectParams);
const { pipe, headers: connectHeaders } = await localUtils._channel.connect(connectParams);
const closePipe = () => pipe.close().catch(() => {});
const connection = new Connection(localUtils);
connection.markAsRemote();
Expand Down Expand Up @@ -198,6 +199,11 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
browser = Browser.from(playwright._initializer.preLaunchedBrowser!);
this._didLaunchBrowser(browser, {}, logger);
browser._shouldCloseConnectionOnClose = true;
browser._connectHeaders = connectHeaders;
for (const header of connectHeaders) {
if (header.name === 'x-playwright-debug-log')
debugLogger.log('browser', header.value);
}
browser.on(Events.Browser.Disconnected, closePipe);
return browser;
}, deadline ? deadline - monotonicTime() : 0);
Expand Down
1 change: 1 addition & 0 deletions packages/playwright-core/src/protocol/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ scheme.LocalUtilsConnectParams = tObject({
});
scheme.LocalUtilsConnectResult = tObject({
pipe: tChannel(['JsonPipe']),
headers: tArray(tType('NameValue')),
});
scheme.LocalUtilsTracingStartedParams = tObject({
tracesDir: tOptional(tString),
Expand Down
5 changes: 5 additions & 0 deletions packages/playwright-core/src/remote/playwrightServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ export class PlaywrightServer {
const browserSemaphore = new Semaphore(this._options.maxConnections);
const controllerSemaphore = new Semaphore(1);
const reuseBrowserSemaphore = new Semaphore(1);
if (process.env.PWTEST_SERVER_WS_HEADERS) {
this._wsServer.on('headers', (headers, request) => {
headers.push(process.env.PWTEST_SERVER_WS_HEADERS!);
});
}
this._wsServer.on('connection', (ws, request) => {
const url = new URL('http://localhost' + (request.url || ''));
const browserHeader = request.headers['x-playwright-browser'];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.
pipe.wasClosed();
};
pipe.on('close', () => transport.close());
return { pipe };
return { pipe, headers: transport.headers };
}, params.timeout || 0);
}

Expand Down
6 changes: 6 additions & 0 deletions packages/playwright-core/src/server/transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type { ClientRequest, IncomingMessage } from 'http';
import type { Progress } from './progress';
import { makeWaitForNextTask } from '../utils';
import { httpHappyEyeballsAgent, httpsHappyEyeballsAgent } from '../utils/happy-eyeballs';
import type { HeadersArray } from './types';

export type ProtocolRequest = {
id: number;
Expand Down Expand Up @@ -55,6 +56,7 @@ export class WebSocketTransport implements ConnectionTransport {
onmessage?: (message: ProtocolResponse) => void;
onclose?: () => void;
readonly wsEndpoint: string;
readonly headers: HeadersArray = [];

static async connect(progress: (Progress|undefined), url: string, headers?: { [key: string]: string; }, followRedirects?: boolean): Promise<WebSocketTransport> {
const logUrl = stripQueryParams(url);
Expand Down Expand Up @@ -103,6 +105,10 @@ export class WebSocketTransport implements ConnectionTransport {
followRedirects,
agent: (/^(https|wss):\/\//.test(url)) ? httpsHappyEyeballsAgent : httpHappyEyeballsAgent
});
this._ws.on('upgrade', request => {
for (let i = 0; i < request.rawHeaders.length; i += 2)
this.headers.push({ name: request.rawHeaders[i], value: request.rawHeaders[i + 1] });
});
this._progress = progress;
// The 'ws' module in node sometimes sends us multiple messages in a single task.
// In Web, all IO callbacks (e.g. WebSocket callbacks)
Expand Down
20 changes: 19 additions & 1 deletion packages/playwright-test/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import * as fs from 'fs';
import * as path from 'path';
import type { APIRequestContext, BrowserContext, BrowserContextOptions, LaunchOptions, Page, Tracing, Video } from 'playwright-core';
import type { APIRequestContext, BrowserContext, Browser, BrowserContextOptions, LaunchOptions, Page, Tracing, Video } from 'playwright-core';
import * as playwrightLibrary from 'playwright-core';
import { createGuid, debugMode, addInternalStackPrefix, mergeTraceFiles, saveTraceFile, removeFolders } from 'playwright-core/lib/utils';
import type { Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, ScreenshotMode, TestInfo, TestType, TraceMode, VideoMode } from '../types/test';
Expand Down Expand Up @@ -311,6 +311,7 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
const listener = createInstrumentationListener(context);
(context as any)._instrumentation.addListener(listener);
(context.request as any)._instrumentation.addListener(listener);
attachConnectedHeaderIfNeeded(testInfo, context.browser());
};
const onDidCreateRequestContext = async (context: APIRequestContext) => {
const tracing = (context as any)._tracing as Tracing;
Expand Down Expand Up @@ -535,6 +536,7 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
}, { scope: 'test', _title: 'context' } as any],

context: async ({ playwright, browser, _reuseContext, _contextFactory }, use, testInfo) => {
attachConnectedHeaderIfNeeded(testInfo, browser);
if (!_reuseContext) {
await use(await _contextFactory());
return;
Expand Down Expand Up @@ -633,6 +635,22 @@ function normalizeScreenshotMode(screenshot: PlaywrightWorkerOptions['screenshot
return typeof screenshot === 'string' ? screenshot : screenshot.mode;
}

function attachConnectedHeaderIfNeeded(testInfo: TestInfo, browser: Browser | null) {
const connectHeaders: { name: string, value: string }[] | undefined = (browser as any)?._connectHeaders;
if (!connectHeaders)
return;
for (const header of connectHeaders) {
if (header.name !== 'x-playwright-attachment')
continue;
const [name, value] = header.value.split('=');
if (!name || !value)
continue;
if (testInfo.attachments.some(attachment => attachment.name === name))
continue;
testInfo.attachments.push({ name, contentType: 'text/plain', body: Buffer.from(value) });
}
}

const kTracingStarted = Symbol('kTracingStarted');
const kIsReusedContext = Symbol('kReusedContext');

Expand Down
1 change: 1 addition & 0 deletions packages/protocol/src/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,7 @@ export type LocalUtilsConnectOptions = {
};
export type LocalUtilsConnectResult = {
pipe: JsonPipeChannel,
headers: NameValue[],
};
export type LocalUtilsTracingStartedParams = {
tracesDir?: string,
Expand Down
3 changes: 3 additions & 0 deletions packages/protocol/src/protocol.yml
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,9 @@ LocalUtils:
socksProxyRedirectPortForTest: number?
returns:
pipe: JsonPipe
headers:
type: array
items: NameValue

tracingStarted:
parameters:
Expand Down
20 changes: 20 additions & 0 deletions tests/playwright-test/playwright.connect.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ test('should work with connectOptions', async ({ runInlineTest }) => {
'global-setup.ts': `
import { chromium } from '@playwright/test';
module.exports = async () => {
process.env.DEBUG = 'pw:browser';
process.env.PWTEST_SERVER_WS_HEADERS =
'x-playwright-debug-log: a-debug-log-string\\r\\n' +
'x-playwright-attachment: attachment-a=value-a\\r\\n' +
'x-playwright-debug-log: b-debug-log-string\\r\\n' +
'x-playwright-attachment: attachment-b=value-b';
const server = await chromium.launchServer();
process.env.CONNECT_WS_ENDPOINT = server.wsEndpoint();
return () => server.close();
Expand All @@ -48,6 +54,20 @@ test('should work with connectOptions', async ({ runInlineTest }) => {
});
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
expect(result.output).toContain('a-debug-log-string');
expect(result.output).toContain('b-debug-log-string');
expect(result.results[0].attachments).toEqual([
{
name: 'attachment-a',
contentType: 'text/plain',
body: 'dmFsdWUtYQ=='
},
{
name: 'attachment-b',
contentType: 'text/plain',
body: 'dmFsdWUtYg=='
}
]);
});

test('should throw with bad connectOptions', async ({ runInlineTest }) => {
Expand Down

0 comments on commit 37d1659

Please sign in to comment.