ForecastPanelSpoolmanHistory.test.tsx 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. /**
  2. * In Spoolman mode the forecast reads no usage history (#3406). That history
  3. * is the built-in inventory's, keyed by its own spool ids: a Spoolman spool
  4. * whose id matches one, left over from before the switch, borrowed that
  5. * spool's prints. The rate is the consumption delta there, as in the
  6. * backend's stock alerts.
  7. */
  8. import { describe, it, expect, vi, beforeEach } from 'vitest';
  9. import { screen } from '@testing-library/react';
  10. import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
  11. import { render } from '../utils';
  12. import { ForecastPanel } from '../../components/ForecastPanel';
  13. import { api } from '../../api/client';
  14. import type { InventorySpool, SpoolUsageRecord } from '../../api/client';
  15. vi.mock('../../contexts/AuthContext', async (importOriginal) => {
  16. const actual = await importOriginal<typeof import('../../contexts/AuthContext')>();
  17. return {
  18. ...actual,
  19. useAuth: () => ({ hasPermission: () => true, hasAnyPermission: () => true, authEnabled: false, user: null }),
  20. };
  21. });
  22. vi.mock('../../api/client', async (importOriginal) => {
  23. const actual = await importOriginal<typeof import('../../api/client')>();
  24. return {
  25. ...actual,
  26. api: {
  27. ...actual.api,
  28. getSettings: vi.fn(),
  29. getUiSettings: vi.fn(),
  30. getAuthStatus: vi.fn(),
  31. getSkuSettings: vi.fn(),
  32. getAllUsageHistory: vi.fn(),
  33. getShoppingList: vi.fn(),
  34. addToShoppingList: vi.fn(),
  35. removeFromShoppingList: vi.fn(),
  36. clearShoppingList: vi.fn(),
  37. updateShoppingListStatus: vi.fn(),
  38. bulkCreateSpools: vi.fn(),
  39. upsertSkuSettings: vi.fn(),
  40. updateSettings: vi.fn(),
  41. },
  42. };
  43. });
  44. const mocked = api as unknown as Record<string, ReturnType<typeof vi.fn>>;
  45. const spool = (id: number, over: Partial<InventorySpool> = {}): InventorySpool => ({
  46. id,
  47. material: 'PLA',
  48. subtype: 'Matte',
  49. brand: 'Polymaker',
  50. color_name: 'Red',
  51. rgba: 'FF0000FF',
  52. label_weight: 750,
  53. core_weight: 250,
  54. core_weight_catalog_id: null,
  55. weight_used: 100,
  56. slicer_filament: null,
  57. slicer_filament_name: null,
  58. nozzle_temp_min: null,
  59. nozzle_temp_max: null,
  60. note: null,
  61. added_full: null,
  62. last_used: null,
  63. encode_time: null,
  64. tag_uid: null,
  65. tray_uuid: null,
  66. data_origin: 'manual',
  67. tag_type: null,
  68. archived_at: null,
  69. created_at: '2025-01-01T00:00:00Z',
  70. updated_at: '2025-01-01T00:00:00Z',
  71. k_profiles: [],
  72. cost_per_kg: null,
  73. last_scale_weight: null,
  74. last_weighed_at: null,
  75. weight_locked: false,
  76. category: 'Active',
  77. low_stock_threshold_pct: null,
  78. ...over,
  79. });
  80. const DAY = 86400000;
  81. const ago = (days: number) => new Date(Date.now() - days * DAY).toISOString();
  82. // The reporter's repro: spool 6, registered 40 days ago, 100 g used (2.5 g/d).
  83. const spoolman6 = spool(6, { label_weight: 1000, weight_used: 100, created_at: ago(40) });
  84. // Seven prints of the built-in spool 6, from half a year ago.
  85. const builtInHistory = [190, 187, 184, 181, 178, 175, 172].map(
  86. (daysAgo, i) => ({ id: i + 1, spool_id: 6, created_at: ago(daysAgo), weight_used: 120 }) as SpoolUsageRecord,
  87. );
  88. describe('ForecastPanel: usage history in Spoolman mode (#3406)', () => {
  89. beforeEach(() => {
  90. vi.clearAllMocks();
  91. mocked.getSettings.mockResolvedValue({ forecast_global_lead_time_days: 0 });
  92. mocked.getUiSettings.mockResolvedValue({ forecast_global_lead_time_days: 0 });
  93. mocked.getAuthStatus.mockResolvedValue({ auth_enabled: false, requires_setup: false });
  94. mocked.getSkuSettings.mockResolvedValue([]);
  95. mocked.getAllUsageHistory.mockResolvedValue(builtInHistory);
  96. mocked.getShoppingList.mockResolvedValue([]);
  97. });
  98. it("a Spoolman spool's rate is its own consumption, not a built-in spool's history", async () => {
  99. render(<ForecastPanel spools={[spoolman6]} spoolmanMode />);
  100. expect(await screen.findByText('2.5g/d')).toBeInTheDocument();
  101. expect(screen.queryByText('Trend')).toBeNull();
  102. // The Spoolman spools' own rows are asked for (#3407); built-in rows that
  103. // come back anyway name no Spoolman spool.
  104. expect(mocked.getAllUsageHistory).toHaveBeenCalledWith(5000, undefined, true);
  105. });
  106. it("the Spoolman spools' own usage rows give Spoolman mode the trend (#3407)", async () => {
  107. const spoolmanHistory = [30, 20, 10].map(
  108. (daysAgo, i) =>
  109. ({ id: i + 1, spool_id: null, spoolman_spool_id: 6, created_at: ago(daysAgo), weight_used: 30 }) as SpoolUsageRecord,
  110. );
  111. mocked.getAllUsageHistory.mockResolvedValue(spoolmanHistory);
  112. render(<ForecastPanel spools={[spoolman6]} spoolmanMode />);
  113. expect(await screen.findByText('Trend')).toBeInTheDocument();
  114. expect(screen.getByText('3.0g/d')).toBeInTheDocument();
  115. });
  116. it('history cached in built-in mode is not used after switching to Spoolman', async () => {
  117. // Same query key in both modes: a disabled query still hands out its cache.
  118. const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
  119. client.setQueryData(['all-usage-history-forecast'], builtInHistory);
  120. render(
  121. <QueryClientProvider client={client}>
  122. <ForecastPanel spools={[spoolman6]} spoolmanMode />
  123. </QueryClientProvider>,
  124. );
  125. expect(await screen.findByText('2.5g/d')).toBeInTheDocument();
  126. expect(screen.queryByText('Trend')).toBeNull();
  127. });
  128. it('the built-in inventory still uses its history', async () => {
  129. render(<ForecastPanel spools={[spoolman6]} />);
  130. expect(await screen.findByText('Trend')).toBeInTheDocument();
  131. expect(screen.queryByText('2.5g/d')).toBeNull();
  132. });
  133. });