filamentPresets.test.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  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("takes a cloud preset's filament id from Bambu's list when it carries one (#3387)", () => {
  103. const [listed] = buildFilamentPresetOptions({
  104. cloudSettings: [cloudSetting({ setting_id: 'GFSX01_02', name: 'New Filament', filament_id: 'GFX07' })],
  105. });
  106. expect(listed.filamentId).toBe('GFX07');
  107. // A bare id that could name two filaments is the one picked by its name:
  108. // "Generic PA" is GFN99, not the rule's GFN98 (Generic PA-CF).
  109. const [bare] = buildFilamentPresetOptions({
  110. cloudSettings: [cloudSetting({ setting_id: 'GFSN98', name: 'Generic PA', filament_id: 'GFN99' })],
  111. });
  112. expect(bare.filamentId).toBe('GFN99');
  113. const [derived] = buildFilamentPresetOptions({
  114. cloudSettings: [cloudSetting({ setting_id: 'GFSNLS03_04', name: 'SUNLU PLA+ @BBL A1' })],
  115. });
  116. expect(derived.filamentId).toBe('GFSNL03');
  117. });
  118. it('leaves a cloud user preset unresolved for the detail lookup', () => {
  119. // PFUS ids are setting ids, not filament ids — the printer rejects them,
  120. // so guessing one here would file the calibration under nothing.
  121. const [option] = buildFilamentPresetOptions({
  122. cloudSettings: [cloudSetting({ setting_id: 'PFUS9ac902733670a9', name: 'My PETG', is_custom: true })],
  123. });
  124. expect(option.filamentId).toBe('');
  125. });
  126. it('gives local and orca presets the generic id for their material', () => {
  127. const options = buildFilamentPresetOptions({
  128. localPresets: [localPreset({ filament_type: 'PETG' })],
  129. orcaProfiles: [orcaProfile({ name: 'Sunlu ABS @BBL X1C' })],
  130. });
  131. expect(options.find(o => o.source === 'local')?.filamentId).toBe('GFG99');
  132. expect(options.find(o => o.source === 'orca_cloud')?.filamentId).toBe('GFB99');
  133. });
  134. it('parses the material from the name when a local preset declares none', () => {
  135. const [option] = buildFilamentPresetOptions({
  136. localPresets: [localPreset({ filament_type: null, name: 'Overture TPU @BBL X1C' })],
  137. });
  138. expect(option.filamentId).toBe('GFU99');
  139. });
  140. it('collapses a cloud filament duplicated once per printer model', () => {
  141. // Every Bambu Cloud account carries one copy per model. They share a
  142. // filament id and, with the "@…" suffix stripped, one visible name.
  143. const options = buildFilamentPresetOptions({
  144. cloudSettings: [
  145. cloudSetting({ setting_id: 'GFSA00_00', name: 'Bambu PLA Basic @BBL X1C' }),
  146. cloudSetting({ setting_id: 'GFSA00_01', name: 'Bambu PLA Basic @BBL P1S' }),
  147. cloudSetting({ setting_id: 'GFSA00_02', name: 'Bambu PLA Basic @BBL A1' }),
  148. ],
  149. });
  150. expect(options).toHaveLength(1);
  151. expect(options[0]).toMatchObject({ name: 'Bambu PLA Basic', filamentId: 'GFA00' });
  152. });
  153. it('collapses a cloud user preset duplicated per model, which has no filament id to key on', () => {
  154. const options = buildFilamentPresetOptions({
  155. cloudSettings: [
  156. cloudSetting({ setting_id: 'PFUSaaa', name: 'My PETG @BBL X1C', is_custom: true }),
  157. cloudSetting({ setting_id: 'PFUSbbb', name: 'My PETG @BBL P1S', is_custom: true }),
  158. ],
  159. });
  160. expect(options).toHaveLength(1);
  161. });
  162. it('keeps distinct cloud filaments apart', () => {
  163. const options = buildFilamentPresetOptions({
  164. cloudSettings: [
  165. cloudSetting({ setting_id: 'GFSA00_00', name: 'Bambu PLA Basic @BBL X1C' }),
  166. cloudSetting({ setting_id: 'GFSA01_00', name: 'Bambu PLA Matte @BBL X1C' }),
  167. ],
  168. });
  169. expect(options.map(o => o.name)).toEqual(['Bambu PLA Basic', 'Bambu PLA Matte']);
  170. });
  171. it('collapses one imported filament re-imported for several printers', () => {
  172. const options = buildFilamentPresetOptions({
  173. localPresets: [
  174. localPreset({ id: 1, name: 'Elegoo PLA+ @BBL X1C 0.4 nozzle' }),
  175. localPreset({ id: 2, name: 'Elegoo PLA+ @BBL P1S 0.4 nozzle' }),
  176. ],
  177. });
  178. expect(options).toHaveLength(1);
  179. });
  180. it('keeps imported presets of different materials that share a generic id path', () => {
  181. // Keyed by name, not by generic id — otherwise two distinct PLA imports
  182. // would collapse into one because both map to GFL99.
  183. const options = buildFilamentPresetOptions({
  184. localPresets: [
  185. localPreset({ id: 1, name: 'Elegoo PLA+' }),
  186. localPreset({ id: 2, name: 'Polymaker PolyLite PLA' }),
  187. ],
  188. });
  189. expect(options).toHaveLength(2);
  190. });
  191. it('collapses Orca Cloud copies of one filament', () => {
  192. const options = buildFilamentPresetOptions({
  193. orcaProfiles: [
  194. orcaProfile({ setting_id: 'u1', name: 'Sunlu PETG @BBL X1C' }),
  195. orcaProfile({ setting_id: 'u2', name: 'Sunlu PETG @BBL A1' }),
  196. ],
  197. });
  198. expect(options).toHaveLength(1);
  199. });
  200. it('drops a builtin the cloud tier covers under a variant setting_id', () => {
  201. // Cloud ids carry a "_NN" variant suffix; without normalising it the
  202. // builtin tier lists the same filament a second time.
  203. const options = buildFilamentPresetOptions({
  204. cloudSettings: [cloudSetting({ setting_id: 'GFSA00_01', name: 'Bambu PLA Basic @BBL X1C' })],
  205. builtinFilaments: [builtin('GFA00', 'Bambu PLA Basic'), builtin('GFA01', 'Bambu PLA Matte')],
  206. });
  207. expect(options.filter(o => o.source === 'builtin').map(o => o.filamentId)).toEqual(['GFA01']);
  208. });
  209. it('drops a builtin already offered by a cloud tier, matching the S-infix spelling', () => {
  210. const options = buildFilamentPresetOptions({
  211. cloudSettings: [cloudSetting({ setting_id: 'GFSA00', name: 'Bambu PLA Basic' })],
  212. builtinFilaments: [builtin('GFA00', 'Bambu PLA Basic'), builtin('GFA01', 'Bambu PLA Matte')],
  213. });
  214. expect(options.filter(o => o.source === 'builtin').map(o => o.filamentId)).toEqual(['GFA01']);
  215. });
  216. it('drops a bambu cloud preset Orca Cloud already covers', () => {
  217. const options = buildFilamentPresetOptions({
  218. orcaProfiles: [orcaProfile({ setting_id: 'shared-id' })],
  219. cloudSettings: [cloudSetting({ setting_id: 'shared-id' })],
  220. });
  221. expect(options.map(o => o.source)).toEqual(['orca_cloud']);
  222. });
  223. it('keeps an Orca Cloud library that overlaps an imported bundle by name', () => {
  224. // These are usually the same profiles reached two ways. Letting the
  225. // imported tier claim the name emptied the Orca Cloud group down to
  226. // whatever happened not to be imported too.
  227. const options = buildFilamentPresetOptions({
  228. localPresets: [
  229. localPreset({ id: 1, name: 'Elegoo PLA+ @BBL X1C' }),
  230. localPreset({ id: 2, name: 'Sunlu PETG @BBL X1C' }),
  231. ],
  232. orcaProfiles: [
  233. orcaProfile({ setting_id: 'u1', name: 'Elegoo PLA+ @BBL X1C' }),
  234. orcaProfile({ setting_id: 'u2', name: 'Sunlu PETG @BBL X1C' }),
  235. ],
  236. });
  237. expect(options.filter(o => o.source === 'local')).toHaveLength(2);
  238. expect(options.filter(o => o.source === 'orca_cloud')).toHaveLength(2);
  239. });
  240. it('still drops a cross-tier row that carries an id a higher tier claimed', () => {
  241. // A shared id is true identity, unlike a shared name.
  242. const options = buildFilamentPresetOptions({
  243. orcaProfiles: [orcaProfile({ setting_id: 'shared-id', name: 'Sunlu PETG' })],
  244. cloudSettings: [cloudSetting({ setting_id: 'shared-id', name: 'Something Else' })],
  245. });
  246. expect(options.map(o => o.source)).toEqual(['orca_cloud']);
  247. });
  248. it('never echoes a filament the tiers above already offered back from the builtin table', () => {
  249. // The builtin tier is a static copy of the same Bambu catalogue, so
  250. // without a name check it re-listed everything under a fourth heading.
  251. const options = buildFilamentPresetOptions({
  252. localPresets: [localPreset({ name: 'Bambu PLA Basic @BBL X1C 0.4 nozzle' })],
  253. builtinFilaments: [builtin('GFA00', 'Bambu PLA Basic'), builtin('GFA01', 'Bambu PLA Matte')],
  254. });
  255. expect(options.map(o => [o.source, o.name])).toEqual([
  256. ['local', 'Bambu PLA Basic'],
  257. ['builtin', 'Bambu PLA Matte'],
  258. ]);
  259. });
  260. it('suppresses a builtin a cloud tier already named, even with no id overlap', () => {
  261. const options = buildFilamentPresetOptions({
  262. cloudSettings: [cloudSetting({ setting_id: 'PFUSaaa', name: 'Bambu PLA Basic @BBL X1C', is_custom: true })],
  263. builtinFilaments: [builtin('GFA00', 'Bambu PLA Basic')],
  264. });
  265. expect(options.map(o => o.source)).toEqual(['cloud']);
  266. });
  267. it('does not let one import swallow every filament of the same material', () => {
  268. // Imports resolve to a shared generic id (all PLA → GFL99); claiming that
  269. // id would hide every other PLA behind the first one imported.
  270. const options = buildFilamentPresetOptions({
  271. localPresets: [
  272. localPreset({ id: 1, name: 'Elegoo PLA+', filament_type: 'PLA' }),
  273. localPreset({ id: 2, name: 'Polymaker PolyLite PLA', filament_type: 'PLA' }),
  274. ],
  275. builtinFilaments: [builtin('GFL99', 'Generic PLA')],
  276. });
  277. expect(options.map(o => o.name)).toEqual([
  278. 'Elegoo PLA+',
  279. 'Polymaker PolyLite PLA',
  280. 'Generic PLA',
  281. ]);
  282. });
  283. it('strips printer suffixes from displayed names', () => {
  284. const [option] = buildFilamentPresetOptions({ localPresets: [localPreset()] });
  285. expect(option.name).toBe('Elegoo PLA+');
  286. });
  287. });
  288. describe('resolveFilamentId', () => {
  289. const option = (over = {}) => ({
  290. id: 'PFUS9ac902733670a9',
  291. name: 'My PETG',
  292. source: 'cloud' as const,
  293. filamentId: '',
  294. filamentType: 'PETG',
  295. ...over,
  296. });
  297. it('returns an already-known id without fetching', async () => {
  298. const fetchDetail = vi.fn();
  299. await expect(resolveFilamentId(option({ filamentId: 'GFA00' }), fetchDetail)).resolves.toBe('GFA00');
  300. expect(fetchDetail).not.toHaveBeenCalled();
  301. });
  302. it('fetches the cloud detail for a user preset', async () => {
  303. const fetchDetail = vi.fn().mockResolvedValue({ filament_id: 'P285e239' });
  304. await expect(resolveFilamentId(option(), fetchDetail)).resolves.toBe('P285e239');
  305. expect(fetchDetail).toHaveBeenCalledWith('PFUS9ac902733670a9');
  306. });
  307. it('returns empty when the detail carries no filament_id', async () => {
  308. // Never fall back to base_id: that collapses a custom preset onto the
  309. // generic it inherits from (#1053).
  310. const fetchDetail = vi.fn().mockResolvedValue({ base_id: 'GFSG98_09' });
  311. await expect(resolveFilamentId(option(), fetchDetail)).resolves.toBe('');
  312. });
  313. it('returns empty when the detail lookup fails', async () => {
  314. const fetchDetail = vi.fn().mockRejectedValue(new Error('offline'));
  315. await expect(resolveFilamentId(option(), fetchDetail)).resolves.toBe('');
  316. });
  317. it('does not fetch for a non-cloud tier that resolved to nothing', async () => {
  318. const fetchDetail = vi.fn();
  319. const unknown = option({ source: 'local' as const, filamentType: 'UNOBTANIUM' });
  320. await expect(resolveFilamentId(unknown, fetchDetail)).resolves.toBe('');
  321. expect(fetchDetail).not.toHaveBeenCalled();
  322. });
  323. });