PlayWrite - realestateexplorer/relatedToCh GitHub Wiki
create SDET for PLAYWRITE interview questions and answers
Of course! Here is a comprehensive guide to SDET interview questions and answers, specifically tailored for roles requiring Playwright expertise.
This guide is structured to cover different levels of expertise, from fundamental concepts to advanced SDET-specific scenarios.
- Fundamental Playwright & Testing Concepts
- Core Playwright Features (Intermediate)
- Advanced Playwright & SDET Scenarios
- Architectural & Strategy Questions
- Practical Coding Challenge Example
These are the "must-know" questions to establish a baseline.
Answer: Playwright is a modern, open-source automation library for web browsers, developed by Microsoft. It's designed to be fast, reliable, and "evergreen," meaning it stays up-to-date with modern web platforms.
Its popularity stems from several key features:
- Cross-Browser Support: It supports all modern rendering engines: Chromium (Chrome, Edge), WebKit (Safari), and Firefox.
- True Cross-Platform & Cross-Language: It runs on Windows, Linux, and macOS and has official APIs for TypeScript/JavaScript, Python, Java, and .NET.
- Auto-Waits: This is a game-changer. Playwright automatically waits for elements to be "actionable" (e.g., visible, stable, enabled) before performing actions. This drastically reduces test flakiness compared to older tools like Selenium where explicit waits are mandatory.
- Network Interception: It provides powerful tools to mock and stub network requests and responses, making it easy to test edge cases and isolate the front-end from back-end issues.
- Parallel Execution: The Playwright Test runner is designed for parallelism out-of-the-box, significantly speeding up test suite execution.
- Superior Tooling: Features like Codegen (records actions and generates scripts), Trace Viewer (a detailed post-mortem tool), and built-in reporting make development and debugging incredibly efficient.
Answer: An SDET (Software Development Engineer in Test) is a hybrid role that blends the skills of a software developer with the mindset of a tester.
- Traditional QA Engineer: Primarily focuses on what to test. They are experts in test case design, exploratory testing, and user-centric validation. They may use automation tools but are not typically expected to build the framework from the ground up.
-
SDET: Focuses on how to test efficiently and at scale. They are responsible for:
- Building & Maintaining Test Automation Frameworks: Creating robust, scalable, and maintainable frameworks (like one using Playwright).
- Writing High-Quality Code: Their test code is expected to follow software development best practices (e.g., DRY principle, SOLID principles).
- CI/CD Integration: Integrating automated tests into the continuous integration and delivery pipeline.
- Tooling & Infrastructure: Developing tools and infrastructure to support testing efforts (e.g., test data generators, environment setup scripts).
- Performance & API Testing: Going beyond the UI to test other layers of the application.
Essentially, an SDET enables the entire team to test better and faster by providing them with powerful automation and tools.
These questions dive into the specifics of using Playwright day-to-day.
Q3: Explain the difference between page.locator()
and page.$()
(or page.querySelector()
). When would you use each?
Answer: This question tests your understanding of Playwright's core interaction model.
-
page.locator()
: This is the recommended, modern approach. It creates aLocator
object that represents the recipe for finding an element. The key is that it's not evaluated immediately. The element is only searched for when you perform an action on the locator (e.g.,.click()
,.fill()
). This makes it resilient to dynamic content and works seamlessly with Playwright's auto-waits. -
page.$()
orpage.querySelector()
: This is an older, less-preferred method. It returns a promise that resolves to anElementHandle
ornull
if the element isn't found at that moment. This can lead to race conditions and flaky tests if the element isn't immediately available on the page. It bypasses Playwright's auto-waiting mechanism for actions.
When to use them:
-
Always prefer
page.locator()
for interacting with elements (clicking, filling, getting text, asserting). - Use
ElementHandle
(frompage.$()
) only in rare, specific scenarios, such as when you need to pass an element reference topage.evaluate()
.
// GOOD: Using Locator
await page.locator('#submit-button').click();
// BAD: Prone to flakiness
const element = await page.$('#submit-button');
if (element) {
await element.click(); // Race condition can happen here
}
Q4: How does Playwright handle waiting? Explain Auto-Waits and the difference between waitForSelector
and actions on a Locator.
Answer: Playwright's waiting mechanism is one of its strongest features.
-
Auto-Waits: For every action on a
Locator
(like.click()
,.fill()
,.textContent()
), Playwright performs a series of "actionability checks" and waits automatically for them to pass. These checks include:- Element is attached to the DOM.
- Element is visible.
- Element is stable (not animating).
- Element is enabled and can receive events. This built-in intelligence eliminates the most common source of flaky tests.
-
Explicit Waits (
waitFor...
): There are still times you need to wait for something specific that isn't tied to an action.-
page.waitForSelector()
: Waits for a specific selector to appear in the DOM. This is useful for knowing when something exists, even if you don't interact with it immediately. -
page.waitForURL()
: Waits for the URL to change to a specific pattern. -
page.waitForResponse()
: Waits for a network request to complete.
-
The key difference is that auto-waits are implicit (part of the action), while explicit waits are standalone commands for specific application states. In modern Playwright, you rarely need waitForSelector
because actions on locators handle it for you.
Answer: A Browser Context is an isolated, incognito-like session within a single browser instance.
Why it's useful:
- Test Isolation: Each test can run in its own context. This means cookies, local storage, and session storage are sandboxed. One test cannot affect another, which is critical for running tests in parallel reliably.
- Multi-User Scenarios: You can easily simulate multiple users interacting with your application simultaneously. For example, in a chat application, you can create two contexts (User A and User B) in a single test to verify real-time communication.
- Clean State: Each context starts fresh, without any leftover data from previous tests. This eliminates the need for complex teardown and cleanup logic.
- Permissions & Geolocation: You can set permissions (e.g., notifications, clipboard access) or emulate geolocation per-context.
// Example of two contexts
test('chat functionality', async ({ browser }) => {
// Context for User A
const userAContext = await browser.newContext();
const userAPage = await userAContext.newPage();
// Context for User B
const userBContext = await browser.newContext();
const userBPage = await userBContext.newPage();
// Now you can orchestrate actions between userAPage and userBPage
});
These questions test your ability to solve complex problems and think about the bigger picture.
Q6: Your team is struggling with flaky tests. Describe your process for debugging and fixing a flaky test in Playwright.
Answer: This is a critical SDET skill. My process would be:
-
Reproduce Reliably: First, I try to reproduce the flakiness. I'd run the test in a loop (
npx playwright test --repeat-each=10
) to see how often it fails. -
Isolate the Failure Point: The error message is the starting point. Is it a timeout? An incorrect assertion? A network error?
-
Utilize Playwright Trace Viewer: This is my primary tool. I'd run the test with the
--trace on
flag. The Trace Viewer provides a goldmine of information:- Timeline View: A frame-by-frame recording of the test execution. I can scrub through it to see exactly what the page looked like at the moment of failure.
- Action Log: A list of every Playwright action, its duration, and its result. I can see if an action timed out or if a locator found multiple elements.
- Network Tab: I inspect all network requests. Did an API call fail or return unexpected data? This is often the root cause.
- Console Logs & Source: I can see browser console errors and the state of the DOM.
-
Analyze Common Causes: Based on the trace, I investigate common culprits:
-
Timing Issues: Is the test trying to interact with an element that is still loading or covered by a spinner? Even with auto-waits, complex animations or delayed API responses can cause issues. I might need a more specific assertion, like
await expect(locator).toBeVisible()
, before proceeding. -
Race Conditions: Is the test dependent on the order of asynchronous operations? For example, clicking a button that triggers two API calls. I might need to use
page.waitForResponse()
to ensure the critical API call has finished before asserting the result. -
Unstable Locators: Is the locator too generic (
div > button
) or based on dynamic IDs? I would refactor it to a more robust, user-facing locator likepage.getByRole('button', { name: 'Submit' })
. - Test Data Issues: Is the test dependent on data that might be changed by another parallel test? I'd ensure my tests create their own isolated data or run in separate contexts.
-
Timing Issues: Is the test trying to interact with an element that is still loading or covered by a spinner? Even with auto-waits, complex animations or delayed API responses can cause issues. I might need a more specific assertion, like
-
Implement the Fix & Verify: I'll apply the fix (e.g., a better locator, an explicit wait for a network response) and then re-run the test in a loop (
--repeat-each=20
) to confirm its stability before merging the code.
Q7: How would you handle authentication in your Playwright tests to avoid logging in for every single test?
Answer:
Logging in via the UI for every test is slow and brittle. The best practice is to "log in once, then reuse the session." Playwright provides a fantastic way to do this using storageState
.
My Strategy:
-
Create a Global Setup File: In my
playwright.config.ts
, I'll define aglobalSetup
file. This script will run once before all tests. -
Perform Login Programmatically: Inside this setup file, I will:
- Launch a browser.
- Navigate to the login page.
- Fill in credentials (ideally from environment variables, not hard-coded).
- Submit the form.
- Wait for a post-login element to ensure login was successful.
-
Save the Authenticated State: After a successful login, I'll use
page.context().storageState({ path: 'storageState.json' })
. This command saves all cookies, local storage, and session storage into a JSON file. -
Configure Tests to Use the Saved State: In
playwright.config.ts
, inside theuse
block, I'll setstorageState: 'storageState.json'
. -
Execution Flow:
- Before the test suite runs,
globalSetup
executes, performs the UI login, and createsstorageState.json
. - For every subsequent test, Playwright automatically creates a new browser context and loads it with the cookies and storage from that file.
- Each test now starts in an already authenticated state, bypassing the login form completely.
- Before the test suite runs,
This approach is fast, reliable, and separates the concern of authentication from the actual test logic.
Q8: Explain the Page Object Model (POM). How do you implement it in a Playwright project, and what are its benefits?
Answer: The Page Object Model (POM) is a design pattern that creates an object repository for the UI elements and interactions of a web page. It's a fundamental concept for creating maintainable and scalable test automation.
How to Implement in Playwright:
I would create a class for each page or major component of the application.
-
Locators: The class properties would be the Playwright
Locator
objects for the elements on that page. These are defined in the constructor. -
Actions: The class methods would represent user interactions on that page (e.g.,
login(user, pass)
,searchFor(item)
,navigateToProfile()
). These methods use the locators to perform actions. -
Assertions (Optional but Recommended): Methods can also return state or perform simple assertions, like
isErrorMessageVisible()
.
Example (LoginPage.ts
):
// pages/LoginPage.ts
import { type Locator, type Page } from '@playwright/test';
export class LoginPage {
// Readonly properties for locators
readonly page: Page;
readonly usernameInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.usernameInput = page.getByLabel('Username');
this.passwordInput = page.getByLabel('Password');
this.loginButton = page.getByRole('button', { name: 'Log in' });
this.errorMessage = page.locator('.error-message');
}
// Action methods
async goto() {
await this.page.goto('/login');
}
async login(username, password) {
await this.usernameInput.fill(username);
await this.passwordInput.fill(password);
await this.loginButton.click();
}
}
Using it in a test (login.spec.ts
):
// tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
test('should allow a user to log in', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('testuser', 'password123');
await expect(page).toHaveURL(/.*dashboard/);
});
Benefits of POM:
- Maintainability: If a UI element's locator changes, you only need to update it in one place (the Page Object class), not in every test that uses it.
- Readability: Test scripts become much cleaner and more readable. They describe what is happening (the user's intent), not how it's being done (the implementation details).
- Reusability: The action methods can be reused across multiple tests, reducing code duplication (DRY principle).
These questions assess your ability to design and lead a testing effort.
Q9: You are tasked with setting up a new Playwright automation framework from scratch. Describe the high-level components and structure you would create.
Answer: My goal would be to create a scalable, maintainable, and easy-to-use framework. Here's the structure:
-
Configuration (
playwright.config.ts
):- Define base URL, timeouts, and trace settings (
on-first-retry
). - Configure projects for cross-browser testing (Chromium, Firefox, WebKit).
- Set up
globalSetup
for authentication. - Configure reporters (HTML, JSON for data analysis).
- Enable parallel execution with a defined number of workers.
- Define base URL, timeouts, and trace settings (
-
Directory Structure:
-
/tests
: Contains all test files (*.spec.ts
). I'd group them by feature (e.g.,/tests/login
,/tests/checkout
). -
/pages
: Contains all Page Object Model classes. -
/utils
or/lib
: For helper functions, constants, and utility classes that are shared across the framework (e.g.,test-data-generator.ts
,api-helpers.ts
). -
/fixtures
: For custom test fixtures if needed (e.g., setting up a test with a pre-created user via an API). -
/test-data
: For static test data (e.g., JSON files with user profiles).
-
-
Core Components:
- Page Objects (POM): As described before, this is the foundation for UI interactions.
-
API Helpers: A set of functions to interact with the application's backend API. This is crucial for setting up test data (e.g.,
createProduct()
,deleteUser()
) and for tests that are a hybrid of UI and API validation. -
Custom Fixtures: For complex setup logic. For example, a
loggedInUser
fixture that not only usesstorageState
but also creates a unique user for that test via an API call and handles cleanup. -
Environment Management: Use environment variables (
.env
files) to manage different configurations forlocal
,staging
, andproduction
environments (e.g., different URLs, different user credentials).
-
CI/CD Integration (
.github/workflows/ci.yml
or similar):- A pipeline that triggers on every pull request.
- Steps: Install dependencies, build the app (if needed), run linters, and then run
npx playwright test
. - On failure, upload the Playwright HTML report and traces as artifacts for easy debugging.
This structure separates concerns, promotes reusability, and is built for scale from day one.
Q10: Playwright can do more than just UI testing. How would you leverage its API testing capabilities in your strategy?
Answer: Leveraging Playwright's API testing is key to following the Test Automation Pyramid. It allows for faster, more stable, and more targeted tests.
My strategy would be:
-
Hybrid Tests: For many UI flows, I'd use the API to handle the setup and the UI to test the actual user-facing functionality.
-
Example (Testing a shopping cart): Instead of using the UI to search for a product, click on it, and add it to the cart (which is slow and tests things unrelated to the cart), I would use an API call (
request.post('/api/cart', ...)
to add the item directly. Then, I would load the cart page (page.goto('/cart')
) and use UI automation to verify that the item is displayed correctly, the price is right, and the "checkout" button works.
-
Example (Testing a shopping cart): Instead of using the UI to search for a product, click on it, and add it to the cart (which is slow and tests things unrelated to the cart), I would use an API call (
-
Pure API Tests: For validating business logic, permissions, and data transformations, I would write API-only tests using
request
from@playwright/test
.-
Example (Testing user permissions): I would write a test that makes an API call to a protected endpoint using the token of a non-admin user and assert that the response is a
403 Forbidden
. This is thousands of times faster and more direct than trying to test it through the UI.
-
Example (Testing user permissions): I would write a test that makes an API call to a protected endpoint using the token of a non-admin user and assert that the response is a
-
Data Seeding and Cleanup:
-
Setup (
beforeEach
): Use API calls to create the specific data needed for a test (e.g., create a new user, a new blog post). -
Teardown (
afterEach
): Use API calls to clean up the data created during the test to ensure the environment remains pristine.
-
Setup (
By combining UI and API testing this way, we can create a much more efficient and robust test suite. We test the UI for what it does best (rendering and user interaction) and the API for what it does best (business logic and data handling).
Interviewer: "Let's do a small coding exercise. Using Playwright, write a test for a login page. The test should verify that a user with valid credentials can log in successfully, and a user with invalid credentials sees an error message. Please use the Page Object Model."
(You would then be given a simple login page to work with or a description of one.)
Your Approach:
-
Clarify Requirements: "Okay, so the login page has a username field, a password field, and a login button. A successful login redirects to
/dashboard
. An unsuccessful login shows an error message with the text 'Invalid credentials'. I'll structure this using POM." -
Write the Page Object (
LoginPage.ts
)import { type Locator, type Page, expect } from '@playwright/test'; export class LoginPage { readonly page: Page; readonly usernameInput: Locator; readonly passwordInput: Locator; readonly loginButton: Locator; readonly errorMessage: Locator; constructor(page: Page) { this.page = page; // Using user-facing roles is best practice! this.usernameInput = page.getByLabel('Username'); this.passwordInput = page.getByLabel('Password'); this.loginButton = page.getByRole('button', { name: 'Login' }); this.errorMessage = page.locator('.error-message'); } async goto() { await this.page.goto('/login'); } async login(username, password) { await this.usernameInput.fill(username); await this.passwordInput.fill(password); await this.loginButton.click(); } }
-
Write the Test File (
login.spec.ts
)import { test, expect } from '@playwright/test'; import { LoginPage } from '../pages/LoginPage'; // Group related tests with describe test.describe('Login Functionality', () => { test('should allow a user with valid credentials to log in', async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.goto(); await loginPage.login('correct-user', 'correct-password'); // Assert that the URL is the dashboard page after successful login await expect(page).toHaveURL(/.*dashboard/); // Assert that the login form is no longer visible await expect(loginPage.loginButton).not.toBeVisible(); }); test('should show an error message for invalid credentials', async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.goto(); await loginPage.login('invalid-user', 'wrong-password'); // Assert that the error message is visible and contains the correct text await expect(loginPage.errorMessage).toBeVisible(); await expect(loginPage.errorMessage).toHaveText('Invalid credentials'); // Assert that we are still on the login page await expect(page).not.toHaveURL(/.*dashboard/); }); });
This solution demonstrates knowledge of POM, best practices for locators (getByRole
, getByLabel
), clear assertions using expect
, and good test structure with test.describe
.
create Playwright interview questions and answers in c#
Of course! Here is a comprehensive guide to SDET interview questions and answers, specifically tailored for roles requiring Playwright for .NET (C#).
This guide is structured for an SDET audience, moving from fundamental concepts to advanced framework design and practical C# implementation.
- Fundamental Playwright & Testing Concepts
- Core Playwright for .NET Features (Intermediate)
- Advanced Playwright & SDET Scenarios in C#
- Architectural & Strategy Questions
- Practical C# Coding Challenge Example
These questions establish a baseline understanding of the tool and the role.
Answer: Playwright is a modern, open-source automation library from Microsoft for controlling web browsers. It's designed for reliability, speed, and capability.
For a .NET team, Playwright is an excellent choice over Selenium for several key reasons:
- First-Class .NET Support: As a Microsoft product, the Playwright for .NET library is a first-class citizen. It's well-maintained, idiomatic, and integrates smoothly into the .NET ecosystem (e.g., with NUnit, MSTest, and Visual Studio).
- Auto-Waits: This is a major advantage. Playwright automatically waits for elements to be actionable (visible, stable, enabled) before performing an action. This eliminates a huge class of flaky tests common in Selenium that require explicit or fluent waits.
- Speed and Parallelism: The Playwright Test runner (though more integrated in the JS world) and its architecture are built for parallel execution from the ground up. Browser contexts allow for true test isolation, making parallel runs much more reliable.
- Superior Tooling: The Trace Viewer is a revolutionary debugging tool that provides a post-mortem of a test run with a full DOM snapshot, action log, and network requests. This drastically reduces debugging time. Codegen also helps bootstrap tests quickly.
- Unified API: It controls Chromium (Chrome, Edge), Firefox, and WebKit (Safari) with a single, consistent API.
-
Built-in Network Control: Playwright provides powerful, easy-to-use APIs for intercepting, mocking, and stubbing network requests (
Page.RouteAsync
), which is much more complex to achieve in Selenium.
Answer: An SDET (Software Development Engineer in Test) is a developer whose focus is on the testability and quality of the software. They are a hybrid of a software engineer and a QA expert.
Within a C#/.NET team, an SDET is responsible for:
- Building Test Automation Frameworks: Designing, creating, and maintaining robust and scalable test automation frameworks using C# and tools like Playwright, NUnit, or MSTest.
- Writing High-Quality Test Code: The test code is treated as production code. It must be clean, maintainable, and follow C# best practices and patterns (e.g., SOLID, DRY).
-
CI/CD Pipeline Integration: Integrating automated tests into the CI/CD pipeline (e.g., Azure DevOps, GitHub Actions) to provide fast feedback on every code change. This involves using
dotnet test
and publishing test results. - Developing Test Tools: Creating custom tools to support testing, such as test data generation utilities, environment management scripts, or reporting dashboards.
- Quality Advocacy: Championing quality throughout the development lifecycle, participating in code reviews (both for application and test code), and promoting testing best practices like the Test Automation Pyramid.
These questions dive into the specifics of using the Microsoft.Playwright
library.
Q3: Explain the difference between ILocator
and IElementHandle
in Playwright for .NET. When should you use each?
Answer: This is a critical concept for writing modern, reliable Playwright tests.
-
ILocator
(viaPage.Locator(...)
): This is the recommended, modern approach. A locator is an object that represents the recipe for how to find an element. The element is only located on the page at the moment an action is performed (e.g.,await myLocator.ClickAsync()
). This delayed resolution means locators work seamlessly with Playwright's auto-waiting mechanism, making them resilient to dynamic UIs where elements might not be present immediately. -
IElementHandle
(viaPage.QuerySelectorAsync(...)
): This is an older approach. An element handle is a pointer (or handle) to a specific element in the DOM at a specific point in time. It returnsnull
if the element isn't found right away. Using anIElementHandle
can lead toElement is not attached to the DOM
errors if the page updates and the element becomes stale. It bypasses many of Playwright's auto-wait features.
Usage:
-
Always prefer
ILocator
for all standard interactions: clicking, filling, getting text, assertions. - Use
IElementHandle
only for rare, specific cases, such as when you need to pass an element reference into a JavaScript function viaPage.EvaluateAsync()
.
// GOOD: Using ILocator
var submitButton = Page.Locator("#submit-button");
await submitButton.ClickAsync();
// BAD: Prone to race conditions and flakiness
var buttonHandle = await Page.QuerySelectorAsync("#submit-button");
if (buttonHandle != null)
{
await buttonHandle.ClickAsync(); // The element could be stale by this point.
}
Answer:
Playwright for .NET provides excellent integration packages for popular testing frameworks. The most common is Microsoft.Playwright.NUnit
.
Here's how it works:
-
NuGet Packages: You add
Microsoft.Playwright.NUnit
andMicrosoft.Playwright
to your C# test project. -
Base Classes: The integration package provides base classes like
PageTest
andBrowserTest
. By inheriting your test class fromPageTest
, you automatically getPage
,Context
, andBrowser
properties injected and managed for you for each test. -
Lifecycle Management: The framework handles the browser and page lifecycle automatically. It creates a new, isolated context and page for each
[Test]
, ensuring tests don't interfere with each other. This is crucial for parallel execution. -
Configuration: You can configure browser options, headless mode, slow motion, etc., using a
.runsettings
file, which is the standard way to configure test runs in the .NET ecosystem.
Example with NUnit:
using Microsoft.Playwright.NUnit;
using NUnit.Framework;
[Parallelizable(ParallelScope.Self)] // Enable parallel execution
[TestFixture]
public class MyTests : PageTest // Inherit from PageTest
{
[Test]
public async Task MyFirstTest()
{
// The 'Page' property is automatically available here.
await Page.GotoAsync("https://playwright.dev");
await Expect(Page).ToHaveTitleAsync(new Regex("Playwright"));
}
}
Answer:
An IBrowserContext
is an isolated, incognito-like session within a single browser instance.
It's critical for parallel testing because:
- Complete Test Isolation: Each browser context has its own cookies, local storage, and session storage. When you run tests in parallel, each test gets its own context. This prevents a login in one test from affecting another, or data created in one test from leaking into a different one. It's the primary mechanism that makes parallel testing with Playwright so stable.
-
Simulating Multiple Users: In a single test, you can create multiple contexts to simulate different users interacting with the app. For instance, testing a chat feature where
userAContext
sends a message anduserBContext
verifies its receipt. - Efficiency: Creating a new context is much faster and less resource-intensive than launching an entirely new browser instance for every test. This speeds up the overall test suite execution.
These questions test your ability to solve complex automation problems.
Q6: Your team's Playwright test suite is becoming flaky. Describe your process for debugging a flaky test in a .NET environment.
Answer: My process for tackling flakiness is systematic and leverages Playwright's powerful tools:
-
Reproduce and Gather Data: First, I'd try to reproduce the failure. I'd run the specific test multiple times using
dotnet test --filter "FullyQualifiedName~MyFlakyTest" -- NUnit.Repeat=10
. -
Enable and Analyze the Trace: The Playwright Trace Viewer is the most important tool. I'd enable it for failing tests. In a .NET project, this is typically done via a
.runsettings
file or a command-line argument:dotnet test --settings:my.runsettings
wheremy.runsettings
contains:<RunSettings> <Playwright> <Trace>OnFirstRetry</Trace> <!-- Best practice: only trace on failure --> </Playwright> </RunSettings>
The generated trace zip file gives me:
- Timeline/Screenshots: A visual filmstrip of the test execution, allowing me to see exactly what the UI looked like at the moment of failure.
- Action Log: A detailed log of every Playwright action, its parameters, and duration. I can spot long delays or timeouts here.
- Network Tab: A full log of all API requests and responses. This is often the root cause—a failed API call or unexpected data.
- Console Logs: Any errors or logs from the browser's console.
-
Investigate Common Causes: Based on the trace, I'd look for common culprits:
-
Unstable Locators: Is the locator too generic or based on a dynamic ID? I would refactor it to use a more robust, user-facing locator like
Page.GetByRole()
or adata-testid
attribute. -
Timing Issues: Is the application waiting for an API call to finish before enabling a button? Even with auto-waits, sometimes you need to explicitly wait for a specific network response before proceeding. I'd use
await Page.WaitForResponseAsync("**/api/my-data")
. - Race Conditions: Is the test asserting something before the application state has fully updated? The trace will show if an action happened too early.
- Test Data Conflicts: If tests aren't properly isolated, one parallel test might delete or modify data another test depends on.
-
Unstable Locators: Is the locator too generic or based on a dynamic ID? I would refactor it to use a more robust, user-facing locator like
-
Fix and Verify: After implementing a fix (e.g., a better locator, an explicit wait), I'll run the test in a loop again to ensure it is now stable before merging the fix.
Q7: How would you implement an authentication strategy to avoid logging in via the UI for every single test?
Answer: Logging in through the UI for every test is slow and brittle. The best practice is to programmatically log in once and reuse the session state.
My Strategy in C#:
-
Create a Global Setup Fixture (NUnit): I would use NUnit's
[OneTimeSetUp]
attribute in a separateGlobalSetup
class. This code runs once before any tests in the assembly. -
Perform Login and Save State: Inside this setup method:
- Launch a new Playwright instance.
- Create a browser and a new page.
- Navigate to the login page and perform the login using credentials stored securely (e.g., in environment variables or a config file, not hard-coded).
- Wait for a successful login signal (e.g., a dashboard element).
-
Save the state: Use
await context.StorageStateAsync(new() { Path = "storageState.json" })
. This serializes all cookies and local storage into a JSON file. - Cleanly dispose of the browser instance used for setup.
-
Configure Tests to Use the Saved State: In the
.runsettings
file, I'll tell Playwright to use this state for all test contexts..runsettings
file:<RunSettings> <Playwright> <StorageStatePath>storageState.json</StorageStatePath> </Playwright> </RunSettings>
-
Execution Flow:
- NUnit runs the
[OneTimeSetUp]
method, which logs in and createsstorageState.json
. - When the test runner starts executing the individual
[Test]
methods, thePageTest
base class automatically creates new browser contexts. - Because of the
.runsettings
configuration, each new context is automatically hydrated with the authentication state fromstorageState.json
. - Every test now begins in a logged-in state, instantly.
- NUnit runs the
This approach is extremely fast, reliable, and perfectly separates the concern of authentication from the test logic.
Q8: Explain the Page Object Model (POM) and show how you would implement it in C# for a Playwright project.
Answer: The Page Object Model (POM) is a design pattern used to create a repository of objects for the UI elements and interactions of a web page. It's essential for writing maintainable and scalable automation code.
Implementation in C#: You create a class for each page or significant component.
-
Locators: The class has
readonly ILocator
properties for the elements on the page, initialized in the constructor. -
Actions: The class has
public async Task
methods that represent user actions (e.g.,LoginAsync
,SearchForItemAsync
). These methods encapsulate the logic of interacting with the locators.
Example (LoginPage.cs
):
// PageObjects/LoginPage.cs
using Microsoft.Playwright;
using System.Threading.Tasks;
public class LoginPage
{
private readonly IPage _page;
// Locators are defined as readonly properties
public ILocator UsernameInput { get; }
public ILocator PasswordInput { get; }
public ILocator LoginButton { get; }
public ILocator ErrorMessage { get; }
public LoginPage(IPage page)
{
_page = page;
// Use user-facing locators where possible
UsernameInput = _page.GetByLabel("Username");
PasswordInput = _page.GetByLabel("Password");
LoginButton = _page.GetByRole(AriaRole.Button, new() { Name = "Log in" });
ErrorMessage = _page.Locator(".error-message");
}
// Action methods encapsulate user flows
public async Task GotoAsync()
{
await _page.GotoAsync("/login");
}
public async Task LoginAsync(string username, string password)
{
await UsernameInput.FillAsync(username);
await PasswordInput.FillAsync(password);
await LoginButton.ClickAsync();
}
}
Using it in an NUnit Test:
// Tests/LoginTests.cs
using Microsoft.Playwright.NUnit;
using NUnit.Framework;
[TestFixture]
public class LoginTests : PageTest
{
[Test]
public async Task ShouldAllowUserToLogin()
{
var loginPage = new LoginPage(Page); // Instantiate the page object
await loginPage.GotoAsync();
await loginPage.LoginAsync("testuser", "password123");
await Expect(Page).ToHaveURLAsync(new Regex(".*dashboard"));
}
}
Benefits:
- Maintainability: If a locator changes, you only update it in the Page Object class, not in dozens of tests.
- Readability & Reusability: Tests become clean, descriptive, and focus on what the user is doing, not how. The login logic can be reused in any test that requires a logged-in user.
These questions assess your ability to design a testing strategy.
Q9: You're setting up a new Playwright for .NET framework from scratch. Describe the project structure and key components.
Answer: I would structure the Visual Studio solution to be scalable, maintainable, and easy for other developers to contribute to.
-
Solution & Project Structure:
- A single Visual Studio Solution (
.sln
). - A C# Test Project (
.csproj
) for the tests (e.g.,MyCompany.WebApp.Tests
). -
Directory Structure inside the project:
-
/Tests
: Contains all test files (*Tests.cs
), organized by feature (e.g.,/Tests/Login/
,/Tests/Checkout/
). -
/PageObjects
: Contains all Page Object Model classes. -
/Utils
or/Helpers
: For shared utilities like API clients, test data generators, or custom assertion helpers. -
/Fixtures
or/Setup
: For setup classes, like theGlobalSetup.cs
for authentication. -
/TestData
: For static test data files (e.g.,users.json
).
-
- A single Visual Studio Solution (
-
Configuration (
.runsettings
file):- This central file would control the test run.
- Define
BaseURL
,Headless
mode,SlowMo
,Trace
settings. - Specify the
StorageStatePath
for authentication. - Define browser-specific settings for cross-browser runs if needed.
-
Core Components:
- Page Objects (POM): The foundation for all UI interaction.
-
API Client Helper: A C# class that uses
IAPIRequestContext
(Playwright.APIRequest
) to interact with the application's backend API. This is crucial for test setup (e.g., creating a user before a test) and cleanup (deleting test data). - Test Data Management: A strategy for handling test data. For simple cases, static JSON files. For complex needs, a helper class that can generate realistic data on the fly using libraries like Bogus.
-
Environment Management: Use
.runsettings
profiles or environment variables to easily switch betweenlocal
,dev
, andstaging
environments (e.g., different URLs, different credentials).
-
CI/CD Integration (e.g.,
azure-pipelines.yml
or GitHub Actions):- A pipeline that triggers on pull requests.
- YAML steps would include:
dotnet restore
dotnet build
dotnet test --settings:my.runsettings
- A step to publish the test results (
.trx
file) and upload the Playwright Trace files as build artifacts on failure.
This structure provides clear separation of concerns and is built for long-term maintainability.
Interviewer: "Let's write some code. Using Playwright with NUnit, write a test for a login page. The test should verify that a user with valid credentials can log in, and a user with invalid credentials sees an error message. Please use the Page Object Model."
Your Solution:
-
Page Object (
LoginPage.cs
):// PageObjects/LoginPage.cs using Microsoft.Playwright; using System.Threading.Tasks; public class LoginPage { private readonly IPage _page; public ILocator UsernameInput { get; } public ILocator PasswordInput { get; } public ILocator LoginButton { get; } public ILocator ErrorMessage { get; } public LoginPage(IPage page) { _page = page; UsernameInput = _page.GetByPlaceholder("Enter username"); PasswordInput = _page.GetByPlaceholder("Enter password"); LoginButton = _page.GetByRole(AriaRole.Button, new() { Name = "Login" }); ErrorMessage = _page.Locator("div.error-message"); } public async Task GotoAsync() { await _page.GotoAsync("https://myapp.com/login"); } public async Task LoginAsync(string username, string password) { await UsernameInput.FillAsync(username); await PasswordInput.FillAsync(password); await LoginButton.ClickAsync(); } }
-
Test Class (
LoginTests.cs
):// Tests/LoginTests.cs using Microsoft.Playwright.NUnit; using NUnit.Framework; using System.Text.RegularExpressions; using System.Threading.Tasks; [TestFixture] public class LoginTests : PageTest { [Test(Description = "A valid user should be redirected to the dashboard.")] public async Task ShouldAllowLoginWithValidCredentials() { var loginPage = new LoginPage(Page); await loginPage.GotoAsync(); await loginPage.LoginAsync("testuser", "ValidPassword123!"); // Use Playwright's built-in assertions (Expect) for robustness await Expect(Page).ToHaveURLAsync(new Regex(".*dashboard")); await Expect(loginPage.LoginButton).Not.ToBeVisibleAsync(); } [Test(Description = "An invalid user should see an error message.")] public async Task ShouldShowErrorWithInvalidCredentials() { var loginPage = new LoginPage(Page); await loginPage.GotoAsync(); await loginPage.LoginAsync("invaliduser", "wrong-password"); // Assert that the error message appears and has the correct text await Expect(loginPage.ErrorMessage).ToBeVisibleAsync(); await Expect(loginPage.ErrorMessage).ToContainTextAsync("Invalid username or password"); // Assert we are still on the login page await Expect(Page).Not.ToHaveURLAsync(new Regex(".*dashboard")); } }
This solution demonstrates a strong command of C#, NUnit, the POM pattern, and modern Playwright for .NET best practices like using Expect
and robust locators.