playwright: [BUG] Videos are not generated when reusing a single page between tests

Note from maintainers

As a workaround, we recommend recording a trace instead of a video. This will also give you much better debugging experience.


Context:

  • Playwright Version: 1.22.2
  • Operating System: Windows 11
  • Node.js version: v 17.7.1
  • Browser: chromium
  • Extra: [any specific details about your environment]

System:

  • OS: Windows 10 10.0.22000
  • Memory: 1.24 GB / 15.79 GB

Binaries:

  • Node: 17.7.1 - C:\Program Files\nodejs\node.EXE
  • Yarn: 1.22.17 - C:\Program Files\nodejs\yarn.CMD
  • npm: 8.5.2 - C:\Program Files\nodejs\npm.CMD

Languages:

  • Bash: 5.0.17 - C:\Windows\system32\bash.EXE

Code Snippet playwright.config.ts

const config: PlaywrightTestConfig = {
  reporter: [['dot'],['html']],
  use: {
    trace: 'retain-on-failure',
    screenshot: 'only-on-failure',
    video:'retain-on-failure',
  },
};

export default config;

broke.spec.ts (sourced from https://playwright.dev/docs/test-retries#reuse-single-page-between-tests)

import { test, Page } from '@playwright/test';

test.describe.configure({ mode: 'serial' });

let page: Page;

test.beforeAll(async ({ browser }) => {
  page = await browser.newPage();
});

test.afterAll(async () => {
  await page.close();
});

test('a failing test', async () => {
  await page.click('text=Get Started fail', {timeout: 1000});  // << deliberate fail
});

Describe the bug

Videos are not generated when reusing a single page between tests

Set the following configuration:

  use: {
    trace: 'retain-on-failure',
    screenshot: 'only-on-failure',
    video:'retain-on-failure',
  },

Run a failing test

Expected Result:

The spec above should successfully generate a screenshot, test steps, trace file and a video within the HTML result.

Actual Result:

The video is not being generated:

2022-06-13_09-09-29

About this issue

  • Original URL
  • State: open
  • Created 2 years ago
  • Reactions: 58
  • Comments: 57 (2 by maintainers)

Most upvoted comments

Hi!

I’d expect this to be a higher prio bug (first report September 2021, almost 🍰 🕯️ 🎉 )

I’m following the simple docs on overriding the page fixture, just as described, and I lose videos because of that? Since not fixing this, there should be a disclaimer on those docs imo

Almost 2 years in and still an issue. I think this is very useful for debugging locally. I hope the Playwright core team starts looking into this with high priority 😄

This is indeed a known issue: video recording produces a single video file from the creation of the page until its closure, as of Jun 2022.

I’m also running into this issue - specifically, that video recording doesn’t work in pages created with brower.newContext, as described here.

My use case: I’m using storageState for authentication, as described in the docs, except that I only want to use it for some tests. So when I need it, I call browser.newContext({ storageState: ... }). But then pages created from that context don’t get video recordings.

For now I plan to use the recordVideo option and manual file manipulation to emulate video: 'retain-on-failure'.

Wondering if this can get a higher priority.

I’m having the same issue as @mweidner037. I use storageState and create pages from browser.newContext({ storageState: ... }). I’m trying to take retain videos on failure, but I’m not seeing any video recordings when my tests fail.

this is my workaround for now in case it helps anyone. not too ugly. (Note: In this redacted example, I always save videos, this may not be what you want. You can use testInfo to check the test status and conditionally save videos).

import { test as base, Page, Video } from '@playwright/test'

import { HomePage } from './../pages/homePage'

/** HomePage.create looks like this

  static async create(browser: Browser, fileName): Promise<Page> {
    if (process.env.BASE_URL == null) {
      throw new Error('No BASE_URL configured')
    }

    const userContext = await browser.newContext({
      storageState: `./e2e-tests/sessionStorage/${fileName}.json`,
      recordVideo:
        {
        dir: `test-results/videos`,
        }
    })
    const userPage = await userContext.newPage()
    await userPage.goto(process.env.BASE_URL)

    return userPage
  }
*/

type Fixtures = {
  userOnePage: Page
  userTwoPage: Page
}
export const test = base.extend<Fixtures & { saveVideos: void }>({
  userOnePage: async ({ browser }, use) => {
    await use(await HomePage.create(browser, 'userOneStorageState'))
  },
  userTwoPage: async ({ browser }, use) => {
    await use(await HomePage.create(browser, 'userTwoStorageState'))
  },
  saveVideos: [async ({ userOnePage, userTwoPage, browser }, use, testInfo) => {
    await use();

    await Promise.all(browser.contexts().map((context) => {
      return context.close()
    }))
    const video1 = await userOnePage.video()?.path()
    const video2 = await userTwoPage.video()?.path()
    const nonNullVideos = [{ title: 'User One', path: video1 }, { title: 'User Two', path: video2 }].filter((video) => Boolean(video.path)) as {title: string, path: string}[]
    nonNullVideos.forEach((video) => {
      testInfo.attachments.push({ name: `${video.title} `, path: video.path, contentType: 'video/webm'})
    })
  }, { scope: 'test', auto: true }],

})

PS: Playwright team, please update the docs for videos or browsercontexts to make it very clear that this “issue”/limitation exists and is not being prioritized

Just adding to this, I’m getting this on MacOS with Playwright version 1.29.1, so it’s not just a windows OS issue.

@mxschmitt can you please help us to prioritise it?

this is an extension of @Derrbal’s comment. This gets rid of the need to copy/paste the video saving logic within individual tests, by use of fixtures:

import * as fs from 'fs';
import * as path from 'path';
import { test as base, Browser, Page, TestInfo } from '@playwright/test';

import { GRAFANA_ADMIN_USERNAME, GRAFANA_EDITOR_USERNAME, GRAFANA_VIEWER_USERNAME } from './utils/constants';
import { VIEWER_USER_STORAGE_STATE, EDITOR_USER_STORAGE_STATE, ADMIN_USER_STORAGE_STATE } from '../playwright.config';

export class BaseRolePage {
  page: Page;
  userName: string;

  constructor(page: Page) {
    this.page = page;
  }
}

type BaseRolePageType = new (page: Page) => BaseRolePage;

class ViewerRolePage extends BaseRolePage {
  userName = GRAFANA_VIEWER_USERNAME;
}

class EditorRolePage extends BaseRolePage {
  userName = GRAFANA_EDITOR_USERNAME;
}

class AdminRolePage extends BaseRolePage {
  userName = GRAFANA_ADMIN_USERNAME;
}

type Fixtures = {
  viewerRolePage: ViewerRolePage;
  editorRolePage: EditorRolePage;
  adminRolePage: AdminRolePage;
};


const _recordTestVideo = async (
  browser: Browser,
  use: (r: BaseRolePage) => Promise<void>,
  testInfo: TestInfo,
  storageStateLocation: string,
  RolePage: BaseRolePageType
) => {
  const videoDir = path.join(testInfo.outputPath(), 'videos');

  const context = await browser.newContext({ storageState: storageStateLocation, recordVideo: { dir: videoDir } });
  const page = new RolePage(await context.newPage());

  try {
    await use(page);
  } finally {
    await context.close();
    const videoFiles = fs.readdirSync(videoDir);

    if (videoFiles.length > 0) {
      for (let i = videoFiles.length; i > 0; i--) {
        let videoFile = path.join(videoDir, videoFiles[i - 1]);
        await testInfo.attach('video', { path: videoFile });
      }
    }
  }
};

export * from '@playwright/test';
export const test = base.extend<Fixtures>({
  viewerRolePage: ({ browser }, use, testInfo) =>
    _recordTestVideo(browser, use, testInfo, VIEWER_USER_STORAGE_STATE, ViewerRolePage),
  editorRolePage: async ({ browser }, use, testInfo) =>
    _recordTestVideo(browser, use, testInfo, EDITOR_USER_STORAGE_STATE, EditorRolePage),
  adminRolePage: async ({ browser }, use, testInfo) =>
    _recordTestVideo(browser, use, testInfo, ADMIN_USER_STORAGE_STATE, AdminRolePage),
});

then within your test files you can do something like this:

import { test, expect, Page } from '../fixtures';

test('my test', async ({ adminRolePage }) => {
  ...

The adminRolePage fixture that is accessed in test in this example will automatically have a video recorded.

Hi @ryanrosello-og If you are picking the browser context configuration directly from xyz.config.js file and have not defined the context in the spec itself, then you would want to try the following configuration in order to get the videos generated.

use: { trace: ‘retain-on-failure’, screenshot: ‘only-on-failure’, video:‘retain-on-failure’, contextOptions: {recordVideo: { dir: “<path_to_store_videos>/videos/'”}} },

It was mentioned above but this is an issue not just for custom fixtures or page reuse, but overriding storageState too, if you specify ‘retain-on-failure’. With mode ‘on’ the video is captured. I’m guessing this is something about in retain-on-failure the videos aren’t written to the outputDir, but some temp location and only copied over if the test fails?

I’d really rather avoid having to write a custom fixture to get around this since I only need the storageState override for auth.

hey @dgozman , any plans on fixing this? Seems users are still hitting this issue. Some have proposed workarounds.

@ibrocodes7 This is hacky but I would just throw a // @ts-expect-error above recordVideo. I should revisit this soon and see if there’s a cleaner way to implement it. It is still working well for me; I have the fixture in an npm package that is used by a handful of Playwright projects daily and videos are still getting attached to my reports.

We discovered a potential solution to allow us to create a video recording in a test which is using the browser fixture:

import * as path from "path"
import * as fs from 'fs';
import { expect, test } from "@playwright/test";

test.only('manually created context', async ({ browser }, testInfo) => {
  const videoDir = path.join(testInfo.outputPath(), 'videos');
  const context = await browser.newContext({ recordVideo: { dir: videoDir } });
  const page = await context.newPage();
  const page2 = await context.newPage();
  try {
    await page2.goto('http://localhost:3030/login?pageSize=10');
    await page.goto('http://localhost:3030/login?pageSize=10');
    await expect(page).toHaveURL('http://localhost:3030/login?pageSize=10');
  } finally {
    await page.context().close();
    const videoFiles = fs.readdirSync(videoDir);
    
    if (videoFiles.length > 0) {
      for (let i = videoFiles.length; i > 0; i--) {
        let videoFile = path.join(videoDir, videoFiles[i - 1]);
        await testInfo.attach('video', { path: videoFile });
      }
    }
  }
});

But adding this change accross all of our tests which uses the browser fixture is tome consuming and not ideal. We are hoping that someone has some ideas how we might abstract this change. We currently have extended test to use custom fixtures. It seems that it can be a good place to globally apply this fix but we are not sure how.

I’ve been using the solution I outlined in my previous comment for months; it’s still working well. I did end up layering another fixture on top of that one for standard page object use.

Edit 9/22/23: cleaned up the fixture so it no longer throws a type error and doesn’t need a beforeAll hook @ibrocodes7

Fixture for enabling video recording:

import { test as base, BrowserContext, Page } from "@playwright/test";
import * as fs from "fs";

/**
 * this fixture is needed to record and attach videos on failed tests when
 * tests are run in serial mode (i.e. browser is not closed between tests)
 */
export const test = base.extend<
    {
        attachVideoPage: Page;
    },
    { createVideoContext: BrowserContext }
>({
    createVideoContext: [
        async ({ browser }, use) => {
            const context = await browser.newContext({
                recordVideo: {
                    dir: "./playwright-video",
                },
            });

            await use(context);

            fs.rmSync("./playwright-video", { recursive: true });
        },
        { scope: "worker" },
    ],

    attachVideoPage: [
        async ({ createVideoContext }, use, testInfo) => {
            let page;
            if (createVideoContext.pages().length === 1) {
                page = createVideoContext.pages()[0];
            } else {
                page = await createVideoContext.newPage();
            }
            await use(page);

            if (
                testInfo.status === "failed" ||
                testInfo.status === "timedOut"
            ) {
                const path = await createVideoContext
                    .pages()[0]
                    .video()
                    ?.path();
                await createVideoContext.close();
                testInfo.attach("video", {
                    path: path,
                });
            }
        },
        { scope: "test", auto: true },
    ],
});

export { expect, Page, Locator, Response } from "@playwright/test";

Then I have my page object fixture:

import { test as base, Page } from [fixture for recording video];
import { BrowserContext } from "@playwright/test";
import { ExamplePage1 } from "./examplePage1";
import { ExamplePage2 } from "./examplePage2";

export type PageObjects = {
    examplePage1: ExamplePage1;
    examplePage2: ExamplePage2;
};

export const test = base.extend<PageObjects>({
    examplePage1: async ({ attachVideoPage }, use) => {
        const examplePage1 = new ExamplePage1(attachVideoPage);
        await use(examplePage1);
    },
    examplePage2: async ({ attachVideoPage }, use) => {
        const examplePage2 = new ExamplePage2(attachVideoPage);
        await use(examplePage2);
    },
});

export { expect, Page, Locator, Response } from "@playwright/test";

Usage in spec file:

import { test, expect } from [page object fixture above];


test("renders", async ({ examplePage1 }) => {
    await examplePage1.open()'
    await expect(examplePage1.heading).toHaveText(
       "Example Page 1"
    );
});

This is indeed a known issue: video recording produces a single video file from the creation of the page until its closure, as of Jun 2022.

Hey @dgozman, even though I follow what has been suggested here

Videos are saved upon browser context closure at the end of a test. If you create a browser context manually, make sure to await browserContext.close().

I indeed await browserContext.close(), but there are no videos created.

Also have this issue with re-using storageState

My test environment: npm list ├── @playwright/test@1.39.0 ├── @types/node@20.8.9 └── dotenv@16.3.1 OS: MacOS 14.1 Chip: Apple M2 Max Browser: Chromium Version 119.0.6045.9 (Developer Build) (arm64)

Result: Adding the following to playwright.config.ts

export default defineConfig({
  use: {
    // Capture screenshot after each test failure.
    screenshot: 'on',
    /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
    trace: 'off',
    /* Record the video for test. See https://playwright.dev/docs/videos */
    // Record video only when retrying a test for the first time.
    video: 'on',
    contextOptions: { recordVideo: { dir: 'test-results/videos/' } }
  },
...
});

Videos are generated in the output folder ‘test-results/videos’. But I have to close the browser for each test, otherwise the video will be only generated once.

Caring is sharing @Derrbal 😃. What’s the solution?

My apology, I’ve updated comment 😉

I think this problem is also present when using worker scoped fixtures. We get no video captures even if we use a new page instance for each test. Would be nice to have a fix for this.

Hi, I’m also facing the same issue with the Page object Model pattern, the video recording does not work. Waiting for the solution. My code looks like below,

`import { test, expect, Page} from “@playwright/test”; import { LoginPage } from “…/pages/loginPage”;

let page: Page; let loginPage: LoginPage;

test.describe(‘Validate login page’, async () => {

test.beforeAll(async ({ browser }) => { page = await browser.newPage(); loginPage = new LoginPage(page);

await loginPage.navigateTo();

});

test(‘login page displayed’, async () => { await expect(loginPage.Title).toHaveText(‘ABC’); await expect(loginPage.Title).toContainText(‘Log in’); });

test(‘test A’, async () => { expect('A).toEqual(testA); });

test(‘testB’, async () => { expect('B).toEqual(testB); });

test.afterAll(async () => { console.log(“Login page validated successfully”); await page.close(); });`

I have the same problem with reusing StorageState as @mweidner037 and @KeionneDerousselle described.

I believe I have this working with a fixture but it’s not pretty. A worker-scoped fixture initiates video recording to a temp dir and clean up of that dir on teardown. A test-scoped fixture attaches the video to the test reports on failure or time out.

fixture:

import { test as base, BrowserContext } from "@playwright/test";
import * as fs from "fs";

export const test = base.extend<{
    recordVideo: BrowserContext;
    attachVideo: BrowserContext;
}>({
    recordVideo: [
        async ({ browser }, use) => {
            const context = await browser.newContext({
                recordVideo: {
                    dir: "./playwright-video",
                },
            });

            await use(context);

            fs.rmSync("./playwright-video", { recursive: true });
        },
        // @ts-expect-error
        { scope: "worker" },
    ],

    attachVideo: [
        async ({ recordVideo }, use, testInfo) => {
            const path = await recordVideo.pages()[0].video()?.path();

            await use(recordVideo);

            if (
                testInfo.status === "failed" ||
                testInfo.status === "timedOut"
            ) {
                await recordVideo.close();
                testInfo.attach("video", {
                    path: path,
                });
            }
        },
        { scope: "test", auto: true },
    ],
});

export { expect, Page, Locator, Response } from "@playwright/test";

usage:

import { test, expect, Page } from "../../video-fixture";
import { PageUnderTest } from "../../pageObjects/PageUnderTest"

let page: Page;
let pageUnderTest: PageUnderTest;

test.describe.serial("manage round", () => {
    test.beforeAll(async ({ recordVideo }) => {
        page = await recordVideo.newPage();

        pageUnderTest = new PageUnderTest(page);
        await pageUnderTest.open();
    });

    test.afterAll(async () => {
        await page.close();
    });

    test("renders", async () => {
        await expect(pageUnderTest.header).toHaveTest("header");
        ...
    });

   test("test other stuff on same page", async () => {
        ...
   });
});