Live Coding in SDET Interviews: 10 Exercises With Step-by-Step Walkthrough Solutions
Contents
Live Coding in SDET Interviews: 10 Exercises With Step-by-Step Walkthrough Solutions
The SDET interview has changed dramatically. In 2026, the live coding round is not about reversing linked lists or balancing binary trees. It is about demonstrating that you can build reliable, maintainable test automation under pressure. Interviewers hand you a Playwright project, describe a testing scenario, and watch you code a solution in real time. They evaluate your framework design instincts, your API fluency, your debugging approach, and most importantly, how you think about test reliability and maintainability.
🎠Want to master this with real projects? Join the Playwright Automation Mastery course at The Testing Academy.
This article presents 10 real SDET interview exercises that are actively used by companies hiring SDETs in 2026. Each exercise includes the problem statement as the interviewer would present it, the recommended approach and thought process, the complete Playwright TypeScript solution, and a line-by-line explanation of why each decision matters. Practice these exercises until the patterns are second nature, and you will walk into your next interview with confidence.
Exercise 1: Write a Login Test With Page Object Model
Problem Statement: “Write a test that validates the login flow for our application. The login page has email and password fields and a submit button. After successful login, the user should see a dashboard with their name displayed. Use the Page Object Model pattern.”
Approach: Start by creating the page object class before writing the test. This shows the interviewer you think about structure first. Define locators as private properties, expose actions as public methods, and keep assertions in the test file rather than the page object. This separation of concerns is what interviewers look for.
// pages/LoginPage.ts
import { type Page, type Locator } from '@playwright/test';
export class LoginPage {
private readonly emailInput: Locator;
private readonly passwordInput: Locator;
private readonly submitButton: Locator;
readonly errorMessage: Locator;
constructor(private page: Page) {
this.emailInput = page.getByLabel('Email');
this.passwordInput = page.getByLabel('Password');
this.submitButton = page.getByRole('button', { name: 'Sign In' });
this.errorMessage = page.getByRole('alert');
}
async goto() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
}
// pages/DashboardPage.ts
import { type Page, type Locator } from '@playwright/test';
export class DashboardPage {
readonly welcomeMessage: Locator;
readonly userName: Locator;
constructor(private page: Page) {
this.welcomeMessage = page.getByRole('heading', { name: /welcome/i });
this.userName = page.getByTestId('user-display-name');
}
}
// tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { DashboardPage } from '../pages/DashboardPage';
test.describe('Login Flow', () => {
let loginPage: LoginPage;
let dashboardPage: DashboardPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
dashboardPage = new DashboardPage(page);
await loginPage.goto();
});
test('successful login displays user dashboard', async ({ page }) => {
await loginPage.login('testuser@example.com', 'SecurePass123');
await expect(page).toHaveURL(/.*dashboard/);
await expect(dashboardPage.welcomeMessage).toBeVisible();
await expect(dashboardPage.userName).toHaveText('Test User');
});
test('invalid credentials show error message', async () => {
await loginPage.login('wrong@example.com', 'wrongpassword');
await expect(loginPage.errorMessage).toBeVisible();
await expect(loginPage.errorMessage).toContainText('Invalid credentials');
});
});
Why each line matters: The locators use semantic selectors (getByLabel, getByRole, getByTestId) rather than CSS selectors. This is intentional and interviewers notice. Semantic locators are resilient to styling changes, follow accessibility best practices, and make the test self-documenting. The page object constructor receives the page instance and stores it as a private property. Locators are defined as class-level properties initialized in the constructor, not created on every method call. The errorMessage locator is public (using readonly) because the test needs to assert against it directly. The beforeEach hook creates fresh page object instances for each test, ensuring test isolation.
Exercise 2: Mock an API Response
Problem Statement: “The dashboard displays a list of user notifications fetched from the API endpoint GET /api/notifications. Mock this endpoint to return a specific set of notifications and verify the UI displays them correctly. Also test the empty state when no notifications exist.”
Approach: Use page.route() to intercept the API call and return controlled data. This eliminates external dependencies and makes the test deterministic. Create separate test cases for the populated state and empty state.
import { test, expect } from '@playwright/test';
const mockNotifications = [
{ id: 1, title: 'New message from Alice', read: false, timestamp: '2026-07-10T10:00:00Z' },
{ id: 2, title: 'Deployment completed', read: true, timestamp: '2026-07-10T09:30:00Z' },
{ id: 3, title: 'Code review requested', read: false, timestamp: '2026-07-10T09:00:00Z' },
];
test.describe('Notification Dashboard', () => {
test('displays notifications from API', async ({ page }) => {
// Intercept the API call and return mock data
await page.route('**/api/notifications', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockNotifications),
});
});
await page.goto('/dashboard');
// Verify the correct number of notifications rendered
const notificationItems = page.getByRole('listitem');
await expect(notificationItems).toHaveCount(3);
// Verify content of the first notification
await expect(notificationItems.first()).toContainText('New message from Alice');
// Verify unread indicator is shown for unread notifications
const unreadBadges = page.getByTestId('unread-badge');
await expect(unreadBadges).toHaveCount(2);
});
test('displays empty state when no notifications', async ({ page }) => {
await page.route('**/api/notifications', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([]),
});
});
await page.goto('/dashboard');
await expect(page.getByText('No notifications')).toBeVisible();
await expect(page.getByRole('listitem')).toHaveCount(0);
});
test('handles API error gracefully', async ({ page }) => {
await page.route('**/api/notifications', async (route) => {
await route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Internal Server Error' }),
});
});
await page.goto('/dashboard');
await expect(page.getByText(/unable to load notifications/i)).toBeVisible();
await expect(page.getByRole('button', { name: /retry/i })).toBeVisible();
});
});
Why each line matters: The page.route() call uses a glob pattern (**/api/notifications) rather than an exact URL, making it resilient to base URL changes between environments. The mock data is defined outside the test as a constant, making it easy to reference in assertions and modify for different scenarios. The error handling test demonstrates defensive testing thinking: interviewers specifically look for candidates who test failure scenarios without being prompted. The route.fulfill() method returns a complete HTTP response, including status code and content type, which shows attention to detail.
Exercise 3: Handle File Upload
Problem Statement: “Write a test that uploads a profile picture through the settings page. The upload input accepts JPG and PNG files up to 5MB. Verify the preview updates after upload.”
import { test, expect } from '@playwright/test';
import path from 'path';
test.describe('Profile Picture Upload', () => {
test('uploads and previews a valid image', async ({ page }) => {
await page.goto('/settings/profile');
// Locate the file input (may be hidden behind a styled button)
const fileInput = page.locator('input[type="file"]');
// Upload the file using setInputFiles
await fileInput.setInputFiles(path.join(__dirname, '../fixtures/profile-pic.jpg'));
// Verify the preview image updates
const previewImage = page.getByAltText('Profile preview');
await expect(previewImage).toBeVisible();
// Verify the image source changed from the default placeholder
await expect(previewImage).not.toHaveAttribute('src', /placeholder/);
// Verify the upload success feedback
await expect(page.getByText(/image uploaded successfully/i)).toBeVisible();
});
test('rejects file exceeding size limit', async ({ page }) => {
await page.goto('/settings/profile');
const fileInput = page.locator('input[type="file"]');
// Upload an oversized file
await fileInput.setInputFiles(path.join(__dirname, '../fixtures/large-image-6mb.jpg'));
// Verify the error message appears
await expect(page.getByText(/file size exceeds/i)).toBeVisible();
// Verify the preview did NOT update (still shows placeholder)
const previewImage = page.getByAltText('Profile preview');
await expect(previewImage).toHaveAttribute('src', /placeholder/);
});
test('rejects unsupported file format', async ({ page }) => {
await page.goto('/settings/profile');
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles(path.join(__dirname, '../fixtures/document.pdf'));
await expect(page.getByText(/unsupported file format/i)).toBeVisible();
});
});
Why each line matters: The setInputFiles() method is Playwright’s native API for file uploads, bypassing the need to interact with OS file dialogs. Using path.join(__dirname, ...) creates a path relative to the test file, which works across operating systems and CI environments. The test verifies both the positive case (preview updates) and negative cases (oversized file, wrong format), which demonstrates thoroughness. The assertion not.toHaveAttribute('src', /placeholder/) checks that the image actually changed rather than just checking it is visible, catching a subtle class of bugs where the upload appears to succeed but the preview does not update.
Exercise 4: Test an Iframe Interaction
Problem Statement: “The payment page embeds a Stripe payment form in an iframe. Write a test that fills in the card details within the iframe and submits the payment.”
import { test, expect } from '@playwright/test';
test('completes payment through Stripe iframe', async ({ page }) => {
await page.goto('/checkout/payment');
// Access the Stripe iframe using frameLocator
const stripeFrame = page.frameLocator('iframe[name="stripe-card-element"]');
// Fill card number within the iframe context
await stripeFrame.getByPlaceholder('Card number').fill('4242424242424242');
await stripeFrame.getByPlaceholder('MM / YY').fill('12/28');
await stripeFrame.getByPlaceholder('CVC').fill('123');
await stripeFrame.getByPlaceholder('ZIP').fill('10001');
// The submit button is in the parent page, not the iframe
await page.getByRole('button', { name: 'Pay Now' }).click();
// Wait for the payment processing and redirect
await expect(page).toHaveURL(/.*order-confirmation/, { timeout: 15000 });
await expect(page.getByText(/payment successful/i)).toBeVisible();
});
Why each line matters: The frameLocator() method creates a scoped locator context that searches within the iframe’s DOM. This is cleaner than Selenium’s switchTo().frame() approach because you do not need to switch back to the parent context afterward. The iframe selector uses an attribute selector on the name attribute, which is more stable than index-based iframe selection. The submit button assertion shows understanding that the button is in the parent page scope, not the iframe. The extended timeout of 15000ms on the URL assertion accounts for real payment processing time, which is a practical consideration interviewers appreciate.
Exercise 5: Write a Custom Fixture for Authentication
Problem Statement: “Many of our tests require an authenticated user. Write a custom Playwright fixture that provides an authenticated page context so tests start already logged in, without repeating the login flow in every test.”
// fixtures/auth.fixture.ts
import { test as base, expect } from '@playwright/test';
type AuthFixtures = {
authenticatedPage: ReturnType<typeof base.extend> extends infer T ? T : never;
};
// Create a fixture that provides an already-authenticated page
export const test = base.extend<{ authenticatedPage: import('@playwright/test').Page }>( {
authenticatedPage: async ({ browser }, use) => {
// Create a new browser context
const context = await browser.newContext();
const page = await context.newPage();
// Perform login once
await page.goto('/login');
await page.getByLabel('Email').fill('testuser@example.com');
await page.getByLabel('Password').fill('SecurePass123');
await page.getByRole('button', { name: 'Sign In' }).click();
// Wait for authentication to complete
await expect(page).toHaveURL(/.*dashboard/);
// Save the storage state for potential reuse
await context.storageState({ path: '.auth/user.json' });
// Provide the authenticated page to the test
await use(page);
// Cleanup after the test
await context.close();
},
});
export { expect } from '@playwright/test';
// tests/dashboard.spec.ts
import { test, expect } from '../fixtures/auth.fixture';
test('authenticated user sees personalized content', async ({ authenticatedPage }) => {
// No login needed - the page is already authenticated
await authenticatedPage.goto('/dashboard');
await expect(authenticatedPage.getByTestId('user-display-name')).toHaveText('Test User');
});
test('authenticated user can access settings', async ({ authenticatedPage }) => {
await authenticatedPage.goto('/settings');
await expect(authenticatedPage.getByRole('heading', { name: 'Account Settings' })).toBeVisible();
});
Why each line matters: The fixture extends Playwright’s base test object with a custom authenticatedPage fixture. The use(page) call is the fixture lifecycle hook: everything before use is setup, and everything after is teardown. Creating a new browser context ensures test isolation (each test gets its own cookies and storage). The storageState() call saves authentication state to a file, which can be reused by other fixtures or tests for faster authentication. The explicit context.close() in teardown prevents resource leaks. This pattern demonstrates advanced Playwright knowledge that sets senior SDET candidates apart from mid-level ones.
🚀 Level Up Your Playwright
From locators to CI pipelines — build a production-grade Playwright + TypeScript framework step by step.
Exercise 6: Create a Data-Driven Test From JSON
Problem Statement: “Write a data-driven test that validates form submission with multiple data sets. The test data should come from a JSON file. Each data set should be a separate test case with clear naming.”
// fixtures/registration-data.json
[
{
"scenario": "valid registration with all fields",
"data": { "name": "John Doe", "email": "john@example.com", "age": "30", "country": "US" },
"expectedResult": "success"
},
{
"scenario": "registration with minimum required fields",
"data": { "name": "Jane", "email": "jane@test.com", "age": "", "country": "" },
"expectedResult": "success"
},
{
"scenario": "registration with invalid email format",
"data": { "name": "Bob", "email": "not-an-email", "age": "25", "country": "UK" },
"expectedResult": "error",
"expectedError": "Please enter a valid email address"
},
{
"scenario": "registration with underage user",
"data": { "name": "Minor User", "email": "minor@test.com", "age": "12", "country": "US" },
"expectedResult": "error",
"expectedError": "You must be at least 18 years old"
}
]
// tests/registration.spec.ts
import { test, expect } from '@playwright/test';
import registrationData from '../fixtures/registration-data.json';
for (const testCase of registrationData) {
test(`Registration: ${testCase.scenario}`, async ({ page }) => {
await page.goto('/register');
// Fill in the form fields from the test data
await page.getByLabel('Full Name').fill(testCase.data.name);
await page.getByLabel('Email Address').fill(testCase.data.email);
if (testCase.data.age) {
await page.getByLabel('Age').fill(testCase.data.age);
}
if (testCase.data.country) {
await page.getByLabel('Country').selectOption(testCase.data.country);
}
// Submit the form
await page.getByRole('button', { name: 'Register' }).click();
// Assert based on expected result
if (testCase.expectedResult === 'success') {
await expect(page.getByText(/registration successful/i)).toBeVisible();
await expect(page).toHaveURL(/.*welcome/);
} else {
await expect(page.getByRole('alert')).toContainText(testCase.expectedError!);
await expect(page).toHaveURL(/.*register/);
}
});
}
Why each line matters: The for...of loop generates separate test cases for each data set, so Playwright’s test runner treats them as independent tests with individual pass/fail status. The template literal in the test name (${testCase.scenario}) creates descriptive test names that appear in reports and make it easy to identify which scenario failed. The conditional field filling (if (testCase.data.age)) handles optional fields gracefully without throwing errors on empty values. The assertion branching based on expectedResult keeps the test logic clean while supporting both positive and negative scenarios from the same data structure. This pattern is infinitely extensible: adding new test scenarios requires only adding JSON entries, not writing new test code.
Exercise 7: Debug a Flaky Test From Trace
Problem Statement: “Here is a test that passes locally but fails intermittently in CI. The trace file from the last failure is available. Identify the root cause and fix the test.”
Approach: This is a debugging exercise, not a writing exercise. Walk the interviewer through your systematic debugging process. Open the trace in the Playwright Trace Viewer, examine the timeline for timing gaps, check network requests for slow responses, inspect DOM snapshots at the point of failure, and look for race conditions between animations and assertions.
// ORIGINAL FLAKY TEST
test('shows success toast after saving settings', async ({ page }) => {
await page.goto('/settings');
await page.getByLabel('Display Name').fill('New Name');
await page.getByRole('button', { name: 'Save' }).click();
// THIS LINE FAILS INTERMITTENTLY
await expect(page.getByText('Settings saved')).toBeVisible();
});
// ANALYSIS FROM TRACE:
// 1. The Save button triggers an API call
// 2. The toast notification appears AFTER the API response
// 3. The API response time varies: 50ms locally, 200-800ms in CI
// 4. The toast has a fade-in animation of 300ms
// 5. The assertion runs before the toast fully appears
// FIXED TEST
test('shows success toast after saving settings', async ({ page }) => {
await page.goto('/settings');
await page.getByLabel('Display Name').fill('New Name');
// Wait for the API response to complete after clicking Save
const saveResponsePromise = page.waitForResponse(
(response) => response.url().includes('/api/settings') && response.status() === 200
);
await page.getByRole('button', { name: 'Save' }).click();
await saveResponsePromise;
// Now assert the toast - Playwright's auto-wait handles the animation
await expect(page.getByText('Settings saved')).toBeVisible({ timeout: 5000 });
});
Why each line matters: The fix uses waitForResponse() to explicitly wait for the API call to complete before asserting the UI state. This is the canonical fix for the most common cause of flaky tests: a race condition between an asynchronous operation and a UI assertion. The waitForResponse promise is created before the click (so it does not miss the response) and awaited after the click. The increased timeout on the visibility assertion accounts for the animation duration. During the interview, explain this thought process: identify the asynchronous gap, add explicit synchronization, and verify the fix addresses the root cause rather than masking it with a sleep.
Exercise 8: Validate API Response Schema
Problem Statement: “Write a test that validates the API response from GET /api/users/me matches the expected schema. Verify the response structure, data types, and required fields.”
import { test, expect } from '@playwright/test';
test.describe('User API Schema Validation', () => {
test('GET /api/users/me returns valid user schema', async ({ request }) => {
const response = await request.get('/api/users/me');
// Verify response status
expect(response.status()).toBe(200);
expect(response.headers()['content-type']).toContain('application/json');
const body = await response.json();
// Validate required top-level fields exist
expect(body).toHaveProperty('id');
expect(body).toHaveProperty('email');
expect(body).toHaveProperty('name');
expect(body).toHaveProperty('createdAt');
expect(body).toHaveProperty('role');
// Validate data types
expect(typeof body.id).toBe('string');
expect(typeof body.email).toBe('string');
expect(typeof body.name).toBe('string');
expect(typeof body.role).toBe('string');
// Validate email format using regex
expect(body.email).toMatch(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
// Validate date format (ISO 8601)
expect(new Date(body.createdAt).toISOString()).toBe(body.createdAt);
// Validate role is one of allowed values
expect(['admin', 'editor', 'viewer']).toContain(body.role);
// Validate nested object structure if present
if (body.preferences) {
expect(body.preferences).toHaveProperty('theme');
expect(body.preferences).toHaveProperty('language');
expect(['light', 'dark', 'system']).toContain(body.preferences.theme);
}
});
test('GET /api/users/me returns 401 without auth', async ({ request }) => {
// Create an unauthenticated request context
const response = await request.get('/api/users/me', {
headers: { 'Authorization': '' },
});
expect(response.status()).toBe(401);
const body = await response.json();
expect(body).toHaveProperty('error');
expect(body.error).toContain('unauthorized');
});
});
Why each line matters: The test uses Playwright’s built-in request fixture for API testing, which is lighter than launching a browser. Schema validation checks both the presence of fields (toHaveProperty) and their types (typeof). The email regex validation and ISO 8601 date check demonstrate data quality verification beyond simple type checking. The role validation using toContain against an array of allowed values shows understanding of enum-like fields. The conditional check for preferences handles optional nested objects without failing when the field is absent. The 401 test for unauthenticated access shows security awareness that interviewers value highly.
Exercise 9: Implement Retry Logic for a Network-Dependent Test
Problem Statement: “Write a test for a real-time dashboard that fetches data via WebSocket. The connection may take a variable amount of time to establish. Implement a robust waiting strategy that does not use arbitrary sleep timers.”
import { test, expect } from '@playwright/test';
test('real-time dashboard displays live data after WebSocket connection', async ({ page }) => {
await page.goto('/dashboard/real-time');
// Wait for WebSocket connection indicator
await expect(page.getByTestId('connection-status')).toHaveText('Connected', {
timeout: 10000,
});
// Wait for the first data point to appear
// Use polling assertion instead of a fixed sleep
await expect(async () => {
const dataPoints = page.getByTestId('data-point');
const count = await dataPoints.count();
expect(count).toBeGreaterThan(0);
}).toPass({
intervals: [500, 1000, 2000],
timeout: 15000,
});
// Verify data freshness - the timestamp should be recent
const timestampText = await page.getByTestId('last-updated').textContent();
const lastUpdated = new Date(timestampText!);
const now = new Date();
const diffSeconds = (now.getTime() - lastUpdated.getTime()) / 1000;
expect(diffSeconds).toBeLessThan(30);
// Verify data structure in the dashboard
const firstDataPoint = page.getByTestId('data-point').first();
await expect(firstDataPoint.getByTestId('value')).toBeVisible();
await expect(firstDataPoint.getByTestId('label')).toBeVisible();
});
test('dashboard handles WebSocket disconnection gracefully', async ({ page }) => {
await page.goto('/dashboard/real-time');
// Wait for connection
await expect(page.getByTestId('connection-status')).toHaveText('Connected', {
timeout: 10000,
});
// Simulate network disconnection by going offline
await page.context().setOffline(true);
// Verify disconnection UI feedback
await expect(page.getByTestId('connection-status')).toHaveText('Disconnected', {
timeout: 5000,
});
// Verify reconnection attempt message
await expect(page.getByText(/reconnecting/i)).toBeVisible();
// Restore network and verify automatic reconnection
await page.context().setOffline(false);
await expect(page.getByTestId('connection-status')).toHaveText('Connected', {
timeout: 15000,
});
});
Why each line matters: The expect().toPass() pattern is Playwright’s polling assertion mechanism, which retries the assertion block at specified intervals until it passes or times out. This replaces arbitrary sleep() calls with intelligent polling. The intervals: [500, 1000, 2000] configuration uses exponential backoff, starting with fast checks and slowing down to reduce resource usage. The data freshness check validates that the timestamp is within 30 seconds of the current time, ensuring the data is actually live. The disconnection test uses context.setOffline(true) to simulate network failure, which is a powerful Playwright feature that interviewers specifically test for. This exercise demonstrates mastery of asynchronous test patterns that are critical for real-world SDET work.
Exercise 10: Write a Parallel-Safe Test With Isolated Data
Problem Statement: “Write a test that creates a new project, verifies it appears in the project list, and deletes it. This test must be safe to run in parallel with other tests that also create projects. No test should interfere with another test’s data.”
import { test, expect } from '@playwright/test';
// Generate a unique project name for each test run using test info
test('creates, verifies, and deletes a project', async ({ page }, testInfo) => {
// Create a unique project name using worker index and timestamp
const uniqueProjectName = `Test Project ${testInfo.workerIndex}-${Date.now()}`;
// Navigate to project creation
await page.goto('/projects/new');
// Fill in project details with unique data
await page.getByLabel('Project Name').fill(uniqueProjectName);
await page.getByLabel('Description').fill(`Automated test project created by worker ${testInfo.workerIndex}`);
await page.getByRole('button', { name: 'Create Project' }).click();
// Verify redirect to the new project page
await expect(page.getByRole('heading', { name: uniqueProjectName })).toBeVisible();
// Navigate to project list and verify our project appears
await page.goto('/projects');
// Use the unique name to find our specific project
const projectRow = page.getByRole('row').filter({ hasText: uniqueProjectName });
await expect(projectRow).toBeVisible();
// Clean up: delete the project we created
await projectRow.getByRole('button', { name: 'Delete' }).click();
// Confirm deletion in the dialog
await page.getByRole('dialog').getByRole('button', { name: 'Confirm Delete' }).click();
// Verify the project is removed from the list
await expect(projectRow).not.toBeVisible();
});
// Alternative approach using API for setup and teardown
test('verifies project details page (API-driven data)', async ({ page, request }) => {
const uniqueName = `API Project ${Date.now()}`;
// Create test data via API (faster and more reliable than UI)
const createResponse = await request.post('/api/projects', {
data: {
name: uniqueName,
description: 'Created via API for testing',
},
});
expect(createResponse.status()).toBe(201);
const project = await createResponse.json();
// Test the UI with the pre-created data
await page.goto(`/projects/${project.id}`);
await expect(page.getByRole('heading', { name: uniqueName })).toBeVisible();
await expect(page.getByText('Created via API for testing')).toBeVisible();
// Cleanup via API (faster and does not depend on UI delete working)
const deleteResponse = await request.delete(`/api/projects/${project.id}`);
expect(deleteResponse.status()).toBe(204);
});
Why each line matters: The testInfo.workerIndex combined with Date.now() creates a project name that is unique across parallel workers and across multiple runs. This eliminates data collisions entirely. The filter({ hasText: uniqueProjectName }) method on locators ensures the test only interacts with its own data, even when the project list contains projects from parallel tests. The cleanup step inside the test ensures that test data does not accumulate, even if the test is run hundreds of times. The alternative API-driven approach demonstrates a best practice where tests use APIs for data setup and teardown while testing the UI for the actual feature under test. This separation makes tests faster and more reliable. Interviewers view this pattern as a signal of senior-level thinking about test architecture.
Interview Tips: Beyond the Code
The code is only half of what interviewers evaluate during live coding exercises. The other half is your process. Talk through your thinking: Explain why you chose a particular locator strategy, why you structured the test a certain way, and what edge cases you are considering. Silence during coding makes interviewers nervous. Start with the happy path: Get a working test first, then add edge cases and error handling. Do not try to write the perfect test on the first pass. Ask clarifying questions: “Should I use the Page Object Model?” “Should the test data be hardcoded or externalized?” These questions show you think about design, not just implementation. Handle mistakes gracefully: If your code has a bug, debug it out loud. Walk through the logic, identify the issue, and fix it. This debugging process is often more impressive than perfect code written in silence.
Master these 10 exercises and the patterns they represent, and you will be prepared for any live coding challenge in an SDET interview. The specific application may differ, but the patterns of page objects, API mocking, fixtures, data-driven testing, debugging, and parallel safety apply universally.
Frequently Asked Questions
Below are answers to the most common questions about live coding exercises in SDET interviews.
🎓 Master Playwright End to End
Join hundreds of SDETs building real automation frameworks. Lifetime access, hands-on projects, and a job-ready portfolio.
