diff --git a/browsers.json b/browsers.json index 3936dda0e7..c5f1a041de 100644 --- a/browsers.json +++ b/browsers.json @@ -8,17 +8,17 @@ }, { "name": "firefox", - "revision": "1257", + "revision": "1258", "installByDefault": true }, { "name": "firefox-stable", - "revision": "1247", + "revision": "1248", "installByDefault": false }, { "name": "webkit", - "revision": "1472", + "revision": "1476", "installByDefault": true, "revisionOverrides": { "mac10.14": "1443" diff --git a/src/server/chromium/crPage.ts b/src/server/chromium/crPage.ts index d90d438075..1e849e53d3 100644 --- a/src/server/chromium/crPage.ts +++ b/src/server/chromium/crPage.ts @@ -289,13 +289,13 @@ export class CRPage implements PageDelegate { return this._sessionForHandle(handle)._scrollRectIntoViewIfNeeded(handle, rect); } - async setScreencastEnabled(enabled: boolean): Promise { - if (enabled) { + async setScreencastOptions(options: { width: number, height: number, quality: number } | null): Promise { + if (options) { await this._mainFrameSession._startScreencast(this, { format: 'jpeg', - quality: 90, - maxWidth: 800, - maxHeight: 600, + quality: options.quality, + maxWidth: options.width, + maxHeight: options.height }); } else { await this._mainFrameSession._stopScreencast(this); diff --git a/src/server/firefox/ffPage.ts b/src/server/firefox/ffPage.ts index 1d3edf5b3f..bbc104d78d 100644 --- a/src/server/firefox/ffPage.ts +++ b/src/server/firefox/ffPage.ts @@ -479,9 +479,9 @@ export class FFPage implements PageDelegate { }); } - async setScreencastEnabled(enabled: boolean): Promise { - if (enabled) { - const { screencastId } = await this._session.send('Page.startScreencast', { width: 800, height: 600, quality: 70 }); + async setScreencastOptions(options: { width: number, height: number, quality: number } | null): Promise { + if (options) { + const { screencastId } = await this._session.send('Page.startScreencast', options); this._screencastId = screencastId; } else { await this._session.send('Page.stopScreencast'); diff --git a/src/server/page.ts b/src/server/page.ts index a658a7ca17..ce53829e94 100644 --- a/src/server/page.ts +++ b/src/server/page.ts @@ -70,7 +70,7 @@ export interface PageDelegate { getBoundingBox(handle: dom.ElementHandle): Promise; getFrameElement(frame: frames.Frame): Promise; scrollRectIntoViewIfNeeded(handle: dom.ElementHandle, rect?: types.Rect): Promise<'error:notvisible' | 'error:notconnected' | 'done'>; - setScreencastEnabled(enabled: boolean): Promise; + setScreencastOptions(options: { width: number, height: number, quality: number } | null): Promise; getAccessibilityTree(needle?: dom.ElementHandle): Promise<{tree: accessibility.AXNode, needle: accessibility.AXNode | null}>; pdf?: (options?: types.PDFOptions) => Promise; @@ -501,8 +501,8 @@ export class Page extends SdkObject { return this._pageBindings.get(identifier) || this._browserContext._pageBindings.get(identifier); } - setScreencastEnabled(enabled: boolean) { - this._delegate.setScreencastEnabled(enabled).catch(e => debugLogger.log('error', e)); + setScreencastOptions(options: { width: number, height: number, quality: number } | null) { + this._delegate.setScreencastOptions(options).catch(e => debugLogger.log('error', e)); } } diff --git a/src/server/trace/recorder/tracing.ts b/src/server/trace/recorder/tracing.ts index a8658bc15c..58f6a5676f 100644 --- a/src/server/trace/recorder/tracing.ts +++ b/src/server/trace/recorder/tracing.ts @@ -102,7 +102,7 @@ export class Tracing implements InstrumentationListener { for (const { sdkObject, metadata } of this._pendingCalls.values()) await this.onAfterCall(sdkObject, metadata); for (const page of this._context.pages()) - page.setScreencastEnabled(false); + page.setScreencastOptions(null); // Ensure all writes are finished. await this._appendEventChain; @@ -185,7 +185,7 @@ export class Tracing implements InstrumentationListener { }; this._appendTraceEvent(event); if (screenshots) - page.setScreencastEnabled(true); + page.setScreencastOptions({ width: 800, height: 600, quality: 90 }); this._eventListeners.push( helper.addEventListener(page, Page.Events.Dialog, (dialog: Dialog) => { diff --git a/src/server/webkit/protocol.ts b/src/server/webkit/protocol.ts index 9eef817b84..14904adfdb 100644 --- a/src/server/webkit/protocol.ts +++ b/src/server/webkit/protocol.ts @@ -8050,6 +8050,7 @@ the top of the viewport and Y increases as it proceeds towards the bottom of the file: string; width: number; height: number; + toolbarHeight: number; scale?: number; } export type startVideoReturnValue = { @@ -8071,6 +8072,7 @@ the top of the viewport and Y increases as it proceeds towards the bottom of the export type startScreencastParameters = { width: number; height: number; + toolbarHeight: number; quality: number; } export type startScreencastReturnValue = { diff --git a/src/server/webkit/wkPage.ts b/src/server/webkit/wkPage.ts index f08655e2ec..b06d32e172 100644 --- a/src/server/webkit/wkPage.ts +++ b/src/server/webkit/wkPage.ts @@ -739,6 +739,12 @@ export class WKPage implements PageDelegate { await this._session.send('Page.setDefaultBackgroundColorOverride', { color }); } + private _toolbarHeight(): number { + if (this._page._browserContext._browser?.options.headful) + return hostPlatform.startsWith('10.15') ? 55 : 59; + return 0; + } + private async _startVideo(options: types.PageScreencastOptions): Promise { assert(!this._recordingVideoFile); const START_VIDEO_PROTOCOL_COMMAND = hostPlatform === 'mac10.14' ? 'Screencast.start' : 'Screencast.startVideo'; @@ -746,6 +752,7 @@ export class WKPage implements PageDelegate { file: options.outputFile, width: options.width, height: options.height, + toolbarHeight: this._toolbarHeight() }); this._recordingVideoFile = options.outputFile; this._browserContext._browser._videoStarted(this._browserContext, screencastId, options.outputFile, this.pageOrError()); @@ -827,9 +834,10 @@ export class WKPage implements PageDelegate { }); } - async setScreencastEnabled(enabled: boolean): Promise { - if (enabled) { - const { generation } = await this._pageProxySession.send('Screencast.startScreencast', { width: 800, height: 600, quality: 70 }); + async setScreencastOptions(options: { width: number, height: number, quality: number } | null): Promise { + if (options) { + const so = { ...options, toolbarHeight: this._toolbarHeight() }; + const { generation } = await this._pageProxySession.send('Screencast.startScreencast', so); this._screencastGeneration = generation; } else { await this._pageProxySession.send('Screencast.stopScreencast'); diff --git a/tests/tracing.spec.ts b/tests/tracing.spec.ts index 1e98bd9be3..d722e032ca 100644 --- a/tests/tracing.spec.ts +++ b/tests/tracing.spec.ts @@ -15,14 +15,16 @@ */ import path from 'path'; -import { expect, contextTest as test } from './config/browserTest'; +import { expect, contextTest as test, browserTest } from './config/browserTest'; import yauzl from 'yauzl'; import removeFolder from 'rimraf'; +import jpeg from 'jpeg-js'; const traceDir = path.join(__dirname, '..', 'test-results', 'trace-' + process.env.FOLIO_WORKER_INDEX); test.useOptions({ traceDir }); -test.beforeEach(async () => { +test.beforeEach(async ({ browserName, headful }) => { + test.fixme(browserName === 'chromium' && headful, 'Chromium screencast on headful has a min width issue'); await new Promise(f => removeFolder(traceDir, f)); }); @@ -99,6 +101,61 @@ test('should collect two traces', async ({ context, page, server }, testInfo) => } }); +for (const params of [ + { + id: 'fit', + width: 500, + height: 500, + }, { + id: 'crop', + width: 400, // Less than 450 to test firefox + height: 800, + }, { + id: 'scale', + width: 1024, + height: 768, + }]) { + browserTest(`should produce screencast frames ${params.id}`, async ({ video, contextFactory, browserName }, testInfo) => { + browserTest.fixme(browserName === 'chromium' && video, 'Same screencast resolution conflicts'); + const scale = Math.min(800 / params.width, 600 / params.height, 1); + const previewWidth = params.width * scale; + const previewHeight = params.height * scale; + + const context = await contextFactory({ viewport: { width: params.width, height: params.height }}); + await (context as any)._tracing.start({ name: 'test', screenshots: true, snapshots: true }); + const page = await context.newPage(); + await page.setContent(''); + // Make sure we have a chance to paint. + for (let i = 0; i < 10; ++i) + await page.evaluate(() => new Promise(requestAnimationFrame)); + await (context as any)._tracing.stop(); + await (context as any)._tracing.export(testInfo.outputPath('trace.zip')); + + const { events, resources } = await parseTrace(testInfo.outputPath('trace.zip')); + const frames = events.filter(e => e.type === 'screencast-frame'); + // Check all frame sizes + for (const frame of frames) { + expect(frame.width).toBe(params.width); + expect(frame.height).toBe(params.height); + const buffer = resources.get('resources/' + frame.sha1); + const image = jpeg.decode(buffer); + expect(image.width).toBe(previewWidth); + expect(image.height).toBe(previewHeight); + } + + // Check last frame content. + const frame = frames[frames.length - 1]; // pick last frame. + const buffer = resources.get('resources/' + frame.sha1); + const image = jpeg.decode(buffer); + expect(image.data.byteLength).toBe(previewWidth * previewHeight * 4); + expectRed(image.data, previewWidth * previewHeight * 4 / 2 + previewWidth * 4 / 2); // center is red + expectBlue(image.data, previewWidth * 5 * 4 + previewWidth * 4 / 2); // top + expectBlue(image.data, previewWidth * (previewHeight - 5) * 4 + previewWidth * 4 / 2); // bottom + expectBlue(image.data, previewWidth * previewHeight * 4 / 2 + 5 * 4); // left + expectBlue(image.data, previewWidth * previewHeight * 4 / 2 + (previewWidth - 5) * 4); // right + }); +} + async function parseTrace(file: string): Promise<{ events: any[], resources: Map }> { const entries = await new Promise(f => { const entries: Promise[] = []; @@ -128,4 +185,26 @@ async function parseTrace(file: string): Promise<{ events: any[], resources: Map events, resources, }; -} \ No newline at end of file +} + +function expectRed(pixels: Buffer, offset: number) { + const r = pixels.readUInt8(offset); + const g = pixels.readUInt8(offset + 1); + const b = pixels.readUInt8(offset + 2); + const a = pixels.readUInt8(offset + 3); + expect(r).toBeGreaterThan(200); + expect(g).toBeLessThan(70); + expect(b).toBeLessThan(70); + expect(a).toBe(255); +} + +function expectBlue(pixels: Buffer, offset: number) { + const r = pixels.readUInt8(offset); + const g = pixels.readUInt8(offset + 1); + const b = pixels.readUInt8(offset + 2); + const a = pixels.readUInt8(offset + 3); + expect(r).toBeLessThan(70); + expect(g).toBeLessThan(70); + expect(b).toBeGreaterThan(200); + expect(a).toBe(255); +} diff --git a/tests/screencast.spec.ts b/tests/video.spec.ts similarity index 100% rename from tests/screencast.spec.ts rename to tests/video.spec.ts