feat: mark PlaywrightClient's connection as remote (#9477)

This makes artifacts work as expected for all remote scenarios.
This commit is contained in:
Dmitry Gozman 2021-10-14 10:41:03 -07:00 committed by GitHub
parent a4d1412463
commit 9dd17773e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 78 additions and 58 deletions

View File

@ -22,15 +22,13 @@ import { ChannelOwner } from './channelOwner';
import { Readable } from 'stream';
export class Artifact extends ChannelOwner<channels.ArtifactChannel, channels.ArtifactInitializer> {
_isRemote = false;
static from(channel: channels.ArtifactChannel): Artifact {
return (channel as any)._object;
}
async pathAfterFinished(): Promise<string | null> {
if (this._isRemote)
throw new Error(`Path is not available when using browserType.connect(). Use saveAs() to save a local copy.`);
if (this._connection.isRemote())
throw new Error(`Path is not available when connecting remotely. Use saveAs() to save a local copy.`);
return this._wrapApiCall(async (channel: channels.ArtifactChannel) => {
return (await channel.pathAfterFinished()).value || null;
});
@ -38,7 +36,7 @@ export class Artifact extends ChannelOwner<channels.ArtifactChannel, channels.Ar
async saveAs(path: string): Promise<void> {
return this._wrapApiCall(async (channel: channels.ArtifactChannel) => {
if (!this._isRemote) {
if (!this._connection.isRemote()) {
await channel.saveAs({ path });
return;
}

View File

@ -29,7 +29,7 @@ export class Browser extends ChannelOwner<channels.BrowserChannel, channels.Brow
readonly _contexts = new Set<BrowserContext>();
private _isConnected = true;
private _closedPromise: Promise<void>;
_remoteType: 'owns-connection' | 'uses-connection' | null = null;
_shouldCloseConnectionOnClose = false;
private _browserType!: BrowserType;
readonly _name: string;
@ -109,7 +109,7 @@ export class Browser extends ChannelOwner<channels.BrowserChannel, channels.Brow
async close(): Promise<void> {
try {
await this._wrapApiCall(async (channel: channels.BrowserChannel) => {
if (this._remoteType === 'owns-connection')
if (this._shouldCloseConnectionOnClose)
this._connection.close();
else
await channel.close();

View File

@ -346,8 +346,6 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
if (this._options.recordHar) {
const har = await this._channel.harExport();
const artifact = Artifact.from(har.artifact);
if (this.browser()?._remoteType)
artifact._isRemote = true;
await artifact.saveAs(this._options.recordHar.path);
await artifact.delete();
}

View File

@ -134,6 +134,7 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel, chann
const { pipe } = await channel.connect({ wsEndpoint, headers: params.headers, slowMo: params.slowMo, timeout: params.timeout });
const closePipe = () => pipe.close().catch(() => {});
const connection = new Connection(closePipe);
connection.markAsRemote();
const onPipeClosed = () => {
// Emulate all pages, contexts and the browser closing upon disconnect.
@ -173,7 +174,7 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel, chann
}
browser = Browser.from(playwright._initializer.preLaunchedBrowser!);
browser._logger = logger;
browser._remoteType = 'owns-connection';
browser._shouldCloseConnectionOnClose = true;
browser._setBrowserType((playwright as any)[browser._name]);
browser.on(Events.Browser.Disconnected, () => {
playwright._cleanup();
@ -221,7 +222,6 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel, chann
const browser = Browser.from(result.browser);
if (result.defaultContext)
browser._contexts.add(BrowserContext.from(result.defaultContext));
browser._remoteType = 'uses-connection';
browser._logger = logger;
browser._setBrowserType(this);
return browser;

View File

@ -62,6 +62,7 @@ export class Connection extends EventEmitter {
private _rootObject: Root;
private _disconnectedErrorMessage: string | undefined;
private _onClose?: () => void;
private _isRemote = false;
constructor(onClose?: () => void) {
super();
@ -69,6 +70,14 @@ export class Connection extends EventEmitter {
this._onClose = onClose;
}
markAsRemote() {
this._isRemote = true;
}
isRemote() {
return this._isRemote;
}
async initializePlaywright(): Promise<Playwright> {
return await this._rootObject.initialize();
}

View File

@ -128,7 +128,6 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
this._channel.on('domcontentloaded', () => this.emit(Events.Page.DOMContentLoaded, this));
this._channel.on('download', ({ url, suggestedFilename, artifact }) => {
const artifactObject = Artifact.from(artifact);
artifactObject._isRemote = !!this._browserContext._browser && !!this._browserContext._browser._remoteType;
this.emit(Events.Page.Download, new Download(this, url, suggestedFilename, artifactObject));
});
this._channel.on('fileChooser', ({ element, isMultiple }) => this.emit(Events.Page.FileChooser, new FileChooser(this, ElementHandle.from(element), isMultiple)));
@ -250,7 +249,7 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
private _forceVideo(): Video {
if (!this._video)
this._video = new Video(this);
this._video = new Video(this, this._connection);
return this._video;
}

View File

@ -57,8 +57,6 @@ export class Tracing implements api.Tracing {
if (!result.artifact)
return;
const artifact = Artifact.from(result.artifact);
if (this._context._browser?._remoteType)
artifact._isRemote = true;
await artifact.saveAs(path!);
await artifact.delete();
}

View File

@ -17,15 +17,15 @@
import { Page } from './page';
import * as api from '../../types/types';
import { Artifact } from './artifact';
import { Connection } from './connection';
export class Video implements api.Video {
private _artifact: Promise<Artifact | null> | null = null;
private _artifactCallback = (artifact: Artifact) => {};
private _isRemote = false;
constructor(page: Page) {
const browser = page.context()._browser;
this._isRemote = !!browser && !!browser._remoteType;
constructor(page: Page, connection: Connection) {
this._isRemote = connection.isRemote();
this._artifact = Promise.race([
new Promise<Artifact>(f => this._artifactCallback = f),
page._closedOrCrashedPromise.then(() => null),
@ -33,13 +33,12 @@ export class Video implements api.Video {
}
_artifactReady(artifact: Artifact) {
artifact._isRemote = this._isRemote;
this._artifactCallback(artifact);
}
async path(): Promise<string> {
if (this._isRemote)
throw new Error(`Path is not available when using browserType.connect(). Use saveAs() to save a local copy.`);
throw new Error(`Path is not available when connecting remotely. Use saveAs() to save a local copy.`);
const artifact = await this._artifact;
if (!artifact)
throw new Error('Page did not produce any video frames');

View File

@ -32,6 +32,7 @@ export class PlaywrightClient {
static async connect(options: PlaywrightClientConnectOptions): Promise<PlaywrightClient> {
const { wsEndpoint, timeout = 30000 } = options;
const connection = new Connection();
connection.markAsRemote();
const ws = new WebSocket(wsEndpoint);
const waitForNextTask = makeWaitForNextTask();
connection.onmessage = message => {

View File

@ -389,7 +389,7 @@ test('should saveAs videos from remote browser', async ({ browserType, startRemo
await page.video().saveAs(savedAsPath);
expect(fs.existsSync(savedAsPath)).toBeTruthy();
const error = await page.video().path().catch(e => e);
expect(error.message).toContain('Path is not available when using browserType.connect(). Use saveAs() to save a local copy.');
expect(error.message).toContain('Path is not available when connecting remotely. Use saveAs() to save a local copy.');
});
test('should be able to connect 20 times to a single server without warnings', async ({ browserType, startRemoteServer }) => {
@ -428,7 +428,7 @@ test('should save download', async ({ server, browserType, startRemoteServer },
expect(fs.existsSync(nestedPath)).toBeTruthy();
expect(fs.readFileSync(nestedPath).toString()).toBe('Hello world');
const error = await download.path().catch(e => e);
expect(error.message).toContain('Path is not available when using browserType.connect(). Use saveAs() to save a local copy.');
expect(error.message).toContain('Path is not available when connecting remotely. Use saveAs() to save a local copy.');
await browser.close();
});

View File

@ -171,7 +171,9 @@ it('should support offline option', async ({ server, launchPersistent }) => {
expect(error).toBeTruthy();
});
it('should support acceptDownloads option', async ({ server, launchPersistent }) => {
it('should support acceptDownloads option', async ({ server, launchPersistent, mode }) => {
it.skip(mode === 'service', 'download.path() is not avaialble in remote mode');
const { page } = await launchPersistent({ acceptDownloads: true });
server.setRoute('/download', (req, res) => {
res.setHeader('Content-Type', 'application/octet-stream');

View File

@ -21,6 +21,8 @@ import crypto from 'crypto';
import type { Download } from 'playwright-core';
it.describe('download event', () => {
it.skip(({ mode }) => mode === 'service', 'download.path() is not available in remote mode');
it.beforeEach(async ({ server }) => {
server.setRoute('/download', (req, res) => {
res.setHeader('Content-Type', 'application/octet-stream');
@ -156,20 +158,6 @@ it.describe('download event', () => {
await page.close();
});
it('should save to user-specified path', async ({ browser, server }, testInfo) => {
const page = await browser.newPage({ acceptDownloads: true });
await page.setContent(`<a href="${server.PREFIX}/download">download</a>`);
const [ download ] = await Promise.all([
page.waitForEvent('download'),
page.click('a')
]);
const userPath = testInfo.outputPath('download.txt');
await download.saveAs(userPath);
expect(fs.existsSync(userPath)).toBeTruthy();
expect(fs.readFileSync(userPath).toString()).toBe('Hello world');
await page.close();
});
it('should save to user-specified path without updating original path', async ({ browser, server }, testInfo) => {
const page = await browser.newPage({ acceptDownloads: true });
await page.setContent(`<a href="${server.PREFIX}/download">download</a>`);
@ -621,6 +609,30 @@ it('should be able to download a inline PDF file', async ({ browser, server, ass
await page.close();
});
it('should save to user-specified path', async ({ browser, server, mode }, testInfo) => {
server.setRoute('/download', (req, res) => {
res.setHeader('Content-Type', 'application/octet-stream');
res.setHeader('Content-Disposition', 'attachment');
res.end(`Hello world`);
});
const page = await browser.newPage({ acceptDownloads: true });
await page.setContent(`<a href="${server.PREFIX}/download">download</a>`);
const [ download ] = await Promise.all([
page.waitForEvent('download'),
page.click('a')
]);
if (mode === 'service') {
const error = await download.path().catch(e => e);
expect(error.message).toContain('Path is not available when connecting remotely. Use saveAs() to save a local copy.');
}
const userPath = testInfo.outputPath('download.txt');
await download.saveAs(userPath);
expect(fs.existsSync(userPath)).toBeTruthy();
expect(fs.readFileSync(userPath).toString()).toBe('Hello world');
await page.close();
});
async function assertDownloadToPDF(download: Download, filePath: string) {
expect(download.suggestedFilename()).toBe(path.basename(filePath));
const stream = await download.createReadStream();

View File

@ -19,6 +19,8 @@ import fs from 'fs';
import path from 'path';
it.describe('downloads path', () => {
it.skip(({ mode }) => mode === 'service', 'download.path() is not available in remote mode');
it.beforeEach(async ({ server }) => {
server.setRoute('/download', (req, res) => {
res.setHeader('Content-Type', 'application/octet-stream');

View File

@ -152,6 +152,7 @@ function expectRedFrames(videoFile: string, size: { width: number, height: numbe
it.describe('screencast', () => {
it.slow();
it.skip(({ mode }) => mode === 'service', 'video.path() is not avaialble in remote mode');
it('videoSize should require videosPath', async ({ browser }) => {
const error = await browser.newContext({ videoSize: { width: 100, height: 100 } }).catch(e => e);
@ -218,26 +219,6 @@ it.describe('screencast', () => {
expect(fs.existsSync(path)).toBeTruthy();
});
it('should saveAs video', async ({ browser }, testInfo) => {
const videosPath = testInfo.outputPath('');
const size = { width: 320, height: 240 };
const context = await browser.newContext({
recordVideo: {
dir: videosPath,
size
},
viewport: size,
});
const page = await context.newPage();
await page.evaluate(() => document.body.style.backgroundColor = 'red');
await page.waitForTimeout(1000);
await context.close();
const saveAsPath = testInfo.outputPath('my-video.webm');
await page.video().saveAs(saveAsPath);
expect(fs.existsSync(saveAsPath)).toBeTruthy();
});
it('saveAs should throw when no video frames', async ({ browser, browserName }, testInfo) => {
const videosPath = testInfo.outputPath('');
const size = { width: 320, height: 240 };
@ -668,5 +649,26 @@ it.describe('screencast', () => {
const files = fs.readdirSync(videoDir);
expect(files.length).toBe(1);
});
});
it('should saveAs video', async ({ browser }, testInfo) => {
it.slow();
const videosPath = testInfo.outputPath('');
const size = { width: 320, height: 240 };
const context = await browser.newContext({
recordVideo: {
dir: videosPath,
size
},
viewport: size,
});
const page = await context.newPage();
await page.evaluate(() => document.body.style.backgroundColor = 'red');
await page.waitForTimeout(1000);
await context.close();
const saveAsPath = testInfo.outputPath('my-video.webm');
await page.video().saveAs(saveAsPath);
expect(fs.existsSync(saveAsPath)).toBeTruthy();
});