feat(m4): project & member management with tests and e2e
- ProjectRepository, ProjectMemberRepository, minimal NotificationRepository - ProjectService: create/update/archive/delete project, add/remove member, permission checks (isMember/admin), member-added notification, getDashboard skeleton, getMyProjects, getMemberRole - projectValidator, API services factory - API routes: projects CRUD, members list/add/remove - Screens: dashboard (project list + create form), project overview, members, settings; layout (Header/Sidebar/ProjectNav) and project components - Unit tests (Project/ProjectMember repositories, ProjectService), integration project-member-permission, e2e project-management
This commit is contained in:
125
tests/e2e/project-management.spec.ts
Normal file
125
tests/e2e/project-management.spec.ts
Normal file
@ -0,0 +1,125 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
function unique(prefix: string): string {
|
||||
return `${prefix}-${Date.now()}-${Math.floor(Math.random() * 1_000_000)}`;
|
||||
}
|
||||
|
||||
async function registerAndLogin(
|
||||
page: import('@playwright/test').Page,
|
||||
email: string
|
||||
) {
|
||||
await page.goto('/login');
|
||||
await page.getByRole('button', { name: '新規登録はこちら' }).click();
|
||||
await page.getByLabel('表示名').fill('Owner');
|
||||
await page.getByLabel('メールアドレス').fill(email);
|
||||
await page.getByLabel('パスワード').fill('password123');
|
||||
await page.getByRole('button', { name: '登録する' }).click();
|
||||
await expect(page).toHaveURL(/\/dashboard/);
|
||||
}
|
||||
|
||||
function extractProjectId(url: string): number {
|
||||
const match = url.match(/\/projects\/(\d+)/);
|
||||
if (!match) throw new Error(`project id not found in ${url}`);
|
||||
return Number(match[1]);
|
||||
}
|
||||
|
||||
test.describe('project management', () => {
|
||||
test('create a project, add and remove a member, then archive', async ({
|
||||
page,
|
||||
request,
|
||||
}) => {
|
||||
const ownerEmail = unique('owner') + '@example.com';
|
||||
const memberEmail = unique('member') + '@example.com';
|
||||
const projectName = unique('Project');
|
||||
|
||||
// オーナー登録+ログイン
|
||||
await registerAndLogin(page, ownerEmail);
|
||||
|
||||
// プロジェクト作成(UI)
|
||||
await page.getByLabel('プロジェクト名').fill(projectName);
|
||||
await page.getByRole('button', { name: '新規プロジェクト' }).click();
|
||||
await expect(page).toHaveURL(/\/projects\/\d+$/);
|
||||
await expect(
|
||||
page.getByRole('heading', { name: projectName })
|
||||
).toBeVisible();
|
||||
const projectId = extractProjectId(page.url());
|
||||
|
||||
// 追加用メンバーを事前登録
|
||||
await request.post('/api/auth/register', {
|
||||
data: {
|
||||
name: 'Member User',
|
||||
email: memberEmail,
|
||||
password: 'password123',
|
||||
},
|
||||
});
|
||||
|
||||
// メンバー追加(オーナーのセッションでAPI呼び出し)
|
||||
const addRes = await page.request.post(
|
||||
`/api/projects/${projectId}/members`,
|
||||
{ data: { email: memberEmail, role: 'member' } }
|
||||
);
|
||||
expect(addRes.ok()).toBeTruthy();
|
||||
const { member } = (await addRes.json()) as {
|
||||
member: { userId: number };
|
||||
};
|
||||
|
||||
// メンバー一覧画面に反映されているか検証(UI)
|
||||
await page.goto(`/projects/${projectId}/members`);
|
||||
await expect(page.getByText('Member User')).toBeVisible();
|
||||
|
||||
// メンバー削除(API) → 一覧から消えるか検証(UI)
|
||||
const delRes = await page.request.delete(
|
||||
`/api/projects/${projectId}/members/${member.userId}`
|
||||
);
|
||||
expect(delRes.ok()).toBeTruthy();
|
||||
await page.reload();
|
||||
await expect(page.getByText('Member User')).toHaveCount(0);
|
||||
|
||||
// アーカイブ(API) → 概要画面に反映されているか検証(UI)
|
||||
const archiveRes = await page.request.patch(`/api/projects/${projectId}`, {
|
||||
data: { status: 'archived' },
|
||||
});
|
||||
expect(archiveRes.ok()).toBeTruthy();
|
||||
await page.goto(`/projects/${projectId}`);
|
||||
await expect(page.getByText('Archived')).toBeVisible();
|
||||
});
|
||||
|
||||
test('non-member cannot access a project they do not belong to', async ({
|
||||
browser,
|
||||
request,
|
||||
}) => {
|
||||
const ownerEmail = unique('owner') + '@example.com';
|
||||
const outsiderEmail = unique('outsider') + '@example.com';
|
||||
|
||||
// オーナーがプロジェクトを作成
|
||||
const ownerContext = await browser.newContext();
|
||||
const ownerPage = await ownerContext.newPage();
|
||||
await registerAndLogin(ownerPage, ownerEmail);
|
||||
await ownerPage.getByLabel('プロジェクト名').fill(unique('Secret'));
|
||||
await ownerPage.getByRole('button', { name: '新規プロジェクト' }).click();
|
||||
const projectUrl = ownerPage.url();
|
||||
|
||||
// 外部ユーザーは登録のみ(プロジェクト非参加)
|
||||
await request.post('/api/auth/register', {
|
||||
data: {
|
||||
name: 'Outsider',
|
||||
email: outsiderEmail,
|
||||
password: 'password123',
|
||||
},
|
||||
});
|
||||
const outsiderContext = await browser.newContext();
|
||||
const outsiderPage = await outsiderContext.newPage();
|
||||
await outsiderPage.goto('/login');
|
||||
await outsiderPage.getByLabel('メールアドレス').fill(outsiderEmail);
|
||||
await outsiderPage.getByLabel('パスワード').fill('password123');
|
||||
await outsiderPage.getByRole('button', { name: 'ログイン' }).click();
|
||||
await expect(outsiderPage).toHaveURL(/\/dashboard/);
|
||||
|
||||
// プロジェクト詳細へ直接アクセス → ダッシュボードへリダイレクト
|
||||
await outsiderPage.goto(projectUrl);
|
||||
await expect(outsiderPage).toHaveURL(/\/dashboard$/);
|
||||
|
||||
await ownerContext.close();
|
||||
await outsiderContext.close();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user