InventoryPageSearch.test.ts 6.0 KB

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