The SDET Take-Home Assignment: Build a Portfolio Project That Gets You Hired
Contents
Introduction: Your Portfolio Is Your Resume Now
In 2026, the SDET job market has fundamentally shifted. Resumes listing years of experience and tool names are no longer enough to stand out. Hiring managers are drowning in applications from candidates who all claim to know Playwright, Selenium, and CI/CD. What separates the candidates who get interviews from those who get rejected is evidence: a portfolio project that demonstrates not just what tools you know, but how you think about test architecture, code quality, and engineering practices.
🎠Want to master this with real projects? Join the Playwright Automation Mastery course at The Testing Academy.
A well-crafted portfolio project is worth more than ten years of resume bullet points because it shows rather than tells. It demonstrates your coding ability, your architectural thinking, your CI/CD skills, your documentation practices, and your understanding of modern test automation patterns. When a hiring manager opens your GitHub repository and sees a clean, well-documented Playwright framework with passing CI badges and Allure reports, they immediately know you are a serious engineer.
In this guide, I will walk you through building a complete SDET portfolio project from scratch in one weekend. I will show you exactly what hiring managers look for, provide complete code examples for every component, highlight the common mistakes that kill applications, and give you a before-and-after resume comparison that shows how to leverage your portfolio for maximum impact.
What Hiring Managers Actually Look For
I have been on both sides of the SDET hiring process, and I can tell you that most candidates miss what actually matters. Hiring managers are not looking for the most complex or feature-rich framework. They are looking for evidence of engineering maturity. Here is what they evaluate in order of importance:
- Code quality and readability. Can you write clean, well-structured code that another engineer can understand without explanation? This includes naming conventions, file organization, consistent style, and appropriate abstractions.
- CI/CD integration. Does your project have a working pipeline? A green CI badge on your README immediately signals that you understand the full test automation lifecycle, not just writing tests locally.
- Meaningful assertions. Do your tests validate business logic or just check that elements exist? Tests that assert specific text content, data values, and state transitions demonstrate deeper understanding than tests that only verify element visibility.
- Documentation. Does your README explain the architecture, setup instructions, and design decisions? Engineers who document their work are engineers who can communicate with teams.
- Test design variety. Do you test happy paths, error scenarios, edge cases, and cross-browser? A portfolio that only tests the golden path suggests limited testing thinking.
The Ideal Project Structure
Here is the complete project structure for a portfolio-ready Playwright TypeScript framework testing SauceDemo (a free public test application). This structure demonstrates all the patterns hiring managers want to see:
sdet-portfolio-project/
|-- .github/
| |-- workflows/
| |-- playwright.yml # GitHub Actions CI pipeline
|-- tests/
| |-- ui/
| | |-- login.spec.ts # Login feature tests
| | |-- inventory.spec.ts # Product listing tests
| | |-- cart.spec.ts # Shopping cart tests
| | |-- checkout.spec.ts # Checkout flow tests
| |-- api/
| |-- products.api.spec.ts # API tests for products
| |-- auth.api.spec.ts # API tests for authentication
|-- pages/
| |-- LoginPage.ts # Login page object
| |-- InventoryPage.ts # Inventory page object
| |-- CartPage.ts # Cart page object
| |-- CheckoutPage.ts # Checkout page object
| |-- components/
| |-- Header.ts # Header component
| |-- ProductCard.ts # Product card component
|-- fixtures/
| |-- auth.fixture.ts # Authentication fixture
| |-- test-data.fixture.ts # Test data fixture
|-- utils/
| |-- test-data.ts # Test data generators
|-- playwright.config.ts # Playwright configuration
|-- package.json
|-- README.md # Project documentation
|-- tsconfig.json
The Complete playwright.config.ts
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 4 : undefined,
reporter: [
['html', { open: 'never' }],
['allure-playwright'],
['list'],
],
use: {
baseURL: 'https://www.saucedemo.com',
trace: 'retain-on-failure',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
],
});
Sample Page Object: LoginPage.ts
// pages/LoginPage.ts
import { type Page, type Locator, 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;
this.usernameInput = page.getByPlaceholder('Username');
this.passwordInput = page.getByPlaceholder('Password');
this.loginButton = page.getByRole('button', { name: 'Login' });
this.errorMessage = page.getByTestId('error');
}
async goto() {
await this.page.goto('/');
}
async login(username: string, password: string) {
await this.usernameInput.fill(username);
await this.passwordInput.fill(password);
await this.loginButton.click();
}
async expectError(message: string) {
await expect(this.errorMessage).toContainText(message);
}
async expectLoginSuccess() {
await expect(this.page).toHaveURL(/inventory/);
}
}
Sample Test File: login.spec.ts
// tests/ui/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../../pages/LoginPage';
test.describe('Login Feature', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
});
test('successful login with valid credentials', async () => {
await loginPage.login('standard_user', 'secret_sauce');
await loginPage.expectLoginSuccess();
});
test('displays error for locked out user', async () => {
await loginPage.login('locked_out_user', 'secret_sauce');
await loginPage.expectError('locked out');
});
test('displays error for invalid credentials', async () => {
await loginPage.login('invalid_user', 'wrong_password');
await loginPage.expectError('Username and password do not match');
});
test('displays error for empty username', async () => {
await loginPage.login('', 'secret_sauce');
await loginPage.expectError('Username is required');
});
test('displays error for empty password', async () => {
await loginPage.login('standard_user', '');
await loginPage.expectError('Password is required');
});
});
GitHub Actions CI Pipeline
# .github/workflows/playwright.yml
name: Playwright Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: '0 6 * * 1-5' # Weekdays at 6 AM UTC
jobs:
test:
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
- name: Upload Playwright Report
uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 30
- name: Upload Allure Results
uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: allure-results
path: allure-results/
retention-days: 30
