Interview question
Test a critical user flow with Playwright
Tests one browser-to-database practice workflow with isolated fixtures, resilient locators, durable assertions, and reliable cleanup.
TL;DR
Tests one browser-to-database practice workflow with isolated fixtures, resilient locators, durable assertions, and reliable cleanup.
Playwright fixtures, browser and API boundaries, isolated data, authentication state, accessible locators, web-first assertions, persistence, parallel safety, and cleanup.
Practice the problem like a real interview: restate, reason, implement, and test.
In a dedicated test environment, create a unique candidate and published drill through a fixture API, authenticate the browser, find the drill through the public filter UI, open it, mark practice complete, reload, and prove that completion persisted. The test must run safely in parallel and clean up its own user, practice state, and content even when an assertion fails.
Move to the linked follow-up, next path step, prerequisite, or deeper variant.
Practice the next layer of the same subject.
A fixture creates SQL joins 7f31 for candidate candidate+7f31@example.test. The browser searches that exact title, opens the matching result, chooses Mark complete, and sees Practice saved. After reload the completion control still reads Completed. A parallel worker uses a different suffix, and cleanup removes only the records created for its own fixture.
/drills, locate search by accessible name, and enter the exact seeded title.import { expect, test as base } from "@playwright/test";
type PracticeFixture = {
candidate: { id: string; email: string };
drill: { id: string; title: string; slug: string };
};
const test = base.extend<{ practiceFixture: PracticeFixture }>({
practiceFixture: async ({ request }, use, testInfo) => {
const suffix = `${testInfo.workerIndex}-${crypto.randomUUID()}`;
const response = await request.post("/api/test-fixtures/practice", {
data: { suffix, topic: "SQL" },
});
expect(response.ok()).toBeTruthy();
const fixture = await response.json() as PracticeFixture;
try {
await use(fixture);
} finally {
await request.delete(`/api/test-fixtures/practice/${fixture.candidate.id}`);
}
},
});
test("candidate completes a filtered drill", async ({
page,
request,
practiceFixture,
}) => {
await authenticateTestCandidate(request, page, practiceFixture.candidate);
await page.goto("/drills");
await page.getByRole("textbox", { name: "Search drills" })
.fill(practiceFixture.drill.title);
const result = page.getByRole("article", {
name: practiceFixture.drill.title,
});
await expect(result).toBeVisible();
await result.getByRole("link", { name: "Practice drill" }).click();
await expect(page).toHaveURL(new RegExp(`/drills/${practiceFixture.drill.slug}`));
await page.getByRole("button", { name: "Mark complete" }).click();
await expect(page.getByText("Practice saved")).toBeVisible();
await page.reload();
await expect(page.getByRole("button", { name: "Completed" })).toBeVisible();
});
The fixture endpoint exists only in the test host and uses an isolated database. In a real suite, authentication and cleanup helpers would be reusable fixtures rather than application routes exposed in production.
This test crosses browser, routing, authentication, API, and persistence boundaries, so it is slower and more failure-prone than a component or API test. That cost is justified for one critical path, not every validation branch. API-based setup avoids unrelated UI work but requires a carefully restricted test fixture surface. Per-worker data enables parallelism at the cost of more setup and cleanup, while traces and screenshots should diagnose retries rather than normalize flaky behavior.
waitForTimeout.Spot a weak answer, missing edge case, or clearer explanation? Send it in.