mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-21 01:57:41 +02:00
fix(slicer-presets): #1581 SliceModal refresh + invalidate on local-profile delete/import
Two-part fix for the reporter's "removed profiles still show on the slice
menu" symptom.
Local half (real bug). LocalProfilesView's import and delete mutations
invalidated ['localPresets'] (the management view's own query) but not
['slicerPresets'] (the SliceModal's unified preset query, staleTime 60s).
A freshly-deleted preset kept rendering in the slice dropdown until that
staleTime elapsed plus a refocus/remount. Both mutations now also call
queryClient.invalidateQueries({queryKey: ['slicerPresets']}).
Cloud half (opt-in cache bypass). _fetch_cloud_presets keeps a 5-minute
per-(user, token) in-process cache (slicer_presets.py:69, balances
"users see freshly-saved presets quickly" against "busy install doesn't
hit Bambu Cloud once per modal open"). Users delete cloud presets in
Bambu Studio / Bambu Handy, not in Bambuddy, so there's no event hook
to invalidate on. Rather than shorten the TTL globally, the listing
endpoint gains an opt-in ?refresh=true query param that bypasses both
the cloud cache AND the 1-hour bundled-preset cache for that one call;
the fresh result is still written back so subsequent normal callers
keep hitting the cache.
New SliceModal "Refresh" button. Lives in the preset section header
next to the cloud-status banner. Calls getSlicerPresets({refresh: true})
and writes the fresh slots into the ['slicerPresets'] cache via
queryClient.setQueryData so the spinner stops immediately rather than
triggering a second refetch. RefreshCw icon spins while in-flight;
disabled during slice enqueue to prevent double-fire.
This commit is contained in:
@@ -1,11 +1,17 @@
|
||||
/**
|
||||
* Tests for LocalProfilesView component.
|
||||
*/
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { screen, waitFor, fireEvent } from '@testing-library/react';
|
||||
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 = {
|
||||
@@ -209,4 +215,62 @@ describe('LocalProfilesView', () => {
|
||||
|
||||
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(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BrowserRouter>
|
||||
<AuthProvider>
|
||||
<ThemeProvider>
|
||||
<ToastProvider>
|
||||
<LocalProfilesView />
|
||||
</ToastProvider>
|
||||
</ThemeProvider>
|
||||
</AuthProvider>
|
||||
</BrowserRouter>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user