|
@@ -350,7 +350,7 @@ describe('SpoolBuddyDashboard', () => {
|
|
|
});
|
|
});
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- it('calls linkTagToSpoolmanSpool with tag_uid when linking in Spoolman mode', async () => {
|
|
|
|
|
|
|
+ it('sends the tray UUID when linking a Bambu tag in Spoolman mode (#984)', async () => {
|
|
|
const { api } = await import('../../api/client');
|
|
const { api } = await import('../../api/client');
|
|
|
(api.getSpoolmanSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
(api.getSpoolmanSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
|
spoolman_enabled: 'true',
|
|
spoolman_enabled: 'true',
|
|
@@ -365,7 +365,7 @@ describe('SpoolBuddyDashboard', () => {
|
|
|
|
|
|
|
|
renderPage({
|
|
renderPage({
|
|
|
unknownTagUid: 'AABB1122334455FF',
|
|
unknownTagUid: 'AABB1122334455FF',
|
|
|
- unknownTrayUuid: 'DEADBEEFDEADBEEFDEADBEEFDEADBEEF',
|
|
|
|
|
|
|
+ unknownTrayUuid: '9E0B0717BEE94D7887EB1D8DFD1A14F3',
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
const linkBtn = await waitFor(() => screen.getByText('Assign Spool'));
|
|
const linkBtn = await waitFor(() => screen.getByText('Assign Spool'));
|
|
@@ -377,6 +377,35 @@ describe('SpoolBuddyDashboard', () => {
|
|
|
const confirmBtn = await waitFor(() => screen.getByText('Link Tag'));
|
|
const confirmBtn = await waitFor(() => screen.getByText('Link Tag'));
|
|
|
fireEvent.click(confirmBtn);
|
|
fireEvent.click(confirmBtn);
|
|
|
|
|
|
|
|
|
|
+ // The route stores tray_uuid over tag_uid -- the value the AMS keeps in
|
|
|
|
|
+ // extra.tag, and the same on both tags of the spool.
|
|
|
|
|
+ await waitFor(() => {
|
|
|
|
|
+ expect(api.linkTagToSpoolmanSpool).toHaveBeenCalledWith(30, {
|
|
|
|
|
+ tag_uid: 'AABB1122334455FF',
|
|
|
|
|
+ tray_uuid: '9E0B0717BEE94D7887EB1D8DFD1A14F3',
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it('links by tag_uid alone in Spoolman mode when the tag has no tray UUID', async () => {
|
|
|
|
|
+ const { api } = await import('../../api/client');
|
|
|
|
|
+ (api.getSpoolmanSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
|
|
|
+ spoolman_enabled: 'true',
|
|
|
|
|
+ spoolman_url: 'http://localhost:7912',
|
|
|
|
|
+ spoolman_sync_mode: 'off',
|
|
|
|
|
+ spoolman_disable_weight_sync: 'false',
|
|
|
|
|
+ spoolman_report_partial_usage: 'false',
|
|
|
|
|
+ });
|
|
|
|
|
+ (api.getSpoolmanInventorySpools as ReturnType<typeof vi.fn>).mockResolvedValue([
|
|
|
|
|
+ { id: 30, material: 'TPU', brand: 'Bambu', tag_uid: null, tray_uuid: null, archived_at: null, color_name: 'Orange', rgba: 'FF6600FF', subtype: null, label_weight: 1000, core_weight: 250, weight_used: 0 },
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ renderPage({ unknownTagUid: 'AABB1122334455FF' });
|
|
|
|
|
+
|
|
|
|
|
+ fireEvent.click(await waitFor(() => screen.getByText('Assign Spool')));
|
|
|
|
|
+ fireEvent.click(await waitFor(() => screen.getByText('Orange')));
|
|
|
|
|
+ fireEvent.click(await waitFor(() => screen.getByText('Link Tag')));
|
|
|
|
|
+
|
|
|
await waitFor(() => {
|
|
await waitFor(() => {
|
|
|
expect(api.linkTagToSpoolmanSpool).toHaveBeenCalledWith(30, {
|
|
expect(api.linkTagToSpoolmanSpool).toHaveBeenCalledWith(30, {
|
|
|
tag_uid: 'AABB1122334455FF',
|
|
tag_uid: 'AABB1122334455FF',
|
|
@@ -385,6 +414,51 @@ describe('SpoolBuddyDashboard', () => {
|
|
|
});
|
|
});
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+ it('shows the spool card for a Spoolman spool stored under the tag\'s tray UUID (#984)', async () => {
|
|
|
|
|
+ // After quick-add or link, Spoolman's extra.tag holds the tray UUID and the
|
|
|
|
|
+ // spool comes back with tray_uuid set and tag_uid null. The card must still
|
|
|
|
|
+ // find it, or the kiosk keeps offering "Add to Inventory" until a re-scan.
|
|
|
|
|
+ const { api } = await import('../../api/client');
|
|
|
|
|
+ (api.getSpoolmanSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
|
|
|
+ spoolman_enabled: 'true',
|
|
|
|
|
+ spoolman_url: 'http://localhost:7912',
|
|
|
|
|
+ spoolman_sync_mode: 'off',
|
|
|
|
|
+ spoolman_disable_weight_sync: 'false',
|
|
|
|
|
+ spoolman_report_partial_usage: 'false',
|
|
|
|
|
+ });
|
|
|
|
|
+ (api.getSpoolmanInventorySpools as ReturnType<typeof vi.fn>).mockResolvedValue([
|
|
|
|
|
+ { id: 40, material: 'PLA', brand: 'Bambu', tag_uid: null, tray_uuid: '9E0B0717BEE94D7887EB1D8DFD1A14F3', archived_at: null, color_name: 'Teal', rgba: '008080FF', subtype: 'Matte', label_weight: 1000, core_weight: 250, weight_used: 0 },
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ renderPage({ unknownTagUid: 'AABB1122', unknownTrayUuid: '9E0B0717BEE94D7887EB1D8DFD1A14F3' });
|
|
|
|
|
+
|
|
|
|
|
+ await waitFor(() => expect(screen.getByText('Sync Weight')).toBeDefined());
|
|
|
|
|
+ expect(screen.queryByText('Add to Inventory')).toBeNull();
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it('quick-adds a Spoolman spool linked by its tray UUID (#984)', async () => {
|
|
|
|
|
+ const { api } = await import('../../api/client');
|
|
|
|
|
+ (api.getSpoolmanSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
|
|
|
+ spoolman_enabled: 'true',
|
|
|
|
|
+ spoolman_url: 'http://localhost:7912',
|
|
|
|
|
+ spoolman_sync_mode: 'off',
|
|
|
|
|
+ spoolman_disable_weight_sync: 'false',
|
|
|
|
|
+ spoolman_report_partial_usage: 'false',
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ renderPage({ unknownTagUid: 'AABB1122334455FF', unknownTrayUuid: '9E0B0717BEE94D7887EB1D8DFD1A14F3' });
|
|
|
|
|
+
|
|
|
|
|
+ fireEvent.click(await waitFor(() => screen.getAllByText('Add to Inventory')[0]));
|
|
|
|
|
+ fireEvent.click(await waitFor(() => screen.getByText('Add Anyway')));
|
|
|
|
|
+
|
|
|
|
|
+ await waitFor(() => {
|
|
|
|
|
+ expect(api.linkTagToSpoolmanSpool).toHaveBeenCalledWith(4, {
|
|
|
|
|
+ tag_uid: 'AABB1122334455FF',
|
|
|
|
|
+ tray_uuid: '9E0B0717BEE94D7887EB1D8DFD1A14F3',
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
it('quick-adds a Spoolman spool without a tare, so it inherits the filament type\'s (#2908)', async () => {
|
|
it('quick-adds a Spoolman spool without a tare, so it inherits the filament type\'s (#2908)', async () => {
|
|
|
// The quick-create has no tare input. The 250 it used to send was a
|
|
// The quick-create has no tare input. The 250 it used to send was a
|
|
|
// placeholder, and since #2908 a sent value lands on the spool's own
|
|
// placeholder, and since #2908 a sent value lands on the spool's own
|
|
@@ -669,6 +743,109 @@ describe('SpoolBuddyDashboard', () => {
|
|
|
});
|
|
});
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+ describe('Bambu tray UUID in local mode (#984)', () => {
|
|
|
|
|
+ const TRAY_UUID = '9E0B0717BEE94D7887EB1D8DFD1A14F3';
|
|
|
|
|
+
|
|
|
|
|
+ beforeEach(async () => {
|
|
|
|
|
+ const { api } = await import('../../api/client');
|
|
|
|
|
+ (api.getSpoolmanSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
|
|
|
+ spoolman_enabled: 'false',
|
|
|
|
|
+ spoolman_url: '',
|
|
|
|
|
+ spoolman_sync_mode: 'off',
|
|
|
|
|
+ spoolman_disable_weight_sync: 'false',
|
|
|
|
|
+ spoolman_report_partial_usage: 'false',
|
|
|
|
|
+ });
|
|
|
|
|
+ (api.getSpools as ReturnType<typeof vi.fn>).mockResolvedValue([
|
|
|
|
|
+ { id: 3, material: 'ABS', brand: 'Polymaker', tag_uid: null, tray_uuid: null, archived_at: null, color_name: 'White', rgba: 'FFFFFFFF', subtype: null, label_weight: 1000, core_weight: 250, weight_used: 0 },
|
|
|
|
|
+ ]);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it('links the tag together with its tray UUID as a Bambu Lab tag', async () => {
|
|
|
|
|
+ const { api } = await import('../../api/client');
|
|
|
|
|
+ renderPage({ unknownTagUid: 'AABB9999', unknownTrayUuid: TRAY_UUID });
|
|
|
|
|
+
|
|
|
|
|
+ fireEvent.click(await waitFor(() => screen.getByText('Assign Spool')));
|
|
|
|
|
+ fireEvent.click(await waitFor(() => screen.getByText('White')));
|
|
|
|
|
+ fireEvent.click(await waitFor(() => screen.getByText('Link Tag')));
|
|
|
|
|
+
|
|
|
|
|
+ await waitFor(() => {
|
|
|
|
|
+ expect(api.linkTagToSpool).toHaveBeenCalledWith(3, {
|
|
|
|
|
+ tag_uid: 'AABB9999',
|
|
|
|
|
+ tray_uuid: TRAY_UUID,
|
|
|
|
|
+ tag_type: 'bambulab',
|
|
|
|
|
+ data_origin: 'nfc_link',
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it('quick-adds the spool with its tray UUID, so the other tag and the AMS find it', async () => {
|
|
|
|
|
+ const { api } = await import('../../api/client');
|
|
|
|
|
+ renderPage({ unknownTagUid: 'AABB9999', unknownTrayUuid: TRAY_UUID });
|
|
|
|
|
+
|
|
|
|
|
+ fireEvent.click(await waitFor(() => screen.getAllByText('Add to Inventory')[0]));
|
|
|
|
|
+ fireEvent.click(await waitFor(() => screen.getByText('Add Anyway')));
|
|
|
|
|
+
|
|
|
|
|
+ await waitFor(() => expect(api.createSpool).toHaveBeenCalledTimes(1));
|
|
|
|
|
+ const payload = (api.createSpool as ReturnType<typeof vi.fn>).mock.calls[0][0];
|
|
|
|
|
+ expect(payload.tag_uid).toBe('AABB9999');
|
|
|
|
|
+ expect(payload.tray_uuid).toBe(TRAY_UUID);
|
|
|
|
|
+ expect(payload.tag_type).toBe('bambulab');
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it('quick-adds a tag without a tray UUID as a generic tag, as before', async () => {
|
|
|
|
|
+ const { api } = await import('../../api/client');
|
|
|
|
|
+ renderPage({ unknownTagUid: 'AABB9999' });
|
|
|
|
|
+
|
|
|
|
|
+ fireEvent.click(await waitFor(() => screen.getAllByText('Add to Inventory')[0]));
|
|
|
|
|
+ fireEvent.click(await waitFor(() => screen.getByText('Add Anyway')));
|
|
|
|
|
+
|
|
|
|
|
+ await waitFor(() => expect(api.createSpool).toHaveBeenCalledTimes(1));
|
|
|
|
|
+ const payload = (api.createSpool as ReturnType<typeof vi.fn>).mock.calls[0][0];
|
|
|
|
|
+ expect(payload.tray_uuid).toBeNull();
|
|
|
|
|
+ expect(payload.tag_type).toBe('generic');
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it('still saves the tray UUID when the spool is lifted off the reader while quick-add is open', async () => {
|
|
|
|
|
+ const { api } = await import('../../api/client');
|
|
|
|
|
+ const setterRef: { current: React.Dispatch<React.SetStateAction<typeof mockOutletContext.sbState>> | null } = { current: null };
|
|
|
|
|
+ function DynWrapper() {
|
|
|
|
|
+ const [sbState, setSbState] = React.useState({
|
|
|
|
|
+ ...mockOutletContext.sbState,
|
|
|
|
|
+ unknownTagUid: 'AABB9999',
|
|
|
|
|
+ unknownTrayUuid: TRAY_UUID,
|
|
|
|
|
+ });
|
|
|
|
|
+ setterRef.current = setSbState;
|
|
|
|
|
+ return <Outlet context={{ ...mockOutletContext, sbState }} />;
|
|
|
|
|
+ }
|
|
|
|
|
+ const qc = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0 } } });
|
|
|
|
|
+ render(
|
|
|
|
|
+ <ToastProvider>
|
|
|
|
|
+ <QueryClientProvider client={qc}>
|
|
|
|
|
+ <MemoryRouter initialEntries={['/spoolbuddy']}>
|
|
|
|
|
+ <Routes>
|
|
|
|
|
+ <Route element={<DynWrapper />}>
|
|
|
|
|
+ <Route path="spoolbuddy" element={<SpoolBuddyDashboard />} />
|
|
|
|
|
+ </Route>
|
|
|
|
|
+ </Routes>
|
|
|
|
|
+ </MemoryRouter>
|
|
|
|
|
+ </QueryClientProvider>
|
|
|
|
|
+ </ToastProvider>
|
|
|
|
|
+ );
|
|
|
|
|
+ fireEvent.click(await waitFor(() => screen.getAllByText('Add to Inventory')[0]));
|
|
|
|
|
+ await waitFor(() => screen.getByText('Add Anyway'));
|
|
|
|
|
+
|
|
|
|
|
+ // Tag removed: the dialog stays open, the live tag state is gone.
|
|
|
|
|
+ act(() => setterRef.current!((prev) => ({ ...prev, unknownTagUid: null, unknownTrayUuid: null })));
|
|
|
|
|
+
|
|
|
|
|
+ fireEvent.click(await waitFor(() => screen.getByText('Add Anyway')));
|
|
|
|
|
+
|
|
|
|
|
+ await waitFor(() => expect(api.createSpool).toHaveBeenCalledTimes(1));
|
|
|
|
|
+ const payload = (api.createSpool as ReturnType<typeof vi.fn>).mock.calls[0][0];
|
|
|
|
|
+ expect(payload.tag_uid).toBe('AABB9999');
|
|
|
|
|
+ expect(payload.tray_uuid).toBe(TRAY_UUID);
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
describe('Spoolman mode', () => {
|
|
describe('Spoolman mode', () => {
|
|
|
const SPOOLMAN_SPOOL = {
|
|
const SPOOLMAN_SPOOL = {
|
|
|
id: 42, material: 'PLA', subtype: null, brand: 'Bambu',
|
|
id: 42, material: 'PLA', subtype: null, brand: 'Bambu',
|