Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions tests/specs/test-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Test Plan

## Application

Sample Web App — a React SPA with login, dashboard (stat cards, activity table, todo list), and navigation. The `feature/todo-persistence-filter` branch adds todo CRUD, localStorage persistence, filtering (All/Active/Completed), and a "Clear completed" action to the dashboard.

## Suites

### Todo CRUD

1. **Add, toggle, and delete a todo** — `tests/todo-crud.spec.ts`
- Preconditions: Authenticated user on `/dashboard`; localStorage cleared so default todos load
- Step/Expectation Pairs:
1. Step: Observe the default todo list
Expectation: 3 default items visible; "Deploy to staging" is checked; count shows "2 items left"
2. Step: Type "Run CI pipeline" in the new-todo input and press Enter
Expectation: "Run CI pipeline" appears as the 4th item (unchecked); count shows "3 items left"; input is cleared
3. Step: Check the "Review pull requests" checkbox
Expectation: Checkbox becomes checked; count decreases to "2 items left"
4. Step: Uncheck the "Review pull requests" checkbox
Expectation: Checkbox becomes unchecked; count returns to "3 items left"
5. Step: Click the delete button (×) on "Run CI pipeline"
Expectation: "Run CI pipeline" is removed from the list; count shows "2 items left"

### Todo Filtering & Clear Completed

2. **Filter todos by status** — `tests/todo-filter.spec.ts`
- Preconditions: Authenticated user on `/dashboard`; localStorage cleared so default todos load (1 completed, 2 active)
- Step/Expectation Pairs:
1. Step: Observe the "All" filter is pressed by default
Expectation: "All" button has `aria-pressed="true"`; all 3 default todos are visible
2. Step: Click the "Active" filter button
Expectation: "Active" button has `aria-pressed="true"`; only 2 active todos are visible; "Deploy to staging" is hidden
3. Step: Click the "Completed" filter button
Expectation: "Completed" button has `aria-pressed="true"`; only "Deploy to staging" is visible
4. Step: Click the "All" filter button
Expectation: All 3 todos are visible again

3. **Clear completed removes done todos** — `tests/todo-filter.spec.ts`
- Preconditions: Authenticated user on `/dashboard`; default todos (1 completed)
- Step/Expectation Pairs:
1. Step: Observe "Clear completed" button is enabled
Expectation: Button is not disabled
2. Step: Click "Clear completed"
Expectation: "Deploy to staging" is removed; only 2 active todos remain; count shows "2 items left"
3. Step: Observe "Clear completed" button state
Expectation: Button is now disabled (no completed todos)

4. **Empty state messages per filter** — `tests/todo-filter.spec.ts`
- Preconditions: Authenticated user on `/dashboard`; localStorage cleared
- Step/Expectation Pairs:
1. Step: Clear all completed todos, then delete all remaining active todos
Expectation: List shows "No tasks yet. Add one above to get started."
2. Step: Add a new todo, then click "Active" filter
Expectation: Active filter shows the new todo (it's active)
3. Step: Click "Completed" filter
Expectation: Shows "No completed tasks yet."
4. Step: Toggle the todo to completed, then click "Active" filter
Expectation: Shows "No active tasks. Nice work!"

### Todo Persistence

5. **Todos persist across page reload via localStorage** — `tests/todo-persistence.spec.ts`
- Preconditions: Authenticated user on `/dashboard`; localStorage cleared so default todos load
- Step/Expectation Pairs:
1. Step: Add a new todo "Persist me" and toggle "Review pull requests" to done
Expectation: 4 todos visible; "Persist me" is unchecked; "Review pull requests" is checked
2. Step: Reload the page
Expectation: Same 4 todos with same done/not-done states; count unchanged
3. Step: Delete "Persist me" and reload the page
Expectation: 3 todos visible; "Persist me" is gone; "Review pull requests" still checked
48 changes: 48 additions & 0 deletions tests/todo-crud.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { test, expect } from '@playwright/test';

const STORAGE_KEY = 'dashboard.todos.v1';

test.describe('Todo CRUD', () => {
test.beforeEach(async ({ page }) => {
await page.addInitScript((key) => {
localStorage.removeItem(key);
}, STORAGE_KEY);
await page.goto('/dashboard');
});

test('should add, toggle, and delete a todo', async ({ page }) => {
const todoList = page.getByTestId('todo-list');
const todoCount = page.getByTestId('todo-count');

// Step 1: Observe default todo list — 3 items, "Deploy to staging" checked, 2 items left
await expect(todoList.locator('li')).toHaveCount(3);
await expect(page.getByRole('checkbox', { name: 'Toggle Deploy to staging' })).toBeChecked();
await expect(todoCount).toHaveText('2 items left');

// Step 2: Add "Run CI pipeline" via Enter key
const input = page.getByRole('textbox', { name: 'Add a new task...' });
await input.fill('Run CI pipeline');
await input.press('Enter');

await expect(page.getByText('Run CI pipeline')).toBeVisible();
await expect(todoList.locator('li')).toHaveCount(4);
await expect(page.getByRole('checkbox', { name: 'Toggle Run CI pipeline' })).not.toBeChecked();
await expect(todoCount).toHaveText('3 items left');
await expect(input).toHaveValue('');

// Step 3: Toggle "Review pull requests" to done
await page.getByRole('checkbox', { name: 'Toggle Review pull requests' }).check();
await expect(page.getByRole('checkbox', { name: 'Toggle Review pull requests' })).toBeChecked();
await expect(todoCount).toHaveText('2 items left');

// Step 4: Uncheck "Review pull requests"
await page.getByRole('checkbox', { name: 'Toggle Review pull requests' }).uncheck();
await expect(page.getByRole('checkbox', { name: 'Toggle Review pull requests' })).not.toBeChecked();
await expect(todoCount).toHaveText('3 items left');

// Step 5: Delete "Run CI pipeline"
await page.getByRole('button', { name: 'Delete Run CI pipeline' }).click();
await expect(page.getByText('Run CI pipeline')).not.toBeVisible();
await expect(todoCount).toHaveText('2 items left');
});
});
79 changes: 79 additions & 0 deletions tests/todo-filter.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { test, expect } from '@playwright/test';

const STORAGE_KEY = 'dashboard.todos.v1';

test.describe('Todo filtering and clear completed', () => {
test.beforeEach(async ({ page }) => {
await page.addInitScript((key) => {
localStorage.removeItem(key);
}, STORAGE_KEY);
await page.goto('/dashboard');
});

test('should filter todos by status', async ({ page }) => {
const todoList = page.getByTestId('todo-list');

// Step 1: "All" filter pressed by default, all 3 default todos visible
await expect(page.getByRole('button', { name: 'All' })).toHaveAttribute('aria-pressed', 'true');
await expect(todoList.locator('li')).toHaveCount(3);

// Step 2: Active filter — only 2 active todos; "Deploy to staging" hidden
await page.getByRole('button', { name: 'Active' }).click();
await expect(page.getByRole('button', { name: 'Active' })).toHaveAttribute('aria-pressed', 'true');
await expect(todoList.locator('li')).toHaveCount(2);
await expect(page.getByText('Deploy to staging')).not.toBeVisible();

// Step 3: Completed filter — only "Deploy to staging" visible
await page.getByRole('button', { name: 'Completed', exact: true }).click();
await expect(page.getByRole('button', { name: 'Completed', exact: true })).toHaveAttribute('aria-pressed', 'true');
await expect(todoList.locator('li')).toHaveCount(1);
await expect(page.getByText('Deploy to staging')).toBeVisible();

// Step 4: Switch back to All — all 3 visible again
await page.getByRole('button', { name: 'All' }).click();
await expect(todoList.locator('li')).toHaveCount(3);
});

test('should clear completed todos', async ({ page }) => {
const clearBtn = page.getByRole('button', { name: 'Clear completed' });
const todoCount = page.getByTestId('todo-count');

// Step 1: Clear completed button is enabled (1 completed default todo)
await expect(clearBtn).toBeEnabled();

// Step 2: Click clear completed — "Deploy to staging" removed, 2 active remain
await clearBtn.click();
await expect(page.getByText('Deploy to staging')).not.toBeVisible();
await expect(page.getByTestId('todo-list').locator('li')).toHaveCount(2);
await expect(todoCount).toHaveText('2 items left');

// Step 3: Button is now disabled (no completed todos)
await expect(clearBtn).toBeDisabled();
});

test('should display empty state messages per filter', async ({ page }) => {
const emptyState = page.getByTestId('todo-empty');

// Step 1: Remove all todos to reach the empty "all" state
await page.getByRole('button', { name: 'Clear completed' }).click();
await page.getByRole('button', { name: 'Delete Review pull requests' }).click();
await page.getByRole('button', { name: 'Delete Write documentation' }).click();
await expect(emptyState).toHaveText('No tasks yet. Add one above to get started.');

// Step 2: Add a todo and verify it shows under Active filter
await page.getByRole('textbox', { name: 'Add a new task...' }).fill('Test task');
await page.getByRole('button', { name: 'Add' }).click();
await page.getByRole('button', { name: 'Active' }).click();
await expect(page.getByText('Test task')).toBeVisible();

// Step 3: Completed filter shows empty message
await page.getByRole('button', { name: 'Completed', exact: true }).click();
await expect(emptyState).toHaveText('No completed tasks yet.');

// Step 4: Toggle todo to completed, then Active shows empty message
await page.getByRole('button', { name: 'All' }).click();
await page.getByRole('checkbox', { name: 'Toggle Test task' }).check();
await page.getByRole('button', { name: 'Active' }).click();
await expect(emptyState).toHaveText('No active tasks. Nice work!');
});
});
42 changes: 42 additions & 0 deletions tests/todo-persistence.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { test, expect } from '@playwright/test';

const STORAGE_KEY = 'dashboard.todos.v1';

test.describe('Todo persistence', () => {
test('should persist todos across page reload via localStorage', async ({ page }) => {
// Clear localStorage only on first load; sessionStorage flag survives reloads
await page.addInitScript((key) => {
if (!sessionStorage.getItem('__test_cleared')) {
localStorage.removeItem(key);
sessionStorage.setItem('__test_cleared', '1');
}
}, STORAGE_KEY);
await page.goto('/dashboard');

const todoList = page.getByTestId('todo-list');
const todoCount = page.getByTestId('todo-count');

// Step 1: Add "Persist me" and toggle "Review pull requests" to done
await page.getByRole('textbox', { name: 'Add a new task...' }).fill('Persist me');
await page.getByRole('button', { name: 'Add' }).click();
await page.getByRole('checkbox', { name: 'Toggle Review pull requests' }).check();

await expect(todoList.locator('li')).toHaveCount(4);
await expect(page.getByRole('checkbox', { name: 'Toggle Persist me' })).not.toBeChecked();
await expect(page.getByRole('checkbox', { name: 'Toggle Review pull requests' })).toBeChecked();

// Step 2: Reload — same 4 todos with same states; count unchanged
await page.reload();
await expect(todoList.locator('li')).toHaveCount(4);
await expect(page.getByRole('checkbox', { name: 'Toggle Persist me' })).not.toBeChecked();
await expect(page.getByRole('checkbox', { name: 'Toggle Review pull requests' })).toBeChecked();
await expect(todoCount).toHaveText('2 items left');

// Step 3: Delete "Persist me", reload — 3 todos, "Review pull requests" still checked
await page.getByRole('button', { name: 'Delete Persist me' }).click();
await page.reload();
await expect(todoList.locator('li')).toHaveCount(3);
await expect(page.getByText('Persist me')).not.toBeVisible();
await expect(page.getByRole('checkbox', { name: 'Toggle Review pull requests' })).toBeChecked();
});
});