/**
* Tests for the post-print outcome confirmation dialog (#1898).
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { render } from '../utils';
import { ConfirmOutcomeDialog } from '../../components/ConfirmOutcomeDialog';
import { http, HttpResponse } from 'msw';
import { server } from '../mocks/server';
// The reject flow can hand off into the full PrintModal — not under test here.
vi.mock('../../components/PrintModal', () => ({
PrintModal: () =>
,
}));
const baseArchive = {
id: 42,
printer_id: 1,
filename: 'bracket.gcode.3mf',
file_path: 'archives/bracket.gcode.3mf',
file_size: 1024,
content_hash: null,
thumbnail_path: null,
timelapse_path: null,
print_name: 'Bracket',
print_time_seconds: 3600,
filament_used_grams: 20,
filament_type: 'PLA',
filament_color: null,
layer_height: 0.2,
total_layers: 100,
nozzle_diameter: 0.4,
bed_temperature: 60,
nozzle_temperature: 220,
status: 'completed',
started_at: null,
completed_at: null,
extra_data: null,
makerworld_url: null,
designer: null,
external_url: null,
is_favorite: false,
tags: null,
notes: null,
cost: null,
photos: ['finish_1.jpg'],
failure_reason: null,
user_verdict: null,
user_verdict_source: null,
confirm_requested: true,
quantity: 1,
energy_kwh: null,
energy_cost: null,
created_at: '2026-09-01T00:00:00Z',
created_by_id: null,
created_by_username: null,
run_count: 1,
last_run_at: null,
total_filament_actual_grams: null,
successful_run_count: 1,
failed_run_count: 0,
};
describe('ConfirmOutcomeDialog', () => {
let patchPayload: Record | null;
beforeEach(() => {
patchPayload = null;
server.use(
http.get('/api/v1/archives/42', () => HttpResponse.json(baseArchive)),
http.patch('/api/v1/archives/42', async ({ request }) => {
patchPayload = (await request.json()) as Record;
return HttpResponse.json({ ...baseArchive, ...patchPayload });
}),
http.get('/api/v1/settings/', () => HttpResponse.json({})),
);
});
it('shows the finish photo and records a good verdict', async () => {
const user = userEvent.setup();
const onClose = vi.fn();
render();
await waitFor(() => {
expect(screen.getByText('Bracket')).toBeInTheDocument();
});
// Finish photo (photos[0]) is rendered
expect(screen.getByAltText('Bracket')).toBeInTheDocument();
await user.click(screen.getByText('Good'));
await waitFor(() => {
expect(patchPayload).toEqual({ user_verdict: 'good', user_verdict_source: 'dialog' });
});
await waitFor(() => {
expect(onClose).toHaveBeenCalled();
});
});
it('records a reject with an optional reason', async () => {
const user = userEvent.setup();
const onClose = vi.fn();
render();
await waitFor(() => {
expect(screen.getByText('Bracket')).toBeInTheDocument();
});
await user.click(screen.getByText('Reject'));
// Reason step appears
const select = await screen.findByRole('combobox');
await user.selectOptions(select, 'warping');
await user.click(screen.getByText('Save'));
await waitFor(() => {
expect(patchPayload).toEqual({
user_verdict: 'reject',
failure_reason: 'warping',
user_verdict_source: 'dialog',
});
});
await waitFor(() => {
expect(onClose).toHaveBeenCalled();
});
});
it('reject-and-reprint hands off into the print modal after saving', async () => {
const user = userEvent.setup();
render();
await waitFor(() => {
expect(screen.getByText('Bracket')).toBeInTheDocument();
});
await user.click(screen.getByText('Reject'));
await user.click(await screen.findByText('Print again'));
await waitFor(() => {
expect(patchPayload).toEqual({ user_verdict: 'reject', user_verdict_source: 'dialog' });
});
await waitFor(() => {
expect(screen.getByTestId('print-modal')).toBeInTheDocument();
});
});
it('shows the already-decided state instead of the buttons', async () => {
server.use(
http.get('/api/v1/archives/42', () =>
HttpResponse.json({ ...baseArchive, user_verdict: 'reject' })
),
);
render();
await waitFor(() => {
expect(screen.getByText('Already marked as rejected.')).toBeInTheDocument();
});
expect(screen.queryByText('Good')).not.toBeInTheDocument();
// No source on file (an archive answered before #1898 recorded it): no hint.
expect(screen.queryByTestId('verdict-source-hint')).not.toBeInTheDocument();
});
it('explains a verdict the plate-clear default recorded', async () => {
server.use(
http.get('/api/v1/archives/42', () =>
HttpResponse.json({ ...baseArchive, user_verdict: 'good', user_verdict_source: 'plate_clear' })
),
);
render();
await waitFor(() => {
expect(screen.getByText('Already confirmed as a good part.')).toBeInTheDocument();
});
expect(screen.getByTestId('verdict-source-hint')).toHaveTextContent(
'Recorded when the plate was cleared.'
);
});
});