From 87fa24c51d19859c4e390706c868777c2d592d7a Mon Sep 17 00:00:00 2001 From: Adam Mathes Date: Sun, 15 Feb 2026 13:48:56 -0800 Subject: Frontend: Implement Playwright UI tests with mocked API responses (NK-4jy0t2) --- frontend/package.json | 3 +- frontend/tests/mocked-api.spec.ts | 167 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 frontend/tests/mocked-api.spec.ts (limited to 'frontend') diff --git a/frontend/package.json b/frontend/package.json index e5475dd..4bed8f9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,7 @@ "preview": "vite preview", "test": "vitest", "test:e2e": "playwright test", + "test:mocked": "playwright test tests/mocked-api.spec.ts", "coverage": "vitest run --coverage" }, "dependencies": { @@ -41,4 +42,4 @@ "vite": "^7.3.1", "vitest": "^4.0.18" } -} +} \ No newline at end of file diff --git a/frontend/tests/mocked-api.spec.ts b/frontend/tests/mocked-api.spec.ts new file mode 100644 index 0000000..06f6c17 --- /dev/null +++ b/frontend/tests/mocked-api.spec.ts @@ -0,0 +1,167 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Mocked API UI Tests', () => { + test.beforeEach(async ({ page }) => { + // Log browser console for debugging + page.on('console', msg => { + if (msg.type() === 'error') console.log(`BROWSER ERROR: ${msg.text()} `); + }); + + // 1. Mock Auth - simulate logged in session + await page.route('**/api/auth', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ status: 'ok', authenticated: true }), + }); + }); + + // 2. Mock Feeds + await page.route('**/api/feed/', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify([ + { _id: 1, title: 'Mock Feed 1', url: 'http://mock1.com', category: 'News' }, + { _id: 2, title: 'Mock Feed 2', url: 'http://mock2.com', category: 'Tech' }, + ]), + }); + }); + + // 3. Mock Tags + await page.route('**/api/tag', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify([ + { title: 'News' }, + { title: 'Tech' }, + ]), + }); + }); + + // 4. Mock Stream/Items + await page.route('**/api/stream*', async (route) => { + const url = new URL(route.request().url()); + const maxId = url.searchParams.get('max_id'); + + if (maxId) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify([]), + }); + return; + } + + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify([ + { + _id: 101, + feed_id: 1, + title: 'Mock Item Unread', + url: 'http://mock1.com/1', + description: 'This is an unread item', + publish_date: new Date().toISOString(), + read: false, + starred: false, + feed_title: 'Mock Feed 1' + }, + { + _id: 102, + feed_id: 2, + title: 'Mock Item Starred', + url: 'http://mock2.com/1', + description: 'This is a starred and read item', + publish_date: new Date().toISOString(), + read: true, + starred: true, + feed_title: 'Mock Feed 2' + } + ]), + }); + }); + + // 5. Mock Item Update (for marking read/starred) + await page.route('**/api/item/**', async (route) => { + if (route.request().method() === 'PUT') { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ status: 'ok' }), + }); + } else { + await route.continue(); + } + }); + + // 6. Mock Logout + await page.route('**/api/logout', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ status: 'ok' }), + }); + }); + }); + + test('should load dashboard with mocked feeds and items', async ({ page }) => { + await page.goto('/v2/'); + + // Wait for the logo to appear (means we are on the dashboard) + await expect(page.locator('h1.logo')).toBeVisible({ timeout: 15000 }); + + // Verify items in main view (ensure they load first) + await expect(page.getByText('Mock Item Unread')).toBeVisible({ timeout: 10000 }); + await expect(page.getByText('Mock Item Starred')).toBeVisible(); + + // Verify feeds in sidebar + // Click on the Feeds header to expand + await page.getByText(/Feeds/i).click(); + + // Wait for mocked feeds to appear in sidebar + // We use a more specific selector to avoid matching the feed_title in the item list + await expect(page.locator('.feed-list-items').getByText('Mock Feed 1')).toBeVisible({ timeout: 10000 }); + await expect(page.locator('.feed-list-items').getByText('Mock Feed 2')).toBeVisible(); + + // Verify "unread" filter is active by default + const unreadFilterLink = page.locator('.unread_filter a'); + await expect(unreadFilterLink).toHaveClass(/active/); + }); + + test('should filter by mocked tag', async ({ page }) => { + await page.goto('/v2/'); + + // Click on Tech tag + await page.getByText('Tech', { exact: true }).click(); + + // URL should update + await expect(page).toHaveURL(/.*\/tag\/Tech/); + + // Verify feed items are still visible (mock stream returns both regardless of tag in this simple mock) + await expect(page.getByText('Mock Item Unread')).toBeVisible(); + }); + + test('should toggle item star status', async ({ page }) => { + await page.goto('/v2/'); + + const unreadItem = page.locator('.feed-item.unread').first(); + const starButton = unreadItem.getByTitle('Star'); + + await starButton.click(); + + // Expect star to change to "Unstar" (UI optimistic update) + await expect(starButton).toHaveAttribute('title', 'Unstar'); + }); + + test('should logout using mocked API', async ({ page }) => { + await page.goto('/v2/'); + + await page.getByText('logout').click(); + + // Should redirect to login + await expect(page).toHaveURL(/.*\/login/); + }); +}); -- cgit v1.2.3