Koding Tree’s Playwright with AI is a 6-month professional course covering everything from core logic to advanced system architecture and multithreading.
What You Will Learn
node --version"type":"module" in package.json for modern ES importsasync – always use awaittest(title, async ({page}) => {}) – {page} fixture is auto-injectedplaywright.config.jsnpm init playwright@latest and select JavaScriptnpx playwright test demo.spec.js --headed{page}, {context}, {browser}
WHY THIS MODULE
Playwright is now the industry’s fastest-growing automation tool – adopted by Microsoft, Google, and hundreds of product companies globally. Setting it up correctly from the start, especially understanding async/await and the {page} fixture, means every module that follows will make immediate sense. Companies hiring for Playwright roles expect you to be able to initialise and run a project from scratch in an interview.
What You Will Learn
playwright → browser → context → pagepage.goBack() / goForward() / reload() / close()page.evaluate(fn) – run JavaScript directly inside the browserpage.goto(url) – open a web addresspage.title() – read current titlepage.setViewportSize({width, height}) – resize the browser windowlaunchOptions: { slowMo: 2000 } – slow down each action by 2 secondsWHY THIS MODULE
You cannot automate something you do not understand. Knowing that every web element is a piece of HTML with a tag, attributes, and text is what makes every locator module possible. The playwright → browser → context → page hierarchy is also how Playwright isolates test sessions — understanding it prevents hours of debugging when you later run tests in parallel or manage multiple tabs.
What You Will Learn
getByAltText() – matches the alt attribute on images; not case-sensitive, allows partial matchgetByPlaceholder() – matches the greyed-out hint text inside inputsgetByText() – finds any element by its visible text contentgetByRole(role, {name}) – finds by ARIA type: button, textbox, link, etc.npx playwright test --headed --debuggetByTitle() – matches the title tooltip attribute; not case-sensitive, allows partial matchgetByLabel() – finds an input by the label that describes itgetByTestId() – matches data-testid; case-sensitive, no partial match{ exact: true } – force case-sensitive exact match (works for locators 1–5, 7)await page.pause()WHY THIS MODULE
Playwright’s smart locators (getByRole, getByLabel, getByText) are designed to survive UI redesigns — they look for meaning, not HTML structure. Using them means your tests keep working even when developers change a button’s class or ID. This is the approach Playwright’s own documentation recommends first, and what experienced automation engineers use before writing a single XPath.
What You Will Learn
tag[attributeName='value']button[class='btn'] → button.btn[name='n1'][title='click me'] – both must match^= starts-with*= contains$= ends-withinput[id='u1'] → input#u1 → #u1button.btn.btn-lg.btn-primary[name='n1'], [title='click me'] – either matches:has(), :text(), :text-is()WHY THIS MODULE
CSS is the fastest locator strategy in Playwright and often the most readable — #login-btn is much cleaner than a long XPath. Understanding partial match operators like *= and ^= is especially useful when element IDs are generated dynamically (e.g., item-4892) — you can target just the stable prefix. CSS is also the backbone of the :has-text() pseudo-class used heavily in test filtering.
What You Will Learn
//input[@name='username'] – search anywhere in the tree (preferred)//*[@id='a1'] – any tag with that attribute//input[@name='x' or @id='y']//a[text()='Google'] – or dot notation: //a[.='Google']//button[contains(.,'Login')]/html/body/div[2]/input – full path from root (fragile)//input[1], (//img)[last()], (//img)[3]//input[@name='x' and @type='text']//input[not(@placeholder='p')]//p[starts-with(text(),'User')]ends-with() is not available nativelyWHY THIS MODULE
XPath is the most powerful locator strategy — it is the only one that can match elements by their text content, navigate up to parent elements, and handle completely dynamic pages. Every automation engineer needs it in their toolkit for situations where CSS and smart locators fall short. Understanding contains() and and/or/not is what separates a capable engineer from one who gets stuck on hard pages.
What You Will Learn
child / – navigate to a direct child elementdescendant // – reach any element inside a container, any depthfollowing-sibling – move sideways to the next element://th[text()='Subject']/following-sibling::th//td[.='soap']/../..//input[@type='checkbox']parent /.. – move up to the containing elementancestor – find the table that contains a specific cell://th[text()='Subject']/ancestor::tablepreceding-sibling – move back to an earlier sibling with optional index//td[.='Pencil']/../td[8] – find price next to product nameWHY THIS MODULE
Most real-world automation challenges involve dynamic tables, e-commerce listings, and dashboards where prices, statuses, and IDs change every page load. The IE & DE pattern — anchor to something stable, navigate to something dynamic — is the professional solution. This is exactly the technique used in production frameworks to automate complex data-entry and verification tasks that no other locator strategy can handle.
Filtering & Multi-Element Handling
.filter({ hasText: '...' }) – keep only elements containing this text.filter({ has: page.locator('...') }) – keep elements that contain a child element.first() / .last() / .nth(n) – pick by position (0-indexed).and(locator) / .or(locator) – combine two locator conditions.allTextContents() / .allInnerTexts() – all visible text as string array.filter({ hasNotText: '...' }) – exclude elements containing this text.filter({ visible: true }) – visible elements only.count() – total number of matched elements.all() – get all matches as an array to loop throughiFrames & Shadow DOM
<iframe> is a page embedded inside another page – common in payment forms and adspage.locator('#f1').contentFrame().locator('#t2') – via content frame methodpage.frames()[1] – access frame by index in the frames arraypage.frameLocator('#f1').locator('#t2') – locate the frame then the element insidepage.frame({ name: 'n1' }) / page.frame({ url: '...' }) – access by name or URLWHY THIS MODULE
Real applications show dozens of similar elements — rows in a table, cards in a grid, items in a list. Filtering lets you target the exact one you need without brittle index numbers. And iFrames appear constantly in the real world: payment gateways, embedded maps, chat widgets, and social login buttons all live inside frames. Being comfortable with frameLocator is what makes you productive on real projects from day one.
click() – left-clickclick({ button: 'right' }) – right-clickdblclick() – double-clickfill(value) – clear and type into an inputclear() – clear onlypressSequentially(text) – type one character at a time like a humanpress('Enter') – press any keyboard keycheck() / uncheck() – tick or untick a checkboxhover() – mouse over to reveal tooltip or dropdowndragTo(targetLocator) – drag and drop one element onto anotherscrollIntoViewIfNeeded() – scroll until element is visibleboundingBox() – get position and size as {x, y, width, height}focus() / blur() / highlight() – focus management and debug aidisVisible() / isHidden() / isEnabled() / isChecked()textContent() – all text including hiddeninnerText() – visible text onlygetAttribute(name) – read any HTML attribute valueinputValue() – current value of an input or textareascreenshot({ path }) – capture just this elementpage.screenshot() – full pagepage.dragAndDrop(src, dst) – drag between two selectors on the pagepage.mouse.move(x, y) / page.keyboard.down('Control') – raw input APIs--trace=on ; replay at trace.playwright.devLocators tell Playwright what to interact with — actions tell it how. These 33 methods cover nearly every browser interaction. Knowing the difference between fill() (clear then type) and pressSequentially() (key by key) matters for apps that validate input as you type. The trace viewer is one of the most powerful debugging tools in Playwright — replaying failures step by step helps you quickly fix broken tests.
<select> and <option> tagsselectOption({ label: 'Chennai' }) – select by visible label textselectOption(['val1','val2']) – select multiple options at onceselectOption({ value: 'b' }) – select by the option’s value attributeselectOption({ index: 3 }) – select by zero-based position$("option[value='a']")alert (message only)confirm (OK/Cancel)prompt (text input)dialog.dismiss() – click Canceldialog.fill(text) – type into a promptpage.once('dialog', d => d.accept()) – handle the next popup onceNew tab from a click:
newPage object for subsequent actionsDropdowns, alerts, and popups appear in almost every real application — login pages, forms, confirmation dialogs, and checkout flows all use them. selectOption is simpler than Selenium’s Select class. Handling JS alerts correctly (using page.once before triggering the action) is one of the most commonly asked interview topics for Playwright roles.
request.post(url, { data: {...} }) – send a POST with a JSON bodyresponse.json() – parse and read the response bodyrequest.get(url) – send a GET request and capture the responseresponse.status() – check the HTTP status code (200, 404, 500)<a href> elements, request each, check for non-200 statusModern QA roles expect you to test both UI and APIs. Playwright’s built-in request fixture means you don’t need a separate tool like Postman inside your automation suite. More practically, setting up test state via API (like creating users or seeding data) is much faster than doing it through the UI — and it makes your UI tests more reliable because there’s less setup that can fail.
test.setTimeout(n)test.slow() – triple the current test timeout with one callpage.setDefaultTimeout(n) – applies to every action on this pagepage.setDefaultNavigationTimeout(n) – overrides only for goto/reload/goBackuse: { actionTimeout: 7000, navigationTimeout: 5000 }timeout: 40000 in defineConfig()page.waitForTimeout(ms) – blind wait (use only as last resort)attached, visible, hidden, detachedlocator.waitFor() – wait until element is visible (default state)page.waitForSelector('#id') – CSS/XPath version of waitForpage.waitForURL('exact-url') – wait for full URL match after navigationpage.waitForURL(url => url.toString().endsWith('home')) – predicate matchpage.waitForLoadState('networkidle') – page fully settled, no pending requestspage.waitForFunction(fn) – wait until a JavaScript condition returns trueFlaky tests — tests that sometimes pass and sometimes fail — are the biggest productivity killer in automation. The root cause is almost always a timing problem: the script moves faster than the page. Understanding the difference between a blind waitForTimeout (slow, unreliable) and a smart waitFor or waitForLoadState (fast, reliable) is what separates test suites that just work from ones that constantly need fixing.
expect(page).toHaveTitle('...') – assert current page titleexpect(page).toHaveURL('...') – assert current URLexpect(page).toHaveScreenshot() – visual regression for the full pagetoHaveAttribute(name, value) – verify any HTML attributetoBeChecked() / toBeEditable() / toBeAttached() / toBeEmpty()toHaveText('...') – visible text content of an elementtoHaveCSS('color', 'rgb(0,0,0)') – verify computed CSS styletoBeVisible() / toBeHidden() / toBeEnabled() / toBeDisabled()toHaveValue('...') – current value of an inputtoHaveClass('...') / toHaveId('...')toHaveScreenshot() – visual regression for a single elementtoBe() / toEqual() / toContain() – strict, deep, and substring checksexpect timeout: 5000ms – override per assertion: { timeout: 7000 }expect: { timeout: 6000 } in config applies to all assertionstoBeLessThan(n) / toBeGreaterThan(n) / toBeLessThanOrEqual(n)await expect.soft(el).toHaveValue('...') – continues, collects all failuresawait expect(page).not.toHaveTitle('Wrong Title')Without assertions, a test cannot pass or fail — it can only run. Playwright’s expect API is powerful because it automatically retries until the condition becomes true or times out, reducing flaky failures. Knowing when to use soft assertions (collect multiple failures) versus hard assertions (fail fast) is something interviewers often check.
import users from './users.json'assert { type: 'json' }npm install xlsxXLSX.readFile() → sheet_to_json()npx playwright run-server --port 56789 – on the remote machinesaucectl + .sauce/config.yml--workers=1 or workers: 2 in configfor...of – run one test per data rowawait chromium.connect('ws://localhost:56789/')npx playwright test --headed – default workers = half your CPUsfullyParallel: true in configData-driven testing transforms a single test into a full test suite. Instead of writing 50 login tests for 50 users, you write one test and drive it from an Excel or JSON file. Parallel execution then runs all tests simultaneously, dramatically reducing execution time and giving faster feedback to developers. These two skills together make automation genuinely useful in real-world teams.
#private fields, expose via getter/setterexport class from each page file; import in spec filesJSDoc:
– enables VS Code autocomplete
LoginPage, HomePage, ItemsPage, ItemEditPagetest.beforeEach / test.afterEach – auto-run before/after each test in a filetest.beforeAll / test.afterAll – run once per file{ page, browser, context, browserName, baseURL } fixturestest.skip / test.only / test.describe.skiptest.describe('group', () => {}) – group tests with shared setupbase_test.js – reusable beforeEach / afterEach across all filesglobal.setup.js – login once, save state to storageState.jsonstorageState: 'storageState.json' – every test inherits logged-in sessionglobal.teardown.js – delete storageState.json after all tests completenpm install --save-dev allure-playwright allure-commandlinenpx allure generate allure-results --clean -o allure-reportnpx allure open allure-reportnpx playwright test --grep @smokegit init → VS Code Source Control → publish to private repocall npm install → call npx playwright install → call npx playwright test{ "allure-playwright": { "outputFolder": "allure-results" } }test('login @smoke', async (page) => {}) – word starting with @--grep "@smoke|@sanity"/main0 20 * * * → 8 PM dailyThis is where everything comes together. The B149_AFW framework is a production-ready automation suite — the same pattern used in professional QA teams at product companies. By the end, you will have a real GitHub repository with POM classes, data-driven tests, Allure reports, and Jenkins integration. This portfolio project is what turns interviews from “tell me about automation” into “let me show you my framework.”
Koding Tree’s Playwright with AI is a 6-month professional course covering everything from core logic to advanced system architecture and multithreading.
What You Will Learn
node --version"type":"module" in package.json for modern ES importsasync – always use awaittest(title, async ({page}) => {}) – {page} fixture is auto-injectedplaywright.config.jsnpm init playwright@latest and select JavaScriptnpx playwright test demo.spec.js --headed{page}, {context}, {browser}
WHY THIS MODULE
Playwright is now the industry’s fastest-growing automation tool – adopted by Microsoft, Google, and hundreds of product companies globally. Setting it up correctly from the start, especially understanding async/await and the {page} fixture, means every module that follows will make immediate sense. Companies hiring for Playwright roles expect you to be able to initialise and run a project from scratch in an interview.
What You Will Learn
playwright → browser → context → pagepage.goBack() / goForward() / reload() / close()page.evaluate(fn) – run JavaScript directly inside the browserpage.goto(url) – open a web addresspage.title() – read current titlepage.setViewportSize({width, height}) – resize the browser windowlaunchOptions: { slowMo: 2000 } – slow down each action by 2 secondsWHY THIS MODULE
You cannot automate something you do not understand. Knowing that every web element is a piece of HTML with a tag, attributes, and text is what makes every locator module possible. The playwright → browser → context → page hierarchy is also how Playwright isolates test sessions — understanding it prevents hours of debugging when you later run tests in parallel or manage multiple tabs.
What You Will Learn
getByAltText() – matches the alt attribute on images; not case-sensitive, allows partial matchgetByPlaceholder() – matches the greyed-out hint text inside inputsgetByText() – finds any element by its visible text contentgetByRole(role, {name}) – finds by ARIA type: button, textbox, link, etc.npx playwright test --headed --debuggetByTitle() – matches the title tooltip attribute; not case-sensitive, allows partial matchgetByLabel() – finds an input by the label that describes itgetByTestId() – matches data-testid; case-sensitive, no partial match{ exact: true } – force case-sensitive exact match (works for locators 1–5, 7)await page.pause()WHY THIS MODULE
Playwright’s smart locators (getByRole, getByLabel, getByText) are designed to survive UI redesigns — they look for meaning, not HTML structure. Using them means your tests keep working even when developers change a button’s class or ID. This is the approach Playwright’s own documentation recommends first, and what experienced automation engineers use before writing a single XPath.
What You Will Learn
tag[attributeName='value']button[class='btn'] → button.btn[name='n1'][title='click me'] – both must match^= starts-with*= contains$= ends-withinput[id='u1'] → input#u1 → #u1button.btn.btn-lg.btn-primary[name='n1'], [title='click me'] – either matches:has(), :text(), :text-is()WHY THIS MODULE
CSS is the fastest locator strategy in Playwright and often the most readable — #login-btn is much cleaner than a long XPath. Understanding partial match operators like *= and ^= is especially useful when element IDs are generated dynamically (e.g., item-4892) — you can target just the stable prefix. CSS is also the backbone of the :has-text() pseudo-class used heavily in test filtering.
What You Will Learn
//input[@name='username'] – search anywhere in the tree (preferred)//*[@id='a1'] – any tag with that attribute//input[@name='x' or @id='y']//a[text()='Google'] – or dot notation: //a[.='Google']//button[contains(.,'Login')]/html/body/div[2]/input – full path from root (fragile)//input[1], (//img)[last()], (//img)[3]//input[@name='x' and @type='text']//input[not(@placeholder='p')]//p[starts-with(text(),'User')]ends-with() is not available nativelyWHY THIS MODULE
XPath is the most powerful locator strategy — it is the only one that can match elements by their text content, navigate up to parent elements, and handle completely dynamic pages. Every automation engineer needs it in their toolkit for situations where CSS and smart locators fall short. Understanding contains() and and/or/not is what separates a capable engineer from one who gets stuck on hard pages.
What You Will Learn
child / – navigate to a direct child elementdescendant // – reach any element inside a container, any depthfollowing-sibling – move sideways to the next element://th[text()='Subject']/following-sibling::th//td[.='soap']/../..//input[@type='checkbox']parent /.. – move up to the containing elementancestor – find the table that contains a specific cell://th[text()='Subject']/ancestor::tablepreceding-sibling – move back to an earlier sibling with optional index//td[.='Pencil']/../td[8] – find price next to product nameWHY THIS MODULE
Most real-world automation challenges involve dynamic tables, e-commerce listings, and dashboards where prices, statuses, and IDs change every page load. The IE & DE pattern — anchor to something stable, navigate to something dynamic — is the professional solution. This is exactly the technique used in production frameworks to automate complex data-entry and verification tasks that no other locator strategy can handle.
Filtering & Multi-Element Handling
.filter({ hasText: '...' }) – keep only elements containing this text.filter({ has: page.locator('...') }) – keep elements that contain a child element.first() / .last() / .nth(n) – pick by position (0-indexed).and(locator) / .or(locator) – combine two locator conditions.allTextContents() / .allInnerTexts() – all visible text as string array.filter({ hasNotText: '...' }) – exclude elements containing this text.filter({ visible: true }) – visible elements only.count() – total number of matched elements.all() – get all matches as an array to loop throughiFrames & Shadow DOM
<iframe> is a page embedded inside another page – common in payment forms and adspage.locator('#f1').contentFrame().locator('#t2') – via content frame methodpage.frames()[1] – access frame by index in the frames arraypage.frameLocator('#f1').locator('#t2') – locate the frame then the element insidepage.frame({ name: 'n1' }) / page.frame({ url: '...' }) – access by name or URLWHY THIS MODULE
Real applications show dozens of similar elements — rows in a table, cards in a grid, items in a list. Filtering lets you target the exact one you need without brittle index numbers. And iFrames appear constantly in the real world: payment gateways, embedded maps, chat widgets, and social login buttons all live inside frames. Being comfortable with frameLocator is what makes you productive on real projects from day one.
click() – left-clickclick({ button: 'right' }) – right-clickdblclick() – double-clickfill(value) – clear and type into an inputclear() – clear onlypressSequentially(text) – type one character at a time like a humanpress('Enter') – press any keyboard keycheck() / uncheck() – tick or untick a checkboxhover() – mouse over to reveal tooltip or dropdowndragTo(targetLocator) – drag and drop one element onto anotherscrollIntoViewIfNeeded() – scroll until element is visibleboundingBox() – get position and size as {x, y, width, height}focus() / blur() / highlight() – focus management and debug aidisVisible() / isHidden() / isEnabled() / isChecked()textContent() – all text including hiddeninnerText() – visible text onlygetAttribute(name) – read any HTML attribute valueinputValue() – current value of an input or textareascreenshot({ path }) – capture just this elementpage.screenshot() – full pagepage.dragAndDrop(src, dst) – drag between two selectors on the pagepage.mouse.move(x, y) / page.keyboard.down('Control') – raw input APIs--trace=on ; replay at trace.playwright.devLocators tell Playwright what to interact with — actions tell it how. These 33 methods cover nearly every browser interaction. Knowing the difference between fill() (clear then type) and pressSequentially() (key by key) matters for apps that validate input as you type. The trace viewer is one of the most powerful debugging tools in Playwright — replaying failures step by step helps you quickly fix broken tests.
<select> and <option> tagsselectOption({ label: 'Chennai' }) – select by visible label textselectOption(['val1','val2']) – select multiple options at onceselectOption({ value: 'b' }) – select by the option’s value attributeselectOption({ index: 3 }) – select by zero-based position$("option[value='a']")alert (message only)confirm (OK/Cancel)prompt (text input)dialog.dismiss() – click Canceldialog.fill(text) – type into a promptpage.once('dialog', d => d.accept()) – handle the next popup onceNew tab from a click:
newPage object for subsequent actionsDropdowns, alerts, and popups appear in almost every real application — login pages, forms, confirmation dialogs, and checkout flows all use them. selectOption is simpler than Selenium’s Select class. Handling JS alerts correctly (using page.once before triggering the action) is one of the most commonly asked interview topics for Playwright roles.
request.post(url, { data: {...} }) – send a POST with a JSON bodyresponse.json() – parse and read the response bodyrequest.get(url) – send a GET request and capture the responseresponse.status() – check the HTTP status code (200, 404, 500)<a href> elements, request each, check for non-200 statusModern QA roles expect you to test both UI and APIs. Playwright’s built-in request fixture means you don’t need a separate tool like Postman inside your automation suite. More practically, setting up test state via API (like creating users or seeding data) is much faster than doing it through the UI — and it makes your UI tests more reliable because there’s less setup that can fail.
test.setTimeout(n)test.slow() – triple the current test timeout with one callpage.setDefaultTimeout(n) – applies to every action on this pagepage.setDefaultNavigationTimeout(n) – overrides only for goto/reload/goBackuse: { actionTimeout: 7000, navigationTimeout: 5000 }timeout: 40000 in defineConfig()page.waitForTimeout(ms) – blind wait (use only as last resort)attached, visible, hidden, detachedlocator.waitFor() – wait until element is visible (default state)page.waitForSelector('#id') – CSS/XPath version of waitForpage.waitForURL('exact-url') – wait for full URL match after navigationpage.waitForURL(url => url.toString().endsWith('home')) – predicate matchpage.waitForLoadState('networkidle') – page fully settled, no pending requestspage.waitForFunction(fn) – wait until a JavaScript condition returns trueFlaky tests — tests that sometimes pass and sometimes fail — are the biggest productivity killer in automation. The root cause is almost always a timing problem: the script moves faster than the page. Understanding the difference between a blind waitForTimeout (slow, unreliable) and a smart waitFor or waitForLoadState (fast, reliable) is what separates test suites that just work from ones that constantly need fixing.
expect(page).toHaveTitle('...') – assert current page titleexpect(page).toHaveURL('...') – assert current URLexpect(page).toHaveScreenshot() – visual regression for the full pagetoHaveAttribute(name, value) – verify any HTML attributetoBeChecked() / toBeEditable() / toBeAttached() / toBeEmpty()toHaveText('...') – visible text content of an elementtoHaveCSS('color', 'rgb(0,0,0)') – verify computed CSS styletoBeVisible() / toBeHidden() / toBeEnabled() / toBeDisabled()toHaveValue('...') – current value of an inputtoHaveClass('...') / toHaveId('...')toHaveScreenshot() – visual regression for a single elementtoBe() / toEqual() / toContain() – strict, deep, and substring checksexpect timeout: 5000ms – override per assertion: { timeout: 7000 }expect: { timeout: 6000 } in config applies to all assertionstoBeLessThan(n) / toBeGreaterThan(n) / toBeLessThanOrEqual(n)await expect.soft(el).toHaveValue('...') – continues, collects all failuresawait expect(page).not.toHaveTitle('Wrong Title')Without assertions, a test cannot pass or fail — it can only run. Playwright’s expect API is powerful because it automatically retries until the condition becomes true or times out, reducing flaky failures. Knowing when to use soft assertions (collect multiple failures) versus hard assertions (fail fast) is something interviewers often check.
import users from './users.json'assert { type: 'json' }npm install xlsxXLSX.readFile() → sheet_to_json()npx playwright run-server --port 56789 – on the remote machinesaucectl + .sauce/config.yml--workers=1 or workers: 2 in configfor...of – run one test per data rowawait chromium.connect('ws://localhost:56789/')npx playwright test --headed – default workers = half your CPUsfullyParallel: true in configData-driven testing transforms a single test into a full test suite. Instead of writing 50 login tests for 50 users, you write one test and drive it from an Excel or JSON file. Parallel execution then runs all tests simultaneously, dramatically reducing execution time and giving faster feedback to developers. These two skills together make automation genuinely useful in real-world teams.
#private fields, expose via getter/setterexport class from each page file; import in spec filesJSDoc:
– enables VS Code autocomplete
LoginPage, HomePage, ItemsPage, ItemEditPagetest.beforeEach / test.afterEach – auto-run before/after each test in a filetest.beforeAll / test.afterAll – run once per file{ page, browser, context, browserName, baseURL } fixturestest.skip / test.only / test.describe.skiptest.describe('group', () => {}) – group tests with shared setupbase_test.js – reusable beforeEach / afterEach across all filesglobal.setup.js – login once, save state to storageState.jsonstorageState: 'storageState.json' – every test inherits logged-in sessionglobal.teardown.js – delete storageState.json after all tests completenpm install --save-dev allure-playwright allure-commandlinenpx allure generate allure-results --clean -o allure-reportnpx allure open allure-reportnpx playwright test --grep @smokegit init → VS Code Source Control → publish to private repocall npm install → call npx playwright install → call npx playwright test{ "allure-playwright": { "outputFolder": "allure-results" } }test('login @smoke', async (page) => {}) – word starting with @--grep "@smoke|@sanity"/main0 20 * * * → 8 PM dailyThis is where everything comes together. The B149_AFW framework is a production-ready automation suite — the same pattern used in professional QA teams at product companies. By the end, you will have a real GitHub repository with POM classes, data-driven tests, Allure reports, and Jenkins integration. This portfolio project is what turns interviews from “tell me about automation” into “let me show you my framework.”
Microservices based airline reservation backend.
Full-featured Amazon clone with Cart & Payment Gateway.
Secure transaction portal with Spring Security & JWT.
Patient records & doctor booking system.