import { describe, expect, it } from 'vitest'; import { filterMachines, machineMatchesSearch, normalizeSearchText, queryMachines, scoreMachineForQuery, searchMachines, sortMachines, tokenizeSearchQuery, } from '../src/utils/search'; type FixtureMachine = { id: string; slug: string; name: string; category: string | { id: string; name: string }; difficulty: string; status: string; tags: string[]; summary: string; components: Array<{ name: string; description: string; tags?: string[] }>; specs: Array<{ label: string; value: string }>; popularity: number; createdAt: string; updatedAt: string; featured?: boolean; }; const machines: FixtureMachine[] = [ { id: 'inline-four-engine', slug: 'inline-four-engine', name: 'Inline Four Engine', category: 'Engines', difficulty: 'Intermediate', status: 'available', tags: ['combustion', 'piston', 'crankshaft'], summary: 'A four-cylinder internal combustion engine showing reciprocating pistons, connecting rods, cam timing, and crankshaft rotation.', components: [ { name: 'Crankshaft', description: 'Converts reciprocating piston force into smooth rotary output torque.', }, { name: 'Camshaft', description: 'Synchronises valve opening with the four-stroke combustion cycle.', }, ], specs: [ { label: 'Cycle', value: 'Four-stroke Otto' }, { label: 'Layout', value: 'Inline transverse' }, ], popularity: 91, createdAt: '2024-02-12T00:00:00.000Z', updatedAt: '2024-11-07T00:00:00.000Z', featured: true, }, { id: 'planetary-gearset', slug: 'planetary-gearbox', name: 'Planetary Gearbox', category: { id: 'gearboxes', name: 'Gearboxes' }, difficulty: 'Advanced', status: 'available', tags: ['gears', 'transmission', 'sun gear'], summary: 'A compact epicyclic gear train where planet gears orbit a central sun gear inside a ring gear to multiply torque.', components: [ { name: 'Sun gear', description: 'Central input gear that drives the surrounding planet gears.', }, { name: 'Planet carrier', description: 'Holds each planet pinion at equal spacing while rotating as an output member.', }, ], specs: [ { label: 'Reduction', value: '4:1' }, { label: 'Stages', value: 'Single epicyclic stage' }, ], popularity: 78, createdAt: '2025-03-02T00:00:00.000Z', updatedAt: '2025-03-20T00:00:00.000Z', }, { id: 'centrifugal-pump', slug: 'centrifugal-pump', name: 'Centrifugal Pump', category: 'Pumps', difficulty: 'Beginner', status: 'coming-soon', tags: ['fluid', 'impeller', 'volute'], summary: 'A dynamic fluid machine that accelerates liquid radially through an impeller and recovers pressure in a volute casing.', components: [ { name: 'Impeller', description: 'Adds kinetic energy to liquid by sweeping it outward from the eye of the pump.', }, { name: 'Volute casing', description: 'Diffuses velocity into static pressure before the outlet flange.', }, ], specs: [ { label: 'Flow regime', value: 'Continuous radial flow' }, { label: 'Prime mover', value: 'Electric motor' }, ], popularity: 34, createdAt: '2023-09-18T00:00:00.000Z', updatedAt: '2023-12-01T00:00:00.000Z', }, ]; describe('catalogue search utilities', () => { it('normalises punctuation, accents, symbols, and mechanical shorthand consistently', () => { expect(normalizeSearchText('Café-racer μ pump V8/2-stroke')).toBe( 'cafe racer micro pump v8 2 stroke', ); expect(tokenizeSearchQuery(' V8 crank-shaft ')).toEqual(['v8', 'crank', 'shaft']); }); it('searches across names, nested components, tags, summaries, and specs', () => { expect(searchMachines(machines, 'sun gear').map((machine) => machine.id)).toEqual([ 'planetary-gearset', ]); expect(searchMachines(machines, 'otto crank').map((machine) => machine.id)).toEqual([ 'inline-four-engine', ]); expect(machineMatchesSearch(machines[2], 'volute pressure')).toBe(true); expect(machineMatchesSearch(machines[0], 'volute pressure')).toBe(false); }); it('combines text search with category and availability filters', () => { expect( queryMachines(machines, { search: 'shaft', category: 'engine', availableOnly: true, }).map((machine) => machine.id), ).toEqual(['inline-four-engine']); expect( queryMachines(machines, { category: 'pump', availableOnly: true, }), ).toEqual([]); }); it('supports strict tag matching and persisted favourite id filters', () => { expect( queryMachines(machines, { tags: ['gears', 'transmission'], tagOperator: 'all', }).map((machine) => machine.id), ).toEqual(['planetary-gearset']); expect( queryMachines(machines, { favouritesOnly: true, favouriteIds: ['centrifugal-pump'], }).map((machine) => machine.id), ).toEqual(['centrifugal-pump']); }); it('sorts stable catalogue results by engineering difficulty and date aliases', () => { expect( sortMachines(machines, { sortBy: 'difficulty', sortDirection: 'asc', }).map((machine) => machine.id), ).toEqual(['centrifugal-pump', 'inline-four-engine', 'planetary-gearset']); expect(sortMachines(machines, 'newest').map((machine) => machine.id)[0]).toBe( 'planetary-gearset', ); expect(sortMachines(machines, 'z-a').map((machine) => machine.id)[0]).toBe( 'planetary-gearset', ); }); it('uses relevance ranking for search-first catalogue interactions', () => { const scoredPump = scoreMachineForQuery(machines[2], 'pump'); const scoredEngine = scoreMachineForQuery(machines[0], 'pump'); expect(scoredPump).toBeGreaterThan(scoredEngine); expect(queryMachines(machines, { search: 'pump', sortBy: 'relevance' })[0].id).toBe( 'centrifugal-pump', ); }); it('keeps the legacy filterMachines(searchString) call path working', () => { expect(filterMachines(machines, 'planet carrier').map((machine) => machine.id)).toEqual([ 'planetary-gearset', ]); }); });