InventoryPageSearch.test.ts 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. /**
  2. * Unit tests for the global search filter used in InventoryPage.
  3. *
  4. * The filter is a pure client-side computation — replicated inline here
  5. * following the same pattern as InventoryPageGrouping.test.ts so no DOM
  6. * render or API mock is needed.
  7. *
  8. * Fields covered: brand, material, color_name, subtype, note,
  9. * slicer_filament_name, storage_location.
  10. */
  11. import { describe, it, expect } from 'vitest';
  12. import type { InventorySpool } from '../../api/client';
  13. import { filterSpoolsByQuery } from '../../utils/inventorySearch';
  14. function applySearch(spools: InventorySpool[], search: string): InventorySpool[] {
  15. return filterSpoolsByQuery(spools, search);
  16. }
  17. function makeSpool(overrides: Partial<InventorySpool> & { id: number }): InventorySpool {
  18. return {
  19. material: 'PLA',
  20. subtype: 'Basic',
  21. brand: 'Bambu Lab',
  22. color_name: 'White',
  23. rgba: 'FFFFFFFF',
  24. label_weight: 1000,
  25. core_weight: 250,
  26. core_weight_catalog_id: null,
  27. weight_used: 0,
  28. weight_locked: false,
  29. slicer_filament: null,
  30. slicer_filament_name: null,
  31. nozzle_temp_min: null,
  32. nozzle_temp_max: null,
  33. note: null,
  34. added_full: null,
  35. last_used: null,
  36. encode_time: null,
  37. tag_uid: null,
  38. tray_uuid: null,
  39. data_origin: null,
  40. tag_type: null,
  41. archived_at: null,
  42. created_at: '2025-01-01T00:00:00Z',
  43. updated_at: '2025-01-01T00:00:00Z',
  44. k_profiles: [],
  45. cost_per_kg: null,
  46. last_scale_weight: null,
  47. last_weighed_at: null,
  48. storage_location: null,
  49. ...overrides,
  50. };
  51. }
  52. describe('InventoryPage search filter', () => {
  53. describe('storage_location', () => {
  54. it('returns spools whose storage_location matches the query', () => {
  55. const spools = [
  56. makeSpool({ id: 1, storage_location: 'IKEA Regal' }),
  57. makeSpool({ id: 2, storage_location: 'Kiste - PLA' }),
  58. makeSpool({ id: 3, storage_location: 'Lagerregal' }),
  59. ];
  60. expect(applySearch(spools, 'IKEA').map((s) => s.id)).toEqual([1]);
  61. expect(applySearch(spools, 'Kiste').map((s) => s.id)).toEqual([2]);
  62. expect(applySearch(spools, 'regal').map((s) => s.id)).toEqual([1, 3]);
  63. });
  64. it('is case-insensitive for storage_location', () => {
  65. const spools = [makeSpool({ id: 1, storage_location: 'IKEA Regal' })];
  66. expect(applySearch(spools, 'ikea regal')).toHaveLength(1);
  67. expect(applySearch(spools, 'IKEA REGAL')).toHaveLength(1);
  68. expect(applySearch(spools, 'ikEa')).toHaveLength(1);
  69. });
  70. it('matches partial storage_location strings', () => {
  71. const spools = [makeSpool({ id: 1, storage_location: 'Kiste - PLA' })];
  72. expect(applySearch(spools, 'Kis')).toHaveLength(1);
  73. expect(applySearch(spools, 'PLA')).toHaveLength(1);
  74. expect(applySearch(spools, '- PLA')).toHaveLength(1);
  75. });
  76. it('does not crash when storage_location is null', () => {
  77. const spools = [makeSpool({ id: 1, storage_location: null })];
  78. expect(() => applySearch(spools, 'regal')).not.toThrow();
  79. expect(applySearch(spools, 'regal')).toHaveLength(0);
  80. });
  81. it('excludes spools whose storage_location does not match', () => {
  82. const spools = [
  83. makeSpool({ id: 1, storage_location: 'IKEA Regal' }),
  84. makeSpool({ id: 2, storage_location: 'Kiste - PLA' }),
  85. ];
  86. expect(applySearch(spools, 'IKEA').map((s) => s.id)).toEqual([1]);
  87. });
  88. });
  89. describe('existing fields (regression)', () => {
  90. it('still finds by brand', () => {
  91. const spools = [
  92. makeSpool({ id: 1, brand: 'Bambu Lab' }),
  93. makeSpool({ id: 2, brand: 'Polymaker' }),
  94. ];
  95. expect(applySearch(spools, 'polymaker').map((s) => s.id)).toEqual([2]);
  96. });
  97. it('still finds by material', () => {
  98. const spools = [
  99. makeSpool({ id: 1, material: 'PLA' }),
  100. makeSpool({ id: 2, material: 'PETG' }),
  101. ];
  102. expect(applySearch(spools, 'petg').map((s) => s.id)).toEqual([2]);
  103. });
  104. it('still finds by color_name', () => {
  105. const spools = [
  106. makeSpool({ id: 1, color_name: 'Jade White' }),
  107. makeSpool({ id: 2, color_name: 'Black' }),
  108. ];
  109. expect(applySearch(spools, 'jade').map((s) => s.id)).toEqual([1]);
  110. });
  111. it('still finds by note', () => {
  112. const spools = [
  113. makeSpool({ id: 1, note: 'fast print only' }),
  114. makeSpool({ id: 2, note: null }),
  115. ];
  116. expect(applySearch(spools, 'fast').map((s) => s.id)).toEqual([1]);
  117. });
  118. it('returns all spools when search is empty', () => {
  119. const spools = [makeSpool({ id: 1 }), makeSpool({ id: 2 })];
  120. expect(applySearch(spools, '')).toHaveLength(2);
  121. });
  122. it('returns empty array when nothing matches', () => {
  123. const spools = [makeSpool({ id: 1, brand: 'Bambu Lab', material: 'PLA' })];
  124. expect(applySearch(spools, 'xxxxxxxx')).toHaveLength(0);
  125. });
  126. });
  127. describe('cross-field matching', () => {
  128. it('matches a spool if any field contains the query', () => {
  129. const spool = makeSpool({
  130. id: 1,
  131. brand: 'Bambu Lab',
  132. material: 'PLA',
  133. storage_location: 'IKEA Regal',
  134. });
  135. // Each individual field matches
  136. expect(applySearch([spool], 'Bambu')).toHaveLength(1);
  137. expect(applySearch([spool], 'PLA')).toHaveLength(1);
  138. expect(applySearch([spool], 'IKEA')).toHaveLength(1);
  139. });
  140. it('a query matching only storage_location is found even when other fields do not match', () => {
  141. const spools = [
  142. makeSpool({ id: 1, brand: 'Polymaker', material: 'PETG', storage_location: 'IKEA Regal' }),
  143. makeSpool({ id: 2, brand: 'Polymaker', material: 'PETG', storage_location: 'Kiste' }),
  144. ];
  145. const result = applySearch(spools, 'IKEA');
  146. expect(result.map((s) => s.id)).toEqual([1]);
  147. });
  148. });
  149. });