Closed amuresia closed 1 month ago
Hi @amuresia ! There was a similar request in discord - setting storageState dynamically in step, pls check out the discussion: https://discord.com/channels/807756831384403968/1254447424450199634
The main idea - create pages
fixture and add pages dynamically to it using different storageStates.
Here is the implementation we ended up: https://github.com/MaxEinEp/playwright-bdd-example/blob/main/test/steps/genericSteps.ts#L16
That approach should work for chat app as well. Lets imagine the following feature:
Feature: Cross-user messaging
Scenario: Send and receive message
Given user "A" logged-in as "a@example.com"
And user "B" logged-in as "b@example.com"
When user "A" sends to user "B" a message "Hello"
Then user "B" sees in the chat with user "A" message "Hello"
pages
fixture is initially empty object:
export const test = base.extend({
pages: async ({}, use) => {
const pages = {} // <- will be initialized in steps
await use(pages);
// todo: close created browser contexts if any
},
});
Steps:
Given('user {string} logged-in as {string}', async ({ pages }, user: string, email: string) => {
const context = await browser.newContext( { storageState:`${email}.json` } );
const page = await auth.context.newPage();
pages[user] = { context, page };
});
When('user {string} sends to user {receiver} a message {message}', async ({ pages }, sender: string, receiver: string, message: string) => {
await pages[sender].page.getByRole('textbox').fill(message);
// ...
});
// similar for Then...
Thanks so much @vitalets for the quick reply and the great suggestion! I got my tests to work using your snippets. There are a couple of items left on my list to wrap things up and perhaps you can point me to the right direction.
pages[sender].page
is a Page object and pages[sender].context
is a Context object and benefit from code completion in my code editor. I tried to define it in the fixture
import { test as base, createBdd } from 'playwright-bdd';
import { BrowserContext, Page } from 'playwright';
export type PageContext = { context: BrowserContext; page: Page; };
export const test = base.extend({ // eslint-disable-next-line no-empty-pattern pages: async ({}, use) => { const pages: PageContext[] = []; await use(pages); // todo: close created browser contexts if any }, });
export const { Given, When, Then } = createBdd(test);
And even tried to enforce it in the step
When( 'user {string} creates a task', async ({ pages } : {pages: PageContext[]}, user: string) => { await pages[user].page.goto('/'); ... await pages[user].page.waitForSelector('h6'); } );
but `pages[user].page` is defined as `any` and code completion doesn't work.
2. When and how should I close the browser pages and contexts?
// todo: close created browser contexts if any
I am not sure when to call the `.close()` action as if I do it in the `pages` fixture then I prematurely lose the context set in `Given('user {string} logged-in as {string}', async ({ pages }, user: string, email: string)` and I won't have it available in subsequent steps
If I add a final step `And all users sign out` it might never be reached if a previous step fails.
Thanks again for your support!
Yes, we can make typescript happy. I suggest to create pages
fixture as object, not array - in that case it's easier to find context for particular user. Here is a example (with closing contexts):
export type PageContext = {
context: BrowserContext;
page: Page;
};
export type PagesFixture = Record<string, PageContext>;
export const test = base.extend({
// eslint-disable-next-line no-empty-pattern
pages: async ({}, use) => {
const pages = {} as PagesFixture;
await use(pages);
// close created browser contexts if any
for (const user of Object.keys(pages)) await pages[user]?.context.close();
},
});
// step
When(
'user {string} creates a task',
async ({ pages }, user: string) => {
await pages[user].page.goto('/'); // <- should be typed correctly
...
await pages[user].page.waitForSelector('h6');
}
);
The only caveat here - if there is no step user {string} logged-in as
, then pages[user].page
will be undefined in subsequent steps, although Typescript will not highlight that. I think this is fair enough.
This is really useful authentication example. When done and If possible - feel free to share the link to your code and I will add it to docs.
Thanks @vitalets! With your guidance I managed to get code completion to work again.
For me, it doesn't work only by typing the pages in the pages
fixture, perhaps this is due to my local setup.
const pages = {} as PagesFixture;
I also have to explicitly type it in the steps
When(
'user {string} creates a task',
async ({ pages }: { pages: PagesFixture }, user: string) => {
...
and that will make sure that pages[user].page
is of type Page.
The only thing that I noticed is that there are no video recordings for features that use this fixture. In my playwright.config.ts
I have
{
name: 'chromium',
timeout: 60_000,
expect: { timeout: 60_000 },
use: {
headless: false,
baseURL,
screenshot: 'only-on-failure',
video: 'on',
trace: 'retain-on-failure',
},
}
but if a test fails I only get screenshots and traces.
For typing I assume we need to provide type to test.extend
:
export const test = base.extend<{ pages: PagesFixture }>({
// eslint-disable-next-line no-empty-pattern
pages: async ({}, use) => {
const pages = {} as PagesFixture;
await use(pages);
// close created browser contexts if any
for (const user of Object.keys(pages)) await pages[user]?.context.close();
},
});
About video I will check. Maybe Playwright records video only for default context.
About video I will check. Maybe Playwright records video only for default context.
Here is the discussion: https://github.com/microsoft/playwright/issues/21994 And the root one: https://github.com/microsoft/playwright/issues/14813
Yes, I guess some default setting are not being passed on because videos are saved when I explicitly set the video directory in the step that creates the context and page.
Given(
'user {string} logged-in',
async ({ browser, pages }, user: string, email: string) => {
const context = await browser.newContext({
storageState: `.playwright/auth/${user}.json`,
recordVideo: { dir: `.playwright/videos` },
});
const page = await context.newPage();
pages[user] = { context, page };
}
);
But as indicated in the above issue the video is not included in the cucumber report. To have it included, I created another fixture called saveVideos
.
export const test = base.extend<{ pages: PagesFixture; saveVideos: void }>({
// eslint-disable-next-line no-empty-pattern
pages: async ({}, use) => {
const pages = {} as PagesFixture;
await use(pages);
// close created browser contexts if any
for (const user of Object.keys(pages)) {
await pages[user]?.context.close();
}
},
saveVideos: [
async ({ pages }, use, testInfo) => {
await use();
Object.keys(pages).forEach(async (user) => {
const video = await pages[user].page.video().path();
testInfo.attachments.push({
name: `${user}`,
path: video,
contentType: 'video/webm',
});
});
},
{ scope: 'test', auto: true },
],
});
Added example with dynamic authentication in steps. Linked to @amuresia comment about video saving.
Hi! I was wondering if there are any suggestions on how to test a scenario like the one below (ideally by using storage states that are saved prior to running the test i.e. admin.json and user.json) Example scenario
The bit where I get stuck is the second login. I don’t know how to tell playwright that it should use the storage state of admin@example.com up until the login and then another context/ page with a different storage state to continue the test.
The playwright documentation for testing multiple roles together makes sense but I am struggling to come up with a best practice to apply it to playwright-bdd.
I came across the isolation documentation for playwright and it looks like what I need but I don’t see how to integrate it in my project.
To expand on the above, I would appreciate any help creating and switching between contexts that use different storage states. Some of my tests are nested on purpose because they test functionality of a chat application where let’s say I need to send a message as userX and then like it as userY and then confirm that userX received a notification when the message was liked so I need to swap between users as part of the same test.
I don’t have an issue saving various state files to disk, but I am struggling to create contexts that use them as part of the same test.
I looked at https://github.com/vitalets/playwright-bdd/issues/184 but my use case seems more complicated, as the same test needs to use multiple storage states.
Any suggestions would be very much appreciated, thanks!