Where this sits, and where everything else lands
Before any code, the whole course went up as a folder tree, created live. Twenty-three numbered topics, and the point of showing it now is that every class for the next month lands somewhere on this list:
01_Basics 09_Frame_Iframe 17_Expect_Assertions
02_TestAnnotations 10_Keyboard_Hover_Drag_Drop 18_Test_hooks
03_Locator_Commands _Calender 19_Data_Driven_Testing
04_Session_Storage 11_JS_Alerts 20_Page_Object_Model
05_Allure_Reporting 12_Handle_SVG 21_Fixture
06_Multiple_Element_Filter 13_Shadow_DOM 22_Misc_AI_Concepts
07_WebTables 14_FileUpload 23_Advance_PW_Framework
08_Web_Select_Frames_Iframe 15_File_Download
16_Scroll_toElement
Two of those fan out further. 22_Misc_AI_Concepts holds Playwright MCP, the Playwright CLI, Playwright AI agents, Selenium to Playwright migration and the 36-skills masterclass, and those run as extra evening sessions rather than in the morning slot. 23_Advance_PW_Framework is the finale, and it has its own order: API testing, then Cucumber, then the AI agent factory, then CI/CD.
Browser, context and page, once more with three roles
Last class covered the model; this one used it. The hierarchy and its cleanup order came back as an interview question: create browser, then context, then page, and close in reverse, page, context, browser.
let browser: Browser = await chromium.launch({ headless: false });
let context1: BrowserContext = await browser.newContext();
let page: Page = await context1.newPage();
// Cleanup - reverse order
await page.close();
await context1.close();
await browser.close();
That is the raw library. Inside the test runner you ask for a fixture instead, and the choice between two of them is the whole decision:
Asked in class: how do I know when to use the page fixture and when to build contexts myself? One role, use page. More than one role at once, take browser and make a context per role. That is the entire rule.
Three roles, three contexts, three pages, all live at the same time and sharing nothing:
test("BCP - in app.vwo.com two roles", async ({ browser }) => {
let adminContext = await browser.newContext();
let userContext = await browser.newContext();
let guestConetxt = await browser.newContext();
let adminPage = await adminContext.newPage();
await adminPage.goto("https://app.thetestingacademy.com/playwright/");
let userPage = await userContext.newPage();
await userPage.goto("https://sdet.live");
let guestPage = await guestConetxt.newPage();
await guestPage.goto("https://scrolltest.com");
await adminPage.close();
await userPage.close();
await guestPage.close();
});
There is no limit on contexts, and no limit on pages inside one context. The run in class hit a typo on the third context name, was fixed on the spot, and the file keeps the misspelling guestConetxt as it was typed.
async goes on functions, await goes on statements. This came up twice. page.goto() returns a promise, which is why it needs await; the arrow function that contains it is what gets async. The return type of goto is itself an interview question worth being able to answer without hesitating.
Context options: one test, a phone in Paris
A context is not only isolation, it is configuration. Everything a real browser session carries can be set when you create it:
test('context with options', async ({ browser }) => {
const context = await browser.newContext({
viewport: { width: 1920, height: 1080 },
locale: 'fr-FR',
timezoneId: 'Europe/Paris',
geolocation: { latitude: 48.8566, longitude: 2.3522 },
permissions: ['geolocation'],
});
const page = await context.newPage();
await page.goto('https://app.vwo.com/#login');
await context.close();
});
And the same mechanism gives you a phone without a phone:
test('mobile context', async ({ browser }) => {
const iPhone = {
viewport: { width: 375, height: 667 },
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X)',
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
};
const context = await browser.newContext(iPhone);
const page = await context.newPage();
await page.goto("https://app.vwo.com/#login");
await context.close();
});
Run both and you watch one window open at full HD in French and another at phone size. Viewport is the window size, not responsiveness. deviceScaleFactor is zoom, how stretched the pixels are.
The five annotations
An annotation is a modifier on test. Five of them, and each does something different to a run:
The file written in class has one of each:
test.skip('checkout with PayPal', async ({ page }) => {
// never executes
});
test.only('login as Pramod', async ({ page }) => {
// only this test runs, everything else in the file is ignored
});
test.fail('cart total is wrong, BUG-451', async () => {
expect(90).toBe(100); // actually returns 90
});
test.fixme('upload 2GB file', async () => {
// skipped, but flagged as "needs fixing"
});
test('full regression report', async () => {
test.slow();
console.log(test.info().timeout); // 90000 instead of 30000
});
test.fail reads backwards the first time. It does not make a test fail. It says "I expect this to fail", so Playwright runs it and reports passed when it does fail. A green tick there means the bug is still present. If the test starts passing, that annotation turns red and tells you the bug is fixed.
test.only is the dangerous one. Running that file gives 1 passed, out of six tests in it. Everything else silently did not run. That is exactly what you want while debugging and exactly what you must not commit, which is why the quality gate in the 2x batch makes a committed test.only an error.
test.slow() triples the timeout. With no timeout set in playwright.config.ts, Playwright's default of 30 seconds applies, so a slow test gets 90 seconds. That is where the 90000 in the comment comes from.
The scenario that ties them together, as told in class: a hundred tests, ninety-seven passing. Of the three failing, two are flaky and get fixme because you intend to fix them. The third fails because there is a real logged bug, so it gets fail and stays green until someone fixes the product.
Conditional, by browser
An annotation can take a condition and a reason, which is how one test opts out of one engine:
test('mobile layout', async ({ page, browserName }) => {
test.fixme(browserName === 'webkit', 'Safari renders menu wrong');
await page.goto("https://sdet.live");
});
browserName is a fixture like page. WebKit is Safari's engine, so this runs everywhere except there.
test.describe, and running a group by name
test.describe takes a title and a function, and everything inside becomes a group:
test.describe('Login Page', () => {
test('valid credentials', async ({ page }) => { ... });
test('invalid password', async ({ page }) => { ... });
test.fixme('1checkout with PayPal', async ({ page }) => {});
test.skip('checkout with PayPal', async ({ page }) => {});
});
Each test still gets its own page; grouping is about organisation and about being able to run the set:
npx playwright test -g "Login Page"
Running 4 tests using 4 workers
2 skipped
2 passed
Four tests collected, the fixme and the skip skipped, the two real ones passed. The -g flag matches on the title, so the group name is a handle you can use from the command line and later from CI.
The locator strategy starts here
The last stretch opened the topic the rest of the month sits on. A locator strategy is how you find an element so you can interact with it, and Playwright gives you two families:
And the command every test opens with turns out to have more to it than a URL:
await page.goto(url, { timeout: 30000, waitUntil: 'load' });
waitUntil tells Playwright which moment counts as "loaded" before it moves to the next line, and it has four settings: commit, domcontentloaded, load and networkidle. What each one means, when to use which, and the timeout and referer options alongside them, are the whole of the next class.
Interview warning, delivered from the hiring side. Candidates who lean entirely on Copilot and Claude are being rejected, including one with seven years of experience who could not write a simple Playwright test when asked, and asked for an AI tool instead. The instruction for this batch: turn the suggestions off and write it from scratch until the advanced framework is built. After that, use AI for everything.
Tasks and announcements
Before the next class
- Complete the exercises up to 225. That is through
03_Locator_Commands/225_LC.spec.ts. - Finish the previous task, the deep research on Playwright architecture, if you have not. Many had not.
- Post questions in the doubt thread. Meeti is posting today's task to the group.
If you missed it
Rewatch last class before this one makes sense: browser versus context versus page is a full class on its own, and several questions in this session were answered there.
Coming up
Next class goes deep on page.goto options: the four waitUntil settings, timeout and referer, then further into the locator strategy. Extra evening sessions on Playwright MCP, the CLI, AI agents and Selenium to Playwright migration are being scheduled for the coming week, likely Wednesday or Friday, with dates to be announced.
Repository: LearningPlaywrightFundamentals3x was restructured into the 23-topic tree during the session, and received 220_BCP.spec.ts, 221_TA.spec.ts, 222_Test_Options.spec.ts, 223_TestAnnotations.spec.ts, 224_TestDescribe.spec.ts and 225_LC.spec.ts.