AI in Playwright Automation: Enhancing Page Object Model for Modern SDETs
Playwright MCP for AI-Driven Test Automation: A Step-by-Step Practical Guide
Advanced Debugging Techniques in Playwright with TypeScript
Debugging is a critical process in ensuring that your Playwright tests operate seamlessly. This guide explores various advanced debugging techniques to help you debug efficiently using TypeScript.
1. Using Debugger Statements
The `debugger` statement in JavaScript and TypeScript is an essential tool for pausing code execution. This pause is crucial as it enables the inspection of the application’s state at key moments. When the debugger statement is reached, execution halts. You can then check variables, DOM elements, and network activity.
In Playwright, by adding `debugger;` to your test code and running it in debug mode, the execution will pause. This pause lets us examine the environment. It includes the page elements, URLs, and console outputs. You can step through the code line by line, pinpointing where issues arise.
Example:
test('Example test with debugger', async ({ page }) => {
await page.goto('https://example.com');
debugger; // Execution pauses here
const title = await page.title();
expect (title).toBe(‘Example Domain’);
}); In this example, execution is paused at the `debugger` statement. It allows a detailed inspection of the `page` object, the current URL, and the expected page title. This advanced debugging techniques is particularly valuable for isolating issues in complex test scenarios.
2. Verbose Logging
Verbose logging provides a detailed record of test execution. It includes every action by Playwright, such as network requests and element interactions. It also includes browser events. This level of detail is vital for debugging complex tests. Problems may not be obvious at first.
To activate verbose logging in Playwright, set the `DEBUG` environment variable to `*`. This command instructs Playwright to log all events. It provides a full overview of what happens during test execution.
Example:
DEBUG= npx playwright test
Verbose logging is exceptionally beneficial for identifying issues in multi-step or asynchronous scenarios. You can refine the logs by setting different logging levels. Use `playwright:browser` or `playwright:network` to focus on specific areas.
For greater control, logging can be implemented directly within your TypeScript code:
test.use({ launchOptions: { logger: { isEnabled: () => true, log: (name, severity, message) =>
Console.log(‘${name}: ${message}’) } } } );
test(‘Verbose logging example’, async ({ page }) => {
await page.goto(‘https://example.com’);
const title= await page.title();
console.log(‘Page title is:’, title);
expect (title).toBe(‘Example Domain’);
}); This approach lets you capture key details during tests. It improves visibility into the tests’ behavior and helps find issues.
3. Playwright Inspector
The Playwright Inspector is a powerful debugging tool. It has a visual interface for navigating your tests and diagnosing problems. Users can also examine elements. It provides a real-time view of what your test is performing, enabling you to pause execution, inspect the DOM, and interact with elements.
To activate the Playwright Inspector, set the `PWDEBUG` environment variable to `1` when executing your tests. This action opens the inspector window. You can then observe the page’s state, step through the test code, and run commands in the DevTools console.
Example:
PWDEBUG=1 npx playwright test
The Playwright Inspector is useful for debugging complex interactions. These include form submissions and loading dynamic content. It also helps with multi-step workflows. By testing and engaging with the page, you can identify the underlying issue.
4. Network Traffic Monitoring
Monitoring network traffic during tests is essential. This is true for apps that rely on APIs or external services. Playwright allows you to capture and analyze network traffic, providing insight into the data being sent and received. This capability is instrumental in identifying issues related to network requests.
You can intercept network requests and responses by setting up event listeners on the `page` object. This setup lets you log details of each request and response. It includes the URL, method, status code, and headers.
Example:
Test(‘Network traffic monitoring example’, async({ page }) => {
Page.on(‘request’, request=> console.log (‘>>’, request.method(), request.url() ));
Page.on(‘response’, response=> console.log (‘<<’, response.status(), response.url() ));
Await page.goto(‘https://example.com’);
}); This example logs every network request and response. You can see exactly what data is exchanged between the browser and the server. Monitoring network traffic is especially valuable for debugging issues related to data fetching, authentication, or performance.
5. Error Handling & Assertions
Effective error handling, along with the use of assertions, is vital for robust debugging. Assertions allow you to verify that specific conditions are met during test execution. When an assertion fails, it reveals what went wrong. This helps diagnose and fix the issue faster.
In Playwright, use the `expect` API to create assertions. They validate the page’s state. For example, check that an element is visible, verify the page title, or ensure specific text is present.
Example:
test(‘Error handling and assertions example’, async ({ page }) => {
try{
Await page.goto(https://example.com’);
Const title= await page.title();
Expect(title).toBe(‘Example Domain’);
} catch (error) {
Concole.error(‘Test failed :’, error);
}
}); By handling errors gracefully, you can offer more detailed feedback when something goes wrong, aiding in understanding and resolving the issue. Assertions are crucial for ensuring that your tests are reliable and accurately reflect your application’s behavior.
Advanced Debugging Techniques
1. Custom Logging Middleware
Custom logging middleware lets you create custom logging functions. They give better debugging insights. Your logging functions can capture info specific to your tests. This includes variable values, action timings, and specific events.
Example:
function log (message:string) {
console.log(‘[LOG]: ${message}’);
}
test (‘Custom logging example’, async ({ page }) => {
log(‘Navigating to example.com’);
await page.goto (‘https://example.com’);
const title = await page.title();
log(‘Page title is: ${title}’);
expect (title).toBe(‘Example Domain’);
}); Custom logging can be further expanded to include more advanced features, such as writing logs to a file, transmitting logs to an external monitoring system, or integrating with existing logging frameworks.
2. Conditional Breakpoints
Conditional breakpoints are a powerful tool. They let you pause execution only when specific conditions are met. This technique is great for debugging specific scenarios. It helps to isolate issues that occur under certain conditions.
In Playwright, you can set conditional breakpoints. Use a `debugger` statement with conditional logic in your test code.
Example:
test (‘Conditional breakpoint example’, async ({ page }) => {
await page.goto(‘https://example.com’);
const title = await page.title();
if (title !== ‘Example Domain’) {
debugger; // Execution will pause here if the condition is met
}
expect(title).toBe(‘Example Domain’);
}); Conditional breakpoints work well for fixing flaky tests. They also help debug issues that occur occasionally or under specific conditions.
3. Mocking API Responses
Mocking API responses is an advanced method. It lets you isolate issues and test specific conditions without relying on external APIs. This lets you simulate scenarios. You can test how your app handles different responses.
Example:
await page.route(’https://api.example.com/data’, route => {
route.fulfill ({
status: 200,
body: JSON.stringify({ data: ‘mocked data’})
});
}); By mocking API responses, you can focus on specific parts of your application, ensuring that your tests are consistent and unaffected by external variables.
Real-World Examples
1. Debugging a Failing Test Case
When a test fails, gathering as much information as possible is crucial to identifying the root cause. Start by examining logs, network traffic, and error messages. Utilize the Playwright Inspector to step through the test and interact with the page, allowing you to comprehend what is happening at each step.
Example:
page.on(’request’, request => console.log (’>>’, request.method(), request.url()));
page.on(’response’, response => console.log (’<<’, response.status(), response.url()));
This methodical approach helps you identify and resolve the issue, ensuring that your tests are both reliable and accurate.
2. Identifying Flaky Tests
Flaky tests are those that pass or fail inconsistently, often due to timing issues, dependencies on external services, or race conditions. To identify flaky tests, rerun them multiple times and analyze the results. Look for patterns in the failures and use conditional breakpoints or verbose logging to locate the cause.
Example:
for (let i=0; i < 5; i++) {
Try {
Await page.goto(‘https://example.com’);
Const title = await page.title();
Expect (title).toBe(‘Example Domain’);
} catch (error) {
Console.error(‘Iteration ${i+1}: Failed’, error);
}
} By identifying and addressing flaky tests, you can enhance the reliability of your test suite and reduce the time spent troubleshooting intermittent failures.
Best Practices for Advanced Debugging Techniques
1. Write Clear and Concise Tests
Writing clear and concise tests is critical for maintaining a dependable test suite. Use descriptive names for test cases, focus on one specific functionality per test, and avoid hardcoding values. This approach simplifies understanding of the test’s purpose and reduces the potential for errors.
2. Maintain Detailed Documentation
Maintaining comprehensive documentation is essential for future debugging and collaboration. Thoroughly document your test cases. Include setup instructions, known issues, and any changes made over time. This practice gives your team the info to fix issues quickly.
3. Continuous Integration and Advanced Debugging Techniques
Incorporating Playwright tests into CI/CD pipelines ensures that your tests are executed automatically as part of your development process. To debug tests in CI environments, enable detailed logging, capture screenshots and videos of failed tests, and use parallel test execution to minimize flakiness. Implementing retry mechanisms for flaky tests and configuring alerts for test failures further enhances the reliability of your test suite.
Conclusion:
Mastering advanced debugging techniques in Playwright with TypeScript is indispensable for developing reliable, efficient tests. By employing tools such as debugger statements, verbose logging, and the Playwright Inspector, alongside advanced strategies like custom logging and API response mocking, you can effectively identify and resolve issues. Adhering to best practices ensures that your test suite remains robust and maintainable, ultimately contributing to smoother development and deployment processes.
Handling Authentication for Multiple User Logins in Playwright
Testing applications that support multiple user roles or account types often requires handling multiple user logins in a single test suite. Playwright is robust authentication tools simplify this by enabling persistent authentication, session management, and seamless switching between different user accounts.
In this blog, we’ll explore how to handle multiple user logins effectively in Playwright, ensuring your tests remain scalable, maintainable, and efficient.
Why Test Multiple User Logins?
Applications often have varying access levels or features based on user roles, such as:
- Admin vs. Regular User: Admins access additional controls or settings.
- Paid vs. Free Users: Paid accounts unlock premium features.
- Guest vs. Authenticated Users: Registered users can perform tasks unavailable to guests.
Testing these scenarios ensures role-specific functionality works as intended and maintains security boundaries.
Approach 1: Saving Storage States for Playwright Multiple Users
Playwright allows you to save storage states for each user after login. These saved states can then be reused in tests. By capturing the authenticated state of a user session (cookies, localStorage, sessionStorage), you can reuse this state across multiple tests without repeating the login process.
Steps to Handle Multiple Users
1. Save Storage States for Each Playwright User
Create a test that logs in with the required credentials for a specific user role and saves the storage state to a file.
Save the storage states to distinct files named according to the user role (e.g., admin.json, regular.json). Store them in a dedicated folder, such as playwright/.auth/, to keep your project organized.
import { test } from '@playwright/test';
test.describe('Admin Login', () => {
test('Admin dashboard access', async ({ page }) => {
// Admin user
await page.goto('https://conduit.bondaracademy.com/login');
await page.fill('[placeholder="Email"]', 'abc@cd.com');
await page.fill('[placeholder="Password"]', 'adminuser');
await page.click('button[type="submit"]');
await page.waitForTimeout(5000);
await page.context().storageState({ path: 'playwright/.auth/admin.json' });
});
});
test.describe('Regular user Login', () => {
test('Regular user access', async ({ page }) => {
// Regular user
await page.goto('https://conduit.bondaracademy.com/login');
await page.fill('[placeholder="Email"]', 'abcd@abc.com');
await page.fill('[placeholder="Password"]', 'regularuser');
await page.click('button[type="submit"]');
await page.waitForTimeout(5000);
await page.context().storageState({ path: 'playwright/.auth/regular.json' });
});
});
This approach can also be implemented under global setup file which would run before the test suite and the login step will not be counted a separate test case.
2. Reuse Storage States in Tests
Load the appropriate storage state for each test. Set up your test to load the storage state file corresponding to the desired user role using the test.use() method. This tells Playwright to initialize the browser context with the specified state.
import { expect, test } from '@playwright/test';
test.describe('Admin tests', () => {
test.use({ storageState: 'playwright/.auth/admin.json' });
test('Admin dashboard access', async ({ page }) => {
await page.goto('https://conduit.bondaracademy.com/');
await expect (page.locator('[class="nav navbar-nav pull-xs-right"]')).toContainText(" Admin_User ")
});
});
test.describe('Regular user tests', () => {
test.use({ storageState: 'playwright/.auth/regular.json' });
test('Regular user access', async ({ page }) => {
await page.goto('https://conduit.bondaracademy.com/');
await expect (page.locator('[class="nav navbar-nav pull-xs-right"]')).toContainText(" Regular_User2 ")
});
}); Benefits
- Tests run faster as logins are avoided for every test.
- Separate storage states ensure no interference between user sessions.
- Supports parallel test execution.
Approach 2: Using Different Contexts for Each Playwright User
When testing applications with multiple user roles, such as Admins, Regular Users, or Guests, using separate contexts in Playwright ensures isolated sessions for each user. Each context represents an independent browser session, complete with its own cookies, localStorage, and sessionStorage. This approach eliminates the risk of interference between different user sessions and allows simultaneous interactions with the application in the same test.
Steps to Use Contexts for Multiple Users
1. Create Contexts for Each Playwright User
Set up multiple contexts and log in as different users in each and then store the session to the dedicated folder.
import { test, chromium } from '@playwright/test';
test('Multiple users using contexts', async () => {
const browser = await chromium.launch();
// Admin context
const adminContext = await browser.newContext();
const adminPage = await adminContext.newPage();
await adminPage.goto('https://conduit.bondaracademy.com/login');
await adminPage.fill('[placeholder="Email"]', 'abc@cd.com');
await adminPage.fill('[placeholder="Password"]', 'adminuser');
await adminPage.click('button[type="submit"]');
await adminPage.waitForTimeout(5000);
await adminContext.storageState({ path: 'playwright/.auth/admin.json' });
// Regular user context
const userContext = await browser.newContext();
const userPage = await userContext.newPage();
await userPage.goto('https://conduit.bondaracademy.com/login');
await userPage.fill('[placeholder="Email"]', 'abcd@abc.com');
await userPage.fill('[placeholder="Password"]', 'regularuser');
await userPage.click('button[type="submit"]');
await userPage.waitForTimeout(5000);
await userContext.storageState({ path: 'playwright/.auth/regular.json' });
await browser.close();
}); 2. Run Tests with Separate Contexts
If you’ve saved storage states for each role, you can load these states into new contexts to skip the login process.
import { expect, test } from '@playwright/test';
test('Simultaneous admin and regular user actions', async ({ browser }) => {
const adminContext = await browser.newContext({ storageState: 'playwright/.auth/admin.json' });
const adminPage = await adminContext.newPage();
const userContext = await browser.newContext({ storageState: 'playwright/.auth/regular.json' });
const userPage = await userContext.newPage();
// Admin actions
await adminPage.goto('https://conduit.bondaracademy.com/');
await expect (adminPage.locator('[class="nav navbar-nav pull-xs-right"]')).toContainText(" Admin_User ")
// Regular user actions
await userPage.goto('https://conduit.bondaracademy.com/');
await expect (userPage.locator('[class="nav navbar-nav pull-xs-right"]')).toContainText(" Regular_User2 ")
await adminContext.close();
await userContext.close();
});Approach 3 : Implementation of authentication in POM fixtures.
We can add the storing of session per browser context in the below manner
Below is an example that creates fixtures for two Page Object Models - admin POM and user POM. It assumes adminStorageState.json and userStorageState.json files were created in the global setup.
import { test as base, type Page } from '@playwright/test';
// Page Object Model for the "admin" page.
class AdminPage {
// Page signed in as "admin".
page: Page;
constructor(page: Page) {
this.page = page;
}
}
// Page Object Model for the "regularUser" page.
class UserPage {
// Page signed in as "user".
page: Page;
constructor(page: Page) {
this.page = page;
}
}
// Declare the types of your fixtures.
type MyFixtures = {
adminPage: AdminPage;
userPage: UserPage;
};
export * from '@playwright/test';
export const test = base.extend<MyFixtures>({
adminPage: async ({ browser }, use) => {
const context = await browser.newContext({ storageState: 'playwright/.auth/admin.json' });
const adminPage = new AdminPage(await context.newPage());
await use(adminPage);
await context.close();
},
userPage: async ({ browser }, use) => {
const context = await browser.newContext({ storageState: 'playwright/.auth/regular.json' });
const userPage = new UserPage(await context.newPage());
await use(userPage);
await context.close();
},
}); Using the page fixtures in your tests
Using this approach makes your tests more clean and you can manage as many user roles sessions in fixture files.
import { test, expect } from '../playwright/fixtures';
// Use adminPage and userPage fixtures in the test.
test('admin and user', async ({ adminPage, userPage }) => {
// ... interact with both adminPage and userPage ...
await expect (adminPage.locator('[class="nav navbar-nav pull-xs-right"]')).toContainText(" Admin_User ")
await expect (userPage.locator('[class="nav navbar-nav pull-xs-right"]')).toContainText(" Regular_User2 ")
});Best Practices for Multiple User Logins in Playwright
- Use Role-Specific Test Descriptions: Clearly define tests based on user roles (e.g., Admin, Guest, Subscriber).
- Avoid Shared States: Keep user sessions isolated using contexts or separate storage states.
- Secure Test Credentials: Use environment variables to manage user credentials securely.
- Minimize Redundancy: Automate login steps once and reuse storage states.
- Parallelize Tests: Utilize Playwright’s parallel execution to run tests for different users simultaneously.
Conclusion
Handling authentication for multiple user logins in Playwright ensures that your application’s role-based functionality is thoroughly tested and secure. Whether you use persistent storage states or isolated browser contexts, by adopting these practices, you can enhance your test efficiency, reliability, and coverage, ensuring a seamless experience for all user roles.
Running Playwright Tests in a Docker Container
Debugging API Test while using Playwright with TypeScript
Handling Dynamic Content and AJAX Requests in Playwright Tests
Effective Error Handling and Retries in Playwright Tests
Playwright is a powerful tool for end-to-end testing across multiple browsers. In modern web applications, ensuring the reliability and stability of automated tests is crucial. Even a small glitch can lead to failed tests, which in turn can cause unnecessary delays in development and deployment. As web apps grow more complex, error handling and implementing retries in Playwright tests are important. This prevents tests from producing unpredictable outcomes. This blog explores ways to improve Playwright tests. It covers error handling and retries to make tests more reliable and easier to use.
What is Test Failure?
Test failure occurs when an automated test does not produce the expected outcome. For example, if a test checks that a user can log in to an app, it should pass if the login works. If you click the login button but the expected result, such as a redirect to the user’s dashboard, does not happen, the test fails.
Test failures can arise from various issues. These include changes in the application’s code, network problems, or slow-loading elements. We must understand the root cause of these failures. It’s key to keeping your test suite intact. It duplicates the application’s behavior exactly.
Causes of Test Failures
- Network Fluctuations: Intermittent network issues cause unexpected test failures. These can manifest as timeouts, failed resource loads, or delayed responses.
- Timing Issues: Elements may not be immediately available due to animations, slow loading, or other delays in the DOM. If a test interacts with an element too early, it will fail.
- Dynamic Content: Unstable web pages with rapid changes or delayed loads cause test failures. Elements undergo sudden state or content alterations during test execution.
- Unexpected Alerts or Pop-ups: Unanticipated dialogs or alerts disrupt the test flow. These can be system alerts, browser-specific pop-ups, or in-page dialogs that need handling.
Playwright Installation:
Before diving into error handling and retries, it’s essential to have Playwright installed in your project. To get started, you can install Playwright using npm:
bash
npm install @playwright/test --save-dev
Once installed, you can run your first Playwright test with minimal configuration. Playwright supports testing on Chromium, Firefox, and WebKit. So, it’s a great tool for cross-browser testing.
Effective Error Handling Strategies
1. Use Playwright’s Built-in Waits
Playwright offers waiting mechanisms to ensure elements are ready before interacting with them.
javascript
await page.waitForSelector('submit-button', { state: 'visible' });
This reduces errors due to elements not being ready, ensuring that interactions occur only when the element is in the desired state.
2. Try-Catch Blocks
Using try-catch blocks for test steps handles exceptions. It allows actions like taking screenshots or logging errors.
javascript
try {
await page.click('submit-button');
} catch (error) {
console.error('Error clicking submit button:', error);
await page.screenshot({ path: 'error-screenshot.png' });
}
This approach is better at finding and diagnosing issues. It adds context, which helps debug failed tests.
3. Assertions with Custom Error Messages
Custom error messages in assertions help identify issues faster.
javascript
expect(await page.isVisible('success-message'), 'Success message should be visible').toBe(true);
These messages provide clear and actionable feedback when an assertion fails, helping to pinpoint the root cause of the issue.
Implementing Retries in Playwright
Retries help mitigate flaky tests by reattempting failed steps. Implement a retry mechanism using a loop and a counter.
Manual Retry Implementation:
If you only want to apply retries to specific tests, you can use the retries option within individual test definitions.
1. Simple Retry Logic
Wrap the test step in a loop that retries several times.
javascript
const maxRetries = 3;
let attempt = 0;
let success = false;
while (attempt < maxRetries && !success) {
try {
await page.click('submit-button');
success = true;
} catch (error) {
attempt++;
if (attempt >= maxRetries) {
console.error('Max retries reached. Failing the test.');
throw error;
}
console.warn(`Attempt ${attempt} failed. Retrying...`);
}
}
This retry logic attempts to click the submit button up to three times before failing the test. It’s beneficial for handling transient issues like minor network glitches.
2. Exponential Backoff
Delays between retries grow longer after every failed attempt.
javascript
const maxRetries = 3;
let attempt = 0;
let success = false;
while (attempt < maxRetries && !success) {
try {
await page.click('submit-button');
success = true;
} catch (error) {
attempt++;
if (attempt >= maxRetries) {
console.error('Max retries reached. Failing the test.');
throw error;
}
const waitTime = Math.pow(2, attempt) 1000;
console.warn(`Attempt ${attempt} failed. Retrying in ${waitTime / 1000} seconds...`);
await new Promise(resolve => setTimeout(resolve, waitTime));
}
}
This approach includes a delay between retries. It helps with temporary network glitches by giving the system time to recover.
Playwright Test’s Built-in Retry Mechanism:
You can configure global retries in your playwright.config.ts file, which will apply to all tests.
Playwright Test, the Playwright test runner, provides a built-in retry mechanism for entire tests. Configure this in the test runner’s configuration file.
1. Configure Retries in Playwright Config:
Set the `retries` option in the Playwright configuration file.
javascript
// playwright.config.js
module.exports = {
retries: 2, // Retries failed tests up to 2 times
use: {
headless: false,
},
};
This tells the Playwright to retry any failed test up to two times. It’s a straightforward way to apply retries across all tests without modifying individual test cases.
2. Marking Individual Tests for Retries:
Specify retries for individual tests using the `test` object.
javascript
test('example test', async ({ page }) => {
await page.goto('https://example.com');
await page.click('submit-button');
}, { retries: 2 });
Tailor retry behavior to the specific needs of each test, applying retries only where necessary. This can reduce unnecessary test execution time and focus retries on the most unstable tests.
Conclusion
Effective error handling and retries strategy is crucial for building reliable Playwright test suites. By implementing proper error-handling mechanisms, customizing retry logic, and adhering to best practices, you can significantly reduce test flakiness and improve the overall stability of your test automation. Remember, the goal is to build a test suite that you can trust—a suite that provides accurate feedback and supports the rapid delivery of high-quality software.
With these strategies in place, you’ll be well-equipped to tackle even the most challenging aspects of test automation in Playwright.























