Files
cms-client/tests/e2e/profile.spec.ts
Noa Virellia 786e1c709e docs: mark M9 Polish as shipped in overview; fix 2 residual E2E tests
- overview.md: M9 row flipped to shipped, links to spec/plan added,
  roadmap paragraph updated with actual deliverables, conventions
  updated to reflect dual-theme support
- tests/e2e/auth.spec.ts: add GET /event/list override so the dashboard
  renders after the magic-link → token flow (was 500ing with no override)
- tests/e2e/profile.spec.ts: use { exact: true } on username assertion to
  avoid strict-mode violation (username 'alice' matched 3 elements)
- Formatting: prettier --write pass on polish spec/plan, layout.css,
  layout.svelte

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 13:27:37 +08:00

171 lines
6.5 KiB
TypeScript

import { test, expect } from './helpers/fixtures';
import { mock } from './helpers/mock';
test('own profile renders in view mode', async ({ page, loggedInUser }) => {
await page.goto(`/app/profile/${loggedInUser.user_id}`);
// Email appears in both the nav menu and the profile dl; scope to main to be unambiguous.
await expect(page.getByRole('main').getByText(loggedInUser.email)).toBeVisible();
await expect(
page.getByRole('main').getByText(loggedInUser.username, { exact: true })
).toBeVisible();
await expect(page.getByRole('button', { name: /编辑/ })).toBeVisible();
});
test('username-taken error renders on the username field', async ({ page, loggedInUser }) => {
await page.goto(`/app/profile/${loggedInUser.user_id}`);
await page.waitForLoadState('networkidle');
await page.getByRole('button', { name: /编辑/ }).click();
// <label for="username"> + <label class="input"> wrapping the input both
// associate with the input; use the input id directly to avoid strict-mode ambiguity.
await page.locator('#username').fill('alice2');
await mock.override('PATCH', '/user/update', {
status: 409,
body: { status: 409, msg: '用户名已被使用' }
});
await page.getByRole('button', { name: /保存/ }).click();
await expect(page.getByText('用户名已被使用')).toBeVisible();
// Form stays in edit mode — username input still visible
await expect(page.locator('#username')).toBeVisible();
});
test('rejects non-http avatar', async ({ page, loggedInUser }) => {
await page.goto(`/app/profile/${loggedInUser.user_id}`);
await page.waitForLoadState('networkidle');
await page.getByRole('button', { name: /编辑/ }).click();
await page.locator('#avatar').fill('data:image/png;base64,xxx');
await page.getByRole('button', { name: /保存/ }).click();
await expect(page.getByText('头像必须是 http(s) 链接')).toBeVisible();
});
test('editing nickname re-renders + sends correct PATCH body', async ({ page, loggedInUser }) => {
await page.goto(`/app/profile/${loggedInUser.user_id}`);
await page.waitForLoadState('networkidle');
await page.getByRole('button', { name: /编辑/ }).click();
await page.locator('#nickname').fill('Bob');
await mock.override('PATCH', '/user/update', { status: 200, body: { status: 200 } });
await mock.override('GET', '/user/info', {
status: 200,
body: { status: 200, data: { ...loggedInUser, nickname: 'Bob' } }
});
await page.getByRole('button', { name: /保存/ }).click();
await expect(page.getByRole('main').getByText('Bob')).toBeVisible();
const patches = await mock.requests({ method: 'PATCH', path: '/user/update' });
expect(patches).toHaveLength(1);
expect(patches[0].body).toMatchObject({ nickname: 'Bob' });
});
test('visiting /profile redirects to canonical /profile/<own_id>', async ({
page,
loggedInUser
}) => {
await page.goto('/app/profile');
await expect(page).toHaveURL(new RegExp(`/app/profile/${loggedInUser.user_id}$`));
});
test('other public profile renders view-only', async ({ page, loggedInUser }) => {
void loggedInUser; // fixture required to gate auth; profile under test is someone else
await mock.override('GET', '/user/info/42', {
status: 200,
body: {
status: 200,
data: {
user_id: '42',
email: 'bob@test.local',
username: 'bob',
nickname: 'Bob',
permission_level: 10,
allow_public: true
}
}
});
await page.goto('/app/profile/42');
// Scope to the h2 (display name) to avoid strict-mode ambiguity with the page title h1.
await expect(page.getByRole('heading', { name: 'Bob', level: 2 })).toBeVisible();
await expect(page.getByRole('button', { name: /编辑/ })).toHaveCount(0);
});
test('other private profile shows access-denied error', async ({ page, loggedInUser }) => {
void loggedInUser; // fixture required to gate auth; profile under test is someone else
await mock.override('GET', '/user/info/99', {
status: 403,
body: { status: 403, msg: 'forbidden' }
});
await page.goto('/app/profile/99');
await expect(page.getByText('该用户未公开个人资料')).toBeVisible();
});
test('bio renders decoded markdown in view mode', async ({ page, loggedInUser }) => {
const bioText = '这是我的**个人简介**。';
const bioBase64 = Buffer.from(bioText).toString('base64');
// Override GET /user/info to include a base64-encoded bio BEFORE navigating.
// Last-writer-wins: this replaces the loggedInUser fixture's override.
await mock.override('GET', '/user/info', {
status: 200,
body: { status: 200, data: { ...loggedInUser, bio: bioBase64 } }
});
await page.goto(`/app/profile/${loggedInUser.user_id}`);
// Decoded content appears in the prose container (not raw base64)
const prose = page.getByRole('main').locator('.prose');
await expect(prose).toBeVisible();
await expect(prose).toContainText('这是我的');
// Markdown bold → <strong>
await expect(prose.locator('strong')).toContainText('个人简介');
});
test('bio edit pre-fills with decoded text and re-encodes on save', async ({
page,
loggedInUser
}) => {
const bioText = '初始简介内容';
const bioBase64 = Buffer.from(bioText).toString('base64');
await mock.override('GET', '/user/info', {
status: 200,
body: { status: 200, data: { ...loggedInUser, bio: bioBase64 } }
});
await page.goto(`/app/profile/${loggedInUser.user_id}`);
await page.waitForLoadState('networkidle');
await page.getByRole('button', { name: /编辑/ }).click();
// Bio textarea pre-fills with decoded (plain text) value
await expect(page.locator('textarea[name="bio"]')).toHaveValue(bioText);
// Change the bio
const newBio = '新的个人简介';
await page.locator('textarea[name="bio"]').fill(newBio);
await mock.override('PATCH', '/user/update', { status: 200, body: { status: 200 } });
await mock.override('GET', '/user/info', {
status: 200,
body: {
status: 200,
data: { ...loggedInUser, bio: Buffer.from(newBio).toString('base64') }
}
});
await page.getByRole('button', { name: /保存/ }).click();
// Wait for onUpdated to fire and switch back to view mode (edit button reappears).
await expect(page.getByRole('button', { name: /编辑/ })).toBeVisible();
// PATCH body must contain re-encoded bio
const patches = await mock.requests({ method: 'PATCH', path: '/user/update' });
expect(patches).toHaveLength(1);
expect(patches[0].body).toMatchObject({ bio: Buffer.from(newBio).toString('base64') });
});
test('no bio shows empty state placeholder', async ({ page, loggedInUser }) => {
// loggedInUser fixture has no bio — default mock has no bio field
await page.goto(`/app/profile/${loggedInUser.user_id}`);
await expect(page.getByText('暂无简介')).toBeVisible();
});