microsoft / playwright

Playwright is a framework for Web Testing and Automation. It allows testing Chromium, Firefox and WebKit with a single API.
https://playwright.dev
Apache License 2.0
65.49k stars 3.57k forks source link

[BUG] LaunchPersistentContext recorded video are not saved and shown in the HTML reporter #21750

Closed DvDream closed 1 year ago

DvDream commented 1 year ago

System info

I am trying to launch for every test of my repo the same persistent context that I got in my userDir folder.. unfortunately as I have read in other issues, launching a persistent context does force you to reconfigure your test options to make everything work well.. With that said..I am having some issue with saving the videos of every test in my reporter folder. I have a playwright HTML report and an Allure-playwright report. For both of them the video would not showing up, even if I managed to save them in the proper output directory. Below the code of how I set the context:

Source code

        let context: BrowserContext;
        let page: Page;
        signinUrl = baseURL;
        console.log(testInfo.outputDir);
        context = await playwright[browserName].launchPersistentContext('C:/Users/admin/Desktop/TEST_REPO/userDir', {
                baseURL: signinUrl,
                viewport: {
                    "width": 1366,
                    "height": 670
                }, recordVideo: {
                    dir: testInfo.outputDir,
                    size: {
                        width: 1280,
                        height: 625
                    }
                },
                locale: "it-IT",
                proxy: {
                    server: "IP:port",
                    bypass: "some.domain"
                },
                timezoneId: "Europe/Rome",
            });
            page = context.pages()[0];

Config file This is the config style tha I would normally have without using a persistent context and that indeed works pretty well (video, screenshots, ecc...)

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  projects: [
    {
      name: 'chromium',
"reporter": [
        [
            "line"
        ],
        [
            "allure-playwright"
        ],
        [
            "html",
            {
                "outputFolder": "playwright-report",
                "open": "never"
            }
        ]
    ],
      "use": {
        "locale": "it-IT",
        "launchOptions": {
            "proxy": {
                "server": "IP:port",
                "bypass": "some.domain"
            }
        },
        "contextOptions": {
            "locale": "it-IT",
            "timezoneId": "Europe/Rome"
        },
        "viewport": {
            "width": 1366,
            "height": 670
        },
        "screenshot": "only-on-failure",
        "video": {
            "mode": "on",
            "size": {
                "width": 1280,
                "height": 625
            }
        },
        "trace": "retain-on-failure"
    }
    }
});

Steps

Expected

Expect to have my recorded videos inside the HTML and Allure report.

Actual

That does not happen.

I want also to add that the screenshots insted do appear in my report even if in my persistent context I do not specify anything about while it is specified inside my config. Also, how can I specify inside my persisten context wheneve I want to record a video? Something like "Only on failure" ecc... thank you.

dgozman commented 1 year ago

@DvDream Built-in video recording from the video option in the config only works for the built-in context and page. Since you call launchPersistentContext yourself, you need to configure video recording manually.

Pass the recordVideo option when creating your context. Let me know if that helps.

DvDream commented 1 year ago

@DvDream Built-in video recording from the video option in the config only works for the built-in context and page. Since you call launchPersistentContext yourself, you need to configure video recording manually.

Pass the recordVideo option when creating your context. Let me know if that helps.

I did...it is in the options for the Persistent context in the code I shared, and still it does not save the video in the reporter folder..

dgozman commented 1 year ago

@DvDream Sorry, I missed that in your code! To attach videos to the html/allure report, you need to manually add an attachment to the TestInfo object.

I'd recommend creating an automatic fixture that would iterate over pages and attach their videos. You can also implement the "only on failure" logic there. Take a look at this guide about automatic fixtures and this example that saves videos.

Let me know if that helps.

DvDream commented 1 year ago

Hi @dgozman, I am reading now your response and I wonder if instead doing as you said I could do the following:

const test_login = base.extend<LoggedInTestFixtures>({
    context: [
        async ({}, use, testInfo) => {
          const userDataDir = 'C:/Users/admin/Desktop/TEST_REPO/userDir';
          const context = await chromium.launchPersistentContext(userDataDir, {
            baseURL: 'https://example.com',
            viewport: {
                "width": 1366,
                "height": 670
            }, recordVideo: {
                dir: testInfo.outputDir,
                size: {
                    width: 1280,
                    height: 625
                }
            },
            locale: "it-IT",,
            timezoneId: "Europe/Rome",
        });
          await use(context);
          await context.close();
        },
        { scope: 'test' },
      ]
})

I thought that by overriding the context used as a default fixture by Playwright, the runner would have picked up the video and saved them into my report folder..but it seems that is not working (that is, the video are not saved in the report folder, even though they are actually generated and save in my test-results folder), am I doing something wrong?

The fact is that I would like to stay with the all the automatism the @playwright/test does provide to me.

dgozman commented 1 year ago

@DvDream Unfortunately, built-in video recording from the config option only works for the default context right now, see also #14813. If you override it, or do not use it, you'll have to do videos yourself. That said, we recommend to look into tracing instead, because it gives you much more.

I'll leave this issue open for prioritization.

DvDream commented 1 year ago

Hello @dgozman. Just wanted to give some new and have some help with a thing that apparently I am not understanding. I got the issue about the HTML/report working following the example that you linked to me.

Here's the code of my fixtures:


type WorkerFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & {
    _browserOptions: LaunchOptions;
    _artifactsDir: () => string;
    _snapshotSuffix: string;
  };

type LoggedInTestFixtures = {

    loggedIn: string;
    _contextFactory: (options?: BrowserContextOptions) => Promise<BrowserContext>;
    _artifactsDir: () => string;
    _reuseContext: boolean;
    _contextReuseMode: 'none' | 'force' | 'when-possible';
}

const test_login = base.extend<LoggedInTestFixtures, WorkerFixtures>({
    _artifactsDir: [async ({}, use, workerInfo) => {
        let dir;
        await use(() => {
          if (!dir) {
            dir = path.join(workerInfo.project.outputDir, '.playwright-artifacts-' + workerInfo.workerIndex);
            fse.mkdirSync(dir, {
              recursive: true
            });
          }
          return dir;
        });
        if (dir) await (0, pw_utils.removeFolders)([dir]);
      }, {
        scope: 'worker',
        _title: 'playwright configuration'
      } as any],
    _contextFactory: [async ({ browser, video, _artifactsDir, _reuseContext, playwright, browserName}, use, testInfo) => {
        const testInfoImpl = testInfo as TestInfoImpl;
        const videoMode = normalizeVideoMode(video);
        const captureVideo = (videoMode === 'on' || videoMode === 'retain-on-failure' || (videoMode === 'on-first-retry' && testInfo.retry === 1)) && !_reuseContext;
        const contexts = new Map<BrowserContext, { pages: Page[] }>();

        await use(async options => {
            const hook = hookType(testInfoImpl);
            if (hook) {
                throw new Error([
                    `"context" and "page" fixtures are not supported in "${hook}" since they are created on a per-test basis.`,
                    `If you would like to reuse a single page between tests, create context manually with browser.newContext(). See https://aka.ms/playwright/reuse-page for details.`,
                    `If you would like to configure your page before each test, do that in beforeEach hook instead.`,
                ].join('\n'));
            }
            const videoOptions: BrowserContextOptions = captureVideo ? {
                recordVideo: {
                    dir: _artifactsDir(),
                    size: typeof video === 'string' ? undefined : video.size,
                }
            } : {};
            //const context = await browser.newContext({ ...videoOptions, ...options });
            const userDataDir = 'C:/Users/admin/Desktop/TEST_REPO/userDir';
            console.log(options);
            const context = await playwright[browserName].launchPersistentContext(userDataDir, {
                ...options,
                ...videoOptions,
                viewport: {
                    "width": 1920,
                    "height": 1080
                },
                screen: {
                    "width": 1920,
                    "height": 1080
                },
                baseURL: 'URLtouse',
                locale: "it-IT",
                proxy: {
                    server: "iP:port",
                    bypass: "some.domain"
                },
                timezoneId: "Europe/Rome",
            });
            const contextData: { pages: Page[] } = { pages: [] };
            contexts.set(context, contextData);
            context.on('page', page => 
            {contextData.pages.push(page);
                console.log('ho ricevuto evento page');
            });
            return context;
        });

        const prependToError = testInfoImpl._didTimeout ?
            formatPendingCalls((browser as any)._connection.pendingProtocolCalls()) : '';

        let counter = 0;
        await Promise.all([...contexts.keys()].map(async context => {
            await context.close();

            const testFailed = testInfo.status !== testInfo.expectedStatus;
            const preserveVideo = captureVideo && (videoMode === 'on' || (testFailed && videoMode === 'retain-on-failure') || (videoMode === 'on-first-retry' && testInfo.retry === 1));
            if (preserveVideo) {
                const { pages } = contexts.get(context)!;
                const videos = pages.map(p => p.video()).filter(Boolean) as Video[];
                await Promise.all(videos.map(async v => {
                    try {
                        const savedPath = testInfo.outputPath(`video${counter ? '-' + counter : ''}.webm`);
                        ++counter;
                        await v.saveAs(savedPath);
                        testInfo.attachments.push({ name: 'video', path: savedPath, contentType: 'video/webm' });
                    } catch (e) {
                        // Silent catch empty videos.
                    }
                }));
            }
        }));

        if (prependToError)
            testInfo.errors.push({ message: prependToError });
    }, { scope: 'test', _title: 'context' } as any],

    _contextReuseMode: process.env.PW_TEST_REUSE_CONTEXT === 'when-possible' ? 'when-possible' : (process.env.PW_TEST_REUSE_CONTEXT ? 'force' : 'none'),

    _reuseContext: [async ({ video, _contextReuseMode }, use, testInfo) => {
        let videoMode = normalizeVideoMode(video);
        const reuse = _contextReuseMode === 'force' || (_contextReuseMode === 'when-possible' && !(videoMode === 'on' || videoMode === 'retain-on-failure' || (videoMode === 'on-first-retry' && testInfo.retry === 1)));
        await use(reuse);
    }, { scope: 'test', _title: 'context' } as any],

    context: [async ({ playwright, browser, _reuseContext, _contextFactory }, use) => {
        if (!_reuseContext) {
            await use(await _contextFactory());
            return;
        }

        const defaultContextOptions = (playwright.chromium as any)._defaultContextOptions as BrowserContextOptions;
        const context = await (browser as any)._newContextForReuse(defaultContextOptions);
        (context as any)[kIsReusedContext] = true;
        await use(context);
    },{ scope: 'test', _title: 'context' } as any],
    loggedIn: async ({ browserName, context, page, baseURL }, use) => {
       // do some operations with page...
       await page.goto("www.login.com");
       // other operations within the page
       await use('logged In!');
       }
});

Usage example:


test_login.beforeEach(async ({ loggedIn, context, page  }) => {
    // Esportazione -> Report
    console.log('beforeeach' + context.pages().length);  // it logs 2 pages
    await page.click('sidebar-component i.sidebar-icon.fa-edit');

});

What happens is that when I run the test it opens me two page, a blank page and the actual page where I say to go to.. I don't know in what part of the code the blank page is being launched..because as far as I understood..the launching of a context does not generate a new page, am I right? So when I use page in my loggedIn fixture I would expect everything to go well...why is it not the case? Thank you!

dgozman commented 1 year ago

@DvDream When you launchPersistentContext, it will open one page for you automatically. That's how browsers work. You'll have to account for that in your fixtures, for example page could just return context.pages()[0] instead of calling newPage.

As a side note, it looks like you copied way too much code, that would be hard to maintain. All you needed was the context fixture that would do the "save video" part after use() call.

DvDream commented 1 year ago

@dgozman This is what I have achieved at the moment and also, since I have some doubts about that, I would like to know your opinion given your expertise:

These are my fixtures:

type WorkerFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & {
    _artifactsDir: () => string;
};

type LoggedInTestFixtures = {

    loggedInPage: Page;

}

const test_login = base.extend<LoggedInTestFixtures,WorkerFixtures>({
    _artifactsDir: [async ({ }, use, workerInfo) => {
        let dir;
        await use(() => {
            if (!dir) {
                dir = path.join(workerInfo.project.outputDir, '.playwright-artifacts-' + workerInfo.workerIndex);
                fse.mkdirSync(dir, {
                    recursive: true
                });
            }
            return dir;
        });
        if (dir) await (0, pw_utils.removeFolders)([dir]);
    }, {
        scope: 'worker',
        _title: 'playwright configuration'
    } as any],
    context: [async ({ browser, playwright, browserName, video, _artifactsDir }, use, testInfo) => {

        const testInfoImpl = testInfo as TestInfoImpl;

        if (!video) return 'off';
        let videoMode = typeof video === 'string' ? video : video.mode;
        if (videoMode === 'retry-with-video') videoMode = 'on-first-retry';
        const captureVideo = (videoMode === 'on' || videoMode === 'retain-on-failure' || (videoMode === 'on-first-retry' && testInfo.retry === 1));

        const contexts = new Map<BrowserContext, { pages: Page[] }>();
        const userDataDir = 'C:/Users/admin/Desktop/TEST_REPO/userDir';
        const context = await playwright[browserName].launchPersistentContext(userDataDir, {
            recordVideo: {
                dir: _artifactsDir(),
                size: typeof video === 'string' ? undefined : video.size,
            },
            viewport: {
                "width": 1366,
                "height": 670
            },
            screen: {
                "width": 1366,
                "height": 670
            },
            baseURL: 'https://login.com',
            locale: "it-IT",
            proxy: {
                server: "IP:port",
                bypass: "someDomain.com"
            },
            timezoneId: "Europe/Rome",
        });
        const contextData: { pages: Page[] } = { pages: [] };
        contexts.set(context, contextData);

        context.on('page', page => {
            console.log('Opening a new page...');
            contextData.pages.push(page);
        });
        contextData.pages.push(context.pages()[0]);   //**IF YOU COMMENT THIS LINE, VIDEO ARE NOT SAVED IN THE TEST-RESULTS FOLDER**
        await use(context);

        const prependToError = testInfoImpl._didTimeout ?
            formatPendingCalls((browser as any)._connection.pendingProtocolCalls()) : '';

        let counter = 0;
        await Promise.all([...contexts.keys()].map(async context => {
            await context.close();

            const testFailed = testInfo.status !== testInfo.expectedStatus;
            const preserveVideo = captureVideo && (videoMode === 'on' || (testFailed && videoMode === 'retain-on-failure') || (videoMode === 'on-first-retry' && testInfo.retry === 1));
            if (preserveVideo) {
                const { pages } = contexts.get(context)!;
                const videos = pages.map(p => p.video()).filter(Boolean) as Video[];
                await Promise.all(videos.map(async v => {
                    try {
                        const savedPath = testInfo.outputPath(`video${counter ? '-' + counter : ''}.webm`);
                        ++counter;
                        await v.saveAs(savedPath);
                        testInfo.attachments.push({ name: 'video', path: savedPath, contentType: 'video/webm' });
                    } catch (e) {
                        // Silent catch empty videos.
                    }
                }));
            }
        }));
        if (prependToError)
            testInfo.errors.push({ message: prependToError });

    }, { scope: 'test', _title: 'context' } as any],
    loggedInPage: async ({ context, browser, browserName, baseURL}, use) => {

        allure.tag(`${browserName} version: ${browser.version()}`)
        const page = context.pages()[0];
        await page.goto(`${baseURL}/signin`);
    }
});

and these are the function I could not delete since they seems very important for the overall functioning:

type StackFrame = {
    file: string,
    line?: number,
    column?: number,
    function?: string,
};

type ParsedStackTrace = {
    frames: StackFrame[];
    frameTexts: string[];
    apiName: string;
};

function formatStackFrame(frame: StackFrame) {
    const file = path.relative(process.cwd(), frame.file) || path.basename(frame.file);
    return `${file}:${frame.line || 1}:${frame.column || 1}`;
}
function formatPendingCalls(calls: ParsedStackTrace[]) {
    calls = calls.filter(call => !!call.apiName);
    if (!calls.length)
        return '';
    return 'Pending operations:\n' + calls.map(call => {
        const frame = call.frames && call.frames[0] ? ' at ' + formatStackFrame(call.frames[0]) : '';
        return `  - ${call.apiName}${frame}\n`;
    }).join('');
}

Usage example:

test_login(async ({ loggedInPage }) => {
    const page = loggedInPage;
    await page.hover('header.header');
    // Other operations...
    await use(page);
});

For this point I want to show you what I mean by posting two screenshots of my debugger:

with contextData.pages.push(context.pages()[0]); commented before the use(context) call without_pages

with contextData.pages.push(context.pages()[0]); NOT commented before the use(context) call with_page

and it is like the persistent context does not initially fire the page event which I handle with

        context.on('page', page => {
            console.log('Opening a new page...');
            contextData.pages.push(page);
        });

so how could that be handled?

Sorry if there are too many questions but it is the first time to me that I really work hard with fixtures and I would like to gain as much knowledge as I can, thank you!

dgozman commented 1 year ago

so how could that be handled?

You are doing the right thing. Persistent context does indeed have a single page at the start, so you are right to account for it. Overall, your snippet looks good.

I would like to use it by calling page as I would do normally, but maybe I would need to overwrite the default page fixture?

Yes, you can do that by just defining your own page fixture, instead of loggedInPage:

page: async ({ context, browser, browserName, baseURL}, use) => {
        allure.tag(`${browserName} version: ${browser.version()}`)
        const page = context.pages()[0];
        await page.goto(`${baseURL}/signin`);
        await use(page);
}

I think you have arrived to something working, so I'll close the issue now. If you encounter more problems, please file a new issue by filling in the "Bug Report" template. Thank you!