/** * E2E tests for core user journeys * * Tests cover the five main user workflows: * 1. Create - Quick capture new entries * 2. Edit - Modify existing entries * 3. Search - Find entries by text * 4. Organize - Tags and pinning * 5. Delete - Remove entries */ import { test, expect, testData } from './index'; test.describe('Create workflow', () => { test('can create a new entry via quick capture', async ({ page, seededDb }) => { await page.goto('/'); // Fill in quick capture form const contentInput = page.locator('textarea[name="content"]'); await contentInput.fill('New test entry from E2E'); // Select task type const typeSelect = page.locator('select[name="type"]'); await typeSelect.selectOption('task'); // Submit the form const addButton = page.locator('button[type="submit"]:has-text("Add")'); await addButton.click(); // Wait for entry to appear in list await expect(page.locator('text=New test entry from E2E')).toBeVisible({ timeout: 5000 }); }); test('created entry persists after page reload', async ({ page, seededDb }) => { await page.goto('/'); const uniqueContent = `Persistence test ${Date.now()}`; // Create an entry const contentInput = page.locator('textarea[name="content"]'); await contentInput.fill(uniqueContent); const addButton = page.locator('button[type="submit"]:has-text("Add")'); await addButton.click(); // Wait for entry to appear await expect(page.locator(`text=${uniqueContent}`)).toBeVisible({ timeout: 5000 }); // Reload page await page.reload(); // Verify entry still exists await expect(page.locator(`text=${uniqueContent}`)).toBeVisible({ timeout: 5000 }); }); test('can create entry with optional title', async ({ page, seededDb }) => { await page.goto('/'); // Fill in title and content const titleInput = page.locator('input[name="title"]'); await titleInput.fill('My Test Title'); const contentInput = page.locator('textarea[name="content"]'); await contentInput.fill('Content with a title'); const addButton = page.locator('button[type="submit"]:has-text("Add")'); await addButton.click(); // Wait for entry to appear with the content await expect(page.locator('text=Content with a title')).toBeVisible({ timeout: 5000 }); }); }); test.describe('Edit workflow', () => { test('can expand and edit an existing entry', async ({ page, seededDb }) => { await page.goto('/'); // Find seeded entry by content and click to expand const entryContent = testData.entries[0].content; // "Buy groceries for the week" const entryCard = page.locator(`article:has-text("${entryContent}")`); await expect(entryCard).toBeVisible(); // Click to expand (the clickable area with role="button") await entryCard.locator('[role="button"]').click(); // Wait for edit textarea to appear const editTextarea = entryCard.locator('textarea'); await expect(editTextarea).toBeVisible({ timeout: 5000 }); // Modify content await editTextarea.fill('Buy groceries for the week - updated'); // Auto-save triggers after 400ms, wait for save indicator await page.waitForTimeout(500); // Collapse the card await entryCard.locator('[role="button"]').click(); // Verify updated content is shown await expect(page.locator('text=Buy groceries for the week - updated')).toBeVisible({ timeout: 5000 }); }); test('edited changes persist after reload', async ({ page, seededDb }) => { await page.goto('/'); // Find and edit an entry const entryContent = testData.entries[3].content; // "Meeting notes with stakeholders" const entryCard = page.locator(`article:has-text("${entryContent}")`); await entryCard.locator('[role="button"]').click(); const editTextarea = entryCard.locator('textarea'); await expect(editTextarea).toBeVisible({ timeout: 5000 }); const updatedContent = 'Meeting notes - edited in E2E test'; await editTextarea.fill(updatedContent); // Wait for auto-save await page.waitForTimeout(600); // Reload page await page.reload(); // Verify changes persisted await expect(page.locator(`text=${updatedContent}`)).toBeVisible({ timeout: 5000 }); }); }); test.describe('Search workflow', () => { test('can search entries by text', async ({ page, seededDb }) => { await page.goto('/'); // Type in search bar const searchInput = page.locator('input[placeholder*="Search"]'); await searchInput.fill('groceries'); // Wait for debounced search (300ms + render time) await page.waitForTimeout(500); // Verify matching entry is visible await expect(page.locator('text=Buy groceries for the week')).toBeVisible(); // Verify non-matching entries are hidden await expect(page.locator('text=Meeting notes with stakeholders')).not.toBeVisible(); }); test('search shows "no results" message when nothing matches', async ({ page, seededDb }) => { await page.goto('/'); const searchInput = page.locator('input[placeholder*="Search"]'); await searchInput.fill('xyznonexistent123'); // Wait for debounced search await page.waitForTimeout(500); // Should show no results message await expect(page.locator('text=No entries match your search')).toBeVisible(); }); test('clearing search shows all entries again', async ({ page, seededDb }) => { await page.goto('/'); // First, search for something specific const searchInput = page.locator('input[placeholder*="Search"]'); await searchInput.fill('groceries'); await page.waitForTimeout(500); // Verify filtered await expect(page.locator('text=Meeting notes')).not.toBeVisible(); // Clear search await searchInput.clear(); await page.waitForTimeout(500); // Verify all entries are visible again (at least our seeded ones) await expect(page.locator('text=Buy groceries')).toBeVisible(); await expect(page.locator('text=Meeting notes')).toBeVisible(); }); }); test.describe('Organize workflow', () => { test('can filter entries by type (tasks vs thoughts)', async ({ page, seededDb }) => { await page.goto('/'); // Click "Tasks" filter button const tasksButton = page.locator('button:has-text("Tasks")'); await tasksButton.click(); // Wait for filter to apply await page.waitForTimeout(300); // Tasks should be visible await expect(page.locator('text=Buy groceries for the week')).toBeVisible(); // Thoughts should be hidden await expect(page.locator('text=Meeting notes with stakeholders')).not.toBeVisible(); }); test('can filter entries by tag', async ({ page, seededDb }) => { await page.goto('/'); // Open tag filter dropdown (Svelecte component) const tagFilter = page.locator('.filter-tag-input'); await tagFilter.click(); // Select "work" tag from dropdown await page.locator('text=work').first().click(); // Wait for filter to apply await page.waitForTimeout(300); // Entries with "work" tag should be visible await expect( page.locator('text=Important pinned thought about project architecture') ).toBeVisible(); await expect(page.locator('text=Meeting notes with stakeholders')).toBeVisible(); // Entries without "work" tag should be hidden await expect(page.locator('text=Buy groceries for the week')).not.toBeVisible(); }); test('pinned entries appear in Pinned section', async ({ page, seededDb }) => { await page.goto('/'); // The seeded entry "Important pinned thought about project architecture" is pinned // Verify Pinned section exists and contains this entry await expect(page.locator('h2:has-text("Pinned")')).toBeVisible(); await expect( page.locator('text=Important pinned thought about project architecture') ).toBeVisible(); }); test('can toggle pin on an entry', async ({ page, seededDb }) => { await page.goto('/'); // Find an unpinned entry and expand it const entryContent = testData.entries[3].content; // "Meeting notes with stakeholders" const entryCard = page.locator(`article:has-text("${entryContent}")`); await entryCard.locator('[role="button"]').click(); // Find and click the pin button (should have pin icon) const pinButton = entryCard.locator('button[aria-label*="pin" i], button:has-text("Pin")'); if ((await pinButton.count()) > 0) { await pinButton.first().click(); await page.waitForTimeout(300); // Verify the entry now appears in Pinned section await expect( page.locator('h2:has-text("Pinned") + div').locator(`text=${entryContent}`) ).toBeVisible(); } }); }); test.describe('Delete workflow', () => { test('can delete an entry via swipe (mobile)', async ({ page, seededDb }) => { // This test simulates mobile swipe-to-delete await page.goto('/'); const entryContent = testData.entries[4].content; // "Review pull request for feature branch" const entryCard = page.locator(`article:has-text("${entryContent}")`); await expect(entryCard).toBeVisible(); // Simulate swipe left (touchstart, touchmove, touchend) const box = await entryCard.boundingBox(); if (box) { // Touch start await page.touchscreen.tap(box.x + box.width / 2, box.y + box.height / 2); // Swipe left await entryCard.evaluate((el) => { // Dispatch touch events to trigger swipe const touchStart = new TouchEvent('touchstart', { bubbles: true, cancelable: true, touches: [ new Touch({ identifier: 0, target: el, clientX: 200, clientY: 50 }) ] }); const touchMove = new TouchEvent('touchmove', { bubbles: true, cancelable: true, touches: [ new Touch({ identifier: 0, target: el, clientX: 50, // Swipe 150px left clientY: 50 }) ] }); const touchEnd = new TouchEvent('touchend', { bubbles: true, cancelable: true, touches: [] }); el.dispatchEvent(touchStart); el.dispatchEvent(touchMove); el.dispatchEvent(touchEnd); }); // Wait for delete confirmation to appear await page.waitForTimeout(300); // Click confirm delete if visible const confirmDelete = page.locator('button:has-text("Delete"), button:has-text("Confirm")'); if ((await confirmDelete.count()) > 0) { await confirmDelete.first().click(); } } }); test('deleted entry is removed from list', async ({ page, seededDb }) => { await page.goto('/'); // Use a known entry we can delete const entryContent = testData.entries[1].content; // "Completed task from yesterday" const entryCard = page.locator(`article:has-text("${entryContent}")`); await expect(entryCard).toBeVisible(); // Expand the entry to find delete button await entryCard.locator('[role="button"]').click(); await page.waitForTimeout(200); // Try to find a delete button in expanded view // If the entry has a delete button accessible via UI (not just swipe) const deleteButton = entryCard.locator( 'button[aria-label*="delete" i], button:has-text("Delete")' ); if ((await deleteButton.count()) > 0) { await deleteButton.first().click(); // Wait for deletion await page.waitForTimeout(500); // Verify entry is no longer visible await expect(page.locator(`text=${entryContent}`)).not.toBeVisible(); } }); test('deleted entry does not appear after reload', async ({ page, seededDb }) => { await page.goto('/'); // Note: This test depends on the previous test having deleted an entry // In a real scenario, we'd delete in this test first // For now, let's verify the seeded data is present, delete it, then reload const entryContent = testData.entries[1].content; const entryCard = page.locator(`article:has-text("${entryContent}")`); // If the entry exists, try to delete it if ((await entryCard.count()) > 0) { // Expand and try to delete await entryCard.locator('[role="button"]').click(); await page.waitForTimeout(200); const deleteButton = entryCard.locator( 'button[aria-label*="delete" i], button:has-text("Delete")' ); if ((await deleteButton.count()) > 0) { await deleteButton.first().click(); await page.waitForTimeout(500); // Reload and verify await page.reload(); await expect(page.locator(`text=${entryContent}`)).not.toBeVisible(); } } }); }); test.describe('Task completion workflow', () => { test('can mark task as complete via checkbox', async ({ page, seededDb }) => { await page.goto('/'); // Find a task entry (has checkbox) const entryContent = testData.entries[0].content; // "Buy groceries for the week" const entryCard = page.locator(`article:has-text("${entryContent}")`); // Find and click the completion checkbox const checkbox = entryCard.locator('button[type="submit"][aria-label*="complete" i]'); await expect(checkbox).toBeVisible(); await checkbox.click(); // Wait for the update await page.waitForTimeout(500); // Verify the task is now shown as complete (strikethrough or checkmark) // The checkbox should now have a green background await expect(checkbox).toHaveClass(/bg-green-500/); }); test('completed task has strikethrough styling', async ({ page, seededDb }) => { await page.goto('/'); // Find the already-completed seeded task const completedEntry = testData.entries[1]; // "Completed task from yesterday" - status: done // Need to enable "show completed" to see it // Click the toggle in the header const completedToggle = page.locator('button:has-text("Show completed"), label:has-text("completed") input'); if ((await completedToggle.count()) > 0) { await completedToggle.first().click(); await page.waitForTimeout(300); } // Verify the completed task has strikethrough class const entryCard = page.locator(`article:has-text("${completedEntry.content}")`); if ((await entryCard.count()) > 0) { const titleElement = entryCard.locator('h3'); await expect(titleElement).toHaveClass(/line-through/); } }); });