Dockerized AI Test Automation Framework
Build a Playwright test suite with Docker and AI-assisted test generation via MCP.
Introduction
30 Second Summary
Every time you push a code change, you need confidence that nothing broke. Manual testing is slow, inconsistent, and impossible to scale across browsers and environments.
In this project, you will build a test automation framework that uses Playwright to test a web app inside Docker containers, with an AI assistant (Claude via Playwright MCP) helping you generate tests through natural language.
What You'll Build
A professional test automation framework combining Docker Compose for managing the test target, Playwright for browser testing, and Playwright MCP for AI-assisted test creation in VS Code.
By the end of this project, you'll have:
- A Docker Compose setup that spins up a todo app as your system under test.
- A Playwright test suite organized with the Page Object Model pattern for maintainable, scalable tests.
- A containerized test runner that executes your entire suite with a single command, identically on any machine.
- Secret Mission: Add visual regression testing with screenshot comparison inside Docker.
Are there any prerequisites?
No prior experience with Playwright, Docker, or MCP is needed. This guide walks you through installing and configuring every tool from scratch.
Set Up the Project and Install Playwright
Before you can test anything, you need a test framework. Playwright is a modern browser automation tool that can run tests across Chromium, Firefox, and WebKit with a single test suite.
In this step, you'll create a new project directory and scaffold a complete Playwright test framework with TypeScript.
In this step, get ready to:
- Create a new project directory and scaffold Playwright with TypeScript.
- Run the example test to verify everything works.
- Review the generated project structure.
Initialize the Playwright project
Playwright's scaffold command creates a fully configured test project in seconds. You'll start by creating a folder on your Desktop, then run the scaffold inside it.
- Open your terminal.
How to open your terminal
Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar. Type Terminal (macOS) or PowerShell (Windows) and press Enter.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir playwright-docker-tests
cd playwright-docker-tests
- Confirm the folder was created by running:
pwd
You should see a path ending in playwright-docker-tests.
- Scaffold the Playwright project by running this command:
npm init playwright@latest
What will this command ask me?
This command launches an interactive wizard that sets up your test framework. It will ask you four questions, one at a time.
- When asked to choose between TypeScript or JavaScript, select TypeScript.
- When asked where to put your end-to-end tests, enter tests.
- When asked to add a GitHub Actions workflow, select false.
- When asked to install Playwright browsers, select true.
The command will download and install Playwright's browser binaries (Chromium, Firefox, and WebKit). This may take a minute or two depending on your internet connection.
Seeing a permission error or install failure?
Make sure you have Node.js version 20 or newer installed. Playwright requires a modern Node.js version to run.
If you see a network timeout error, check your internet connection and run the command again.
help me troubleshoot my Playwright installation error
Verify the installation
The scaffold created an example test file that visits the Playwright documentation site. Running it confirms that the browsers installed correctly and your framework is working.
- Run the example test suite by executing this command:
npx playwright test
What does this command do?
This runs all test files in your tests/ folder across three browsers (Chromium, Firefox, and WebKit) in headless mode. Headless mode means the browsers run in the background without opening visible windows.
You should see output showing 6 tests passed (2 tests across 3 browsers).
- View the HTML test report by running:
npx playwright show-report
Your browser opens with a visual report showing each test result grouped by browser. You should see green checkmarks next to each test.
Tests failing or browsers not found?
If you see errors about missing browsers, the browser download may not have completed. Run npx playwright install --with-deps to retry the browser installation.
If tests fail with network errors, check that you have an active internet connection. The example tests visit an external website.
help me fix my failing Playwright tests
Review the project structure
The scaffold generated several files that form the foundation of your test framework. Let's open the project in VS Code and explore what was created.
- Open the project in VS Code by running:
code .
If code command is not found
On macOS, you may need to install the code command first. Open VS Code manually, press Cmd+Shift+P, type Shell Command: Install, and select the option to install it.
- In the VS Code Explorer sidebar, open playwright.config.ts.
This is the central configuration file for your test framework. It should look like this:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
],
});
What does this config do?
- testDir tells Playwright where to find your test files.
- fullyParallel runs tests simultaneously for faster execution.
- projects defines which browsers to test against. You get Chromium, Firefox, and WebKit out of the box.
- reporter: 'html' generates the visual HTML report you just viewed.
- Now open tests/example.spec.ts.
This is the example test that you just ran successfully:
import { test, expect } from '@playwright/test';
test('has title', async ({ page }) => {
await page.goto('https://playwright.dev/');
await expect(page).toHaveTitle(/Playwright/);
});
test('get started link', async ({ page }) => {
await page.goto('https://playwright.dev/');
await page.getByRole('link', { name: 'Get started' }).click();
await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
});
What does this test file do?
- The first test navigates to the Playwright website and checks that the page title contains "Playwright".
- The second test clicks the "Get started" link and verifies that the Installation heading appears.
- Both tests use page to control the browser and expect to assert outcomes. You'll follow this same pattern when writing your own tests.
✔️ Awesome, I've got everything!
Great. Make sure you've saved any open files in VS Code before moving on.
ⓧ I'd like to double check the full code
Here is what your playwright.config.ts should look like:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
],
});
And here is what tests/example.spec.ts should look like:
import { test, expect } from '@playwright/test';
test('has title', async ({ page }) => {
await page.goto('https://playwright.dev/');
await expect(page).toHaveTitle(/Playwright/);
});
test('get started link', async ({ page }) => {
await page.goto('https://playwright.dev/');
await page.getByRole('link', { name: 'Get started' }).click();
await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
});
Your test framework is ready to go. Next up, you'll launch a real web app inside a Docker container and point your tests at it.
Launch the Test Target App with Docker Compose
Tests need something to test against. Before you write any test logic, you need a real running application that Playwright can open in a browser and interact with.
Docker Compose lets you define and run that application with a single command. You will create a configuration file that tells Docker which app to pull, which port to expose, and how to confirm the app is healthy.
In this step, get ready to:
- Create a Docker Compose file that runs a todo app as your test target.
- Start the container and confirm the app loads in your browser.
- Update Playwright's config to automatically manage the Docker app when tests run.
Create the Docker Compose file
Docker Compose reads a YAML file that describes the services (containers) your project needs. You will define one service that pulls a pre-built todo app image and exposes it on a local port.
- In VS Code's Explorer sidebar, right-click the empty area below your files and select New File.
- Name the file docker-compose.yml and press Enter.
- Paste the following code into docker-compose.yml:
services:
app:
image: docker/getting-started
ports:
- "3001:80"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:80"]
interval: 5s
timeout: 3s
retries: 5
What does this code do?
This file defines a single service called app that Docker Compose will manage.
- image: docker/getting-started pulls a pre-built todo app from Docker Hub. This is a lightweight app from Docker's official samples.
- ports: "3001:80" maps port 80 inside the container to port 3001 on your machine. The app listens on port 80 internally, but you will access it at http://localhost:3001.
- healthcheck tells Docker how to verify the app is actually ready to serve requests. It curls the app every 5 seconds and retries up to 5 times.
- Save docker-compose.yml.
- Confirm the file exists by running:
ls docker-compose.yml
You should see docker-compose.yml printed in the output.
Don't see the file?
Make sure you created the file in your project's root folder (the same folder that contains package.json). If ls shows nothing, check that you saved the file in VS Code.
help me find where my docker-compose.yml was saved
Start the container and verify
With the Compose file in place, a single command will pull the app image and start it in the background.
- Start the todo app container by running this command:
docker compose up -d
- Wait for the pull to finish. You will see output showing the image being downloaded and the container starting.
- Open your browser and navigate to http://localhost:3001.
You should see the todo app loaded in your browser. This is the application your Playwright tests will interact with.
Seeing "connection refused" or a blank page?
The container may still be starting up. Wait 10-15 seconds and refresh the page.
If it still does not load, confirm Docker Desktop is running. Look for the Docker whale icon in your system tray (Windows) or menu bar (macOS).
You can also check container status by running docker compose ps in your terminal. The app service should show as "running" with a "healthy" status.
help me troubleshoot why my Docker container is not responding on port 3001
Update Playwright config for the base URL
Right now, Playwright does not know about your Docker app. You need to tell it where the app lives and how to start it automatically when tests run.
- In VS Code, open playwright.config.ts.
- Find the use block, which currently looks like this:
use: {
trace: 'on-first-retry',
},
- Replace the entire use block with the following, which adds a base URL and screenshot settings:
use: {
baseURL: process.env.BASE_URL || 'http://localhost:3001',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
What do these new settings do?
baseURL tells Playwright to use http://localhost:3001 as the starting point for all page navigations. When you write page.goto('/') in a test, Playwright will navigate to http://localhost:3001/.
The process.env.BASE_URL part lets you override this when running inside Docker (where the app has a different network address). You will use this in a later step.
screenshot: 'only-on-failure' captures a screenshot whenever a test fails. This gives you a visual record of what went wrong without cluttering passing test results.
Next, you will add a webServer block that tells Playwright to start Docker Compose automatically before running tests.
- Scroll to the bottom of the file. Find the closing of the projects array (the line with ],).
- After that ], line, add the following webServer block before the final });:
webServer: {
command: 'docker compose up -d app && sleep 3',
url: 'http://localhost:3001',
reuseExistingServer: true,
},
What does webServer do?
The webServer config tells Playwright to run a command before tests start and wait for a URL to become reachable.
- command starts the Docker Compose app service and waits 3 seconds for it to be ready.
- url is the address Playwright pings to confirm the server is up before running tests.
- reuseExistingServer: true means if the app is already running (from your earlier docker compose up -d), Playwright will not try to start it again.
- Save playwright.config.ts.
- Run your tests to confirm the new config works:
npx playwright test
The example tests from Step 1 navigate to playwright.dev, not your todo app, so they will still pass. The important thing is that Playwright starts without config errors.
Seeing a config error?
Check for missing commas between properties in your use block. Every property line except the last must end with a comma.
Make sure the webServer block sits inside the defineConfig({ call but outside the projects array. It should be at the same indentation level as projects.
help me fix a syntax error in my playwright.config.ts after adding webServer
✔️ Awesome, I've got everything!
Great. Double check you have saved playwright.config.ts and that your tests pass without errors.
ⓧ I'd like to double check the full code
Here is the complete docker-compose.yml:
services:
app:
image: docker/getting-started
ports:
- "3001:80"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:80"]
interval: 5s
timeout: 3s
retries: 5
Here is the complete playwright.config.ts:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: process.env.BASE_URL || 'http://localhost:3001',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
],
webServer: {
command: 'docker compose up -d app && sleep 3',
url: 'http://localhost:3001',
reuseExistingServer: true,
},
});
Your test target is running and Playwright knows where to find it. Next up, you will build the Page Object Model that gives your tests a clean, reusable way to interact with the todo app.
Build the Page Object Model
Your todo app is running and Playwright knows where to find it. Now you need a clean way to interact with the app in your tests.
Without structure, test code quickly becomes a mess of duplicated selectors and repeated actions. The Page Object Model pattern solves this by giving each page its own class that holds all locators and user actions in one place.
In this step, get ready to:
- Create a pages/ directory for your page object classes.
- Build a BasePage class with shared navigation logic.
- Implement a TodoPage class with locators and action methods for the todo app.
Create the page objects directory
Page object classes live in their own folder, separate from test files. This keeps test logic and page interactions clearly organized.
- Create the pages/ directory by running this command in your VS Code terminal:
mkdir pages
- Confirm the folder exists by running:
ls
You should see pages listed alongside tests, node_modules, and your config files.
Create a BasePage class
Every page object in a professional test suite shares common behaviour like navigating to a URL and waiting for the page to load. A base class holds this shared logic so individual page classes stay focused on their own elements.
- Create a new file called BasePage.ts inside the pages/ folder, then paste the following code:
import { Page } from '@playwright/test';
export class BasePage {
readonly page: Page;
constructor(page: Page) {
this.page = page;
}
// Navigate to a given path (defaults to root)
async navigate(path: string = '/') {
await this.page.goto(path);
}
// Return the page title
async getTitle(): Promise<string> {
return await this.page.title();
}
// Wait until all network requests have settled
async waitForPageLoad() {
await this.page.waitForLoadState('networkidle');
}
}
What does this code do?
- readonly page: Page stores the Playwright page instance that every subclass will use to interact with the browser.
- navigate() wraps page.goto() so subclasses can navigate without importing Playwright directly.
- waitForPageLoad() waits until there are no in-flight network requests, which confirms the page has fully rendered.
- Save pages/BasePage.ts.
You will not see a visible change yet. The next file uses this base class.
Seeing a red underline on the import?
Make sure the file is saved inside the pages/ folder, not the tests/ folder. Also confirm you typed the import path exactly as shown.
help me fix a TypeScript import error in my BasePage file
✔️ Awesome, I've got everything!
Great. Make sure you have saved pages/BasePage.ts before moving on.
ⓧ I'd like to double check the full code
import { Page } from '@playwright/test';
export class BasePage {
readonly page: Page;
constructor(page: Page) {
this.page = page;
}
async navigate(path: string = '/') {
await this.page.goto(path);
}
async getTitle(): Promise<string> {
return await this.page.title();
}
async waitForPageLoad() {
await this.page.waitForLoadState('networkidle');
}
}
Implement the TodoPage class
The TodoPage class is where the real power of the Page Object Model pattern shows up. It defines every locator and action for the todo app in one place, so your tests read like plain English descriptions of user behaviour.
Why Page Object Model instead of raw locators in tests?
When a UI element changes (a class name, a placeholder, a button label), you fix it in one place instead of hunting through dozens of test files. This is the industry-standard pattern that QA engineers and SDETs use at scale.
Playwright's built-in locators (getByRole, getByPlaceholder, locator) already provide resilient element targeting. The page object wraps them into meaningful action methods like addTodo() and completeTodo().
- Create a new file called TodoPage.ts inside the pages/ folder, then paste the following code:
import { Page, Locator, expect } from '@playwright/test';
import { BasePage } from './BasePage';
export class TodoPage extends BasePage {
// Locators for key elements on the todo page
readonly newTodoInput: Locator;
readonly todoItems: Locator;
readonly todoCount: Locator;
readonly filterAll: Locator;
readonly filterActive: Locator;
readonly filterCompleted: Locator;
constructor(page: Page) {
super(page);
// Input field where users type new todos
this.newTodoInput = page.getByPlaceholder('What needs to be done?');
// All todo list items
this.todoItems = page.locator('.todo-list li');
// Counter showing remaining items
this.todoCount = page.locator('.todo-count');
// Filter links at the bottom of the list
this.filterAll = page.getByRole('link', { name: 'All' });
this.filterActive = page.getByRole('link', { name: 'Active' });
this.filterCompleted = page.getByRole('link', { name: 'Completed' });
}
// Add a new todo by typing and pressing Enter
async addTodo(text: string) {
await this.newTodoInput.fill(text);
await this.newTodoInput.press('Enter');
}
// Mark a todo as complete by checking its toggle
async completeTodo(index: number) {
await this.todoItems.nth(index).locator('.toggle').check();
}
// Delete a todo by hovering and clicking the destroy button
async deleteTodo(index: number) {
await this.todoItems.nth(index).hover();
await this.todoItems.nth(index).locator('.destroy').click();
}
// Get the current number of todo items
async getTodoCount(): Promise<number> {
return await this.todoItems.count();
}
// Filter methods to switch between views
async filterByActive() {
await this.filterActive.click();
}
async filterByCompleted() {
await this.filterCompleted.click();
}
async filterByAll() {
await this.filterAll.click();
}
// Assertion helpers that use Playwright's auto-waiting expect
async expectTodoCount(count: number) {
await expect(this.todoItems).toHaveCount(count);
}
async expectTodoText(index: number, text: string) {
await expect(this.todoItems.nth(index)).toContainText(text);
}
}
What does this code do?
The constructor sets up six locators that target real elements in the todo app. getByPlaceholder and getByRole are Playwright's recommended locator strategies because they match how users perceive the page, not internal CSS classes.
The action methods (addTodo, completeTodo, deleteTodo) simulate real user interactions. The assertion helpers (expectTodoCount, expectTodoText) use Playwright's auto-waiting expect so tests automatically retry until the condition is met.
- Save pages/TodoPage.ts.
- In VS Code, confirm you see no red underlines in either file. The import { BasePage } from './BasePage' line should resolve without errors.
Red underline on the BasePage import?
Double-check that both files are inside the same pages/ folder. The import path ./BasePage is relative, so TodoPage.ts and BasePage.ts must be siblings in the same directory.
help me fix the BasePage import in my TodoPage file
✔️ Awesome, I've got everything!
Great. Make sure both pages/BasePage.ts and pages/TodoPage.ts are saved.
ⓧ I'd like to double check the full code
Here is the full pages/TodoPage.ts for comparison:
import { Page, Locator, expect } from '@playwright/test';
import { BasePage } from './BasePage';
export class TodoPage extends BasePage {
readonly newTodoInput: Locator;
readonly todoItems: Locator;
readonly todoCount: Locator;
readonly filterAll: Locator;
readonly filterActive: Locator;
readonly filterCompleted: Locator;
constructor(page: Page) {
super(page);
this.newTodoInput = page.getByPlaceholder('What needs to be done?');
this.todoItems = page.locator('.todo-list li');
this.todoCount = page.locator('.todo-count');
this.filterAll = page.getByRole('link', { name: 'All' });
this.filterActive = page.getByRole('link', { name: 'Active' });
this.filterCompleted = page.getByRole('link', { name: 'Completed' });
}
async addTodo(text: string) {
await this.newTodoInput.fill(text);
await this.newTodoInput.press('Enter');
}
async completeTodo(index: number) {
await this.todoItems.nth(index).locator('.toggle').check();
}
async deleteTodo(index: number) {
await this.todoItems.nth(index).hover();
await this.todoItems.nth(index).locator('.destroy').click();
}
async getTodoCount(): Promise<number> {
return await this.todoItems.count();
}
async filterByActive() {
await this.filterActive.click();
}
async filterByCompleted() {
await this.filterCompleted.click();
}
async filterByAll() {
await this.filterAll.click();
}
async expectTodoCount(count: number) {
await expect(this.todoItems).toHaveCount(count);
}
async expectTodoText(index: number, text: string) {
await expect(this.todoItems.nth(index)).toContainText(text);
}
}
Your page objects are ready. In the next step, you will write end-to-end tests that use these classes to interact with the todo app through clean, readable method calls.
Write End-to-End Tests Using Page Objects
In the previous step, you built page object classes that encapsulate all the locators and actions for the todo app. Now it's time to put them to work.
You'll write end-to-end tests that exercise the todo app's core features: adding items, completing them, deleting them, and filtering. Each test reads like a user story because the page object methods hide the complexity of raw locators.
In this step, get ready to:
- Create a test file that uses the TodoPage class to verify adding todos.
- Add test scenarios for completing, deleting, and filtering todos.
- Run the full test suite and view the HTML report.
Write the first test file
Every test needs a starting point. You'll create a test file that imports your TodoPage class and sets up a fresh page before each test runs.
- Create a new file called todo.spec.ts inside the tests/ folder by running this command:
touch tests/todo.spec.ts
- Open tests/todo.spec.ts in VS Code and paste the following code:
import { test, expect } from '@playwright/test';
import { TodoPage } from '../pages/TodoPage';
test.describe('Todo App', () => {
let todoPage: TodoPage;
test.beforeEach(async ({ page }) => {
todoPage = new TodoPage(page);
await todoPage.navigate();
await todoPage.waitForPageLoad();
});
test('should add a new todo item', async () => {
await todoPage.addTodo('Buy groceries');
await todoPage.expectTodoCount(1);
await todoPage.expectTodoText(0, 'Buy groceries');
});
test('should add multiple todo items', async () => {
await todoPage.addTodo('Buy groceries');
await todoPage.addTodo('Walk the dog');
await todoPage.addTodo('Read a book');
await todoPage.expectTodoCount(3);
});
});
What does this code do?
The test.describe block groups related tests under one label. The test.beforeEach hook runs before every test, creating a fresh TodoPage instance and navigating to the app.
The first test adds a single todo and checks both the count and the text. The second test adds three items and verifies the total count. Notice how readable the tests are because the page object methods describe user actions, not DOM selectors.
- Save tests/todo.spec.ts.
Seeing import errors in VS Code?
If you see red squiggly lines under the imports, make sure your pages/ folder is at the same level as your tests/ folder inside the project root. The relative path ../pages/TodoPage goes up one level from tests/ into the project root, then into pages/.
help me fix import path errors in my Playwright test file
Add more test scenarios
Two tests cover adding items, but a real test suite exercises the full workflow. You'll add tests for completing, deleting, and filtering todos.
- In tests/todo.spec.ts, find the closing of the 'should add multiple todo items' test:
test('should add multiple todo items', async () => {
await todoPage.addTodo('Buy groceries');
await todoPage.addTodo('Walk the dog');
await todoPage.addTodo('Read a book');
await todoPage.expectTodoCount(3);
});
- Add the complete and delete tests directly below it, before the final }); that closes the describe block:
test('should complete a todo item', async () => {
await todoPage.addTodo('Buy groceries');
await todoPage.completeTodo(0);
await expect(todoPage.todoItems.nth(0)).toHaveClass(/completed/);
});
test('should delete a todo item', async () => {
await todoPage.addTodo('Buy groceries');
await todoPage.addTodo('Walk the dog');
await todoPage.deleteTodo(0);
await todoPage.expectTodoCount(1);
await todoPage.expectTodoText(0, 'Walk the dog');
});
What do these tests verify?
The complete test adds an item, marks it done via the checkbox, and asserts that the list item's class includes completed. This uses toHaveClass with a regex pattern to allow partial class matching.
The delete test adds two items, removes the first one, then verifies only 'Walk the dog' remains. This confirms the delete action targets the correct item by index.
- Now add the two filter tests directly below the delete test, still before the closing });:
test('should filter active todos', async () => {
await todoPage.addTodo('Buy groceries');
await todoPage.addTodo('Walk the dog');
await todoPage.completeTodo(0);
await todoPage.filterByActive();
await todoPage.expectTodoCount(1);
await todoPage.expectTodoText(0, 'Walk the dog');
});
test('should filter completed todos', async () => {
await todoPage.addTodo('Buy groceries');
await todoPage.addTodo('Walk the dog');
await todoPage.completeTodo(0);
await todoPage.filterByCompleted();
await todoPage.expectTodoCount(1);
await todoPage.expectTodoText(0, 'Buy groceries');
});
What do the filter tests verify?
Both filter tests set up the same scenario: two items where the first is completed. The active filter should show only 'Walk the dog' (the uncompleted item). The completed filter should show only 'Buy groceries' (the completed item).
This pattern of testing the same data from different angles is common in professional test suites. It catches bugs where the filter logic accidentally shows or hides the wrong items.
- Save tests/todo.spec.ts.
Seeing a syntax error?
Make sure you have exactly one }); at the very end of the file to close the test.describe block. Each test block also needs its own closing });. Mismatched brackets are the most common cause of syntax errors in test files.
help me fix bracket or syntax errors in my Playwright test file
✔️ Awesome, I've got everything!
Great. Double check you've saved tests/todo.spec.ts before continuing.
ⓧ I'd like to double check the full code
Here is the complete tests/todo.spec.ts file:
import { test, expect } from '@playwright/test';
import { TodoPage } from '../pages/TodoPage';
test.describe('Todo App', () => {
let todoPage: TodoPage;
test.beforeEach(async ({ page }) => {
todoPage = new TodoPage(page);
await todoPage.navigate();
await todoPage.waitForPageLoad();
});
test('should add a new todo item', async () => {
await todoPage.addTodo('Buy groceries');
await todoPage.expectTodoCount(1);
await todoPage.expectTodoText(0, 'Buy groceries');
});
test('should add multiple todo items', async () => {
await todoPage.addTodo('Buy groceries');
await todoPage.addTodo('Walk the dog');
await todoPage.addTodo('Read a book');
await todoPage.expectTodoCount(3);
});
test('should complete a todo item', async () => {
await todoPage.addTodo('Buy groceries');
await todoPage.completeTodo(0);
await expect(todoPage.todoItems.nth(0)).toHaveClass(/completed/);
});
test('should delete a todo item', async () => {
await todoPage.addTodo('Buy groceries');
await todoPage.addTodo('Walk the dog');
await todoPage.deleteTodo(0);
await todoPage.expectTodoCount(1);
await todoPage.expectTodoText(0, 'Walk the dog');
});
test('should filter active todos', async () => {
await todoPage.addTodo('Buy groceries');
await todoPage.addTodo('Walk the dog');
await todoPage.completeTodo(0);
await todoPage.filterByActive();
await todoPage.expectTodoCount(1);
await todoPage.expectTodoText(0, 'Walk the dog');
});
test('should filter completed todos', async () => {
await todoPage.addTodo('Buy groceries');
await todoPage.addTodo('Walk the dog');
await todoPage.completeTodo(0);
await todoPage.filterByCompleted();
await todoPage.expectTodoCount(1);
await todoPage.expectTodoText(0, 'Buy groceries');
});
});
Run the test suite locally
Your test file is complete. Time to run the full suite against the Docker Compose app and confirm all six tests pass across all three browsers.
- Make sure your Docker Compose app is still running. If you stopped it earlier, start it again by running:
docker compose up -d
- Run the full test suite by running this command:
npx playwright test
- You should see output showing all 18 tests passing (6 tests across 3 browser projects: Chromium, Firefox, and WebKit).
- View the HTML test report by running:
npx playwright show-report
- Your browser opens with a detailed report showing each test, its duration, and which browser ran it.
Why 18 tests instead of 6?
Your playwright.config.ts defines three projects: Chromium, Firefox, and WebKit. Playwright runs every test in each browser project, so 6 tests multiplied by 3 browsers equals 18 total test runs.
This is one of Playwright's biggest strengths over other frameworks. You get cross-browser coverage without writing separate test files for each browser.
Tests failing with connection errors?
If tests fail with messages about being unable to connect, confirm the app is running by opening http://localhost:3001 in your browser. If the page does not load, run docker compose up -d and wait a few seconds for the container to start.
help me debug why my Playwright tests cannot connect to the Docker Compose app
Your test suite is running and all six scenarios pass across three browsers. Next up, you'll connect Playwright MCP to VS Code so Claude can explore the app and help you generate even more tests using natural language.
Configure Playwright MCP in VS Code
Your test suite is running, your page objects are clean, and all six tests pass. Now it is time to supercharge your workflow with AI-assisted test creation.
In this step, you will connect Playwright MCP to VS Code so that Claude can control a real browser, inspect page structure, and help you generate tests using natural language.
In this step, get ready to:
- Create the MCP configuration file for your workspace.
- Start the Playwright MCP server and verify it is running.
- Use Claude to explore the todo app and generate a test via natural language.
Create the MCP configuration
The Model Context Protocol (MCP) is an open standard that lets AI models interact with external tools through a unified interface. Playwright's MCP server gives Claude the ability to launch a browser, navigate pages, take accessibility snapshots, and generate locators.
VS Code discovers MCP servers through a .vscode/mcp.json file in your workspace. You will create this file now.
- In the VS Code Explorer sidebar, right-click in the file list and select New Folder.
- Name the folder .vscode and press Enter.
- Right-click the .vscode folder and select New File.
- Name the file mcp.json and press Enter.
- Paste the following configuration into .vscode/mcp.json:
{
"servers": {
"playwright": {
"command": "npx",
"args": ["@playwright/mcp@latest", "--caps=testing,storage"]
}
}
}
What does this configuration do?
The command field tells VS Code to start the server using npx. This downloads and runs the latest Playwright MCP package automatically.
The --caps=testing,storage flag enables testing capabilities (locator generation, element verification, text verification) and storage capabilities (auth persistence between sessions).
- Save .vscode/mcp.json.
Why testing and storage capabilities?
The testing capability unlocks tools like browser_generate_locator, browser_verify_element_visible, browser_verify_text_visible, and browser_verify_value. These are purpose-built for test writing workflows.
The storage capability lets the MCP server persist authentication state between sessions. This is useful when testing apps that require login.
Don't see the .vscode folder?
Some file explorers hide folders that start with a dot by default. In VS Code's Explorer sidebar, the .vscode folder should always be visible. If you accidentally created it outside your project folder, delete it and try again inside the correct directory.
Help me find the .vscode folder in my project
Start the MCP server and verify it is running
VS Code detects the mcp.json file and offers to start the server. Once running, the Playwright MCP tools become available in the Chat view for Claude to use.
- Open the Chat view by pressing Cmd+Alt+I (macOS) or Ctrl+Alt+I (Windows).
- VS Code will prompt you to trust and start the Playwright MCP server. Click Allow to start the server.
- Select Agent from the mode dropdown at the top of the Chat view (if not already selected).
- Select Claude from the model picker to ensure you are using Claude for this interaction.
What is Playwright MCP?
Playwright MCP gives Claude direct access to a real browser. Instead of guessing what your page looks like, Claude can navigate to it, read the accessibility tree, identify interactive elements, and generate precise locators. The browser opens in headed mode so you can watch what is happening.
- Verify the server is running by opening the Command Palette with Cmd+Shift+P (macOS) or Ctrl+Shift+P (Windows).
- Type MCP: List Servers and press Enter.
- You should see the playwright server listed with a running status.
- Now ask Claude to navigate to the todo app. Type this prompt into the Chat view:
Navigate to http://localhost:3001 and take an accessibility snapshot of the page. Tell me what interactive elements you find.
- Press Enter to send the prompt.
- A Chromium browser window will open automatically. Claude navigates to the todo app and returns a description of the page elements including the input field and any existing UI components.
The browser window is expected
Playwright MCP runs the browser in headed mode by default. You will see a Chromium window appear on your screen as Claude interacts with the page. This is normal and lets you visually confirm what Claude is doing.
MCP server not starting?
If you see an error indicator in the Chat view, select it and choose Show Output to see the server logs. Common causes include Node.js not being on your PATH or a network issue downloading the package.
You can also restart the server by running MCP: List Servers from the Command Palette, selecting the playwright server, and choosing Restart Server.
Help me troubleshoot my Playwright MCP server not starting in VS Code
Use MCP to generate a test
The real power of Playwright MCP is that Claude can explore your app and write tests based on what it actually sees. Instead of manually finding locators, you describe what you want to test in plain English.
- In the Chat view, type the following prompt to ask Claude to generate a test:
Explore the todo app at http://localhost:3001. Identify the form elements used to add a new todo item. Then generate a Playwright test that adds a todo called 'Write documentation' and verifies it appears in the list. Use the TodoPage page object pattern from my pages/TodoPage.ts file.
- Press Enter to send the prompt.
- Claude will open the browser, navigate to the app, inspect the page structure, and generate a test file.
- Review the generated test code. Claude should produce something similar to your existing tests in tests/todo.spec.ts that uses the TodoPage class.
- Copy the generated test into tests/todo.spec.ts (inside the existing test.describe block) or save it as a new test file.
Why use MCP instead of writing tests manually?
MCP-generated tests use the actual page structure rather than assumptions. Claude reads the accessibility snapshot to find the real element roles, labels, and placeholder text. This produces resilient locators that match what Playwright recommends.
You still review and edit the generated tests before committing them. MCP accelerates the writing process without replacing your judgment.
- Run the test suite to confirm the new test passes by running:
npx playwright test
- You should see all tests passing, including your newly generated test.
Generated test failing?
If Claude generated a test that uses raw locators instead of the TodoPage class, update it to use your page object methods (addTodo, expectTodoText). The page object pattern keeps your tests consistent and maintainable.
If the test fails with a timeout, make sure your Docker Compose app is still running at http://localhost:3001.
Help me fix my AI-generated Playwright test
✔️ Awesome, I've got everything!
Great. Double check you have saved .vscode/mcp.json and that your MCP server is running in the Chat view.
ⓧ I'd like to double check the full code
Here is the complete .vscode/mcp.json file:
{
"servers": {
"playwright": {
"command": "npx",
"args": ["@playwright/mcp@latest", "--caps=testing,storage"]
}
}
}
You now have AI-powered browser exploration at your fingertips. Next up, you will containerize the entire test suite so it runs identically anywhere with a single command.
Containerize the Test Suite with Docker
Your tests pass on your machine, but what about your teammate's machine? Local environments are unpredictable. Different machines have different browsers, fonts, and system libraries installed.
Docker solves this by packaging your tests into a container with the exact browser versions and dependencies they need. In this step, you'll create a Dockerfile for the test suite and wire it into Docker Compose for one-command execution.
In this step, get ready to:
- Create a Dockerfile that packages the test suite with Playwright's official browser image.
- Build and run the containerized tests to verify they pass inside Docker.
- Add a test service to Docker Compose for single-command execution.
Create the test Dockerfile
A Dockerfile is a recipe that tells Docker how to build an image. Your test image needs Playwright's browsers, your project dependencies, and your test files.
- In VS Code's Explorer sidebar, create a new file called Dockerfile in your project root (the same folder as package.json).
- Paste the following code into Dockerfile:
FROM mcr.microsoft.com/playwright:v1.60.0-noble
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY playwright.config.ts ./
COPY tests/ ./tests/
COPY pages/ ./pages/
RUN npx playwright install --with-deps
CMD ["npx", "playwright", "test"]
What does this Dockerfile do?
- FROM mcr.microsoft.com/playwright:v1.60.0-noble starts from Playwright's official image built on Ubuntu 24.04. This image comes with all supported browsers pre-installed.
- COPY package.json package-lock.json followed by RUN npm ci installs the exact dependency versions from your lock file.
- The second group of COPY commands brings your config, test files, and page objects into the container.
- RUN npx playwright install --with-deps ensures browser binaries and system dependencies are fully present.
- CMD sets npx playwright test as the default command when the container starts.
- Save Dockerfile.
- Confirm the file exists by running:
ls Dockerfile
You should see Dockerfile printed in the output.
Don't see the file?
Make sure you created the file in your project root (the same directory as package.json), not inside a subfolder like tests/ or pages/.
Also confirm the filename is exactly Dockerfile with a capital D and no file extension.
help me find where my Dockerfile should go
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
FROM mcr.microsoft.com/playwright:v1.60.0-noble
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY playwright.config.ts ./
COPY tests/ ./tests/
COPY pages/ ./pages/
RUN npx playwright install --with-deps
CMD ["npx", "playwright", "test"]
Build and run the containerized tests
Now you'll turn that Dockerfile into an image and run your tests inside a container. This proves the tests work independently of your local machine.
- Build the Docker image by running this command:
docker build -t playwright-tests .
This takes a few minutes the first time because Docker downloads the Playwright base image and installs dependencies. You should see each step complete and a final line containing Successfully tagged playwright-tests:latest.
Build failing?
Make sure Docker Desktop is running. If you see a "Cannot connect to the Docker daemon" message, open Docker Desktop and wait for it to finish starting.
If the build fails on npm ci, confirm that both package.json and package-lock.json exist in your project root.
help me debug my Docker build failure
- Run the test suite inside the container by running this command:
docker run --rm --init --ipc=host playwright-tests
Why these flags?
- --rm removes the container when it finishes so you don't accumulate stopped containers.
- --init adds a tiny init process as PID 1, which prevents zombie processes from accumulating.
- --ipc=host shares the host's shared memory with the container. Chromium needs this to avoid crashing when it runs out of shared memory space.
You should see Playwright's test output showing all six tests running across Chromium, Firefox, and WebKit. The output ends with a summary showing passed tests.
Tests failing inside the container?
If you see connection errors, make sure the todo app is still running. Check with docker compose ps to confirm the app container is healthy.
If Chromium crashes with a shared memory error, double-check you included the --ipc=host flag.
help me fix tests failing inside Docker
Add a test service to Docker Compose
Running docker build then docker run works, but it is two separate commands. Docker Compose can orchestrate both the app and the test runner with a single command.
- In docker-compose.yml, find the end of the app service (after the retries: 5 line).
- Add the following tests service below it, at the same indentation level as app:
tests:
build:
context: .
dockerfile: Dockerfile
depends_on:
app:
condition: service_healthy
environment:
- BASE_URL=http://app:80
ipc: host
init: true
What does this service config do?
- build tells Docker Compose to build the image from your local Dockerfile instead of pulling one from a registry.
- depends_on with condition: service_healthy waits for the app's healthcheck to pass before starting tests. This prevents tests from running before the app is ready.
- BASE_URL=http://app:80 points the tests at the app service via Docker's internal network. Inside Docker Compose, services communicate by service name, not localhost.
- ipc: host and init: true are the Compose equivalents of the --ipc=host and --init flags you used earlier.
- Save docker-compose.yml.
- Run the full test suite through Docker Compose by running this command:
docker compose run tests
Docker Compose builds the test image, waits for the app to be healthy, then runs all tests inside the container. You should see the same passing test output you saw with docker run earlier.
Seeing connection refused errors?
If the tests cannot reach the app, check that the BASE_URL environment variable is set to http://app:80 (not localhost:3001). Inside Docker Compose, containers use service names to find each other.
Also verify the depends_on block uses condition: service_healthy. Without this, tests may start before the app is ready to accept requests.
help me fix Docker Compose test connection errors
✔️ Awesome, I've got everything!
Great. Double check you've saved docker-compose.yml before moving on.
ⓧ I'd like to double check the full code
services:
app:
image: docker/getting-started
ports:
- "3001:80"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:80"]
interval: 5s
timeout: 3s
retries: 5
tests:
build:
context: .
dockerfile: Dockerfile
depends_on:
app:
condition: service_healthy
environment:
- BASE_URL=http://app:80
ipc: host
init: true
Your test suite now runs identically anywhere with a single command. Next up, you'll tackle a Secret Mission that adds visual regression testing to catch pixel-level changes.
Secret mission
Add Visual Regression Testing
Your test suite catches functional bugs, but what about visual ones? A button that shifts 3 pixels to the left, a font that renders differently, a border that disappears. In this secret mission, you'll add screenshot-based visual regression testing that catches pixel-level changes inside Docker.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally using Docker containers and local files. There are no cloud resources or ongoing costs to worry about.
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Docker containers running the todo app.
- Docker images stored locally (the test image you built and the pulled docker/getting-started image).
- Playwright browser binaries installed on your machine.
- Local project folder with all test files and configuration.
Keep everything running
No action needed. Choose this if you are still actively building on this test framework or plan to run tests again soon.
- Your Docker Compose containers will continue running in the background using minimal resources.
- Playwright browsers remain installed so you can run tests instantly.
- Your project folder stays on your Desktop ready for further development.
Pause - I'll come back to this later
Stop the running containers to free up memory without removing anything. You can resume later without rebuilding.
- Stop the Docker Compose containers by running:
docker compose stop
- When you are ready to resume, start them again by running:
docker compose start
- Your images, project files, and Playwright browsers all remain intact.
Delete - I don't want to use this again
Remove all project resources and reclaim disk space. This is irreversible.
- Stop and remove the Docker Compose containers and locally-built images by running:
docker compose down --rmi local
- Remove unused Docker images and reclaim disk space by running:
docker system prune
- Remove Playwright browser binaries from your machine by running:
npx playwright uninstall --all
- Delete the project folder by running the command for your operating system.
macOS
rm -rf ~/Desktop/playwright-docker-tests
Windows
rmdir /s /q %USERPROFILE%\Desktop\playwright-docker-tests
Nice Work!
Nice Work!
Nice work! You just built a production-grade test automation framework that combines Playwright, Docker, and AI-assisted test creation with Playwright MCP.
You learned how to:
- Created a Playwright test suite using the Page Object Model pattern for maintainable, scalable browser tests.
- Integrated Playwright MCP with VS Code so Claude can explore pages and generate tests via natural language.
- Containerized the entire test execution with Docker Compose so the suite runs identically anywhere with a single command.
- Added visual regression testing that catches pixel-level changes inside Docker.
Ready to quiz yourself?