/**
* Tests for LocalProfilesView component.
*/
import React from 'react';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { screen, waitFor, fireEvent, render as rawRender } from '@testing-library/react';
import { http, HttpResponse } from 'msw';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BrowserRouter } from 'react-router-dom';
import { server } from '../mocks/server';
import { render } from '../utils';
import { ThemeProvider } from '../../contexts/ThemeContext';
import { ToastProvider } from '../../contexts/ToastContext';
import { AuthProvider } from '../../contexts/AuthContext';
import { LocalProfilesView } from '../../components/LocalProfilesView';
const mockLocalPresets = {
filament: [
{
id: 1,
name: 'Overture PLA Matte @BBL X1C',
preset_type: 'filament',
source: 'orcaslicer',
filament_type: 'PLA',
filament_vendor: 'Overture',
nozzle_temp_min: 190,
nozzle_temp_max: 230,
pressure_advance: '["0.04"]',
default_filament_colour: '["#FFAA00"]',
filament_cost: '24.99',
filament_density: '1.24',
compatible_printers: '["Bambu Lab X1 Carbon 0.4 nozzle"]',
inherits: 'Bambu PLA Basic @BBL X1C',
version: '2.3.0.4',
created_at: '2026-01-01T00:00:00Z',
updated_at: '2026-01-01T00:00:00Z',
},
{
id: 2,
name: 'eSUN PETG @Bambu Lab H2D',
preset_type: 'filament',
source: 'orcaslicer',
filament_type: 'PETG',
filament_vendor: null,
nozzle_temp_min: 220,
nozzle_temp_max: 250,
pressure_advance: null,
default_filament_colour: null,
filament_cost: null,
filament_density: null,
compatible_printers: null,
inherits: null,
version: null,
created_at: '2026-01-01T00:00:00Z',
updated_at: '2026-01-01T00:00:00Z',
},
],
process: [
{
id: 3,
name: '0.20mm Standard @BBL X1C',
preset_type: 'process',
source: 'orcaslicer',
filament_type: null,
filament_vendor: null,
nozzle_temp_min: null,
nozzle_temp_max: null,
pressure_advance: null,
default_filament_colour: null,
filament_cost: null,
filament_density: null,
compatible_printers: null,
inherits: '0.20mm Standard @BBL X1C',
version: '2.3.0.4',
created_at: '2026-01-01T00:00:00Z',
updated_at: '2026-01-01T00:00:00Z',
},
],
printer: [],
};
describe('LocalProfilesView', () => {
beforeEach(() => {
server.use(
http.get('/api/v1/local-presets/', () => {
return HttpResponse.json(mockLocalPresets);
}),
http.delete('/api/v1/local-presets/:id', () => {
return HttpResponse.json({ success: true });
}),
);
});
it('renders filament and process columns', async () => {
render();
await waitFor(() => {
expect(screen.getByText('Overture PLA Matte @BBL X1C')).toBeInTheDocument();
});
expect(screen.getByText('eSUN PETG @Bambu Lab H2D')).toBeInTheDocument();
expect(screen.getByText('0.20mm Standard @BBL X1C')).toBeInTheDocument();
});
it('shows material badges from filament_type', async () => {
render();
await waitFor(() => {
expect(screen.getByText('Overture PLA Matte @BBL X1C')).toBeInTheDocument();
});
// PLA badge should appear for the first preset
const plaBadges = screen.getAllByText('PLA');
expect(plaBadges.length).toBeGreaterThan(0);
});
it('shows vendor from filament_vendor field', async () => {
render();
await waitFor(() => {
expect(screen.getByText('Overture')).toBeInTheDocument();
});
});
it('parses vendor from name when filament_vendor is null', async () => {
render();
await waitFor(() => {
expect(screen.getByText('eSUN PETG @Bambu Lab H2D')).toBeInTheDocument();
});
// eSUN should be parsed from the name
expect(screen.getByText('eSUN')).toBeInTheDocument();
});
it('filters presets by search query', async () => {
render();
await waitFor(() => {
expect(screen.getByText('Overture PLA Matte @BBL X1C')).toBeInTheDocument();
});
const searchInput = screen.getByPlaceholderText(/search/i);
fireEvent.change(searchInput, { target: { value: 'PETG' } });
expect(screen.queryByText('Overture PLA Matte @BBL X1C')).not.toBeInTheDocument();
expect(screen.getByText('eSUN PETG @Bambu Lab H2D')).toBeInTheDocument();
});
it('keeps the search bar visible when no presets match the query (#1470)', async () => {
render();
await waitFor(() => {
expect(screen.getByText('Overture PLA Matte @BBL X1C')).toBeInTheDocument();
});
const searchInput = screen.getByPlaceholderText(/search/i);
fireEvent.change(searchInput, { target: { value: 'zzz-nothing-matches' } });
// The search bar must stay mounted so the query can be cleared/edited
// without a page refresh, and it keeps the typed value.
expect(screen.getByPlaceholderText(/search/i)).toBeInTheDocument();
expect(screen.getByPlaceholderText(/search/i)).toHaveValue('zzz-nothing-matches');
// A no-matches message replaces the columns (not the "import some" empty state).
expect(screen.getByText(/no presets match your search/i)).toBeInTheDocument();
});
it('shows empty state when no presets', async () => {
server.use(
http.get('/api/v1/local-presets/', () => {
return HttpResponse.json({ filament: [], process: [], printer: [] });
}),
);
render();
await waitFor(() => {
expect(screen.getByText(/no local presets/i)).toBeInTheDocument();
});
});
it('shows Local badge on preset cards', async () => {
render();
await waitFor(() => {
expect(screen.getByText('Overture PLA Matte @BBL X1C')).toBeInTheDocument();
});
const badges = screen.getAllByText(/^Local$/i);
expect(badges.length).toBeGreaterThan(0);
});
it('shows delete confirmation modal', async () => {
render();
await waitFor(() => {
expect(screen.getByText('Overture PLA Matte @BBL X1C')).toBeInTheDocument();
});
// Click first delete button
const deleteButtons = screen.getAllByTitle(/delete/i);
fireEvent.click(deleteButtons[0]);
await waitFor(() => {
expect(screen.getByText(/are you sure/i)).toBeInTheDocument();
});
});
it('shows import zone', async () => {
render();
await waitFor(() => {
expect(screen.getByText(/import profiles/i)).toBeInTheDocument();
});
expect(screen.getByText(/\.bbscfg/i)).toBeInTheDocument();
});
it('invalidates the slicerPresets query after a delete (#1581)', async () => {
// Without this invalidation a preset deleted in Local Profiles still
// shows in the SliceModal until the modal's ['slicerPresets'] query
// staleTime (60s) expires + a refocus / remount. The bug report:
// "Removed local profiles still show on the slice menu even tho they
// have been deleted." We mirror the production provider tree but inject
// our own QueryClient so we can spy on invalidateQueries.
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false, gcTime: 0 },
mutations: { retry: false },
},
});
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
rawRender(
,
);
await waitFor(() => {
expect(screen.getByText('Overture PLA Matte @BBL X1C')).toBeInTheDocument();
});
// Open the delete confirmation, then confirm. The card-level delete
// icon buttons and the modal's Delete button both expose the accessible
// name "Delete"; the modal button is the last one to mount, so it's the
// tail of the findAllByRole result.
const deleteButtons = screen.getAllByTitle(/delete/i);
fireEvent.click(deleteButtons[0]);
await screen.findByText(/are you sure/i);
const confirmButtons = await screen.findAllByRole('button', { name: /^delete$/i });
fireEvent.click(confirmButtons[confirmButtons.length - 1]);
await waitFor(() => {
expect(
invalidateSpy.mock.calls.some(
([arg]) => arg && (arg as { queryKey: unknown[] }).queryKey?.[0] === 'slicerPresets',
),
).toBe(true);
});
// Sanity: the local-only invalidation is still there too.
expect(
invalidateSpy.mock.calls.some(
([arg]) => arg && (arg as { queryKey: unknown[] }).queryKey?.[0] === 'localPresets',
),
).toBe(true);
});
});