filamentPresets.test.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. import { describe, it, expect, vi } from 'vitest';
  2. import {
  3. buildFilamentPresetOptions,
  4. genericFilamentIdForMaterial,
  5. presetDisplayName,
  6. resolveFilamentId,
  7. } from '../../utils/filamentPresets';
  8. import type { BuiltinFilament, LocalPreset, OrcaProfileMeta, SlicerSetting } from '../../api/client';
  9. const localPreset = (over: Partial<LocalPreset> = {}): LocalPreset => ({
  10. id: 1,
  11. name: 'Elegoo PLA+ @BBL X1C 0.4 nozzle',
  12. preset_type: 'filament',
  13. source: 'orca',
  14. filament_type: 'PLA',
  15. filament_vendor: 'Elegoo',
  16. nozzle_temp_min: 190,
  17. nozzle_temp_max: 230,
  18. pressure_advance: null,
  19. default_filament_colour: null,
  20. filament_cost: null,
  21. filament_density: null,
  22. compatible_printers: null,
  23. inherits: null,
  24. version: null,
  25. created_at: '',
  26. updated_at: '',
  27. ...over,
  28. });
  29. const orcaProfile = (over: Partial<OrcaProfileMeta> = {}): OrcaProfileMeta => ({
  30. setting_id: 'a1b2c3',
  31. name: 'Sunlu PETG',
  32. type: 'filament',
  33. version: null,
  34. user_id: null,
  35. updated_time: null,
  36. is_custom: true,
  37. ...over,
  38. });
  39. const cloudSetting = (over: Partial<SlicerSetting> = {}): SlicerSetting => ({
  40. setting_id: 'GFSA00',
  41. name: 'Bambu PLA Basic @BBL X1C',
  42. type: 'filament',
  43. version: null,
  44. user_id: null,
  45. updated_time: null,
  46. is_custom: false,
  47. ...over,
  48. });
  49. const builtin = (filament_id: string, name: string): BuiltinFilament => ({ filament_id, name });
  50. describe('genericFilamentIdForMaterial', () => {
  51. it('maps an exact material', () => {
  52. expect(genericFilamentIdForMaterial('PETG')).toBe('GFG99');
  53. });
  54. it('is case and whitespace tolerant', () => {
  55. expect(genericFilamentIdForMaterial(' pla ')).toBe('GFL99');
  56. });
  57. it('falls back to the base material when a suffix is unknown', () => {
  58. // "PLA-GF" has no generic of its own; the PLA generic is the honest answer.
  59. expect(genericFilamentIdForMaterial('PLA-GF')).toBe('GFL99');
  60. });
  61. it('returns empty rather than guessing for an unknown material', () => {
  62. expect(genericFilamentIdForMaterial('UNOBTANIUM')).toBe('');
  63. expect(genericFilamentIdForMaterial('')).toBe('');
  64. expect(genericFilamentIdForMaterial(null)).toBe('');
  65. });
  66. });
  67. describe('presetDisplayName', () => {
  68. it('strips the printer/nozzle suffix', () => {
  69. expect(presetDisplayName('Bambu PLA Basic @BBL X1C 0.4 nozzle')).toBe('Bambu PLA Basic');
  70. });
  71. it('strips the custom-preset marker', () => {
  72. expect(presetDisplayName('# My PLA @BBL P1S')).toBe('My PLA');
  73. });
  74. });
  75. describe('buildFilamentPresetOptions', () => {
  76. it('is empty when every source is', () => {
  77. expect(buildFilamentPresetOptions({})).toEqual([]);
  78. });
  79. it('ranks the tiers local > orca > cloud > builtin', () => {
  80. const options = buildFilamentPresetOptions({
  81. builtinFilaments: [builtin('GFA00', 'Bambu PLA Basic')],
  82. cloudSettings: [cloudSetting({ setting_id: 'GFSB99', name: 'Generic ABS' })],
  83. orcaProfiles: [orcaProfile()],
  84. localPresets: [localPreset()],
  85. });
  86. expect(options.map(o => o.source)).toEqual(['local', 'orca_cloud', 'cloud', 'builtin']);
  87. });
  88. it('sorts by name inside a tier', () => {
  89. const options = buildFilamentPresetOptions({
  90. builtinFilaments: [builtin('GFA01', 'Bambu PLA Matte'), builtin('GFA00', 'Bambu PLA Basic')],
  91. });
  92. expect(options.map(o => o.name)).toEqual(['Bambu PLA Basic', 'Bambu PLA Matte']);
  93. });
  94. it('takes a builtin filament id straight from the table', () => {
  95. const [option] = buildFilamentPresetOptions({ builtinFilaments: [builtin('GFA00', 'Bambu PLA Basic')] });
  96. expect(option).toMatchObject({ id: 'builtin_GFA00', filamentId: 'GFA00' });
  97. });
  98. it('derives a Bambu official cloud preset id from its setting_id', () => {
  99. const [option] = buildFilamentPresetOptions({ cloudSettings: [cloudSetting({ setting_id: 'GFSG98_09' })] });
  100. expect(option.filamentId).toBe('GFG98');
  101. });
  102. it('leaves a cloud user preset unresolved for the detail lookup', () => {
  103. // PFUS ids are setting ids, not filament ids — the printer rejects them,
  104. // so guessing one here would file the calibration under nothing.
  105. const [option] = buildFilamentPresetOptions({
  106. cloudSettings: [cloudSetting({ setting_id: 'PFUS9ac902733670a9', name: 'My PETG', is_custom: true })],
  107. });
  108. expect(option.filamentId).toBe('');
  109. });
  110. it('gives local and orca presets the generic id for their material', () => {
  111. const options = buildFilamentPresetOptions({
  112. localPresets: [localPreset({ filament_type: 'PETG' })],
  113. orcaProfiles: [orcaProfile({ name: 'Sunlu ABS @BBL X1C' })],
  114. });
  115. expect(options.find(o => o.source === 'local')?.filamentId).toBe('GFG99');
  116. expect(options.find(o => o.source === 'orca_cloud')?.filamentId).toBe('GFB99');
  117. });
  118. it('parses the material from the name when a local preset declares none', () => {
  119. const [option] = buildFilamentPresetOptions({
  120. localPresets: [localPreset({ filament_type: null, name: 'Overture TPU @BBL X1C' })],
  121. });
  122. expect(option.filamentId).toBe('GFU99');
  123. });
  124. it('collapses a cloud filament duplicated once per printer model', () => {
  125. // Every Bambu Cloud account carries one copy per model. They share a
  126. // filament id and, with the "@…" suffix stripped, one visible name.
  127. const options = buildFilamentPresetOptions({
  128. cloudSettings: [
  129. cloudSetting({ setting_id: 'GFSA00_00', name: 'Bambu PLA Basic @BBL X1C' }),
  130. cloudSetting({ setting_id: 'GFSA00_01', name: 'Bambu PLA Basic @BBL P1S' }),
  131. cloudSetting({ setting_id: 'GFSA00_02', name: 'Bambu PLA Basic @BBL A1' }),
  132. ],
  133. });
  134. expect(options).toHaveLength(1);
  135. expect(options[0]).toMatchObject({ name: 'Bambu PLA Basic', filamentId: 'GFA00' });
  136. });
  137. it('collapses a cloud user preset duplicated per model, which has no filament id to key on', () => {
  138. const options = buildFilamentPresetOptions({
  139. cloudSettings: [
  140. cloudSetting({ setting_id: 'PFUSaaa', name: 'My PETG @BBL X1C', is_custom: true }),
  141. cloudSetting({ setting_id: 'PFUSbbb', name: 'My PETG @BBL P1S', is_custom: true }),
  142. ],
  143. });
  144. expect(options).toHaveLength(1);
  145. });
  146. it('keeps distinct cloud filaments apart', () => {
  147. const options = buildFilamentPresetOptions({
  148. cloudSettings: [
  149. cloudSetting({ setting_id: 'GFSA00_00', name: 'Bambu PLA Basic @BBL X1C' }),
  150. cloudSetting({ setting_id: 'GFSA01_00', name: 'Bambu PLA Matte @BBL X1C' }),
  151. ],
  152. });
  153. expect(options.map(o => o.name)).toEqual(['Bambu PLA Basic', 'Bambu PLA Matte']);
  154. });
  155. it('collapses one imported filament re-imported for several printers', () => {
  156. const options = buildFilamentPresetOptions({
  157. localPresets: [
  158. localPreset({ id: 1, name: 'Elegoo PLA+ @BBL X1C 0.4 nozzle' }),
  159. localPreset({ id: 2, name: 'Elegoo PLA+ @BBL P1S 0.4 nozzle' }),
  160. ],
  161. });
  162. expect(options).toHaveLength(1);
  163. });
  164. it('keeps imported presets of different materials that share a generic id path', () => {
  165. // Keyed by name, not by generic id — otherwise two distinct PLA imports
  166. // would collapse into one because both map to GFL99.
  167. const options = buildFilamentPresetOptions({
  168. localPresets: [
  169. localPreset({ id: 1, name: 'Elegoo PLA+' }),
  170. localPreset({ id: 2, name: 'Polymaker PolyLite PLA' }),
  171. ],
  172. });
  173. expect(options).toHaveLength(2);
  174. });
  175. it('collapses Orca Cloud copies of one filament', () => {
  176. const options = buildFilamentPresetOptions({
  177. orcaProfiles: [
  178. orcaProfile({ setting_id: 'u1', name: 'Sunlu PETG @BBL X1C' }),
  179. orcaProfile({ setting_id: 'u2', name: 'Sunlu PETG @BBL A1' }),
  180. ],
  181. });
  182. expect(options).toHaveLength(1);
  183. });
  184. it('drops a builtin the cloud tier covers under a variant setting_id', () => {
  185. // Cloud ids carry a "_NN" variant suffix; without normalising it the
  186. // builtin tier lists the same filament a second time.
  187. const options = buildFilamentPresetOptions({
  188. cloudSettings: [cloudSetting({ setting_id: 'GFSA00_01', name: 'Bambu PLA Basic @BBL X1C' })],
  189. builtinFilaments: [builtin('GFA00', 'Bambu PLA Basic'), builtin('GFA01', 'Bambu PLA Matte')],
  190. });
  191. expect(options.filter(o => o.source === 'builtin').map(o => o.filamentId)).toEqual(['GFA01']);
  192. });
  193. it('drops a builtin already offered by a cloud tier, matching the S-infix spelling', () => {
  194. const options = buildFilamentPresetOptions({
  195. cloudSettings: [cloudSetting({ setting_id: 'GFSA00', name: 'Bambu PLA Basic' })],
  196. builtinFilaments: [builtin('GFA00', 'Bambu PLA Basic'), builtin('GFA01', 'Bambu PLA Matte')],
  197. });
  198. expect(options.filter(o => o.source === 'builtin').map(o => o.filamentId)).toEqual(['GFA01']);
  199. });
  200. it('drops a bambu cloud preset Orca Cloud already covers', () => {
  201. const options = buildFilamentPresetOptions({
  202. orcaProfiles: [orcaProfile({ setting_id: 'shared-id' })],
  203. cloudSettings: [cloudSetting({ setting_id: 'shared-id' })],
  204. });
  205. expect(options.map(o => o.source)).toEqual(['orca_cloud']);
  206. });
  207. it('keeps an Orca Cloud library that overlaps an imported bundle by name', () => {
  208. // These are usually the same profiles reached two ways. Letting the
  209. // imported tier claim the name emptied the Orca Cloud group down to
  210. // whatever happened not to be imported too.
  211. const options = buildFilamentPresetOptions({
  212. localPresets: [
  213. localPreset({ id: 1, name: 'Elegoo PLA+ @BBL X1C' }),
  214. localPreset({ id: 2, name: 'Sunlu PETG @BBL X1C' }),
  215. ],
  216. orcaProfiles: [
  217. orcaProfile({ setting_id: 'u1', name: 'Elegoo PLA+ @BBL X1C' }),
  218. orcaProfile({ setting_id: 'u2', name: 'Sunlu PETG @BBL X1C' }),
  219. ],
  220. });
  221. expect(options.filter(o => o.source === 'local')).toHaveLength(2);
  222. expect(options.filter(o => o.source === 'orca_cloud')).toHaveLength(2);
  223. });
  224. it('still drops a cross-tier row that carries an id a higher tier claimed', () => {
  225. // A shared id is true identity, unlike a shared name.
  226. const options = buildFilamentPresetOptions({
  227. orcaProfiles: [orcaProfile({ setting_id: 'shared-id', name: 'Sunlu PETG' })],
  228. cloudSettings: [cloudSetting({ setting_id: 'shared-id', name: 'Something Else' })],
  229. });
  230. expect(options.map(o => o.source)).toEqual(['orca_cloud']);
  231. });
  232. it('never echoes a filament the tiers above already offered back from the builtin table', () => {
  233. // The builtin tier is a static copy of the same Bambu catalogue, so
  234. // without a name check it re-listed everything under a fourth heading.
  235. const options = buildFilamentPresetOptions({
  236. localPresets: [localPreset({ name: 'Bambu PLA Basic @BBL X1C 0.4 nozzle' })],
  237. builtinFilaments: [builtin('GFA00', 'Bambu PLA Basic'), builtin('GFA01', 'Bambu PLA Matte')],
  238. });
  239. expect(options.map(o => [o.source, o.name])).toEqual([
  240. ['local', 'Bambu PLA Basic'],
  241. ['builtin', 'Bambu PLA Matte'],
  242. ]);
  243. });
  244. it('suppresses a builtin a cloud tier already named, even with no id overlap', () => {
  245. const options = buildFilamentPresetOptions({
  246. cloudSettings: [cloudSetting({ setting_id: 'PFUSaaa', name: 'Bambu PLA Basic @BBL X1C', is_custom: true })],
  247. builtinFilaments: [builtin('GFA00', 'Bambu PLA Basic')],
  248. });
  249. expect(options.map(o => o.source)).toEqual(['cloud']);
  250. });
  251. it('does not let one import swallow every filament of the same material', () => {
  252. // Imports resolve to a shared generic id (all PLA → GFL99); claiming that
  253. // id would hide every other PLA behind the first one imported.
  254. const options = buildFilamentPresetOptions({
  255. localPresets: [
  256. localPreset({ id: 1, name: 'Elegoo PLA+', filament_type: 'PLA' }),
  257. localPreset({ id: 2, name: 'Polymaker PolyLite PLA', filament_type: 'PLA' }),
  258. ],
  259. builtinFilaments: [builtin('GFL99', 'Generic PLA')],
  260. });
  261. expect(options.map(o => o.name)).toEqual([
  262. 'Elegoo PLA+',
  263. 'Polymaker PolyLite PLA',
  264. 'Generic PLA',
  265. ]);
  266. });
  267. it('strips printer suffixes from displayed names', () => {
  268. const [option] = buildFilamentPresetOptions({ localPresets: [localPreset()] });
  269. expect(option.name).toBe('Elegoo PLA+');
  270. });
  271. });
  272. describe('resolveFilamentId', () => {
  273. const option = (over = {}) => ({
  274. id: 'PFUS9ac902733670a9',
  275. name: 'My PETG',
  276. source: 'cloud' as const,
  277. filamentId: '',
  278. filamentType: 'PETG',
  279. ...over,
  280. });
  281. it('returns an already-known id without fetching', async () => {
  282. const fetchDetail = vi.fn();
  283. await expect(resolveFilamentId(option({ filamentId: 'GFA00' }), fetchDetail)).resolves.toBe('GFA00');
  284. expect(fetchDetail).not.toHaveBeenCalled();
  285. });
  286. it('fetches the cloud detail for a user preset', async () => {
  287. const fetchDetail = vi.fn().mockResolvedValue({ filament_id: 'P285e239' });
  288. await expect(resolveFilamentId(option(), fetchDetail)).resolves.toBe('P285e239');
  289. expect(fetchDetail).toHaveBeenCalledWith('PFUS9ac902733670a9');
  290. });
  291. it('returns empty when the detail carries no filament_id', async () => {
  292. // Never fall back to base_id: that collapses a custom preset onto the
  293. // generic it inherits from (#1053).
  294. const fetchDetail = vi.fn().mockResolvedValue({ base_id: 'GFSG98_09' });
  295. await expect(resolveFilamentId(option(), fetchDetail)).resolves.toBe('');
  296. });
  297. it('returns empty when the detail lookup fails', async () => {
  298. const fetchDetail = vi.fn().mockRejectedValue(new Error('offline'));
  299. await expect(resolveFilamentId(option(), fetchDetail)).resolves.toBe('');
  300. });
  301. it('does not fetch for a non-cloud tier that resolved to nothing', async () => {
  302. const fetchDetail = vi.fn();
  303. const unknown = option({ source: 'local' as const, filamentType: 'UNOBTANIUM' });
  304. await expect(resolveFilamentId(unknown, fetchDetail)).resolves.toBe('');
  305. expect(fetchDetail).not.toHaveBeenCalled();
  306. });
  307. });