Some checks failed
CD / Build and push images (push) Successful in 1m6s
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m9s
CD / Promote to Int (push) Successful in 10s
CI / Lint, typecheck, test (push) Successful in 3m19s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Failing after 2m44s
CI / Import/export fidelity gate (push) Has been skipped
The seed already grants fixture users additional_ponds headroom (100); overriding it down to 10 made pond creation fail in CI, where the earlier packs' ponds count against the limit. The pack now relies on the seeded override 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
151 lines
6.8 KiB
TypeScript
151 lines
6.8 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;
|
|
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();
|
|
});
|