fix(m6): mission detail page can now edit metadata, append scenarios, edit members

The M6 SPA shipped the create wizard but the detail page was read-only —
even though the backend already exposed PUT /missions/{id}, POST
/missions/{id}/scenarios, and PUT /missions/{id}/members. So once a
mission was created you couldn't fix a typo in the client name, add a
scenario you forgot, or change member assignments without curl.

Added three modals on the detail page, gated by `is_admin ||
mission.update`:

- Edit metadata (header button, 3xl modal): name + client + dates +
  markdown description, same validation as the wizard step 1.
- Add scenarios (Tests tab): scenario picker matching wizard step 2,
  calls POST /missions/{id}/scenarios which appends snapshots at
  current_max_position + 1.
- Edit members (Members tab): roster + red/blue toggles, calls
  PUT /missions/{id}/members (full-set replace), pre-populated with
  the current member set.

The detail page now imports useAuth so `canEdit` is computed once and
shared between the three buttons.

E2E: new "detail page edits metadata, appends scenarios, edits members"
spec exercises the three modals end-to-end. M6 e2e count is now 6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Knacky
2026-05-14 07:37:06 +02:00
parent 4d2b6731ac
commit b62651a215
3 changed files with 590 additions and 3 deletions

View File

@@ -292,6 +292,90 @@ test.describe('M6 — Missions', () => {
await expect(page.getByText('spa-wizard-t3')).toBeVisible();
});
test('SPA — detail page edits metadata, appends scenarios, edits members', async ({
page,
request,
}) => {
const auth = await adminAuth(request);
// Pre-seed: one mission with one initial scenario; a second scenario to
// append; and a second user we can assign as a member from the SPA.
const initialTestId = await makeTest(request, auth, 'spa-edit-initial-t');
const initialScenarioId = await makeScenario(
request,
auth,
'spa-edit-initial-scenario',
[initialTestId],
);
const extraTestId = await makeTest(request, auth, 'spa-edit-appended-t');
const extraScenarioId = await makeScenario(
request,
auth,
'spa-edit-appended-scenario',
[extraTestId],
);
const mission = await request
.post('/api/v1/missions', {
headers: auth,
data: {
name: 'spa-edit-target',
client_target: 'Initial Co.',
scenario_template_ids: [initialScenarioId],
},
})
.then((r) => r.json());
// A second user the admin can add as a member via the modal.
const teammateEmail = `spa-edit-mate-${crypto.randomUUID().slice(0, 8)}@metamorph.local`;
const inv = await request
.post('/api/v1/invitations', {
headers: auth,
data: { email_hint: teammateEmail },
})
.then((r) => r.json());
await request.post(`/api/v1/invitations/accept/${inv.token}`, {
data: { email: teammateEmail, password: 'MatePass1234!' },
});
await loginViaSpa(page, ADMIN_EMAIL, ADMIN_PASSWORD);
await page.goto(`/missions/${mission.id}`);
await expect(page.getByText('Initial Co.')).toBeVisible();
// --- Edit metadata --------------------------------------------------
await page.getByTestId('mission-edit-meta').click();
const metaModal = page.getByTestId('mission-edit-meta-modal');
await expect(metaModal).toBeVisible();
await metaModal.getByTestId('meta-edit-client').fill('Renamed Co.');
await metaModal.getByTestId('meta-edit-save').click();
await expect(metaModal).toBeHidden();
await expect(page.getByText('Renamed Co.')).toBeVisible();
// --- Append a scenario ---------------------------------------------
await page.getByTestId('mission-add-scenarios').click();
const addModal = page.getByTestId('mission-add-scenarios-modal');
await expect(addModal).toBeVisible();
await addModal.getByTestId(`add-scenario-toggle-${extraScenarioId}`).click();
await addModal.getByTestId('add-scenarios-save').click();
await expect(addModal).toBeHidden();
// Both scenarios now visible in the Tests tab
await expect(page.getByText('spa-edit-initial-scenario')).toBeVisible();
await expect(page.getByText('spa-edit-appended-scenario')).toBeVisible();
await expect(page.getByText('spa-edit-appended-t')).toBeVisible();
// --- Edit members ---------------------------------------------------
await page.getByTestId('mission-tab-members').click();
await page.getByTestId('mission-edit-members').click();
const memModal = page.getByTestId('mission-edit-members-modal');
await expect(memModal).toBeVisible();
// The roster row test-ids encode the new user's id; we don't know it here
// but the email is unique, so locate the row by email text and toggle red.
const teammateRow = memModal.getByText(teammateEmail).locator('..').locator('..');
await teammateRow.getByRole('button', { name: /red/i }).click();
await memModal.getByTestId('edit-members-save').click();
await expect(memModal).toBeHidden();
await expect(page.getByText(teammateEmail)).toBeVisible();
});
test('SPA — list page filters by status', async ({ page, request }) => {
const auth = await adminAuth(request);
// Seed two missions with distinct statuses.