import { expect, test } from '@playwright/test'; import { createRequire } from 'node:module'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; const require = createRequire(import.meta.url); const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..'); const e2eTmp = process.env.E2E_TMP; if (!e2eTmp) throw new Error('E2E_TMP must be set by playwright.config.ts'); const dbPath = resolve(e2eTmp, 'e2e.db'); type SeedRepository = { ensureLocalUser: () => void; createUser: (input: Record) => { id: string }; createSpace: (input: Record) => Promise<{ id: string }>; createLocalTask: (input: Record) => Promise<{ id: number }>; recordEvent: (input: Record) => { id: number }; createNotificationItem: (input: Record) => { id: string }; createAgentReminder: (input: Record) => { id: string }; close: () => void; }; const openRepository = (): SeedRepository => { const { Repository } = require(resolve(repoRoot, 'dist/db/repository.js')) as { Repository: new (path: string) => SeedRepository; }; const repository = new Repository(dbPath); // The notification_items recipient FK must not depend on the web server // having completed its no-auth bootstrap before this fixture runs. repository.ensureLocalUser(); return repository; }; test('notification bell shows unread inbox and opens the notification center', async ({ page }) => { const title = `E2E未読通知-${Date.now()}`; const repo = openRepository(); try { const space = await repo.createSpace({ kind: 'case', title: `E2E通知-${Date.now()}`, ownerId: 'local', visibility: 'private' }); const task = await repo.createLocalTask({ title, body: '', ownerId: 'local', spaceId: space.id }); const event = repo.recordEvent({ eventType: 'task.failed', resourceType: 'task', resourceId: `e2e-${Date.now()}`, result: 'failed', dedupeKey: crypto.randomUUID(), correlationId: crypto.randomUUID(), }); repo.createNotificationItem({ eventId: event.id, recipientUserId: 'local', category: 'failed', title, summary: 'E2E notification summary', spaceId: space.id, resourceType: 'job', resourceId: `e2e-${event.id}`, occurredAt: new Date().toISOString(), metadata: { localTaskId: task.id }, }); } finally { repo.close(); } await page.goto('/ui'); const seededInbox = await page.request.get('/api/notifications/inbox?limit=5'); expect(seededInbox.ok()).toBeTruthy(); expect((await seededInbox.json() as { items: Array<{ title: string }> }).items.map(item => item.title)).toContain(title); const bell = page.getByTestId('notification-bell-trigger'); await expect(bell).toBeVisible(); await expect(page.getByTestId('notification-bell-badge')).toBeVisible(); await bell.click(); const panel = page.getByTestId('notification-bell-panel'); await expect(panel).toBeVisible(); await expect(panel.getByRole('button', { name: new RegExp(title) })).toBeVisible(); await panel.getByRole('button', { name: /すべて表示|View all/ }).click(); await expect(page).toHaveURL(/[?&]page=notifications(&|$)/); await expect(page.getByRole('heading', { name: /通知センター|Notification center/ })).toBeVisible(); const row = page.getByRole('button', { name: new RegExp(title) }); await expect(row).toBeVisible(); await expect(row.getByText(/未読|Unread/)).toBeAttached(); await row.click(); await expect(page).toHaveURL(/[?&]space=[^&]+.*[?&]chat=\d+/); await expect(page.getByTestId('notification-bell-badge')).toHaveCount(0); }); test('upcoming reminders show only reminders assigned to the current user', async ({ page }) => { const mine = `E2E本人の予定-${Date.now()}`; const other = `E2E他人の予定-${Date.now()}`; const repo = openRepository(); try { const otherUser = repo.createUser({ email: `other-reminder-${crypto.randomUUID()}@example.test`, name: 'Other reminder owner', role: 'user', status: 'active', }); repo.createAgentReminder({ ownerId: 'local', title: mine, body: mine, dueAt: '2099-01-02T03:04:00.000Z', timezoneId: 'UTC' }); repo.createAgentReminder({ ownerId: otherUser.id, title: other, body: other, dueAt: '2099-01-02T04:04:00.000Z', timezoneId: 'UTC' }); } finally { repo.close(); } await page.goto('/ui?page=notifications¬ificationTab=upcoming'); await expect(page.getByRole('tab', { name: /予定|Upcoming/ })).toHaveAttribute('aria-selected', 'true'); await expect(page.getByText(mine, { exact: true })).toBeVisible(); await expect(page.getByText(other, { exact: true })).toHaveCount(0); const response = await page.request.get('/api/notifications/upcoming'); expect(response.ok()).toBeTruthy(); const body = await response.json() as { items: Array<{ title: string }> }; expect(body.items.map(item => item.title)).toContain(mine); expect(body.items.map(item => item.title)).not.toContain(other); });