/**
 * Copyright Microsoft Corporation. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import fs from 'fs';
import path from 'path';
import { jpegjs } from 'playwright-core/lib/utilsBundle';
import { expect, browserTest as it } from '../config/browserTest';
import { ensureSomeFrames, parseTraceRaw } from '../config/utils';
import { VideoPlayer } from './videoPlayer';

type Pixel = { r: number, g: number, b: number, alpha: number };
type PixelPredicate = (pixel: Pixel) => boolean;

function isAlmostRed({ r, g, b, alpha }: Pixel): boolean {
  return r > 185 && g < 70 && b < 70 && alpha === 255;
}

function isAlmostBlack({ r, g, b, alpha }: Pixel): boolean {
  return r < 70 && g < 70 && b < 70 && alpha === 255;
}

function isAlmostGray({ r, g, b, alpha }: Pixel): boolean {
  return r > 70 && r < 185 &&
    g > 70 && g < 185 &&
    b > 70 && b < 185 &&
    alpha === 255;
}

function findPixel(pixels: Buffer, pixelPredicate: PixelPredicate): Pixel|undefined {
  for (let i = 0, n = pixels.length; i < n; i += 4) {
    const pixel = {
      r: pixels[i],
      g: pixels[i + 1],
      b: pixels[i + 2],
      alpha: pixels[i + 3],
    };
    if (pixelPredicate(pixel))
      return pixel;
  }
  return undefined;
}

function everyPixel(pixels: Buffer, pixelPredicate: PixelPredicate) {
  const badPixel = findPixel(pixels, pixel => !pixelPredicate(pixel));
  return !badPixel;
}

function expectAll(pixels: Buffer, pixelPredicate: PixelPredicate) {
  const badPixel = findPixel(pixels, pixel => !pixelPredicate(pixel));
  if (!badPixel)
    return;
  const rgba = [badPixel.r, badPixel.g, badPixel.b, badPixel.alpha].join(', ');
  throw new Error([
    `Expected all pixels to satisfy ${pixelPredicate.name}, found bad pixel (${rgba})`,
    `Actual pixels=[${pixels.join(',')}]`,
  ].join('\n'));
}

function findVideos(videoDir: string) {
  const files = fs.readdirSync(videoDir);
  return files.filter(file => file.endsWith('webm')).map(file => path.join(videoDir, file));
}

function expectRedFrames(videoFile: string, size: { width: number, height: number }) {
  expectFrames(videoFile, size, isAlmostRed);
}

function expectFrames(videoFile: string, size: { width: number, height: number }, pixelPredicate: PixelPredicate) {
  const videoPlayer = new VideoPlayer(videoFile);
  const duration = videoPlayer.duration;
  expect(duration).toBeGreaterThan(0);

  expect(videoPlayer.videoWidth).toBe(size.width);
  expect(videoPlayer.videoHeight).toBe(size.height);

  {
    const pixels = videoPlayer.seekLastFrame().data;
    expectAll(pixels, pixelPredicate);
  }
  {
    const pixels = videoPlayer.seekLastFrame({ x: size.width - 20, y: 10 }).data;
    expectAll(pixels, pixelPredicate);
  }
}

it.skip(({ video }) => video === 'on', 'conflicts with built-in video recording');

it.describe('screencast', () => {
  it.slow();
  it.skip(({ mode }) => mode !== 'default', 'video.path() is not available in remote mode');

  it('should not have video by default', async ({ page }) => {
    expect(page.video()).toBeNull();
  });

  it('should not throw without recordVideo.dir', async ({ browser }) => {
    await browser.newContext({ recordVideo: {} });
  });

  it('should capture static page', async ({ browser, browserName, trace, headless, isWindows }, testInfo) => {
    // Firefox does not have a mobile variant and has a large minimum size (500 on windows and 450 elsewhere).
    const size = browserName === 'firefox' ? { width: 500, height: 400 } : { width: 320, height: 240 };
    const context = await browser.newContext({
      recordVideo: {
        dir: testInfo.outputPath(''),
        size
      },
      viewport: size,
    });
    const page = await context.newPage();

    await page.evaluate(() => document.body.style.backgroundColor = 'red');
    await ensureSomeFrames(page);
    await context.close();

    const videoFile = await page.video().path();
    expectRedFrames(videoFile, size);
  });

  it('should continue recording main page after popup closes', async ({ browser, browserName }, testInfo) => {
    it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30837' });
    // Firefox does not have a mobile variant and has a large minimum size (500 on windows and 450 elsewhere).
    const size = browserName === 'firefox' ? { width: 500, height: 400 } : { width: 320, height: 240 };
    const context = await browser.newContext({
      recordVideo: {
        dir: testInfo.outputPath(''),
        size
      },
      viewport: size,
    });
    const page = await context.newPage();
    await page.setContent('<a target=_blank href="about:blank">clickme</a>');
    const [popup] = await Promise.all([
      page.waitForEvent('popup'),
      await page.click('a'),
    ]);
    await popup.close();

    await page.evaluate(() => {
      document.body.textContent = ''; // remove link
      document.body.style.backgroundColor = 'red';
    });
    await ensureSomeFrames(page);
    await context.close();

    const videoFile = await page.video().path();
    expectRedFrames(videoFile, size);
  });

  it('should expose video path', 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');
    const path = await page.video()!.path();
    expect(path).toContain(videosPath);
    await context.close();
    expect(fs.existsSync(path)).toBeTruthy();
  });

  it('should delete 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();
    const deletePromise = page.video().delete();
    await page.evaluate(() => document.body.style.backgroundColor = 'red');
    await ensureSomeFrames(page);
    await context.close();

    const videoPath = await page.video().path();
    await deletePromise;
    expect(fs.existsSync(videoPath)).toBeFalsy();
  });

  it('should expose video path blank page', 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();
    const path = await page.video()!.path();
    expect(path).toContain(videosPath);
    await context.close();
    expect(fs.existsSync(path)).toBeTruthy();
  });

  it('should work with weird screen resolution', async ({ browser }, testInfo) => {
    it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/22069' });
    const videosPath = testInfo.outputPath('');
    const size = { width: 1904, height: 609 };
    const context = await browser.newContext({
      recordVideo: {
        dir: videosPath,
        size
      },
      viewport: size,
    });
    const page = await context.newPage();
    const path = await page.video()!.path();
    expect(path).toContain(videosPath);
    await context.close();
    expect(fs.existsSync(path)).toBeTruthy();
  });

  it('should work with relative path for recordVideo.dir', async ({ browser }, testInfo) => {
    it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/27086' });
    const videosPath = path.relative(process.cwd(), testInfo.outputPath(''));
    const size = { width: 320, height: 240 };
    const context = await browser.newContext({
      recordVideo: {
        dir: videosPath,
        size
      },
      viewport: size,
    });
    const page = await context.newPage();
    const videoPath = await page.video()!.path();
    await context.close();
    expect(fs.existsSync(videoPath)).toBeTruthy();
  });

  it('should expose video path blank popup', 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();
    const [popup] = await Promise.all([
      page.waitForEvent('popup'),
      page.evaluate('window.open("about:blank")')
    ]);
    const path = await popup.video()!.path();
    expect(path).toContain(videosPath);
    await context.close();
    expect(fs.existsSync(path)).toBeTruthy();
  });

  it('should capture navigation', async ({ browser, browserName, server, trace }, testInfo) => {
    const context = await browser.newContext({
      recordVideo: {
        dir: testInfo.outputPath(''),
        size: { width: 1280, height: 720 }
      },
    });
    const page = await context.newPage();

    await page.goto(server.PREFIX + '/background-color.html#rgb(0,0,0)');
    await ensureSomeFrames(page);
    await page.goto(server.CROSS_PROCESS_PREFIX + '/background-color.html#rgb(100,100,100)');
    await ensureSomeFrames(page);
    await context.close();

    const videoFile = await page.video().path();
    const videoPlayer = new VideoPlayer(videoFile);
    const duration = videoPlayer.duration;
    expect(duration).toBeGreaterThan(0);

    {
      // Find a frame with all almost-black pixels.
      const frame = videoPlayer.findFrame(pixels => everyPixel(pixels, isAlmostBlack));
      expect(frame).not.toBeUndefined();
    }

    {
      const pixels = videoPlayer.seekLastFrame().data;
      expectAll(pixels, isAlmostGray);
    }
  });

  it('should capture css transformation', async ({ browser, server, headless, browserName, platform, trace }, testInfo) => {
    it.fixme(!headless, 'Fails on headed');
    it.fixme(browserName === 'webkit' && platform === 'win32');

    const size = { width: 600, height: 400 };
    // Set viewport equal to screencast frame size to avoid scaling.
    const context = await browser.newContext({
      recordVideo: {
        dir: testInfo.outputPath(''),
        size,
      },
      viewport: size,
    });
    const page = await context.newPage();

    await page.goto(server.PREFIX + '/rotate-z.html');
    await ensureSomeFrames(page);
    await context.close();

    const videoFile = await page.video().path();
    const videoPlayer = new VideoPlayer(videoFile);
    const duration = videoPlayer.duration;
    expect(duration).toBeGreaterThan(0);

    {
      const pixels = videoPlayer.seekLastFrame({ x: 95, y: 45 }).data;
      expectAll(pixels, isAlmostRed);
    }
  });

  it('should work for popups', async ({ browser, server, browserName, trace }, testInfo) => {
    it.fixme(browserName === 'firefox', 'https://github.com/microsoft/playwright/issues/14557');
    const videosPath = testInfo.outputPath('');
    const size = { width: 600, height: 400 };
    const context = await browser.newContext({
      recordVideo: {
        dir: videosPath,
        size,
      },
      viewport: size,
    });

    const page = await context.newPage();
    await page.goto(server.EMPTY_PAGE);
    const [popup] = await Promise.all([
      page.waitForEvent('popup'),
      page.evaluate(() => { window.open('about:blank'); }),
    ]);
    await popup.evaluate(() => document.body.style.backgroundColor = 'red');
    await Promise.all([
      ensureSomeFrames(page),
      ensureSomeFrames(popup),
    ]);
    await context.close();

    const pageVideoFile = await page.video().path();
    const popupVideoFile = await popup.video().path();
    expect(pageVideoFile).not.toEqual(popupVideoFile);
    expectRedFrames(popupVideoFile, size);

    const videoFiles = findVideos(videosPath);
    expect(videoFiles.length).toBe(2);
  });

  it('should scale frames down to the requested size ', async ({ browser, browserName, server, headless, isHeadlessShell }, testInfo) => {
    it.fixme(!headless, 'Fails on headed');
    it.fixme(browserName === 'chromium' && !isHeadlessShell, 'Chromium (but not headless shell) has a min width issue');

    const context = await browser.newContext({
      recordVideo: {
        dir: testInfo.outputPath(''),
        // Set size to 1/2 of the viewport.
        size: { width: 320, height: 240 },
      },
      viewport: { width: 640, height: 480 },
    });
    const page = await context.newPage();

    await page.goto(server.PREFIX + '/checkerboard.html');
    // Update the picture to ensure enough frames are generated.
    await page.$eval('.container', container => {
      container.firstElementChild.classList.remove('red');
    });
    await ensureSomeFrames(page);
    await page.$eval('.container', container => {
      container.firstElementChild.classList.add('red');
    });
    await ensureSomeFrames(page);
    await context.close();

    const videoFile = await page.video().path();
    const videoPlayer = new VideoPlayer(videoFile);
    const duration = videoPlayer.duration;
    expect(duration).toBeGreaterThan(0);

    {
      const pixels = videoPlayer.seekLastFrame({ x: 10, y: 10 }).data;
      expectAll(pixels, isAlmostRed);
    }
    {
      const pixels = videoPlayer.seekLastFrame({ x: 300, y: 10 }).data;
      expectAll(pixels, isAlmostGray);
    }
    {
      const pixels = videoPlayer.seekLastFrame({ x: 10, y: 200 }).data;
      expectAll(pixels, isAlmostGray);
    }
    {
      const pixels = videoPlayer.seekLastFrame({ x: 300, y: 200 }).data;
      expectAll(pixels, isAlmostRed);
    }
  });

  it('should use viewport scaled down to fit into 800x800 as default size', async ({ browser }, testInfo) => {
    const size = { width: 1600, height: 1200 };
    const context = await browser.newContext({
      recordVideo: {
        dir: testInfo.outputPath(''),
      },
      viewport: size,
    });

    const page = await context.newPage();
    await ensureSomeFrames(page);
    await context.close();

    const videoFile = await page.video().path();
    const videoPlayer = new VideoPlayer(videoFile);
    expect(videoPlayer.videoWidth).toBe(800);
    expect(videoPlayer.videoHeight).toBe(600);
  });

  it('should be 800x450 by default', async ({ browser }, testInfo) => {
    const context = await browser.newContext({
      recordVideo: {
        dir: testInfo.outputPath(''),
      },
    });

    const page = await context.newPage();
    await ensureSomeFrames(page);
    await context.close();

    const videoFile = await page.video().path();
    const videoPlayer = new VideoPlayer(videoFile);
    expect(videoPlayer.videoWidth).toBe(800);
    expect(videoPlayer.videoHeight).toBe(450);
  });

  it('should be 800x600 with null viewport', async ({ browser, headless, browserName }, testInfo) => {
    it.fixme(browserName === 'firefox' && headless, 'Fails in headless on bots');

    const context = await browser.newContext({
      recordVideo: {
        dir: testInfo.outputPath(''),
      },
      viewport: null
    });

    const page = await context.newPage();
    await ensureSomeFrames(page);
    await context.close();

    const videoFile = await page.video().path();
    const videoPlayer = new VideoPlayer(videoFile);
    expect(videoPlayer.videoWidth).toBe(800);
    expect(videoPlayer.videoHeight).toBe(600);
  });

  it('should capture static page in persistent context @smoke', async ({ launchPersistent, browserName, trace, isMac }, testInfo) => {
    it.skip(browserName === 'webkit' && isMac && process.arch === 'arm64', 'Is only failing on self-hosted github actions runner on M1 mac; not reproducible locally');
    const size = { width: 600, height: 400 };
    const { context, page } = await launchPersistent({
      recordVideo: {
        dir: testInfo.outputPath(''),
        size,
      },
      viewport: size,
    });

    await page.evaluate(() => document.body.style.backgroundColor = 'red');
    await ensureSomeFrames(page);
    await context.close();

    const videoFile = await page.video().path();
    const videoPlayer = new VideoPlayer(videoFile);
    const duration = videoPlayer.duration;
    expect(duration).toBeGreaterThan(0);

    expect(videoPlayer.videoWidth).toBe(600);
    expect(videoPlayer.videoHeight).toBe(400);

    {
      const pixels = videoPlayer.seekLastFrame().data;
      expectAll(pixels, isAlmostRed);
    }
  });

  it('should emulate an iphone', async ({ contextFactory, playwright, browserName }, testInfo) => {
    it.skip(browserName === 'firefox', 'isMobile is not supported in Firefox');

    const device = playwright.devices['iPhone 6'];
    const context = await contextFactory({
      ...device,
      recordVideo: {
        dir: testInfo.outputPath(''),
      },
    });

    const page = await context.newPage();
    await ensureSomeFrames(page);
    await context.close();

    const videoFile = await page.video().path();
    const videoPlayer = new VideoPlayer(videoFile);
    expect(videoPlayer.videoWidth).toBe(374);
    expect(videoPlayer.videoHeight).toBe(666);
  });

  it('should throw on browser close', async ({ browserType }, testInfo) => {
    const size = { width: 320, height: 240 };
    const browser = await browserType.launch();
    const context = await browser.newContext({
      recordVideo: {
        dir: testInfo.outputPath(''),
        size,
      },
      viewport: size,
    });

    const page = await context.newPage();
    await ensureSomeFrames(page);
    await browser.close();

    const file = testInfo.outputPath('saved-video-');
    const saveResult = await page.video().saveAs(file).catch(e => e);
    expect(saveResult.message).toContain('browser has been closed');
  });

  it('should throw if browser dies', async ({ browserType }, testInfo) => {
    const size = { width: 320, height: 240 };
    const browser = await browserType.launch();

    const context = await browser.newContext({
      recordVideo: {
        dir: testInfo.outputPath(''),
        size,
      },
      viewport: size,
    });

    const page = await context.newPage();
    await ensureSomeFrames(page);
    await (browser as any)._channel.killForTests();

    const file = testInfo.outputPath('saved-video-');
    const saveResult = await page.video().saveAs(file).catch(e => e);
    expect(saveResult.message).toContain('rowser has been closed');
  });

  it('should wait for video to finish if page was closed', async ({ browserType }, testInfo) => {
    const size = { width: 320, height: 240 };
    const browser = await browserType.launch();

    const videoDir = testInfo.outputPath('');
    const context = await browser.newContext({
      recordVideo: {
        dir: videoDir,
        size,
      },
      viewport: size,
    });

    const page = await context.newPage();
    await ensureSomeFrames(page);
    await page.close();
    await context.close();
    await browser.close();

    const videoFiles = findVideos(videoDir);
    expect(videoFiles.length).toBe(1);
    const videoPlayer = new VideoPlayer(videoFiles[0]);
    expect(videoPlayer.videoWidth).toBe(320);
    expect(videoPlayer.videoHeight).toBe(240);
  });

  it('should close ffmpeg even if there were no frames', {
    annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/39872' }
  }, async ({ browserType }, testInfo) => {
    const size = { width: 320, height: 240 };
    const browser = await browserType.launch();

    const videoDir = testInfo.outputPath('');
    const context = await browser.newContext({
      recordVideo: {
        dir: videoDir,
        size,
      },
      viewport: size,
    });

    const page1 = await context.newPage();
    await page1.close();

    const page2 = await context.newPage();
    await page2.close();

    await context.close();
    await browser.close();

    const videoFiles = findVideos(videoDir);
    expect(videoFiles.length).toBe(2);
  });

  it('should not create video for internal pages', async ({ browser, server }, testInfo) => {
    server.setRoute('/empty.html', (req, res) => {
      res.setHeader('Set-Cookie', 'name=value');
      res.end();
    });

    const videoDir = testInfo.outputPath('');
    const context = await browser.newContext({
      recordVideo: {
        dir: videoDir
      }
    });

    const page = await context.newPage();
    await page.goto(server.EMPTY_PAGE);
    await ensureSomeFrames(page);

    const cookies = await context.cookies();
    expect(cookies.length).toBe(1);
    await context.storageState();
    await context.close();

    const files = fs.readdirSync(videoDir);
    expect(files.length).toBe(1);
  });

  it('should capture full viewport', async ({ browserType, browserName, isWindows, headless, isHeadlessShell }, testInfo) => {
    it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/22411' });
    it.fixme(browserName === 'chromium' && !isHeadlessShell, 'The square is not on the video');
    it.fixme(browserName === 'firefox' && isWindows, 'https://github.com/microsoft/playwright/issues/14405');
    const size = { width: 600, height: 400 };
    const browser = await browserType.launch();

    const videoDir = testInfo.outputPath('');
    const context = await browser.newContext({
      viewport: size,
      recordVideo: {
        dir: videoDir,
        size,
      },
    });

    const page = await context.newPage();
    await page.setContent(`<div style='margin: 0; background: red; position: fixed; right:0; bottom:0; width: 30; height: 30;'></div>`);
    await ensureSomeFrames(page);
    await page.close();
    await context.close();
    await browser.close();

    const videoFiles = findVideos(videoDir);
    expect(videoFiles.length).toBe(1);
    const videoPlayer = new VideoPlayer(videoFiles[0]);
    expect(videoPlayer.videoWidth).toBe(size.width);
    expect(videoPlayer.videoHeight).toBe(size.height);

    // Bottom right corner should be part of the red border.
    // However, headed browsers on mac have rounded corners, so offset by 10.
    const pixels = videoPlayer.seekLastFrame({ x: size.width - 20, y: size.height - 20 }).data;
    expectAll(pixels, isAlmostRed);
  });

  it('should capture full viewport on hidpi', async ({ browserType, browserName, headless, isWindows, isLinux, isHeadlessShell, channel }, testInfo) => {
    it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/22411' });
    it.fixme(browserName === 'chromium' && !isHeadlessShell, 'The square is not on the video');
    it.fixme(browserName === 'firefox' && isWindows, 'https://github.com/microsoft/playwright/issues/14405');
    it.fixme(browserName === 'webkit' && !headless && (isLinux || (isWindows && channel === 'webkit-wsl')), 'https://github.com/microsoft/playwright/issues/22617');
    const size = { width: 600, height: 400 };
    const browser = await browserType.launch();

    const videoDir = testInfo.outputPath('');
    const context = await browser.newContext({
      viewport: size,
      deviceScaleFactor: 3,
      recordVideo: {
        dir: videoDir,
        size,
      },
    });

    const page = await context.newPage();
    await page.setContent(`<div style='margin: 0; background: red; position: fixed; right:0; bottom:0; width: 30; height: 30;'></div>`);
    await ensureSomeFrames(page);
    await page.close();
    await context.close();
    await browser.close();

    const videoFiles = findVideos(videoDir);
    expect(videoFiles.length).toBe(1);
    const videoPlayer = new VideoPlayer(videoFiles[0]);
    expect(videoPlayer.videoWidth).toBe(size.width);
    expect(videoPlayer.videoHeight).toBe(size.height);

    // Bottom right corner should be part of the red border.
    // However, headed browsers on mac have rounded corners, so offset by 10.
    const pixels = videoPlayer.seekLastFrame({ x: size.width - 20, y: size.height - 20 }).data;
    expectAll(pixels, isAlmostRed);
  });

  it('should work with video+trace', async ({ browser, trace, headless, browserName, isHeadlessShell }, testInfo) => {
    it.skip(trace === 'on');
    it.fixme(!headless, 'different trace screencast image size on all browsers');
    it.fixme(browserName === 'chromium' && !isHeadlessShell, 'different trace screencast image size');

    const size = { width: 500, height: 400 };
    const traceFile = testInfo.outputPath('trace.zip');

    const context = await browser.newContext({
      recordVideo: {
        dir: testInfo.outputPath(''),
        size
      },
      viewport: size,
    });
    await context.tracing.start({ screenshots: true });
    const page = await context.newPage();

    await page.evaluate(() => document.body.style.backgroundColor = 'red');
    await ensureSomeFrames(page);
    await context.tracing.stop({ path: traceFile });
    await context.close();

    const videoFile = await page.video().path();
    expectRedFrames(videoFile, size);

    const { events, resources } = await parseTraceRaw(traceFile);
    const frame = events.filter(e => e.type === 'screencast-frame').pop();
    const buffer = resources.get('resources/' + frame.sha1);
    const image = jpegjs.decode(buffer);
    expect(image.width).toBe(size.width);
    expect(image.height).toBe(size.height);
    const offset = size.width * size.height / 2 * 4 + size.width * 4 / 2; // Center should be red.
    const pixel: Pixel = {
      r: image.data.readUInt8(offset),
      g: image.data.readUInt8(offset + 1),
      b: image.data.readUInt8(offset + 2),
      alpha: image.data.readUInt8(offset + 3),
    };
    expect(isAlmostRed(pixel)).toBe(true);
  });

});

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 ensureSomeFrames(page);
  await context.close();

  const saveAsPath = testInfo.outputPath('my-video.webm');
  await page.video().saveAs(saveAsPath);
  expect(fs.existsSync(saveAsPath)).toBeTruthy();
});
