Day 12: Playwright Storage State, Allure, Lists, and Web Tables
This is Day 12 of the 21-day JS to Playwright Framework series. One lesson a day. JavaScript first. TypeScript next. Playwright Test now. Framework later.
Days 10 and 11 opened the browser, wrote the first test(), and learned locators — goto, CSS, XPath, getByRole, cookies. That is enough to log in to VWO fifty times in a row. It is not enough to ship a suite. A real suite logs in once, snapshots the authenticated browser, and reuses that snapshot. Then it labels the run so a manager can read Allure. Then it stops clicking the first a on the page and starts walking a list. Then it reads a table the way a human reads a table: find the name, read the country next to it.
I am Pramod Dutta. I teach SDETs in India for a living. The week I introduce storageState, someone always asks: “Why not put login in beforeEach?” Because beforeEach is a tax. Every test pays it. Every retry pays it. Every worker pays it. VWO, OrangeHRM, any app with a slow auth wall — you will burn minutes on a form you already proved works. storageState is the receipt. You keep it. You reuse it.
This is not the existing 21-Day Playwright with TypeScript Challenge. That series starts with npx playwright test. This series started at console.log. Today we are in the fundamentals repo, labs 228 through 233, plus an empty 234 I will not invent.
All labs today come from my public fundamentals repo: LearningPlaywrightFundamentals on branch main. I fetched each file from raw GitHub. I quote those files. I will not invent a file that is not there.
Three honesty notes before we open a browser.
One. tests/04_Session_Storage/228_Session.spec.ts looks like a spec. The filename ends in .spec.ts. It is not a Playwright Test. There is no import { test, expect } from "@playwright/test". There is no test(). It is a library script: import { chromium } from "playwright", a saveSession() function, and a top-level saveSession() call. The folder README runs it with node --experimental-strip-types, not npx playwright test. I say this in the classroom every time, because npx playwright test tests/04_Session_Storage will also try to load 228 as a spec. Do not do that.
Two. Allure is installed. package.json lists allure-playwright at ^3.7.1. The Allure reporter line in root playwright.config.ts is commented out. The live reporter array is HTML plus the custom TTA reporter. Lab 230 still calls allure.epic, allure.feature, allure.story, and allure.description. Those labels do nothing useful until a reporter consumes them. I will show you the commented line. I will not pretend the default npx playwright test writes an Allure report.
Three. Lab number 234 is used twice on main. tests/07_WebTables/234_WebTABLE_Employe_Management.spec.ts is present and empty — 0 bytes. I will not invent an employee-management CRUD table for it. The other 234 is tests/08_Web_Select_Frames_Iframe/234_Web.spec.ts. That file belongs to Day 13. I will name the collision and leave the body for tomorrow.
If you want the video plus project path after you finish these 21 posts, the course is here: Playwright Automation Mastery. The series hub for every day lives here: JavaScript to TypeScript to Playwright Advanced Framework 21-Day Guide.
*Want to master this with real projects? Join the Playwright Automation Mastery course at The Testing Academy.*

Contents
What you will be able to do after Day 12
By the end of this post you can:
- Explain why
context.storageState({ path })is a snapshot of cookies and local storage, and why you must wait for the dashboard URL before you write the file. - Run
228_Session.spec.tsas a Node library script, not as Playwright Test, and produce./user-session.jsonat the repo root. - Load that JSON with
test.use({ storageState: "./user-session.json" })in229.TestVWo.spec.tsand open dashboard and settings without typing a password. - Add Allure epic / feature / story / description in a spec, and tell an interviewer the reporter is commented out in this repo’s config.
- Collect a list of matching links with
allInnerTexts()andall(), then click one item without failing Playwright’s strict mode. - Walk a static HTML table with a dynamic XPath and with a native locator
{ hasText }plustd.nth(). - Extract row text from a second table with
rows.nth(i).locator('td').allInnerTexts(). - Refuse to invent content for an empty employee-table file, and name the duplicated lab 234.
That is a framework day dressed as four folders. Login reuse. Reporting labels. Collections. Tables. Tomorrow we leave the table and enter frames, selects, keyboard, hover, drag, and JS alerts.
Clone LearningPlaywrightFundamentals and stay on main
Days 1-9 lived in LearningPlaywrightBatch. Days 10-16 live in LearningPlaywrightFundamentals. Different repo. Same classroom numbering. Labs here start at 209 so they map onto the live batch.
Clone the fundamentals repo from GitHub. Stay on branch main. Install dependencies from the repository root with the project’s lockfile.
We will run, quote, and adapt these files only.
04 — Session storage
tests/04_Session_Storage/228_Session.spec.ts— library script, not a Testtests/04_Session_Storage/229.TestVWo.spec.ts— Playwright Test that loads the saved statetests/04_Session_Storage/README.md
05 — Allure reporting
tests/05_Allure_Reporting/230_Login.spec.tstests/05_Allure_Reporting/README.md- root
playwright.config.ts(Allure reporter commented out) - root
package.json(allure-playwrightis a devDependency)
06 — Multiple elements (the folder name on GitHub is 06_Multiple_Element_ with a trailing underscore)
tests/06_Multiple_Element_/231_Multiple_Element.spec.tstests/06_Multiple_Element_/README.md
07 — Web tables
tests/07_WebTables/232_WebTable_Basic.spec.tstests/07_WebTables/233_WebTable_Dyanamic.spec.ts(filename is spelledDyanamicon GitHub)tests/07_WebTables/234_WebTABLE_Employe_Management.spec.ts— empty, 0 bytestests/07_WebTables/README.md
Skipped as a body: the employee-management spec. Present. Empty. I will not write a fake CRUD flow and pretend it is on main.
Not today: tests/08_Web_Select_Frames_Iframe/234_Web.spec.ts. Same lab number. Different folder. Day 13.
You need the repo’s Playwright install. package.json pins @playwright/test at ^1.59.1. The root config runs Chromium, Full HD 1920×1080, headed, with trace, video, and screenshot all set to 'on'. That is the classroom machine. CI in this repo is a separate workflow. Today we stay local.
Why Playwright storageState is the first framework habit
A locator finds one control. A storageState file finds a session.
When you log in to https://app.vwo.com/#login, the server sets cookies. The app may also write local storage. Playwright’s browserContext.storageState() snapshots that authenticated context to JSON: cookies, local storage origins, and related browser storage the API supports. You hand that JSON to the next context. The next context is already logged in.
This is not “sessionStorage the Web API.” The folder is named 04_Session_Storage. The method is storageState. Do not confuse the two in an interview. HTML sessionStorage is tab-scoped and Playwright does not persist it through storageState the way it persists cookies and local storage. If an app keeps the auth token only in sessionStorage, this lab’s JSON will not save you. VWO’s classroom flow is cookie-backed enough that the snapshot works — after you wait for the dashboard.
That last clause is the whole lab. Look at the comment inside 228. I put it there because students snapshot too early.
// Wait for login to actually complete before snapshotting storage —
// otherwise the auth cookie isn't set yet and the saved state is empty.
Click #js-login-btn. Wait 200 ms. Call storageState. Open the JSON. Empty cookies. Empty origins. Then 229 opens the dashboard and VWO throws you back to login. The file is not magic. It is a photograph. Photograph the room after the guest has sat down, not while they are still in the hallway.
Day 11 already taught context.cookies() and context.addCookies() in lab 227. That is the manual version. storageState is the packaged version: write the jar, load the jar, stop touching individual cookie names.
In a later framework we will put this behind a setup project so CI generates the JSON, then every project consumes it. That file is not in folders 04-07. I will not invent a playwright/auth/user.json setup project today. Today is the classroom two-step: script writes, spec reads.
Lab 228 — a library script that saves user-session.json
Lab: tests/04_Session_Storage/228_Session.spec.ts
I am going to say this again, because the filename is a trap.
This file is not a Playwright Test.
- It imports
chromiumfrom"playwright", the library. - It does not import
testorexpectfrom"@playwright/test". - It defines
async function saveSession(). - It calls
saveSession()at the bottom of the file. That call runs when Node loads the module.
If Playwright Test collects this path, the worker imports the file. The import runs saveSession(). A headed Chromium window appears during collection. That is not a test. That is a side effect. The folder README is explicit. Run it with Node.
Exact file on main (imports and launch):
import { chromium } from "playwright";
async function saveSession() {
let browser = await chromium.launch({ headless: false });
let context = await browser.newContext();
let page = await context.newPage();
Then the script opens the VWO hash login route, fills the username and password CSS ids that sit in the file on GitHub, and clicks #js-login-btn. Classroom warning, same sentence as the folder README: do not commit real credentials. This lesson hardcodes a single-use demo mailbox and a password in the script. If you expand the lesson, move login data to environment variables. I am quoting the file as GitHub serves it. I am not going to pretend the strings are process.env.VWO_USER. They are not. Not in this blob.
await page.goto("https://app.vwo.com/#login");
await page.waitForTimeout(2000);
await page.fill("#login-username", "opg73@singleuseemail.site");
await page.fill("#login-password", "Wingify@4321");
await page.waitForTimeout(1500);
await page.click("#js-login-btn");
The wait that matters is the next block. The waitForTimeout calls are classroom pauses so a student can see the form. Playwright already auto-waits on fill and click. Do not copy waitForTimeout into a CI suite as your wait strategy.
// Wait for login to actually complete before snapshotting storage —
// otherwise the auth cookie isn't set yet and the saved state is empty.
await page.waitForURL(/#\/(dashboard|home)/, { timeout: 15000 });
await page.waitForTimeout(3000);
await context.storageState({ path: "./user-session.json" });
console.log("Session saved to user-session.json");
await page.waitForTimeout(2000);
await browser.close();
}
saveSession();
Launch, context, page. chromium.launch headed so you can watch. browser.newContext() is a fresh cookie jar. context.newPage() is one tab in that jar. This is the same Browser, Context, Page hierarchy from labs 212-214. Those three were also library scripts. 228 is the same shape with a purpose: persist the jar.
Wait for the authenticated route. page.waitForURL with a regex for #/dashboard or #/home. VWO uses a hash router. Fifteen seconds. If this times out, you do not have a session. Do not snapshot.
Snapshot. context.storageState({ path: "./user-session.json" }). Relative path. From the repository root. The README says it: when you run from the repo root, the file lands as user-session.json at the project root. Lab 229 loads ./user-session.json. Same place. If you change directory into tests/04_Session_Storage and run the script, the JSON appears next to the spec and 229 will not find it. Run from root.
Close. Another pause. browser.close(). The JSON stays on disk. The browser is gone. That is the point. The session outlives the process.
How the README runs it, from the repository root: install dependencies, then execute the TypeScript file with Node’s strip-types flag.
node --experimental-strip-types tests/04_Session_Storage/228_Session.spec.ts
--experimental-strip-types lets current Node execute a .ts file without a separate compile step. This repo’s labs are TypeScript. The script is TypeScript. Node strips the types and runs the JavaScript. If your Node is too old for that flag, you already have the wrong runtime for Playwright 1.59.
After a successful run you should see a console line that the session was saved to user-session.json. Open that file. You should see a cookies array and an origins array. If both look empty, you snapshotted before auth. Delete the file. Run 228 again. Watch the URL bar. Snapshot only after #/dashboard or #/home.
If the saved session expires, login fails, or the account IDs change, regenerate the file before you run 229. The README says that. I am not adding a refresh helper. There is no refresh helper in this folder.
Lab 229 — Playwright Test loads the snapshot and skips login
Lab: tests/04_Session_Storage/229.TestVWo.spec.ts
Filename has a dot: 229.TestVWo.spec.ts. I use that name. I do not “fix” it to 229_TestVWO.spec.ts.
This one is a Playwright Test.
Exact file on main:
import { test, expect } from "@playwright/test";
// Load saved session — already logged in
test.use({
storageState: "./user-session.json"
});
Then two tests. The first opens the VWO dashboard get-started URL with accountId=1227004 and asserts the page URL matches /dashboard/. The second opens settings accounts general with accountId=1227007 and asserts /settings/. Each test logs a checkmark and pauses three seconds so you can see the headed window. I quote the names as they are: “go directly to dashboard — no login” and “go directly to settings — no login”.
Three lines of framework. The rest is proof.
test.use({ storageState: "./user-session.json" }) is a Test Options override for every test in this file. Playwright creates each test’s context with that file already applied. The page fixture you receive is a tab in an authenticated context. You do not call fill. You do not click Login. You goto a logged-in URL and assert the URL still contains dashboard or settings.
Two tests. Two deep links. Two different accountId query values in the file. I am not going to invent a reason those IDs differ. They are what main has. If VWO rejects an ID, the URL assertion fails and you regenerate state or update the ID. I will not invent a third test that “fixes” the IDs.
test.use at file scope applies to both tests. You could put storageState in playwright.config.ts under use and every spec in the project would inherit it. This repo’s config does not do that. The root use block sets trace, video, screenshot, headless, and viewport. It does not set storageState. 229 is local on purpose: only these two tests skip login. A login-negative test in another folder still gets a clean context. That is the right default.
Run from the repo root, after 228 has written the JSON:
npx playwright test tests/04_Session_Storage/229.TestVWo.spec.ts
If you skip 228, 229 fails at context creation: the path ./user-session.json does not exist. Order is the lesson. Setup script first. Consumer spec second.
What you should see: two passing tests, two console checkmarks, a headed Chromium that never types in #login-username. If you see the login form, the JSON is stale or empty. Delete it. Re-run 228. Re-run 229.
This is the same idea as a setup project in Playwright’s auth docs, minus the project dependency graph. We will build that graph when the series reaches the advanced framework repo. Today you should be able to draw the arrow: library script, then JSON, then test.use. If you cannot draw it, you are not ready to put storageState in CI.
*Want the live classroom version of this — VWO auth, a setup project, and the Day 21 framework that loads state per role? Playwright Automation Mastery.*
What storageState is not
A short list I give every batch, because the folder name lies a little.
It is not HTML sessionStorage. Playwright’s own auth docs say storageState covers cookies, local storage, IndexedDB, and WebAuthn passkeys. Tab sessionStorage is a separate, manual page.evaluate plus addInitScript pattern. There is no addInitScript in 228 or 229. Do not claim this lab persists sessionStorage.
It is not a secret store. The JSON on disk is a credential. Gitignore it. The folder README does not add a .gitignore line for you. Check your root ignore before you commit a live session. I am not inventing an ignore file that is not part of today’s fetch list.
It is not forever. Tokens expire. Accounts change. The README: “If the saved session expires, login fails, or the account IDs change, regenerate user-session.json before running the dependent tests.”
It is not isolation. 229’s two tests share the same saved identity. They do not share a live context — Playwright still gives each test a fresh context hydrated from the same file. Mutations one test makes to cookies after start are not written back unless you call storageState again. Neither test does.
It is not a substitute for a login test. You still need one test that types the password and proves login works. Lab 230 is that test, with Allure labels on top. 228 and 229 prove you can skip login after you have proved it.
Lab 230 — Allure labels on a real VWO login
Lab: tests/05_Allure_Reporting/230_Login.spec.ts
This folder has one spec and a README. That is the whole module. I will not invent allure.step wrappers, severity, or a screenshot attachment helper. Those are not in 230.
The spec imports test and expect from @playwright/test and * as allure from allure-js-commons. The test name is Verify that the login works.
Four Allure calls, quoted as GitHub serves them:
allure.epic("VWO Login Tests")— high-level product or test areaallure.description("Verify that the login is page works")— readable blurb. I quote the wording. “the login is page works.” Classroom English. I am not silently editing it.allure.feature("Essential features")— the feature under testallure.story("Authentication")— the user story
Epic, then feature, then story is Allure’s grouping. When a reporter is actually wired, the HTML tree shows VWO Login Tests / Essential features / Authentication / this test name. Without a reporter, these calls are function invocations that write metadata into Allure’s runtime. Nobody renders them.
Then the browser flow is 228 without storageState and with a title assertion. Same VWO login URL. Same CSS ids. Same password. Same waitForURL for dashboard or home. Then expect(page).toHaveTitle("Dashboard"). That is the proof login worked — the document title, not a cookie dump.
The comment above waitForURL is copy-pasted from 228. This spec does not snapshot storage. The comment is leftover. I leave it. I tell students: comments that mention a call you did not make are a smell. We do not invent a storageState line to make the comment true.
The Allure reporter is commented out in playwright.config.ts
This is the part students miss, then they open playwright-report and ask where Allure went.
Root playwright.config.ts on main, reporter block only. The live reporter array is HTML plus ./utils/CustomTTAReporter.ts. Directly under that line, commented out, is the same array with "allure-playwright" inserted in the middle. I am describing the file, not inventing a second config.
The package is installed. The config does not use it.
package.json lists allure-playwright at ^3.7.1 under devDependencies. There is no allure script in scripts. Scripts are go, test, test:headed, test:ui, report, report:tta. Default test is Playwright Test. That run writes the Playwright HTML report and the TTA report. It does not write allure-results unless you pass a reporter on the CLI.
The folder README is the honest runbook.
Default, using the configured project reporters:
npx playwright test tests/05_Allure_Reporting/230_Login.spec.ts
Allure for this command only, still from that README: pass --reporter=line,allure-playwright, then generate HTML from ./allure-results into ./allure-report, then open the report. That CLI override does not change playwright.config.ts. It does not uncomment the line. It attaches Allure for one invocation. You need the Allure command line available. I am not inventing a package.json script named allure:open. It is not there.
If you uncomment the config line yourself, you are editing the repo. This post does not do that. This post tells you the line is commented, the package is present, and the README shows the CLI escape hatch.
Why leave it commented in a teaching repo? Because the default classroom loop is headed Chromium plus the HTML report plus the TTA report. Allure is a second HTML tree. Students who have not installed the Allure CLI think the run is broken when allure-results appears and nothing opens. We teach the labels first. We wire the reporter when the machine has the CLI. Day 12 is labels plus honesty about the comment.
Interview answer, short: “Allure metadata lives in the spec via allure-js-commons. The reporter lives in config. This repo has the metadata. The reporter line is commented. I can enable it per run with --reporter=line,allure-playwright.”
Lab 231 — lists, allInnerTexts, and .first() so strict mode does not slap you
Lab: tests/06_Multiple_Element_/231_Multiple_Element.spec.ts
Folder name on GitHub: 06_Multiple_Element_. Trailing underscore. Path must match.
The spec wraps one test in test.describe('Multiple Elements Handling'). The test title is Basic Test - Verify page title. The body never calls toHaveTitle. There is an expect import and zero expect() calls. I am not going to invent an assertion and claim it is in the file. The lab is a collection drill. The title is leftover classroom naming. Say so in a code review.
The page is The Testing Academy multiple-element filter page: https://app.thetestingacademy.com/playwright/multiple_element_filter. The repeated selector is a.list-group-item — Bootstrap list-group links.
allInnerTexts() returns a string[]. One string per matching node, visible text. The spec logs the length, then loops. When the text is exactly My Account, it clicks. That is “filter the collection in JavaScript, then act.” You already wrote for...of and === in Days 2 and 4. Here they earn a click.
.first() is the strict-mode escape. page.getByText('My Account') may resolve more than one node. Playwright actions are strict. Click on a locator that matches two nodes throws. .first() picks one. The folder README says this out loud: the .first() call avoids strict-mode issues when more than one element has the same text. Prefer a locator that is unique. When the page gives you duplicates, .first(), .nth(), and .last() are the valves. Day 11 already used those on CSS. Same valves. New collection.
all() returns an array of Locator objects. Not strings. Locators. The second loop calls getAttribute("href") on each. Text extraction and attribute extraction are different APIs on purpose. allInnerTexts() when you need to read. all() when you still need locator methods.
Do not store a Locator from all() and expect it to freeze the DOM. A locator is lazy. It re-queries. If the click on My Account navigates, the second loop is a new page’s a.list-group-item set — or an empty set. The spec does not assert navigation. It prints hrefs of whatever matches after the click. Run it headed. Watch. That is the lab.
Run the spec path tests/06_Multiple_Element_/231_Multiple_Element.spec.ts with Playwright Test. For a headed run, the repo script test:headed accepts the same path.
Why this sits on the same day as tables: a table is a list with columns. allInnerTexts() on td is the same muscle as allInnerTexts() on a.list-group-item. Lab 231 is the warm-up. Labs 232 and 233 are the set.
Lab 232 — Helen Bennett, XPath indexes, then the native locator
Lab: tests/07_WebTables/232_WebTable_Basic.spec.ts
This is the table lesson I have taught for years. Find a name. Read the country in the same row. First with the XPath you already know from Selenium. Then with the Playwright locator I want you to keep.
The test lives in test.describe('Web Table Tests'). The test name is Verify that Helen Bennett is actually living in the UK. The page is https://awesomeqa.com/webtable.html. Table id customers. Classic company / contact / country table. Helen Bennett is a contact. Her country in that demo table is the United Kingdom. The test name says UK. The assertions are console.log. Again: expect is imported and never called. The “verify” is your eyes on the log. I will not invent expect(country1).toBe('UK') and paste it as if it were on main.
The comments in the file draw the XPath pattern: //table[@id="customers"]/tbody/tr[ i ]/td[ j ]. Three string slices — firstPart, secondPart, thirdPart — concatenate with i and j. This is the Selenium habit. I keep it in the file so a Java SDET can see the old muscle, then I retire it.
rows is the tr count under tbody. cols is the td count in tr[2] — the first data row. Row 1 in this table is the header, so the data loop starts at i = 2. XPath indexes are one-based. tr[1] is the first tr. td[1] is the first td.
The inner loop walks every cell. If the cell text includes Helen Bennett, the country is the following sibling td. Axis: following-sibling::td. Same row, next cell. On this table the country sits in the third column, immediately after the contact name. The axis works because of that layout. If a column appeared between name and country, the first following sibling would be wrong. Axes are layout-dependent. Say that in the interview.
After the nested loop, four lines I want you to keep. A row locator: #customers tbody tr with { hasText: 'Helen Bennett' }. Then row1.locator('td').nth(2).innerText() for the country. locator(selector, { hasText }) filters the row list to the row that contains that text. No string-built XPath. No i and j. Then nth(2) is the third cell. nth is zero-based. nth(0) company, nth(1) contact, nth(2) country.
That is the conversion table for the rest of your career. XPath first row is tr[1]. Playwright first row is nth(0). XPath third cell is td[3]. Playwright third cell is nth(2). Off-by-one is the web-table interview. I fail people on it every month. Lab 232 exists so you feel both indexes in one file.
The comment in the spec: “Playwright Native Locators is very much recommended.” I agree with my own comment. Teach XPath so you can read a legacy suite. Write hasText plus nth in a new suite.
Run tests/07_WebTables/232_WebTable_Basic.spec.ts. You should see a flood of dynamic XPath strings, cell texts, then a log line that Helen Bennett is in the UK — twice. Once from the axis, once from nth(2). If the demo site changes Helen’s country, the log changes. The file does not freeze the value in an expect.
Lab 233 — a second table, count, nth, allInnerTexts
Lab: tests/07_WebTables/233_WebTable_Dyanamic.spec.ts
Filename spelling on GitHub: Dyanamic. I use that name.
Same describe title: Web Table Tests. The test name is test_web_table_login - structured extraction. There is no login. There is no storageState. Classroom leftover. Structured extraction is the real name of the work.
Different URL: https://awesomeqa.com/webtable1.html, not webtable.html. Different table: table[summary="Sample Table"]. The locator is a collection of tbody tr. count() prints how many rows the body has.
Then the native strategy: for each index, rows.nth(i).locator('td').allInnerTexts(). That is lab 231’s allInnerTexts() applied to one row’s cells. You get a string[] per row. Log it.
Read the loop bounds the way I make a batch read them.
nth is zero-based. Valid indexes are 0 to rowCount - 1.
This loop starts at i = 1 and goes i <= rowCount. That skips nth(0) — the first body row — and on the last iteration asks for nth(rowCount), which is past the last row. Playwright will wait, then fail or return empty depending on the run. I am not rewriting the file in this post. I am telling you what main contains. If you change the loop in your clone, change it to start at 0 and stop before rowCount, and log Row ${i + 1}. That edit is yours. It is not in the blob I fetched.
Run tests/07_WebTables/233_WebTable_Dyanamic.spec.ts. Or the whole folder tests/07_WebTables. The folder run will collect 232, 233, and 234. 234 is empty. An empty .spec.ts collects as a file with zero tests. Playwright will list it and move on, or warn. It will not invent employee CRUD.
Lab 234 in 07_WebTables is empty — and the number is used again in folder 08
File: tests/07_WebTables/234_WebTABLE_Employe_Management.spec.ts
GitHub serves 0 bytes. The blob SHA is the empty-blob SHA. The folder README says it in one line: currently an empty placeholder for a future employee management table lesson.
Filename spelling: WebTABLE in mixed case, Employe without the second e. I use that name.
I will not write an add / edit / delete employee flow and claim it is lab 234. There is no table URL in this file. There is no test(). There is no locator. A future lesson may fill it. Today the honest sentence is: the file is an empty placeholder.
The number 234 is also used here: tests/08_Web_Select_Frames_Iframe/234_Web.spec.ts.
That file has a body. It is a table-row plus checkbox lesson that sits in the select/dropdown folder. The root course README calls this out as a numbering quirk: lab 234 is used twice — empty employee-table spec and 08/234_Web.spec.ts. Day 13 owns folder 08. I will not quote 08’s 234 today. I will not pretend the employee file contains the select-folder file. Two paths. One number. One of them is empty.
If an interviewer asks “how do you automate employee table CRUD in Playwright?”, the answer from this repo today is: “The planned file is 234_WebTABLE_Employe_Management.spec.ts and it is empty on main. I would reuse 232’s hasText row plus nth cells, and 231’s collection loop, and I would not paste a spec I did not fetch.”
That answer gets you hired more often than a fake gist.
How these four folders become one framework habit
I do not want you to leave Day 12 with four disconnected tricks. I want one picture.
Auth is a file. 228 writes it. 229 reads it. A later setup project will write it in CI. Page objects will assume the context is already logged in. That is why Day 8’s LoginPage and Day 9’s typed POM still matter: you keep a login page for the setup script and for the negative tests. You do not call login() in every dashboard spec.
Reporting is labels plus a reporter. 230 writes epic / feature / story. Config must attach allure-playwright or the CLI must pass --reporter. This repo’s default config does not. Your framework repo will. Know which side of that comment you are on before you promise Allure in a sprint demo.
Lists are locators that return many. allInnerTexts() for text. all() for locator methods. .first() when strict mode is right and the DOM is messy. Tables are lists with a second axis.
Tables are rows, then cells. Count first. Decide zero-based vs one-based. Prefer tr plus hasText plus td.nth. Keep XPath axes in your head for legacy. Do not string-build XPath in a new POM.
When we reach Day 16 (POM plus fixtures plus TTA Cart / Bank) and Day 17 (advanced framework layers), these four habits show up as: an auth setup, a reporter array that is actually on, a table helper that returns row text, and locators that never click the wrong list-group-item. I am not inventing those helpers today. I am pointing at the labs that make them obvious.
The root tests/README.md says the shared config runs Chromium, collects traces, videos, and screenshots, and writes the HTML report and the TTA report. That is your debug surface when 229 bounces to login or 232 cannot find Helen: open the trace, do not add another waitForTimeout.
Common failures I see in this week’s homework
229 fails because the storageState path does not exist. You ran 229 before 228, or you ran 228 from the wrong working directory. JSON must sit at ./user-session.json from the repo root.
229 opens the login page. Snapshot was empty or expired. Open the JSON. If cookies is an empty array, 228 did not wait for waitForURL. Watch the headed window. Wait until the hash is dashboard or home.
Playwright launches Chromium during collection of folder 04. You ran Playwright Test against the whole tests/04_Session_Storage folder. 228 is a library script with a top-level saveSession(). Run 228 with Node. Run 229 with Playwright Test. The README order is not decoration.
Allure report folder is missing. You ran 230 with the default config. Allure is commented out. Use the README’s --reporter=line,allure-playwright or accept HTML plus TTA.
The Allure CLI is not found. The reporter wrote allure-results. The CLI that turns results into HTML is a separate install. The README uses npx allure generate and npx allure open. If npx cannot see the package, install the Allure command line. I am not inventing a Docker one-liner. It is not in this folder.
Strict mode violation on My Account. You copied 231 and dropped .first(). Two nodes, one click, exception. Put .first() back, or write a tighter locator.
Helen Bennett loop never prints a country. Demo table changed, or your XPath i started at 1 and you are reading header cells. 232 starts data rows at i = 2 for a reason.
233 times out on the last nth. The loop uses i <= rowCount with a zero-based nth. That is the file. Fix the bounds in your clone if you want a green extraction. Do not tell a lead “the course file is a complete table API.” It is a teaching loop.
You opened 234 and started writing Selenium-style employee CRUD, then committed it as if I wrote it. Do not. The placeholder is empty. Your CRUD is your CRUD. Label it as yours.
FAQ — Playwright storageState, Allure, lists, and web tables
Is 228_Session.spec.ts a Playwright Test?
No. It is a library script. It imports chromium from "playwright", defines saveSession(), and calls saveSession() at module load. There is no test(). The folder README runs it with node --experimental-strip-types tests/04_Session_Storage/228_Session.spec.ts. 229.TestVWo.spec.ts is the Playwright Test.
How does Playwright storageState reuse a login?
context.storageState({ path: "./user-session.json" }) writes cookies and local storage after a successful login. test.use({ storageState: "./user-session.json" }) hydrates the next test’s context from that file. Labs 228 and 229. Wait for waitForURL on the dashboard or home hash before you write the file or the snapshot is empty.
Does storageState save sessionStorage?
Not the way this lab uses it. Playwright’s storageState snapshot is cookies, local storage, and related supported storage. HTML sessionStorage needs a separate evaluate / addInitScript pattern. 228 and 229 do not contain that pattern.
Why is there no Allure report after npx playwright test?
Because the Allure reporter line in root playwright.config.ts is commented out. Live reporters are html and ./utils/CustomTTAReporter.ts. allure-playwright is installed (^3.7.1). Lab 230 still sets epic / feature / story / description. Enable Allure per run with --reporter=line,allure-playwright as the folder README shows, then generate and open the Allure HTML.
How do I click one item in a list of matching Playwright locators?
Collect text with locator.allInnerTexts(), pick the string you want, click with page.getByText(text).first() so strict mode does not throw, then use locator.all() when you need per-element methods like getAttribute('href'). Lab 231 against a.list-group-item on the TTA multiple-element page.
How do I read a cell from a Playwright web table?
Prefer a row locator with { hasText: 'Helen Bennett' }, then row.locator('td').nth(2) for the country. XPath tr[i]/td[j] plus following-sibling::td is in lab 232 as the Selenium-shaped half. XPath indexes are one-based. nth is zero-based. Lab 233 uses rows.nth(i).locator('td').allInnerTexts() on a second table. Remember 233’s loop starts at i = 1 and goes to i <= rowCount.
Why is 234_WebTABLE_Employe_Management.spec.ts empty?
Because that is what GitHub serves: 0 bytes. The folder README calls it a placeholder for a future employee management table lesson. I do not invent CRUD. Lab number 234 is also used by tests/08_Web_Select_Frames_Iframe/234_Web.spec.ts, which is a Day 13 file.
What is Day 13 of this series?
Selects, frames, iframes, keyboard, hover, drag-and-drop, and JS alerts — fundamentals folders 08 through 11. Folder 08’s own README says there is no active frameLocator() in that folder yet; real iframes start at lab 239 in 09_Frame_Iframe. Alerts are lab 243 in 11_JS_Alerts.
<script type=”application/ld+json”> { “@context”: “https://schema.org”, “@type”: “FAQPage”, “mainEntity”: [ { “@type”: “Question”, “name”: “Is 228_Session.spec.ts a Playwright Test?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “No. It is a library script. It imports chromium from playwright, defines saveSession(), and calls saveSession() at module load. There is no test() from @playwright/test. Run it with node –experimental-strip-types. 229.TestVWo.spec.ts is the Playwright Test that loads user-session.json.” } }, { “@type”: “Question”, “name”: “How does Playwright storageState reuse a login?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “After a successful VWO login, context.storageState writes cookies and local storage to user-session.json. test.use storageState hydrates later tests so they can open dashboard and settings URLs without filling the login form. Wait for the dashboard or home hash route before snapshotting or the file is empty.” } }, { “@type”: “Question”, “name”: “Does Playwright storageState save sessionStorage?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Not in labs 228 and 229. storageState snapshots cookies and local storage. HTML sessionStorage is a separate evaluate and addInitScript pattern that these files do not use.” } }, { “@type”: “Question”, “name”: “Why is there no Allure report after npx playwright test in LearningPlaywrightFundamentals?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “The Allure reporter line in playwright.config.ts is commented out. The live reporters are html and the custom TTA reporter. allure-playwright is installed. Lab 230 sets epic, feature, story, and description. Pass –reporter=line,allure-playwright for one run, then generate and open the Allure HTML as the module README shows.” } }, { “@type”: “Question”, “name”: “How do I handle multiple matching elements in Playwright?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Use locator.allInnerTexts() for the visible text of every match, then page.getByText(text).first().click() to act on one item without a strict-mode violation. Use locator.all() when you need locator methods such as getAttribute href. Lab 231 uses a.list-group-item on the Testing Academy multiple-element page.” } }, { “@type”: “Question”, “name”: “How do I read a Playwright web table cell?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Prefer page.locator table tbody tr with hasText Helen Bennett, then locator td nth 2. Lab 232 also builds a dynamic XPath and uses following-sibling td. XPath indexes are one-based; nth is zero-based. Lab 233 extracts each row with allInnerTexts.” } }, { “@type”: “Question”, “name”: “Why is 234_WebTABLE_Employe_Management.spec.ts empty?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “The file is present on main and is 0 bytes. The 07_WebTables README calls it a placeholder for a future employee management table lesson. Do not invent CRUD. Lab number 234 is also used by tests/08_Web_Select_Frames_Iframe/234_Web.spec.ts, which belongs to Day 13.” } }, { “@type”: “Question”, “name”: “What is next after Day 12 of the JS to Playwright Framework series?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Day 13 covers fundamentals folders 08 through 11: native and custom selects, frames and iframes with frameLocator, keyboard, hover, drag-and-drop, and JS alerts via page.once dialog. Folder 08 is named for frames but its README says there is no active frameLocator there yet; iframes start in folder 09.” } } ] } </script>
Tomorrow — Day 13: frames, selects, keyboard, and JS alerts
Tables stay in the top-level page. Tomorrow the page grows extra documents.
Day 13 of this series takes LearningPlaywrightFundamentals folders 08_Web_Select_Frames_Iframe through 11_JS_Alerts on main.
Folder 08 is named for frames. Its own README says the current specs run against top-level pages and there is no active frameLocator() in that folder yet. What 08 actually holds is the second 234 (234_Web.spec.ts — row plus checkbox), then native selectOption, custom div dropdowns, and React-Select labs 235-238 plus a util.ts helper. I will quote those files tomorrow. I will not quote them today.
Real iframes start at 09_Frame_Iframe: lab 239 single iframe, 240 multiple frames, 241 nested frameLocator. Keyboard, hover, and drag live in 10_Keyboard_Hover_Drag_Drop. JS alert / confirm / prompt live in 11_JS_Alerts lab 243 — page.once('dialog'). Lab 243 sits after 247 in numbering. Another classroom quirk. We will say so.
If you cannot reuse a session, label a test, walk a list, and read Helen Bennett’s country, do not open an iframe yet. The iframe is another page. You need today’s locators inside it.
Series hub (bookmark this): JavaScript to TypeScript to Playwright Advanced Framework — 21-Day Guide.
Master Playwright end to end
If you want these labs as a live classroom — with storageState setup projects, Allure actually wired in config, table helpers inside a POM, VWO login, and the framework we assemble on Day 21 — join Playwright Automation Mastery at The Testing Academy. Lifetime access. Real projects. A job-ready suite, not a folder of waitForTimeout.
*This is Day 12 of 21. Draft only. Not published.*
