feat(m15): full test completion + quality gate + validation fixes

- Add E2E notifications.spec (member-added notification -> list -> mark read)
  and activity-log.spec (board_posted + todo_created recorded)
- Set Playwright workers=1 for deterministic full-suite runs (SSE test under
  parallel load); full E2E suite (18 tests / 13 specs incl. 12 mandated
  scenarios) green
- Quality gate: 251 unit/integration tests, lint clean, typecheck clean,
  build succeeds, coverage 91.13% Repository/Service (>=80%)
- Validation fixes (mandatory): SearchService now searches ALL items
  (large pageSize) instead of first page only; MeetingService.
  checkScheduleConflicts now requires membership (actorId) to prevent
  non-member info enumeration
This commit is contained in:
Ken Yasue
2026-06-25 02:54:48 +02:00
parent 2bc883cb6f
commit f9720850b2
7 changed files with 145 additions and 5 deletions

View File

@ -0,0 +1,49 @@
import { test, expect } from '@playwright/test';
function unique(prefix: string): string {
return `${prefix}-${Date.now()}-${Math.floor(Math.random() * 1_000_000)}`;
}
async function setupOwner(page: import('@playwright/test').Page) {
const email = unique('owner') + '@example.com';
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/);
await page.getByLabel('プロジェクト名').fill(unique('Proj'));
await page.getByRole('button', { name: '新規プロジェクト' }).click();
await expect(page).toHaveURL(/\/projects\/\d+$/);
return Number(page.url().match(/\/projects\/(\d+)/)![1]);
}
test.describe('activity log', () => {
test('board post and todo creation are recorded in the project activity log', async ({
page,
}) => {
const projectId = await setupOwner(page);
// 掲示板投稿 → board_posted アクティビティ
await page.request.post(`/api/projects/${projectId}/board/threads`, {
data: { title: unique('Thread'), bodyMd: 'body', category: 'notice' },
});
// ToDo作成 → todo_created アクティビティ
const cols = (
(await (
await page.request.get(`/api/projects/${projectId}/todos/columns`)
).json()) as {
columns: { id: number }[];
}
).columns;
await page.request.post(`/api/projects/${projectId}/todos/items`, {
data: { title: unique('Task'), columnId: cols[0].id },
});
// アクティビティ画面に両方のアクションが記録されている
await page.goto(`/projects/${projectId}/activity`);
await expect(page.getByText('掲示板投稿')).toBeVisible();
await expect(page.getByText('ToDo作成')).toBeVisible();
});
});

View File

@ -0,0 +1,69 @@
import { test, expect, type Page } from '@playwright/test';
function unique(prefix: string): string {
return `${prefix}-${Date.now()}-${Math.floor(Math.random() * 1_000_000)}`;
}
async function registerAndLogin(page: Page, email: string, name: string) {
await page.goto('/login');
await page.getByRole('button', { name: '新規登録はこちら' }).click();
await page.getByLabel('表示名').fill(name);
await page.getByLabel('メールアドレス').fill(email);
await page.getByLabel('パスワード').fill('password123');
await page.getByRole('button', { name: '登録する' }).click();
await expect(page).toHaveURL(/\/dashboard/);
}
test.describe('notifications', () => {
test('member-added notification appears and can be marked read', async ({
browser,
}) => {
const ownerEmail = unique('owner') + '@example.com';
const memberEmail = unique('member') + '@example.com';
// メンバーを先に登録(UIで作成+ログイン)
const memberContext = await browser.newContext();
const memberPage = await memberContext.newPage();
await registerAndLogin(memberPage, memberEmail, 'Member');
// オーナー登録+プロジェクト作成
const ownerContext = await browser.newContext();
const ownerPage = await ownerContext.newPage();
await registerAndLogin(ownerPage, ownerEmail, 'Owner');
await ownerPage.getByLabel('プロジェクト名').fill(unique('Proj'));
await ownerPage.getByRole('button', { name: '新規プロジェクト' }).click();
await expect(ownerPage).toHaveURL(/\/projects\/\d+$/);
const projectId = Number(ownerPage.url().match(/\/projects\/(\d+)/)![1]);
// メンバー追加 → project_added 通知がメンバーへ
const addRes = await ownerPage.request.post(
`/api/projects/${projectId}/members`,
{ data: { email: memberEmail, role: 'member' } }
);
expect(addRes.ok()).toBeTruthy();
// メンバーの通知一覧に表示
await memberPage.goto('/notifications');
await expect(
memberPage.getByText('プロジェクトに追加されました')
).toBeVisible();
// 既読化(API) → 一覧が空になる
const listRes = await memberPage.request.get('/api/notifications?page=1');
const { items } = (await listRes.json()) as {
items: { id: number }[];
};
expect(items).toHaveLength(1);
const readRes = await memberPage.request.post(
`/api/notifications/${items[0].id}/read`
);
expect(readRes.ok()).toBeTruthy();
await memberPage.reload();
await expect(
memberPage.getByText('未読の通知はありません。')
).toBeVisible();
await ownerContext.close();
await memberContext.close();
});
});