|
|
@@ -22,6 +22,10 @@ vi.mock('../../api/client', () => ({
|
|
|
sliceLibraryFile: vi.fn(),
|
|
|
sliceArchive: vi.fn(),
|
|
|
getSliceJob: vi.fn(),
|
|
|
+ getLibraryFilePlates: vi.fn(),
|
|
|
+ getArchivePlates: vi.fn(),
|
|
|
+ getLibraryFileFilamentRequirements: vi.fn(),
|
|
|
+ getArchiveFilamentRequirements: vi.fn(),
|
|
|
getSettings: vi.fn().mockResolvedValue({}),
|
|
|
updateSettings: vi.fn().mockResolvedValue({}),
|
|
|
},
|
|
|
@@ -32,6 +36,10 @@ const mockApi = api as unknown as {
|
|
|
sliceLibraryFile: ReturnType<typeof vi.fn>;
|
|
|
sliceArchive: ReturnType<typeof vi.fn>;
|
|
|
getSliceJob: ReturnType<typeof vi.fn>;
|
|
|
+ getLibraryFilePlates: ReturnType<typeof vi.fn>;
|
|
|
+ getArchivePlates: ReturnType<typeof vi.fn>;
|
|
|
+ getLibraryFileFilamentRequirements: ReturnType<typeof vi.fn>;
|
|
|
+ getArchiveFilamentRequirements: ReturnType<typeof vi.fn>;
|
|
|
};
|
|
|
|
|
|
function makeUnified(overrides: Partial<UnifiedPresetsResponse> = {}): UnifiedPresetsResponse {
|
|
|
@@ -84,6 +92,33 @@ describe('SliceModal', () => {
|
|
|
started_at: null,
|
|
|
completed_at: null,
|
|
|
});
|
|
|
+ // Default: single-plate (or non-3MF). Multi-plate tests override this.
|
|
|
+ mockApi.getLibraryFilePlates.mockResolvedValue({
|
|
|
+ file_id: 100,
|
|
|
+ filename: 'Cube.stl',
|
|
|
+ plates: [],
|
|
|
+ is_multi_plate: false,
|
|
|
+ });
|
|
|
+ mockApi.getArchivePlates.mockResolvedValue({
|
|
|
+ archive_id: 100,
|
|
|
+ filename: 'Cube.3mf',
|
|
|
+ plates: [],
|
|
|
+ is_multi_plate: false,
|
|
|
+ });
|
|
|
+ // Default: no per-plate filament metadata available (mirrors STL or
|
|
|
+ // unsliced source). Multi-color tests override this.
|
|
|
+ mockApi.getLibraryFileFilamentRequirements.mockResolvedValue({
|
|
|
+ file_id: 100,
|
|
|
+ filename: 'Cube.stl',
|
|
|
+ plate_id: 1,
|
|
|
+ filaments: [],
|
|
|
+ });
|
|
|
+ mockApi.getArchiveFilamentRequirements.mockResolvedValue({
|
|
|
+ archive_id: 100,
|
|
|
+ filename: 'Cube.3mf',
|
|
|
+ plate_id: 1,
|
|
|
+ filaments: [],
|
|
|
+ });
|
|
|
});
|
|
|
|
|
|
it('auto-selects the highest-priority tier per slot on first load', async () => {
|
|
|
@@ -92,44 +127,45 @@ describe('SliceModal', () => {
|
|
|
onClose: vi.fn(),
|
|
|
});
|
|
|
|
|
|
- // The cloud tier wins — printer dropdown should land on the cloud entry.
|
|
|
+ // SliceModal-specific tier priority: imported (local) wins over cloud
|
|
|
+ // and standard so the user's curated picks come first.
|
|
|
await waitFor(() => {
|
|
|
expect(screen.getByText('My Custom X1C')).toBeDefined();
|
|
|
});
|
|
|
const selects = screen.getAllByRole('combobox') as HTMLSelectElement[];
|
|
|
expect(selects).toHaveLength(3);
|
|
|
- expect(selects[0].value).toBe('cloud:PFUcloud-printer');
|
|
|
- expect(selects[1].value).toBe('cloud:PFUcloud-process');
|
|
|
- expect(selects[2].value).toBe('cloud:PFUcloud-filament');
|
|
|
+ expect(selects[0].value).toBe('local:1');
|
|
|
+ expect(selects[1].value).toBe('local:2');
|
|
|
+ expect(selects[2].value).toBe('local:3');
|
|
|
|
|
|
// Slice button is enabled because all three slots auto-defaulted.
|
|
|
const sliceBtn = screen.getByRole('button', { name: /^Slice$/ });
|
|
|
expect((sliceBtn as HTMLButtonElement).disabled).toBe(false);
|
|
|
});
|
|
|
|
|
|
- it('renders Cloud / Imported / Standard sections via <optgroup>', async () => {
|
|
|
+ it('renders Imported / Cloud / Standard sections via <optgroup>', async () => {
|
|
|
renderWithTracker({
|
|
|
source: { kind: 'libraryFile', id: 100, filename: 'Cube.stl' },
|
|
|
onClose: vi.fn(),
|
|
|
});
|
|
|
|
|
|
- await waitFor(() => expect(screen.getByText('My Custom X1C')).toBeDefined());
|
|
|
+ await waitFor(() => expect(screen.getByText('Imported X1C 0.4')).toBeDefined());
|
|
|
|
|
|
const printerSelect = screen.getAllByRole('combobox')[0];
|
|
|
const groups = printerSelect.querySelectorAll('optgroup');
|
|
|
expect(Array.from(groups).map((g) => g.label)).toEqual([
|
|
|
- 'Cloud',
|
|
|
'Imported',
|
|
|
+ 'Cloud',
|
|
|
'Standard',
|
|
|
]);
|
|
|
|
|
|
- // The cloud entry sits inside the Cloud group, the local entry inside
|
|
|
- // Imported, the standard entry inside Standard — pin the assignment so
|
|
|
- // a future render-shape change can't quietly mix them.
|
|
|
- const cloudGroup = groups[0];
|
|
|
- expect(within(cloudGroup as HTMLElement).getByText('My Custom X1C')).toBeDefined();
|
|
|
- const localGroup = groups[1];
|
|
|
+ // Each entry sits inside its own tier's group — pin the assignment so
|
|
|
+ // a future render-shape change can't quietly mix them. Order matches
|
|
|
+ // SLICE_MODAL_TIER_ORDER (local → cloud → standard).
|
|
|
+ const localGroup = groups[0];
|
|
|
expect(within(localGroup as HTMLElement).getByText('Imported X1C 0.4')).toBeDefined();
|
|
|
+ const cloudGroup = groups[1];
|
|
|
+ expect(within(cloudGroup as HTMLElement).getByText('My Custom X1C')).toBeDefined();
|
|
|
const standardGroup = groups[2];
|
|
|
expect(within(standardGroup as HTMLElement).getByText('Bambu Lab X1 Carbon 0.4 nozzle')).toBeDefined();
|
|
|
});
|
|
|
@@ -184,10 +220,14 @@ describe('SliceModal', () => {
|
|
|
await user.click(screen.getByRole('button', { name: /^Slice$/ }));
|
|
|
|
|
|
await waitFor(() => {
|
|
|
+ // SliceModal-specific tier priority puts imported (local) above cloud,
|
|
|
+ // so the auto-pick lands on the local entries even when a cloud entry
|
|
|
+ // with the same slot is also available in the listing.
|
|
|
expect(mockApi.sliceLibraryFile).toHaveBeenCalledWith(100, {
|
|
|
- printer_preset: { source: 'cloud', id: 'PFUcloud-printer' },
|
|
|
- process_preset: { source: 'cloud', id: 'PFUcloud-process' },
|
|
|
- filament_preset: { source: 'cloud', id: 'PFUcloud-filament' },
|
|
|
+ printer_preset: { source: 'local', id: '1' },
|
|
|
+ process_preset: { source: 'local', id: '2' },
|
|
|
+ filament_preset: { source: 'local', id: '3' },
|
|
|
+ filament_presets: [{ source: 'local', id: '3' }],
|
|
|
});
|
|
|
});
|
|
|
await waitFor(() => expect(onClose).toHaveBeenCalled());
|
|
|
@@ -324,4 +364,331 @@ describe('SliceModal', () => {
|
|
|
// No status-role banner should be rendered on the happy path.
|
|
|
expect(screen.queryByRole('status')).toBeNull();
|
|
|
});
|
|
|
+
|
|
|
+ // ----- Multi-plate flow -----------------------------------------------
|
|
|
+
|
|
|
+ function makeMultiPlateLibraryResponse() {
|
|
|
+ return {
|
|
|
+ file_id: 100,
|
|
|
+ filename: 'Multi.3mf',
|
|
|
+ is_multi_plate: true,
|
|
|
+ plates: [
|
|
|
+ {
|
|
|
+ index: 1,
|
|
|
+ name: 'Plate 1',
|
|
|
+ objects: ['Cube'],
|
|
|
+ object_count: 1,
|
|
|
+ has_thumbnail: false,
|
|
|
+ thumbnail_url: null,
|
|
|
+ print_time_seconds: 600,
|
|
|
+ filament_used_grams: 10,
|
|
|
+ filaments: [],
|
|
|
+ },
|
|
|
+ {
|
|
|
+ index: 2,
|
|
|
+ name: 'Plate 2',
|
|
|
+ objects: ['Pyramid'],
|
|
|
+ object_count: 1,
|
|
|
+ has_thumbnail: false,
|
|
|
+ thumbnail_url: null,
|
|
|
+ print_time_seconds: 800,
|
|
|
+ filament_used_grams: 12,
|
|
|
+ filaments: [],
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ it('shows the plate picker first for multi-plate library files', async () => {
|
|
|
+ mockApi.getLibraryFilePlates.mockResolvedValue(makeMultiPlateLibraryResponse());
|
|
|
+ renderWithTracker({
|
|
|
+ source: { kind: 'libraryFile', id: 100, filename: 'Multi.3mf' },
|
|
|
+ onClose: vi.fn(),
|
|
|
+ });
|
|
|
+
|
|
|
+ // Plate picker renders one button per plate — the accessible name
|
|
|
+ // joins the heading ("Plate N — name") with the object summary line.
|
|
|
+ await screen.findByRole('button', { name: /Plate 1.*Cube/ });
|
|
|
+ expect(screen.getByRole('button', { name: /Plate 2.*Pyramid/ })).toBeDefined();
|
|
|
+ // Profile dropdowns must NOT be visible yet — the user has to pick a
|
|
|
+ // plate first.
|
|
|
+ expect(screen.queryByRole('combobox')).toBeNull();
|
|
|
+ });
|
|
|
+
|
|
|
+ it('skips the plate picker for single-plate sources', async () => {
|
|
|
+ mockApi.getLibraryFilePlates.mockResolvedValue({
|
|
|
+ file_id: 100,
|
|
|
+ filename: 'Single.3mf',
|
|
|
+ is_multi_plate: false,
|
|
|
+ plates: [
|
|
|
+ {
|
|
|
+ index: 1,
|
|
|
+ name: 'Plate 1',
|
|
|
+ objects: [],
|
|
|
+ has_thumbnail: false,
|
|
|
+ thumbnail_url: null,
|
|
|
+ print_time_seconds: null,
|
|
|
+ filament_used_grams: null,
|
|
|
+ filaments: [],
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ });
|
|
|
+ renderWithTracker({
|
|
|
+ source: { kind: 'libraryFile', id: 100, filename: 'Single.3mf' },
|
|
|
+ onClose: vi.fn(),
|
|
|
+ });
|
|
|
+
|
|
|
+ // Should jump straight to the profile dropdowns.
|
|
|
+ await waitFor(() => expect(screen.getByText('My Custom X1C')).toBeDefined());
|
|
|
+ });
|
|
|
+
|
|
|
+ it('passes the picked plate to the slice request', async () => {
|
|
|
+ mockApi.getLibraryFilePlates.mockResolvedValue(makeMultiPlateLibraryResponse());
|
|
|
+ mockApi.sliceLibraryFile.mockResolvedValue({
|
|
|
+ job_id: 42,
|
|
|
+ status: 'pending',
|
|
|
+ status_url: '/api/v1/slice-jobs/42',
|
|
|
+ });
|
|
|
+
|
|
|
+ renderWithTracker({
|
|
|
+ source: { kind: 'libraryFile', id: 100, filename: 'Multi.3mf' },
|
|
|
+ onClose: vi.fn(),
|
|
|
+ });
|
|
|
+
|
|
|
+ const user = userEvent.setup();
|
|
|
+ // Step 1: pick Plate 2.
|
|
|
+ const plate2Button = await screen.findByRole('button', { name: /Plate 2.*Pyramid/ });
|
|
|
+ await user.click(plate2Button);
|
|
|
+
|
|
|
+ // Step 2: profile dropdowns are now visible.
|
|
|
+ await waitFor(() => expect(screen.getByText('My Custom X1C')).toBeDefined());
|
|
|
+
|
|
|
+ // Step 3: submit and verify the plate index made it into the body.
|
|
|
+ await user.click(screen.getByRole('button', { name: /^Slice$/ }));
|
|
|
+ await waitFor(() => {
|
|
|
+ expect(mockApi.sliceLibraryFile).toHaveBeenCalledWith(
|
|
|
+ 100,
|
|
|
+ expect.objectContaining({ plate: 2 }),
|
|
|
+ );
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ it('routes the plate fetch through getArchivePlates for archive sources', async () => {
|
|
|
+ mockApi.getArchivePlates.mockResolvedValue({
|
|
|
+ ...makeMultiPlateLibraryResponse(),
|
|
|
+ archive_id: 100,
|
|
|
+ filename: 'Multi.3mf',
|
|
|
+ });
|
|
|
+ renderWithTracker({
|
|
|
+ source: { kind: 'archive', id: 100, filename: 'Multi.3mf' },
|
|
|
+ onClose: vi.fn(),
|
|
|
+ });
|
|
|
+
|
|
|
+ await screen.findByRole('button', { name: /Plate 1.*Cube/ });
|
|
|
+ expect(mockApi.getArchivePlates).toHaveBeenCalledWith(100);
|
|
|
+ expect(mockApi.getLibraryFilePlates).not.toHaveBeenCalled();
|
|
|
+ });
|
|
|
+
|
|
|
+ it('cancelling the plate picker closes the entire slice flow', async () => {
|
|
|
+ const onClose = vi.fn();
|
|
|
+ mockApi.getLibraryFilePlates.mockResolvedValue(makeMultiPlateLibraryResponse());
|
|
|
+ renderWithTracker({
|
|
|
+ source: { kind: 'libraryFile', id: 100, filename: 'Multi.3mf' },
|
|
|
+ onClose,
|
|
|
+ });
|
|
|
+
|
|
|
+ await screen.findByRole('button', { name: /Plate 1.*Cube/ });
|
|
|
+
|
|
|
+ const user = userEvent.setup();
|
|
|
+ await user.click(screen.getByRole('button', { name: /^Close$/i }));
|
|
|
+
|
|
|
+ expect(onClose).toHaveBeenCalled();
|
|
|
+ });
|
|
|
+
|
|
|
+ it('omits the plate field when the source is single-plate', async () => {
|
|
|
+ mockApi.sliceLibraryFile.mockResolvedValue({
|
|
|
+ job_id: 42,
|
|
|
+ status: 'pending',
|
|
|
+ status_url: '/api/v1/slice-jobs/42',
|
|
|
+ });
|
|
|
+
|
|
|
+ renderWithTracker({
|
|
|
+ source: { kind: 'libraryFile', id: 100, filename: 'Cube.stl' },
|
|
|
+ onClose: vi.fn(),
|
|
|
+ });
|
|
|
+
|
|
|
+ await waitFor(() => expect(screen.getByText('My Custom X1C')).toBeDefined());
|
|
|
+
|
|
|
+ const user = userEvent.setup();
|
|
|
+ await user.click(screen.getByRole('button', { name: /^Slice$/ }));
|
|
|
+
|
|
|
+ await waitFor(() => {
|
|
|
+ const [, body] = mockApi.sliceLibraryFile.mock.calls[0];
|
|
|
+ expect(body).not.toHaveProperty('plate');
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ // ----- Multi-color flow ------------------------------------------------
|
|
|
+
|
|
|
+ function makeMultiColorPlateResponse() {
|
|
|
+ // Single-plate 3MF that uses two filament slots — mirrors the realistic
|
|
|
+ // "I have a multi-color file with one plate" case. Multi-plate is a
|
|
|
+ // separate axis that's already covered above.
|
|
|
+ return {
|
|
|
+ file_id: 100,
|
|
|
+ filename: 'TwoColor.3mf',
|
|
|
+ is_multi_plate: false,
|
|
|
+ plates: [
|
|
|
+ {
|
|
|
+ index: 1,
|
|
|
+ name: 'Plate 1',
|
|
|
+ objects: ['Logo'],
|
|
|
+ object_count: 1,
|
|
|
+ has_thumbnail: false,
|
|
|
+ thumbnail_url: null,
|
|
|
+ print_time_seconds: 600,
|
|
|
+ filament_used_grams: 20,
|
|
|
+ filaments: [],
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ function makeMultiColorRequirementsResponse() {
|
|
|
+ return {
|
|
|
+ file_id: 100,
|
|
|
+ filename: 'TwoColor.3mf',
|
|
|
+ plate_id: 1,
|
|
|
+ filaments: [
|
|
|
+ { slot_id: 1, type: 'PLA', color: '#000000', used_grams: 10, used_meters: 3 },
|
|
|
+ { slot_id: 2, type: 'PLA', color: '#FFFFFF', used_grams: 10, used_meters: 3 },
|
|
|
+ ],
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ function makeColorAwarePresets(): UnifiedPresetsResponse {
|
|
|
+ // Two filament presets in cloud: one black PLA, one white PLA. Pre-pick
|
|
|
+ // should match each plate slot to the same-colour preset so the user
|
|
|
+ // doesn't have to manually align them.
|
|
|
+ return {
|
|
|
+ cloud: {
|
|
|
+ printer: [{ id: 'P1', name: 'X1C', source: 'cloud' }],
|
|
|
+ process: [{ id: 'PR1', name: '0.20mm', source: 'cloud' }],
|
|
|
+ filament: [
|
|
|
+ { id: 'F-BLACK', name: 'Cloud PLA Black', source: 'cloud', filament_type: 'PLA', filament_colour: '#000000' },
|
|
|
+ { id: 'F-WHITE', name: 'Cloud PLA White', source: 'cloud', filament_type: 'PLA', filament_colour: '#FFFFFF' },
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ local: { printer: [], process: [], filament: [] },
|
|
|
+ standard: { printer: [], process: [], filament: [] },
|
|
|
+ cloud_status: 'ok',
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ it('renders one filament dropdown per plate slot when the source is multi-color', async () => {
|
|
|
+ mockApi.getLibraryFilePlates.mockResolvedValue(makeMultiColorPlateResponse());
|
|
|
+ mockApi.getLibraryFileFilamentRequirements.mockResolvedValue(makeMultiColorRequirementsResponse());
|
|
|
+ mockApi.getSlicerPresets.mockResolvedValue(makeColorAwarePresets());
|
|
|
+
|
|
|
+ renderWithTracker({
|
|
|
+ source: { kind: 'libraryFile', id: 100, filename: 'TwoColor.3mf' },
|
|
|
+ onClose: vi.fn(),
|
|
|
+ });
|
|
|
+
|
|
|
+ await waitFor(() => expect(screen.getByText('X1C')).toBeDefined());
|
|
|
+ // 1 printer + 1 process + 2 filament = 4 dropdowns.
|
|
|
+ expect(screen.getAllByRole('combobox')).toHaveLength(4);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('pre-picks each filament slot by matching colour metadata', async () => {
|
|
|
+ mockApi.getLibraryFilePlates.mockResolvedValue(makeMultiColorPlateResponse());
|
|
|
+ mockApi.getLibraryFileFilamentRequirements.mockResolvedValue(makeMultiColorRequirementsResponse());
|
|
|
+ mockApi.getSlicerPresets.mockResolvedValue(makeColorAwarePresets());
|
|
|
+ mockApi.sliceLibraryFile.mockResolvedValue({
|
|
|
+ job_id: 42,
|
|
|
+ status: 'pending',
|
|
|
+ status_url: '/api/v1/slice-jobs/42',
|
|
|
+ });
|
|
|
+
|
|
|
+ renderWithTracker({
|
|
|
+ source: { kind: 'libraryFile', id: 100, filename: 'TwoColor.3mf' },
|
|
|
+ onClose: vi.fn(),
|
|
|
+ });
|
|
|
+
|
|
|
+ await waitFor(() => expect(screen.getByText('X1C')).toBeDefined());
|
|
|
+
|
|
|
+ const user = userEvent.setup();
|
|
|
+ await user.click(screen.getByRole('button', { name: /^Slice$/ }));
|
|
|
+
|
|
|
+ await waitFor(() => {
|
|
|
+ const [, body] = mockApi.sliceLibraryFile.mock.calls[0];
|
|
|
+ // Slot 1 was black plate → cloud black preset; slot 2 was white →
|
|
|
+ // cloud white preset. Pre-pick aligns them by metadata so the user
|
|
|
+ // doesn't have to swap them manually.
|
|
|
+ expect(body.filament_presets).toEqual([
|
|
|
+ { source: 'cloud', id: 'F-BLACK' },
|
|
|
+ { source: 'cloud', id: 'F-WHITE' },
|
|
|
+ ]);
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ it('still sends the legacy filament_preset for single-color flows', async () => {
|
|
|
+ // Backwards-compat with backends / proxies that read the singular field.
|
|
|
+ mockApi.sliceLibraryFile.mockResolvedValue({
|
|
|
+ job_id: 42,
|
|
|
+ status: 'pending',
|
|
|
+ status_url: '/api/v1/slice-jobs/42',
|
|
|
+ });
|
|
|
+
|
|
|
+ renderWithTracker({
|
|
|
+ source: { kind: 'libraryFile', id: 100, filename: 'Cube.stl' },
|
|
|
+ onClose: vi.fn(),
|
|
|
+ });
|
|
|
+
|
|
|
+ await waitFor(() => expect(screen.getByText('My Custom X1C')).toBeDefined());
|
|
|
+
|
|
|
+ const user = userEvent.setup();
|
|
|
+ await user.click(screen.getByRole('button', { name: /^Slice$/ }));
|
|
|
+
|
|
|
+ await waitFor(() => {
|
|
|
+ const [, body] = mockApi.sliceLibraryFile.mock.calls[0];
|
|
|
+ // Single-color path mirrors the array's first entry into the legacy
|
|
|
+ // singular so older backend clients that only know about
|
|
|
+ // `filament_preset` still work.
|
|
|
+ expect(body.filament_preset).toEqual(body.filament_presets[0]);
|
|
|
+ expect(body.filament_presets).toHaveLength(1);
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ it('lets the user override a pre-picked filament slot', async () => {
|
|
|
+ mockApi.getLibraryFilePlates.mockResolvedValue(makeMultiColorPlateResponse());
|
|
|
+ mockApi.getLibraryFileFilamentRequirements.mockResolvedValue(makeMultiColorRequirementsResponse());
|
|
|
+ mockApi.getSlicerPresets.mockResolvedValue(makeColorAwarePresets());
|
|
|
+ mockApi.sliceLibraryFile.mockResolvedValue({
|
|
|
+ job_id: 42,
|
|
|
+ status: 'pending',
|
|
|
+ status_url: '/api/v1/slice-jobs/42',
|
|
|
+ });
|
|
|
+
|
|
|
+ renderWithTracker({
|
|
|
+ source: { kind: 'libraryFile', id: 100, filename: 'TwoColor.3mf' },
|
|
|
+ onClose: vi.fn(),
|
|
|
+ });
|
|
|
+
|
|
|
+ await waitFor(() => expect(screen.getByText('X1C')).toBeDefined());
|
|
|
+
|
|
|
+ const user = userEvent.setup();
|
|
|
+ const selects = screen.getAllByRole('combobox') as HTMLSelectElement[];
|
|
|
+ // Slots 0 (printer) and 1 (process) are auto-picked. Slots 2 and 3 are
|
|
|
+ // the two filament dropdowns. Swap slot-2 (was black) to white.
|
|
|
+ await user.selectOptions(selects[2], 'cloud:F-WHITE');
|
|
|
+ await user.click(screen.getByRole('button', { name: /^Slice$/ }));
|
|
|
+
|
|
|
+ await waitFor(() => {
|
|
|
+ const [, body] = mockApi.sliceLibraryFile.mock.calls[0];
|
|
|
+ expect(body.filament_presets[0]).toEqual({ source: 'cloud', id: 'F-WHITE' });
|
|
|
+ // Slot 1 stayed at the auto-picked white.
|
|
|
+ expect(body.filament_presets[1]).toEqual({ source: 'cloud', id: 'F-WHITE' });
|
|
|
+ });
|
|
|
+ });
|
|
|
});
|