All checks were successful
CD / Build and push images (push) Successful in 1m8s
CD / Deploy to Test (push) Successful in 10s
CD / Smoke tests against Test (push) Successful in 1m9s
CD / Promote to Int (push) Successful in 10s
CI / Lint, typecheck, test (push) Successful in 3m18s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 5m24s
CI / Import/export fidelity gate (push) Successful in 46s
The previous commit's automated edit silently missed the beforeAll block; the pack still lowered the seeded additional_ponds override to 10, which CI's accumulated fixture ponds exceed. The pack now relies on the seeded headroom and reports the create-pond response on failure. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
142 lines
6.4 KiB
TypeScript
142 lines
6.4 KiB
TypeScript
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;
|
|
|
|
// 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. The seed
|
|
// already gives fixture users additional-pond headroom (override 100) —
|
|
// never lower it here: in CI the earlier packs' ponds count against it.
|
|
test.beforeAll(async ({ browser }) => {
|
|
const owner = await contextForUser(browser, BASE_URL, 'fixture-user');
|
|
const pondRes = await owner.request.post('/api/v1/ponds', {
|
|
data: { name: `Comments stage ${Date.now()}` },
|
|
});
|
|
if (!pondRes.ok()) {
|
|
throw new Error(`create pond failed: ${pondRes.status()} ${await pondRes.text()}`);
|
|
}
|
|
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();
|
|
});
|