This commit is contained in:
@@ -211,7 +211,7 @@ it('preserves original evidence context, linked restrictions, and staff history
|
||||
expect(document.querySelector('a[href^="/matches/17"]')).not.toBeNull();
|
||||
for (const label of [
|
||||
'Edit case',
|
||||
'Review case',
|
||||
'Change status',
|
||||
'Reopen case',
|
||||
'Restrict player',
|
||||
'Revoke restriction',
|
||||
@@ -251,12 +251,12 @@ it('locks case restrictions to the recorded identity and revision without closin
|
||||
durationMinutes: 60
|
||||
});
|
||||
expect(fetch.mock.calls[1][0]).toBe('/api/moderation/cases/case-1');
|
||||
expect(button('Review case')).toBeDefined();
|
||||
expect(button('Change status')).toBeDefined();
|
||||
});
|
||||
|
||||
it('records specific outcomes and reopens a closed case with a reason and the latest revision', async () => {
|
||||
it('closes and reopens through lifecycle status with a reason and the latest revision', async () => {
|
||||
const closed = detail();
|
||||
closed.case.status = 'warning_issued';
|
||||
closed.case.status = 'closed';
|
||||
closed.case.revision = 5;
|
||||
const reopened = detail();
|
||||
reopened.case.status = 'open';
|
||||
@@ -269,37 +269,34 @@ it('records specific outcomes and reopens a closed case with a reason and the la
|
||||
.mockResolvedValueOnce(response(reopened));
|
||||
vi.stubGlobal('fetch', fetch);
|
||||
show();
|
||||
button('Review case').click();
|
||||
button('Change status').click();
|
||||
await tick();
|
||||
expect([...field<HTMLSelectElement>('Outcome').options].map((option) => option.value)).toEqual([
|
||||
'resolved',
|
||||
'dismissed',
|
||||
'warning_issued',
|
||||
'restriction_applied',
|
||||
'no_action',
|
||||
'duplicate',
|
||||
'appeal_upheld',
|
||||
'appeal_denied'
|
||||
expect([...field<HTMLSelectElement>('Status').options].map((option) => option.value)).toEqual([
|
||||
'open',
|
||||
'in_review',
|
||||
'closed'
|
||||
]);
|
||||
select('Outcome', 'warning_issued');
|
||||
enter(field('Review reason'), 'Warning was sufficient');
|
||||
select('Status', 'closed');
|
||||
enter(field('Change reason'), 'Warning was sufficient');
|
||||
submit();
|
||||
await vi.waitFor(() => expect(button('Reopen case')).toBeDefined());
|
||||
await vi.waitFor(() => expect(document.querySelector('dialog')?.open).not.toBe(true));
|
||||
expect(JSON.parse(fetch.mock.calls[0][1].body)).toMatchObject({
|
||||
op: 'resolve-case',
|
||||
op: 'update-case',
|
||||
id: 'case-1',
|
||||
revision: 4,
|
||||
status: 'warning_issued',
|
||||
status: 'closed',
|
||||
reason: 'Warning was sufficient'
|
||||
});
|
||||
expect(button('Restrict player')).toBeUndefined();
|
||||
button('Reopen case').click();
|
||||
button('Change status').click();
|
||||
await tick();
|
||||
enter(field('Reopening reason'), 'New evidence received');
|
||||
select('Status', 'open');
|
||||
enter(field('Change reason'), 'New evidence received');
|
||||
submit();
|
||||
await vi.waitFor(() => expect(button('Review case')).toBeDefined());
|
||||
await vi.waitFor(() => expect(fetch).toHaveBeenCalledTimes(4));
|
||||
expect(JSON.parse(fetch.mock.calls[2][1].body)).toMatchObject({
|
||||
op: 'reopen-case',
|
||||
op: 'update-case',
|
||||
status: 'open',
|
||||
id: 'case-1',
|
||||
revision: 5,
|
||||
reason: 'New evidence received'
|
||||
@@ -307,7 +304,7 @@ it('records specific outcomes and reopens a closed case with a reason and the la
|
||||
expect(document.body.textContent).toContain('Check the earlier warning.');
|
||||
});
|
||||
|
||||
it('edits priority, assignment, and review workflow with a required transition reason', async () => {
|
||||
it('edits priority and assignment separately from lifecycle status', async () => {
|
||||
const fetch = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(response({ ok: true, id: 'case-1' }))
|
||||
@@ -319,8 +316,8 @@ it('edits priority, assignment, and review workflow with a required transition r
|
||||
enter(field('Case title'), 'Urgent report');
|
||||
select('Priority', 'urgent');
|
||||
select('Assigned to', 'staff-2');
|
||||
select('Workflow status', 'escalated');
|
||||
expect(field<HTMLTextAreaElement>('Change reason').required).toBe(true);
|
||||
expect(document.querySelector('dialog')?.textContent).not.toContain('Workflow status');
|
||||
expect(field<HTMLTextAreaElement>('Change reason').required).toBe(false);
|
||||
enter(field('Change reason'), 'Needs administrator review');
|
||||
submit();
|
||||
await vi.waitFor(() => expect(fetch).toHaveBeenCalled());
|
||||
@@ -331,14 +328,13 @@ it('edits priority, assignment, and review workflow with a required transition r
|
||||
title: 'Urgent report',
|
||||
priority: 'urgent',
|
||||
assigneeId: 'staff-2',
|
||||
status: 'escalated',
|
||||
reason: 'Needs administrator review'
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves an unavailable assignee when editing metadata on a closed case', async () => {
|
||||
const initial = detail();
|
||||
initial.case.status = 'no_action';
|
||||
initial.case.status = 'closed';
|
||||
initial.case.assigneeId = 'disabled-staff';
|
||||
initial.case.assignee = 'Former staff';
|
||||
const fetch = vi
|
||||
@@ -431,13 +427,11 @@ it('links a prior mute with its recorded outcome without issuing another restric
|
||||
expect(button('Link existing action').disabled).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['warning_issued', 'action:warning-1', 'action', 'warning-1'],
|
||||
['restriction_applied', 'restriction:restriction-1', 'restriction', 'restriction-1']
|
||||
] as const)(
|
||||
'references a linked action when recording %s',
|
||||
async (outcome, selection, type, id) => {
|
||||
it.each(['in_review', 'closed'] as const)(
|
||||
'changes status to %s without replacing findings or issuing an action',
|
||||
async (status) => {
|
||||
const initial = detail();
|
||||
initial.case.status = 'open';
|
||||
initial.linkedActions = [
|
||||
{
|
||||
id: 'warning-1',
|
||||
@@ -476,31 +470,34 @@ it.each([
|
||||
.mockResolvedValue(response(initial));
|
||||
vi.stubGlobal('fetch', fetch);
|
||||
show(initial);
|
||||
button('Review case').click();
|
||||
button('Change status').click();
|
||||
await tick();
|
||||
select('Outcome', outcome);
|
||||
const options = [...field<HTMLSelectElement>('Supporting action').options].map(
|
||||
(option) => option.value
|
||||
);
|
||||
expect(options).toEqual(['', selection]);
|
||||
select('Supporting action', selection);
|
||||
enter(field('Review reason'), 'Reviewed the recorded action');
|
||||
expect(button('Save').disabled).toBe(true);
|
||||
select('Status', status);
|
||||
await tick();
|
||||
expect(button('Save').disabled).toBe(false);
|
||||
expect(document.querySelector('dialog')?.textContent).not.toContain('Supporting action');
|
||||
enter(field('Change reason'), 'Reviewed the recorded action');
|
||||
submit();
|
||||
await vi.waitFor(() => expect(fetch).toHaveBeenCalledTimes(2));
|
||||
expect(JSON.parse(fetch.mock.calls[0][1].body)).toMatchObject({
|
||||
op: 'resolve-case',
|
||||
op: 'update-case',
|
||||
id: 'case-1',
|
||||
revision: 4,
|
||||
status: outcome,
|
||||
actionReference: { type, id }
|
||||
status
|
||||
});
|
||||
expect(JSON.parse(fetch.mock.calls[0][1].body)).not.toHaveProperty('actionReference');
|
||||
expect(document.querySelector('[aria-labelledby="actions-title"]')?.textContent).toContain(
|
||||
'Confirmed warning'
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
it('retains a conflicted review and lets staff refresh before making another decision', async () => {
|
||||
const updated = detail();
|
||||
updated.case.revision = 8;
|
||||
updated.case.status = 'escalated';
|
||||
updated.case.title = 'Updated case after concurrent edit';
|
||||
updated.case.status = 'in_review';
|
||||
const fetch = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(
|
||||
@@ -509,28 +506,29 @@ it('retains a conflicted review and lets staff refresh before making another dec
|
||||
.mockResolvedValue(response(updated));
|
||||
vi.stubGlobal('fetch', fetch);
|
||||
show();
|
||||
button('Review case').click();
|
||||
button('Change status').click();
|
||||
await tick();
|
||||
select('Outcome', 'appeal_denied');
|
||||
enter(field('Review reason'), 'Evidence supports the restriction');
|
||||
select('Status', 'closed');
|
||||
enter(field('Change reason'), 'Evidence supports the restriction');
|
||||
submit();
|
||||
await vi.waitFor(() =>
|
||||
expect(document.querySelector('dialog [role="alert"]')?.textContent).toContain(
|
||||
'This case changed.'
|
||||
)
|
||||
);
|
||||
expect(field<HTMLSelectElement>('Outcome').value).toBe('appeal_denied');
|
||||
expect(field<HTMLSelectElement>('Status').value).toBe('closed');
|
||||
expect(document.querySelector('dialog')!.open).toBe(true);
|
||||
expect(button('Reopen case')).toBeUndefined();
|
||||
button('Cancel').click();
|
||||
await tick();
|
||||
button('Refresh').click();
|
||||
await vi.waitFor(() =>
|
||||
expect(document.querySelector('.case-summary')?.textContent).toContain('Escalated')
|
||||
expect(document.querySelector('h1')?.textContent).toBe('Updated case after concurrent edit')
|
||||
);
|
||||
button('Review case').click();
|
||||
button('Change status').click();
|
||||
await tick();
|
||||
enter(field('Review reason'), 'Reviewed new context');
|
||||
select('Status', 'closed');
|
||||
enter(field('Change reason'), 'Reviewed new context');
|
||||
submit();
|
||||
await vi.waitFor(() => expect(fetch.mock.calls.length).toBeGreaterThanOrEqual(3));
|
||||
expect(JSON.parse(fetch.mock.calls[2][1].body).revision).toBe(8);
|
||||
|
||||
@@ -21,7 +21,7 @@ const item: ModerationCase = {
|
||||
game: 't5',
|
||||
guid: '123',
|
||||
title: 'Chat review',
|
||||
status: 'resolved',
|
||||
status: 'closed',
|
||||
priority: 'normal',
|
||||
assignee: null,
|
||||
assigneeId: null,
|
||||
|
||||
@@ -353,7 +353,7 @@ it('links case records to workspaces and preserves case filters while paging', (
|
||||
actor: 'Moderator',
|
||||
createdAt: '2026-10-16T12:00:00Z'
|
||||
},
|
||||
status: 'escalated',
|
||||
status: 'in_review',
|
||||
priority: 'high',
|
||||
assigneeId: 'staff-1',
|
||||
assignee: 'Moderator',
|
||||
@@ -369,7 +369,7 @@ it('links case records to workspaces and preserves case filters while paging', (
|
||||
expect(document.querySelector('a[href="/moderation/cases/case-1"]')?.textContent).toBe(
|
||||
'Chat investigation'
|
||||
);
|
||||
expect(document.querySelector('#moderation-results')?.textContent).toContain('Escalated');
|
||||
expect(document.querySelector('#moderation-results')?.textContent).toContain('In progress');
|
||||
expect(document.querySelector('#moderation-results')?.textContent).toContain('Moderator');
|
||||
expect(document.querySelector('#moderation-results')?.textContent).toContain(
|
||||
'The threat remains ambiguous.'
|
||||
|
||||
Reference in New Issue
Block a user