import { expect, test } from '@playwright/test'; import { contextForUser } from './helpers'; const BASE_URL = process.env.E2E_BASE_URL ?? 'http://localhost:5173'; /** * Comments UI (issue #92): the full two-user lifecycle in the panel, * resolve/unresolve with the collapsed section, the permission variant * (composer hidden with a hint), and the localStorage unread badge. * The pack provisions its own page and grant, so it is repeatable. */ let pondId: string; let pageUrl: string; let ownerId: string; // The pack provisions its OWN pond: the shared fixture pond accumulates // grants from earlier packs in the same CI job (access-rules & friends), so // "fixture-editor is only a reader here" would not hold there. A Site Admin // lifts fixture-user's additional-pond quota for the run. test.beforeAll(async ({ browser }) => { const admin = await contextForUser(browser, BASE_URL, 'fixture-admin'); const lookup = (await (await admin.request.get('/api/v1/admin/users?q=fixture-user')).json()) as { users: { id: string; username: string }[]; }; ownerId = lookup.users.find((u) => u.username === 'fixture-user')!.id; const quota = await admin.request.put(`/api/v1/admin/quotas/user/${ownerId}/additional_ponds`, { data: { value: 10 }, }); expect(quota.ok()).toBe(true); await admin.close(); const owner = await contextForUser(browser, BASE_URL, 'fixture-user'); const pondRes = await owner.request.post('/api/v1/ponds', { data: { name: `Comments stage ${Date.now()}` }, }); expect(pondRes.ok()).toBe(true); const pond = (await pondRes.json()) as { id: string; slug: string }; pondId = pond.id; const created = await owner.request.post(`/api/v1/ponds/${pondId}/pages`, { data: { title: 'Discussion' }, }); expect(created.ok()).toBe(true); const page = (await created.json()) as { slug: string }; pageUrl = `/p/${pond.slug}/${page.slug}`; // fixture-editor is exactly a reader in this fresh pond — nothing else. const member = await owner.request.post(`/api/v1/ponds/${pondId}/members`, { data: { usernameOrEmail: 'fixture-editor', role: 'reader' }, }); expect(member.ok()).toBe(true); await owner.close(); }); test.afterAll(async ({ browser }) => { const owner = await contextForUser(browser, BASE_URL, 'fixture-user'); if (pondId) await owner.request.delete(`/api/v1/ponds/${pondId}`); await owner.close(); }); test('two users run the full comment lifecycle with resolve/unresolve', async ({ browser }) => { // The reader starts the thread. const reader = await contextForUser(browser, BASE_URL, 'fixture-editor'); const readerPage = await reader.newPage(); await readerPage.goto(pageUrl); await readerPage.locator('.editor-shell__comments-toggle').click(); const readerPanel = readerPage.locator('.comments-panel'); await readerPanel.locator('.comments-composer textarea').fill('Is this **final**?'); await readerPanel.locator('.comments-composer button[type="submit"]').click(); await expect(readerPanel.locator('.comment__body strong')).toHaveText('final'); // The owner sees the unread badge, replies, and resolves the thread. const owner = await contextForUser(browser, BASE_URL, 'fixture-user'); const ownerPage = await owner.newPage(); await ownerPage.goto(pageUrl); await expect(ownerPage.locator('.comments-unread-badge')).toBeVisible(); await ownerPage.locator('.editor-shell__comments-toggle').click(); const ownerPanel = ownerPage.locator('.comments-panel'); await ownerPanel .locator('.comments-thread .comments-link-button', { hasText: /reply|antwort/i }) .click(); await ownerPanel.locator('.comments-thread textarea').fill('Yes, shipping it.'); await ownerPanel.locator('.comments-thread button[type="submit"]').click(); await expect(ownerPanel.locator('.comments-thread__replies .comment__body')).toContainText( 'shipping', ); // Resolve collapses the thread into the resolved section. await ownerPanel .locator('.comment__actions .comments-link-button', { hasText: /resolve|erledig/i }) .first() .click(); const resolvedSection = ownerPanel.locator('.comments-panel__resolved'); await expect(resolvedSection).toBeVisible(); await expect(resolvedSection.locator('details, summary').first()).toBeVisible(); // Collapsed: the thread body is hidden until the section is opened. await expect(resolvedSection.locator('.comment__body strong')).toBeHidden(); await resolvedSection.locator('summary').click(); await expect(resolvedSection.locator('.comment__body strong')).toBeVisible(); // Unresolve restores it to the open list. await resolvedSection.locator('.comments-link-button', { hasText: /reopen|öffnen/i }).click(); await expect(ownerPanel.locator('.comments-panel__resolved')).toHaveCount(0); await expect(ownerPanel.locator('.comments-thread .comment__body strong')).toBeVisible(); // Author edits and deletes own reply. const replyItem = ownerPanel.locator('.comments-thread__replies .comment'); await replyItem.locator('.comments-link-button', { hasText: /edit|bearbeit/i }).click(); await replyItem.locator('textarea').fill('Yes — shipped.'); await replyItem.locator('button[type="submit"]').click(); await expect(replyItem.locator('.comment__body')).toContainText('shipped.'); await expect(replyItem.locator('.comment__edited')).toBeVisible(); await replyItem.locator('.comments-link-button', { hasText: /delete|löschen/i }).click(); await expect(ownerPanel.locator('.comments-thread__replies .comment')).toHaveCount(0); // Cleanup: the reader deletes their own (now reply-free) root. await readerPage.reload(); await readerPage.locator('.editor-shell__comments-toggle').click(); await readerPanel.locator('.comments-link-button', { hasText: /delete|löschen/i }).click(); await expect(readerPanel.locator('.comments-panel__empty')).toBeVisible(); await reader.close(); await owner.close(); }); test('the composer hides with a hint when the policy bars readers', async ({ browser }) => { const owner = await contextForUser(browser, BASE_URL, 'fixture-user'); const patched = await owner.request.patch(`/api/v1/ponds/${pondId}`, { data: { commentPolicy: 'editors' }, }); expect(patched.ok()).toBe(true); await owner.close(); const reader = await contextForUser(browser, BASE_URL, 'fixture-editor'); const page = await reader.newPage(); await page.goto(pageUrl); await page.locator('.editor-shell__comments-toggle').click(); const panel = page.locator('.comments-panel'); await expect(panel.locator('.comments-panel__policy-hint')).toBeVisible(); await expect(panel.locator('.comments-composer')).toHaveCount(0); await reader.close(); const ownerAgain = await contextForUser(browser, BASE_URL, 'fixture-user'); await ownerAgain.request.patch(`/api/v1/ponds/${pondId}`, { data: { commentPolicy: 'readers' }, }); await ownerAgain.close(); });