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
2 changes: 0 additions & 2 deletions src/dispatchers/artifactDispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ export class ArtifactDispatcher extends Dispatcher<Artifact, channels.ArtifactIn
}
try {
const readable = fs.createReadStream(localPath);
await new Promise(f => readable.on('readable', f));
const stream = new StreamDispatcher(this._scope, readable);
// Resolve with a stream, so that client starts saving the data.
resolve({ stream });
Expand All @@ -83,7 +82,6 @@ export class ArtifactDispatcher extends Dispatcher<Artifact, channels.ArtifactIn
if (!fileName)
return {};
const readable = fs.createReadStream(fileName);
await new Promise(f => readable.on('readable', f));
return { stream: new StreamDispatcher(this._scope, readable) };
}

Expand Down
16 changes: 15 additions & 1 deletion src/dispatchers/streamDispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,26 @@ import * as stream from 'stream';
import { createGuid } from '../utils/utils';

export class StreamDispatcher extends Dispatcher<{ guid: string, stream: stream.Readable }, channels.StreamInitializer> implements channels.StreamChannel {
private _ended: boolean = false;
constructor(scope: DispatcherScope, stream: stream.Readable) {
super(scope, { guid: createGuid(), stream }, 'Stream', {});
// In Node v12.9.0+ we can use readableEnded.
stream.once('end', () => this._ended = true);
stream.once('error', () => this._ended = true);
}

async read(params: channels.StreamReadParams): Promise<channels.StreamReadResult> {
const buffer = this._object.stream.read(Math.min(this._object.stream.readableLength, params.size || this._object.stream.readableLength));
const stream = this._object.stream;
if (this._ended)
return { binary: '' };
if (!stream.readableLength) {
await new Promise((fulfill, reject) => {
stream.once('readable', fulfill);
stream.once('end', fulfill);
stream.once('error', reject);
});
}
const buffer = stream.read(Math.min(stream.readableLength, params.size || stream.readableLength));
return { binary: buffer ? buffer.toString('base64') : '' };
}

Expand Down
31 changes: 31 additions & 0 deletions tests/download.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { test as it, expect } from './config/browserTest';
import fs from 'fs';
import path from 'path';
import util from 'util';
import crypto from 'crypto';

it.describe('download event', () => {
it.beforeEach(async ({server}) => {
Expand Down Expand Up @@ -433,4 +434,34 @@ it.describe('download event', () => {
expect(downloadPath).toBe(null);
expect(saveError.message).toContain('File deleted upon browser context closure.');
});

it('should download large binary.zip', async ({browser, server, browserName}, testInfo) => {
const zipFile = testInfo.outputPath('binary.zip');
const content = crypto.randomBytes(1 << 20);
fs.writeFileSync(zipFile, content);
server.setRoute('/binary.zip', (req, res) => server.serveFile(req, res, zipFile));

const page = await browser.newPage({ acceptDownloads: true });
await page.goto(server.PREFIX + '/empty.html');
await page.setContent(`<a href="${server.PREFIX}/binary.zip" download="binary.zip">download</a>`);
const [ download ] = await Promise.all([
page.waitForEvent('download'),
page.click('a')
]);
const downloadPath = await download.path();
const fileContent = fs.readFileSync(downloadPath);
expect(fileContent.byteLength).toBe(content.byteLength);
expect(fileContent.equals(content)).toBe(true);

const stream = await download.createReadStream();
const data = await new Promise<Buffer>((fulfill, reject) => {
const bufs = [];
stream.on('data', d => bufs.push(d));
stream.on('error', reject);
stream.on('end', () => fulfill(Buffer.concat(bufs)));
});
expect(data.byteLength).toBe(content.byteLength);
expect(data.equals(content)).toBe(true);
await page.close();
});
});