Introduction: Why Beginners Make Playwright Mistakes
Playwright is powerful, but beginners can still create unreliable automation if they carry old Selenium habits into a Playwright project.
The good news is that most Playwright beginner mistakes to avoid are easy to recognize once you understand how Playwright is designed to work.
Common problems include using fragile selectors, adding unnecessary hard waits, sharing test data, skipping assertions, creating overly complicated frameworks, and using retries to hide flaky tests.
For QA Automation Engineers and SDETs, avoiding these problems is important. A test suite should not simply pass on a developer’s laptop. It should remain reliable when executed in parallel, across browsers, and inside CI/CD.
This guide explains the most important Playwright beginner mistakes, including incorrect and correct TypeScript examples.
Quick Overview: Common Playwright Mistakes
| Mistake | Typical Problem | Better Practice |
| Unreliable locators | Tests break after UI changes | Use semantic locators |
| Hard waits | Slow/flaky tests | Use auto-waiting |
| Weak assertions | False confidence | Assert business outcomes |
| Repeated login | Slow test suite | Reuse authentication state |
| Shared state | Tests affect each other | Isolate tests |
| Hard-coded credentials | Security risk | Use environment variables |
| No tracing | Difficult debugging | Enable traces/screenshots |
| One browser only | Missed compatibility bugs | Test required browser matrix |
| Hidden flaky tests | CI failures | Investigate root causes |
| Over-engineering | Difficult maintenance | Start simple |
Understanding these issues is the foundation of Playwright best practices for beginners.
Mistake 1: Skipping Playwright Installation and Project Configuration
One of the first Playwright mistakes beginners make is immediately creating test files without understanding the project configuration.
A properly initialized project provides the test runner, configuration, TypeScript support, browser projects, and other useful defaults.
❌ Incorrect Approach
Installing only a random package and manually creating an incomplete project.
Why It Fails
You may encounter:
- Missing browser binaries
- Incorrect test discovery
- TypeScript configuration problems
- Missing Playwright Test features
- Inconsistent local and CI environments
✅ Correct Approach
Initialize a Playwright project:
npm init playwright@latest
For Linux CI environments:
npx playwright install –with-deps
Best Practice
Keep package.json, playwright.config.ts, tsconfig.json, and your test directories under source control.
Mistake 2: Using Unreliable Locators
Locators are one of the biggest sources of automation instability.
❌ Incorrect Approach
await page.locator(‘div.container > div:nth-child(2) button’).click();
Why It Fails
The selector depends on the DOM structure.
If developers add another <div>, change the layout, or reorder elements, your test may break.
✅ Correct Approach
await page.getByRole(‘button’, { name: ‘Login’ }).click();
Or:
await page.getByLabel(‘Username’).fill(‘testuser’);
Best Practice
Prefer locators based on how users interact with the application:
page.getByRole()
page.getByLabel()
page.getByText()
page.getByPlaceholder()
Use locator() when a CSS selector or another locator strategy is genuinely appropriate.
Mistake 3: Overusing CSS and XPath Selectors
Selenium engineers transitioning to Playwright often continue using CSS and XPath for everything.
❌ Incorrect Approach
await page.locator(‘//button[@id=”login”]’).click();
Or:
await page.locator(‘div.login-panel button.login’).click();
Why It Fails
These selectors can become tightly coupled to implementation details.
✅ Correct Approach
await page.getByRole(‘button’, { name: ‘Login’ }).click();
If there is no suitable semantic locator:
await page.locator(‘[data-testid=”login-button”]’).click();
Best Practice
A practical locator priority is:
- getByRole()
- getByLabel()
- getByPlaceholder()
- getByText()
- Test IDs
- CSS/XPath when necessary
Do not avoid CSS completely. Avoid unnecessary dependence on fragile CSS or XPath.
Mistake 4: Using waitForTimeout() Unnecessarily
This is one of the most common Playwright automation mistakes.
❌ Incorrect Approach
await page.click(‘#login’);
await page.waitForTimeout(5000);
await expect(page.getByText(‘Dashboard’)).toBeVisible();
Why It Fails
The application might be ready after one second or might require seven seconds.
A fixed five-second delay does not understand application state.
✅ Correct Approach
await page.getByRole(‘button’, { name: ‘Login’ }).click();
await expect(
page.getByRole(‘heading’, { name: ‘Dashboard’ })
).toBeVisible();
Or when navigation is the actual condition:
await page.getByRole(‘button’, { name: ‘Login’ }).click();
await page.waitForURL(‘**/dashboard’);
Best Practice
Use meaningful synchronization rather than sleeping.
Avoid:
await page.waitForTimeout(5000);
unless you have a specific debugging or exceptional use case.
Mistake 5: Not Understanding Playwright Auto-Waiting
Beginners sometimes add waits because they assume Playwright behaves like a basic browser-control library.
Playwright automatically waits for many actionability conditions before performing actions.
❌ Incorrect Approach
await page.waitForTimeout(3000);
await page.getByRole(‘button’, { name: ‘Submit’ }).click();
Why It Fails
The test becomes slower without necessarily becoming more reliable.
✅ Correct Approach
await page.getByRole(‘button’, { name: ‘Submit’ }).click();
Playwright can wait for the element to become actionable.
Assertions also provide useful synchronization:
await expect(page.getByText(‘Order placed’)).toBeVisible();
Best Practice
Learn Playwright’s auto-waiting model before adding manual synchronization.
Mistake 6: Writing Weak or Missing Assertions
A test that performs actions without validating the result provides little value.
❌ Incorrect Approach
await page.goto(‘/login’);
await page.getByLabel(‘Username’).fill(‘admin’);
await page.getByLabel(‘Password’).fill(‘password’);
await page.getByRole(‘button’, { name: ‘Login’ }).click();
The test could pass even if login failed.
Why It Fails
There is no business validation.
✅ Correct Approach
await page.getByRole(‘button’, { name: ‘Login’ }).click();
await expect(page).toHaveURL(/dashboard/);
await expect(
page.getByRole(‘heading’, { name: ‘Dashboard’ })
).toBeVisible();
Best Practice
Ask:
“What business outcome does this test prove?”
Then assert that outcome.
Mistake 7: Repeating Login Steps in Every Test
Imagine 100 tests that all perform login.
❌ Incorrect Approach
test(‘test 1’, async ({ page }) => {
await login(page);
// test
});
test(‘test 2’, async ({ page }) => {
await login(page);
// test
});
Why It Fails
It makes the suite:
- Slower
- More repetitive
- Harder to maintain
✅ Correct Approach: Authentication State
Playwright can reuse authenticated browser state.
For example, a setup project can create a storage state:
await page.goto(‘/login’);
await page.getByLabel(‘Username’).fill(
process.env.TEST_USERNAME!
);
await page.getByLabel(‘Password’).fill(
process.env.TEST_PASSWORD!
);
await page.getByRole(‘button’, { name: ‘Login’ }).click();
await page.context().storageState({
path: ‘playwright/.auth/user.json’
});
Then configure:
use: {
storageState: ‘playwright/.auth/user.json’
}
Best Practice
Reuse authentication state when the scenario does not specifically test login itself.
Mistake 8: Ignoring Test Isolation
Each test should ideally be independent.
❌ Incorrect Approach
test(‘create customer’, async ({ page }) => {
// creates customer
});
test(‘delete customer created above’, async ({ page }) => {
// assumes previous test succeeded
});
Why It Fails
If the first test fails, the second test also fails.
Parallel execution makes this even more problematic.
✅ Correct Approach
Each test should create or obtain the data it needs.
test(‘delete customer’, async ({ page }) => {
// Arrange required customer
// Delete customer
// Assert deletion
});
Best Practice
Tests should be independently runnable:
npx playwright test -g “delete customer”
If a test cannot run independently, investigate the dependency.
Mistake 9: Sharing Pages or Test Data Incorrectly
❌ Incorrect Approach
Creating one global page:
let page: Page;
beforeAll(async ({ browser }) => {
page = await browser.newPage();
});
Then allowing multiple tests to modify it.
Why It Fails
Tests can interfere with each other.
✅ Correct Approach
Use Playwright’s built-in fixtures:
test(‘customer test’, async ({ page }) => {
await page.goto(‘/customers’);
});
Playwright creates isolated test environments through its fixture system.
Best Practice
Avoid global mutable browser state.
Mistake 10: Not Using Fixtures Effectively
Fixtures help provide reusable test dependencies.
❌ Incorrect Approach
Copying setup into every test:
test(‘test 1’, async ({ page }) => {
// repeated setup
});
test(‘test 2’, async ({ page }) => {
// same setup
});
✅ Correct Approach
Create a custom fixture:
import { test as base } from ‘@playwright/test’;
type Fixtures = {
loggedInPage: void;
};
export const test = base.extend<Fixtures>({
loggedInPage: async ({ page }, use) => {
await page.goto(‘/login’);
await page.getByLabel(‘Username’)
.fill(process.env.TEST_USERNAME!);
await page.getByLabel(‘Password’)
.fill(process.env.TEST_PASSWORD!);
await page.getByRole(‘button’, { name: ‘Login’ }).click();
await use();
}
});
Best Practice
Use fixtures for reusable setup, authentication, test data, and service dependencies.
Do not create fixtures merely to make simple code look sophisticated.
Mistake 11: Avoiding Page Object Model in Larger Projects
A beginner may put everything into test files.
❌ Incorrect Approach
test(‘login’, async ({ page }) => {
await page.goto(‘/login’);
await page.getByLabel(‘Username’).fill(‘admin’);
await page.getByLabel(‘Password’).fill(‘password’);
await page.getByRole(‘button’, { name: ‘Login’ }).click();
});
This is acceptable for a tiny test.
The problem appears when dozens of tests repeat the same workflow.
✅ Correct Approach
Create a page object:
import { Page } from ‘@playwright/test’;
export class LoginPage {
constructor(private page: Page) {}
async login(username: string, password: string) {
await this.page.getByLabel(‘Username’).fill(username);
await this.page.getByLabel(‘Password’).fill(password);
await this.page.getByRole(‘button’, { name: ‘Login’ }).click();
}
}
Then:
const loginPage = new LoginPage(page);
await loginPage.login(
process.env.TEST_USERNAME!,
process.env.TEST_PASSWORD!
);
Best Practice
Use Page Object Model when the application and test suite are large enough to benefit from abstraction.
Do not create a 50-class framework for five tests.
Mistake 12: Hard-Coding Credentials and Sensitive Data
❌ Incorrect Approach
await page.getByLabel(‘Username’).fill(‘admin’);
await page.getByLabel(‘Password’).fill(‘MyRealPassword123’);
Why It Fails
Credentials can accidentally enter:
- Git repositories
- Pull requests
- Logs
- Screenshots
- CI artifacts
✅ Correct Approach
Use environment variables:
const username = process.env.TEST_USERNAME;
const password = process.env.TEST_PASSWORD;
if (!username || !password) {
throw new Error(‘Test credentials are missing’);
}
await page.getByLabel(‘Username’).fill(username);
await page.getByLabel(‘Password’).fill(password);
Then configure credentials in your local environment or CI secret store.
Best Practice
Never commit production passwords, API keys, tokens, or other secrets to source control.
Mistake 13: Ignoring Screenshots, Traces, and Reports
A failed test without diagnostic evidence can take much longer to troubleshoot.
❌ Incorrect Approach
Running:
npx playwright test
and looking only at the final error message.
✅ Correct Approach
Configure:
use: {
screenshot: ‘only-on-failure’,
video: ‘on-first-retry’,
trace: ‘on-first-retry’
}
Then run:
npx playwright test
For the HTML report:
npx playwright show-report
Best Practice
Use traces and screenshots strategically, especially for CI failures.
Trace Viewer can help identify the exact action, page state, and timing around a failure.
Mistake 14: Running Tests Only in One Browser
❌ Incorrect Approach
Only testing Chromium:
npx playwright test –project=chromium
Why It Fails
Your application may have browser-specific behavior.
✅ Correct Approach
Configure browser projects:
projects: [
{
name: ‘chromium’,
use: { …devices[‘Desktop Chrome’] }
},
{
name: ‘firefox’,
use: { …devices[‘Desktop Firefox’] }
},
{
name: ‘webkit’,
use: { …devices[‘Desktop Safari’] }
}
]
Best Practice
Do not blindly run every test on every browser.
Choose the browser matrix according to your application’s supported browsers and risk.
Mistake 15: Ignoring Parallel Execution and CI/CD Differences
A test that works locally may fail in CI.
Common causes
- Shared test data
- Race conditions
- Environment variables missing
- Different browser versions
- Resource constraints
- Timing assumptions
- Tests depending on execution order
❌ Incorrect Approach
Assuming:
Local = CI
✅ Correct Approach
Run tests similarly to CI:
npx playwright test
Use isolated test data and avoid order dependencies.
A CI workflow can install dependencies and browsers:
– run: npm ci
– run: npx playwright install –with-deps
– run: npx playwright test
Best Practice
Treat CI as a first-class execution environment, not an afterthought.
Mistake 16: Using Retries to Hide Flaky Tests
Retries are useful, but they should not become a permanent solution for unstable automation.
❌ Incorrect Approach
export default defineConfig({
retries: 5
});
and assuming the suite is reliable because most failures eventually pass.
Why It Fails
Retries can hide:
- Race conditions
- Bad locators
- Application defects
- Environment problems
- Test-data collisions
✅ Correct Approach
Use a small retry count in CI and investigate failures.
export default defineConfig({
retries: process.env.CI ? 2 : 0
});
Best Practice
A retry is a diagnostic safety net, not a replacement for fixing flaky tests.
Mistake 17: Not Handling Dynamic Elements Correctly
Modern applications frequently load content asynchronously.
❌ Incorrect Approach
await page.waitForTimeout(3000);
await page.locator(‘.result’).click();
Why It Fails
The result may take one second or ten seconds.
✅ Correct Approach
Use a locator and assertion:
const result = page.getByRole(‘link’, {
name: ‘Product A’
});
await expect(result).toBeVisible();
await result.click();
Best Practice
Synchronize against meaningful application state rather than arbitrary time.
Mistake 18: Creating an Overly Complex Framework Too Early
This is one of the most subtle Playwright beginner mistakes to avoid.
Beginners sometimes create:
- Multiple utility layers
- Huge fixture hierarchies
- Excessive abstraction
- Complex factories
- Dozens of page classes
- Custom reporting systems
before writing enough tests to understand the application.
❌ Incorrect Approach
Building an enterprise framework before understanding basic Playwright APIs.
✅ Correct Approach
Start with:
tests/
pages/
playwright.config.ts
Then add fixtures, utilities, test data, and services as real needs appear.
Best Practice
Build the simplest framework that solves the current problem.
Good framework design evolves with the test suite.
Common Playwright Errors Caused by Beginner Mistakes
| Error | Likely Cause | Solution |
| Locator timeout | Wrong/fragile locator | Inspect and improve locator |
| Element not visible | Incorrect state assumption | Assert expected state |
| Navigation timeout | Slow/unavailable environment | Check URL/network/application |
| Browser executable missing | Browsers not installed | npx playwright install |
| Test passes locally, fails CI | Environment/state issue | Reproduce CI conditions |
| Random timeout | Poor synchronization | Remove hard waits |
| Test works alone, fails in suite | Shared state | Improve isolation |
| Tests fail after UI change | Fragile selectors | Use semantic locators |
| Tests pass after retries | Flakiness | Find root cause |
Recommended Playwright Project Structure
A growing Playwright TypeScript project can use:
playwright-project/
│
├── tests/
│ ├── login.spec.ts
│ ├── checkout.spec.ts
│ └── search.spec.ts
│
├── pages/
│ ├── LoginPage.ts
│ ├── CheckoutPage.ts
│ └── SearchPage.ts
│
├── fixtures/
│ └── testFixtures.ts
│
├── utils/
│ └── testData.ts
│
├── playwright.config.ts
├── package.json
└── tsconfig.json
This structure is not mandatory.
Use it when your project becomes large enough to justify separation.
Playwright Best Practices Checklist
Before considering your Playwright suite reliable, check:
- Tests use stable locators.
- Tests have meaningful assertions.
- Hard waits are avoided.
- Tests are independent.
- Authentication is handled efficiently.
- Credentials are stored securely.
- Fixtures are used where appropriate.
- Page Objects are introduced when useful.
- Screenshots/traces are available for failures.
- CI runs the same essential test suite as local development.
- Browser coverage matches application requirements.
- Retries do not hide flaky tests.
- Test data does not collide during parallel execution.
- Framework complexity matches project size.
Playwright Interview Questions Based on Common Mistakes
1. Why should you avoid waitForTimeout()?
Because fixed waits do not synchronize with application state. They can make tests slow and flaky.
2. What locators should beginners prefer?
Prefer user-facing and semantic locators such as getByRole() and getByLabel() where appropriate.
3. Does Playwright automatically wait for elements?
Playwright performs automatic waiting for many actionability conditions, and its web-first assertions can wait for expected states.
4. Why is test isolation important?
Independent tests can run reliably in different orders and in parallel without modifying each other’s state.
5. Should retries be used?
Yes, limited retries can be useful in CI, but they should not be used to hide flaky tests.
6. Why use environment variables?
They prevent credentials and other sensitive configuration from being hard-coded into source code.
7. When should you use Page Object Model?
Use it when the test suite becomes large enough that reusable page behavior improves maintainability.
8. How would you investigate a test that fails only in CI?
Check the trace, screenshot, video, logs, browser version, environment variables, test data, timing assumptions, and resource constraints.
This type of troubleshooting question is common in SDET and senior QA interviews because it tests practical framework knowledge rather than command memorization.
Playwright Learning Roadmap for Beginners
If you are learning Playwright for beginners, follow this progression:
Phase 1: Fundamentals
Learn:
- Playwright installation
- Project structure
- First test
- Basic commands
- Browser, context, and page
- TypeScript fundamentals
Phase 2: UI Automation
Learn:
- Locators
- Clicks
- Forms
- Dropdowns
- Checkboxes
- Keyboard and mouse
- Auto-waiting
- Assertions
Phase 3: Framework Development
Learn:
- Fixtures
- Page Object Model
- Authentication
- Test data
- Configuration
- Environment variables
Phase 4: Advanced Automation
Learn:
- API testing
- Network mocking
- Parallel execution
- Cross-browser testing
- Reporting
- Trace Viewer
Phase 5: DevOps
Learn:
- CI/CD
- GitHub Actions
- Docker
- Test artifacts
- Failure analysis
Related learning topics include Playwright Basics, Playwright Installation Guide, Playwright First Test Script, Playwright Getting Started Guide, Playwright Simple Example, Playwright Basic Commands, Playwright Learning Roadmap, Playwright TypeScript Tutorial, Playwright Locators, Playwright Auto Waiting, Playwright Page Object Model, Playwright Fixtures Tutorial, Playwright Data Driven Testing Tutorial, Playwright API Testing, Playwright Authentication Tutorial, Playwright Reporting Tutorial, Playwright Parallel Execution Tutorial, Playwright CI/CD Tutorial, Playwright GitHub Actions Tutorial, Playwright Docker Tutorial, Playwright Troubleshooting, and Playwright Interview Questions.
FAQs: Playwright Beginner Mistakes to Avoid
What are the most common Playwright mistakes?
The most common mistakes include using fragile locators, relying on hard waits, skipping assertions, sharing state between tests, hard-coding credentials, ignoring traces, and using retries to hide flaky tests.
How do I avoid Playwright test failures?
Use reliable locators, Playwright’s auto-waiting, meaningful assertions, independent test data, proper fixtures, diagnostic artifacts, and CI-compatible configuration.
Should beginners use XPath in Playwright?
XPath is supported, but it should not automatically be the first choice. Prefer stable, user-facing locators when possible.
Is waitForTimeout() bad in Playwright?
It is not inherently invalid, but it is usually a poor synchronization strategy for production tests because a fixed delay does not represent application readiness.
Why does my Playwright test pass locally but fail in CI?
Common causes include environment differences, missing variables, resource limitations, browser differences, shared state, test-data collisions, and timing assumptions.
Should I use Page Object Model in every Playwright project?
No. POM is useful when it improves maintainability. A small project may not need a complex abstraction layer.
How can I debug flaky Playwright tests?
Start with the HTML report, trace, screenshots, video, console output, and CI logs. Then determine whether the root cause is the locator, application state, test data, environment, or synchronization.
