diff --git a/e2e/core-flows.spec.ts b/e2e/core-flows.spec.ts new file mode 100644 index 00000000..508ce119 --- /dev/null +++ b/e2e/core-flows.spec.ts @@ -0,0 +1,233 @@ +import { test, expect } from '@playwright/test' + +test.beforeEach(async ({ page }) => { + await page.goto('/') + await page.waitForTimeout(500) // Wait for mock data +}) + +// --- Flow 1: Open app, click note, verify editor shows content --- + +test('clicking a note opens it in the editor with content', async ({ page }) => { + // Click "Build Laputa App" in the note list + await page.locator('.note-list__item', { hasText: 'Build Laputa App' }).click() + await page.waitForTimeout(300) + + // Tab should appear and be active + await expect(page.locator('.editor__tab--active')).toHaveText(/Build Laputa App/) + + // Editor should show note content (heading in live preview) + await expect(page.locator('.cm-editor')).toBeVisible() + + // Inspector should show properties + await expect(page.locator('.inspector__prop-value', { hasText: 'Project' })).toBeVisible() + + await page.screenshot({ path: 'test-results/core-open-note.png', fullPage: true }) +}) + +test('editor shows markdown content with live preview', async ({ page }) => { + await page.locator('.note-list__item', { hasText: 'Build Laputa App' }).click() + await page.waitForTimeout(300) + + // CodeMirror should render the content + const editor = page.locator('.cm-editor') + await expect(editor).toBeVisible() + + // Should contain the heading text (rendered by live preview) + await expect(page.locator('.cm-content')).toContainText('Build Laputa App') +}) + +// --- Flow 2: Sidebar filter changes note list --- + +test('sidebar filter: People shows only Person entries', async ({ page }) => { + await page.locator('.sidebar__filter-item', { hasText: 'People' }).click() + await page.waitForTimeout(200) + + await expect(page.locator('.note-list__count')).toHaveText('1') + await expect(page.locator('.note-list__title', { hasText: 'Matteo Cellini' })).toBeVisible() + await expect(page.locator('.note-list__title', { hasText: 'Build Laputa App' })).not.toBeVisible() +}) + +test('sidebar filter: Events shows only Event entries', async ({ page }) => { + await page.locator('.sidebar__filter-item', { hasText: 'Events' }).click() + await page.waitForTimeout(200) + + await expect(page.locator('.note-list__count')).toHaveText('1') + await expect(page.locator('.note-list__title', { hasText: 'Laputa App Design Session' })).toBeVisible() +}) + +test('sidebar filter: clicking back to All Notes restores full list', async ({ page }) => { + await page.locator('.sidebar__filter-item', { hasText: 'People' }).click() + await page.waitForTimeout(200) + await expect(page.locator('.note-list__count')).toHaveText('1') + + await page.locator('.sidebar__filter-item', { hasText: 'All Notes' }).click() + await page.waitForTimeout(200) + await expect(page.locator('.note-list__count')).toHaveText('12') +}) + +// --- Flow 3: Search for a note --- + +test('search filters notes by title', async ({ page }) => { + await page.fill('.note-list__search-input', 'stock') + await page.waitForTimeout(200) + + await expect(page.locator('.note-list__count')).toHaveText('1') + await expect(page.locator('.note-list__title', { hasText: 'Stock Screener' })).toBeVisible() + await expect(page.locator('.note-list__title', { hasText: 'Build Laputa App' })).not.toBeVisible() +}) + +test('clearing search restores all results', async ({ page }) => { + await page.fill('.note-list__search-input', 'stock') + await page.waitForTimeout(200) + await expect(page.locator('.note-list__count')).toHaveText('1') + + await page.fill('.note-list__search-input', '') + await page.waitForTimeout(200) + await expect(page.locator('.note-list__count')).toHaveText('12') +}) + +// --- Flow 4: Open multiple tabs and switch between them --- + +test('opening multiple notes creates multiple tabs', async ({ page }) => { + // Open first note + await page.locator('.note-list__item', { hasText: 'Build Laputa App' }).click() + await page.waitForTimeout(300) + + // Open second note + await page.locator('.note-list__item', { hasText: 'Grow Newsletter' }).click() + await page.waitForTimeout(300) + + // Both tabs should exist + const tabs = page.locator('.editor__tab') + await expect(tabs).toHaveCount(2) + + // Second tab should be active + await expect(page.locator('.editor__tab--active')).toHaveText(/Grow Newsletter/) + + await page.screenshot({ path: 'test-results/core-multi-tabs.png', fullPage: true }) +}) + +test('clicking a tab switches to it', async ({ page }) => { + // Open two notes + await page.locator('.note-list__item', { hasText: 'Build Laputa App' }).click() + await page.waitForTimeout(300) + await page.locator('.note-list__item', { hasText: 'Grow Newsletter' }).click() + await page.waitForTimeout(300) + + // Switch back to first tab + await page.locator('.editor__tab', { hasText: 'Build Laputa App' }).click() + await page.waitForTimeout(200) + + // First tab should now be active + await expect(page.locator('.editor__tab--active')).toHaveText(/Build Laputa App/) + + // Editor should show first note's content + await expect(page.locator('.cm-content')).toContainText('Build Laputa App') +}) + +test('closing a tab removes it and switches to adjacent', async ({ page }) => { + // Open two notes + await page.locator('.note-list__item', { hasText: 'Build Laputa App' }).click() + await page.waitForTimeout(300) + await page.locator('.note-list__item', { hasText: 'Grow Newsletter' }).click() + await page.waitForTimeout(300) + + // Close active tab (Grow Newsletter) + await page.locator('.editor__tab--active .editor__tab-close').click() + await page.waitForTimeout(200) + + // Should have 1 tab left + await expect(page.locator('.editor__tab')).toHaveCount(1) + await expect(page.locator('.editor__tab--active')).toHaveText(/Build Laputa App/) +}) + +// --- Flow 5: Inspector shows correct properties --- + +test('inspector shows properties for selected note', async ({ page }) => { + await page.locator('.note-list__item', { hasText: 'Build Laputa App' }).click() + await page.waitForTimeout(300) + + // Type + await expect(page.locator('.inspector__prop-value', { hasText: 'Project' })).toBeVisible() + // Status + await expect(page.locator('.inspector__status-pill', { hasText: 'Active' })).toBeVisible() + // Owner + await expect(page.locator('.inspector__prop-value', { hasText: 'Luca Rossi' })).toBeVisible() + + await page.screenshot({ path: 'test-results/core-inspector.png', fullPage: true }) +}) + +test('inspector shows relationships', async ({ page }) => { + // Open a note with relationships + await page.locator('.note-list__item', { hasText: 'Build Laputa App' }).click() + await page.waitForTimeout(300) + + // Should show "Related to" relationships + await expect(page.locator('.inspector__section', { hasText: 'Relationships' })).toBeVisible() +}) + +test('inspector shows backlinks', async ({ page }) => { + // Open a note that has backlinks (Build Laputa App is referenced by Facebook Ads and Budget Allocation) + await page.locator('.note-list__item', { hasText: 'Build Laputa App' }).click() + await page.waitForTimeout(300) + + // Backlinks section should show + const backlinksSection = page.locator('.inspector__section', { hasText: 'Backlinks' }) + await expect(backlinksSection).toBeVisible() +}) + +test('inspector shows git history', async ({ page }) => { + await page.locator('.note-list__item', { hasText: 'Build Laputa App' }).click() + await page.waitForTimeout(300) + + // Should show commits + await expect(page.locator('.inspector__commit-hash').first()).toBeVisible() + await expect(page.locator('.inspector__commit-msg').first()).toContainText('26q1-laputa-app') +}) + +test('inspector updates when switching tabs', async ({ page }) => { + // Open first note + await page.locator('.note-list__item', { hasText: 'Build Laputa App' }).click() + await page.waitForTimeout(300) + await expect(page.locator('.inspector__prop-value', { hasText: 'Project' })).toBeVisible() + + // Open second note with different type — use title locator for precision + await page.locator('.note-list__item').filter({ has: page.locator('.note-list__title', { hasText: 'Matteo Cellini' }) }).click() + await page.waitForTimeout(300) + await expect(page.locator('.inspector__prop-value', { hasText: 'Person' })).toBeVisible() +}) + +// --- Flow 6: Note list preview snippets --- + +test('note list items show preview snippets', async ({ page }) => { + // Check that snippets are visible + const snippet = page.locator('.note-list__snippet').first() + await expect(snippet).toBeVisible() + // Snippet should have some text content + const text = await snippet.textContent() + expect(text!.length).toBeGreaterThan(10) +}) + +// --- Flow 7: Create note and verify it appears --- + +test('full create note flow', async ({ page }) => { + // Count before + const countBefore = await page.locator('.note-list__count').textContent() + + // Create new note + await page.click('.note-list__add-btn') + await page.waitForTimeout(200) + await page.fill('.create-dialog__input', 'E2E Test Note') + await page.click('.create-dialog__type-btn:text("Experiment")') + await page.click('.create-dialog__btn--create') + await page.waitForTimeout(500) + + // Count should increase + const countAfter = await page.locator('.note-list__count').textContent() + expect(parseInt(countAfter!)).toBe(parseInt(countBefore!) + 1) + + // Note should be opened in editor + await expect(page.locator('.editor__tab--active')).toHaveText(/E2E Test Note/) + + await page.screenshot({ path: 'test-results/core-create-note.png', fullPage: true }) +})