import { describe, expect, it } from 'vitest'; import { AddNodeCommand, SetNodeParamCommand } from '../../src/core/commands/graphCommands'; import { requireNodeDef } from '../../src/core/commands/require'; import { AddVariableCommand } from '../../src/core/commands/variableCommands'; import { FIND, FIND_MODE, type FindMode, GraphInterpreter, resolveDef, searchOutcome, } from '../../src/core/graph'; import { graphLookup } from '../../src/core/model/graphLookup'; import type { PropertyValue } from '../../src/core/model/Properties'; import { paletteEntries, rankedEntries } from '../../src/editor/graph/paletteEntries'; import { fakeContext, graphEditor, makeBehavior, makeProject, fire } from '../helpers'; /** A behavior with a list variable `names`, a Find over it testing `item == wanted`, and its Match. */ function block(wanted: PropertyValue = 'b', mode?: FindMode) { const fixture = makeProject(); const behavior = makeBehavior(fixture); fixture.stack.execute(new AddVariableCommand(behavior.id, 'names', 'list')); const edit = graphEditor(fixture, behavior); const add = new AddNodeCommand(behavior.id, FIND, 0, 0, mode ? { [FIND_MODE]: mode } : {}); fixture.stack.execute(add); const find = add.node; const match = add.closing!; const start = edit.addNode('OnStart'); const get = edit.variables(['names']); const equals = edit.addNode('Equals', { b: wanted }); edit.wire([ [start, 'exec', find, 'exec'], [get, 'names', find, 'list'], [find, 'item', equals, 'a'], [equals, 'result', match, 'match'], ]); const outputs = () => resolveDef(match, behavior.graph, graphLookup(fixture.project))!.outputs.map((p) => p.key); const setMode = (value: FindMode) => fixture.stack.execute(new SetNodeParamCommand(behavior.id, find.id, FIND_MODE, value)); return { ...fixture, behavior, edit, find, match, outputs, setMode }; } /** Runs a block in `mode` over `names`, logging each of the Match's `ports` after `done`. */ function answer(mode: FindMode, names: readonly PropertyValue[], ports: string[]): string[] { const { behavior, edit, match } = block('b', mode); let from = { node: match, port: 'done' }; for (const port of ports) { const log = edit.addNode('Log'); edit.wire([ [from.node, from.port, log, 'exec'], [match, port, log, 'message'], ]); from = { node: log, port: 'exec' }; } const ctx = fakeContext({ variables: { names: [...names] } }); fire(new GraphInterpreter(behavior.graph), 'OnStart', ctx); return ctx.logs; } describe('Find modes at run time (#259)', () => { it('All hands on every item the test is true for, and their indices', () => { expect(answer('all', ['a', 'b', 'c', 'b'], ['items', 'indices'])).toEqual(['b, b', '1, 3']); }); it('All with no hit goes on at done with two empty lists', () => { expect(answer('all', ['a', 'c'], ['items', 'indices'])).toEqual(['', '']); }); it('Any says whether there is one', () => { expect(answer('any', ['a', 'b'], ['any'])).toEqual(['true']); expect(answer('any', ['a', 'c'], ['any'])).toEqual(['false']); expect(answer('any', [], ['any'])).toEqual(['false']); }); it('Count counts every hit', () => { expect(answer('count', ['b', 'a', 'b', 'b'], ['count'])).toEqual(['3']); expect(answer('count', [], ['count'])).toEqual(['0']); }); it('First and Any stop at the first hit, All and Count look at every element', () => { const values = ['x', 'y', 'z']; expect(searchOutcome('first', values, [1])).toEqual({ next: 'found', match: { item: 'y', index: 1 }, find: { item: 'y', index: 1 }, }); expect(searchOutcome('first', values, []).next).toBe('notFound'); expect(searchOutcome('all', values, [0, 2])).toEqual({ next: 'done', match: { items: ['x', 'z'], indices: [0, 2] }, find: { item: null, index: -1 }, }); expect(searchOutcome('any', values, [2]).match).toEqual({ any: true }); expect(searchOutcome('count', values, [0, 1]).match).toEqual({ count: 2 }); }); }); describe('the Match follows its Find', () => { it('has the outputs of its Find’s mode, in the order they are drawn', () => { const { outputs, setMode } = block(); expect(outputs()).toEqual(['found', 'item', 'index', 'notFound']); setMode('all'); expect(outputs()).toEqual(['done', 'items', 'indices']); setMode('any'); expect(outputs()).toEqual(['done', 'any']); setMode('count'); expect(outputs()).toEqual(['done', 'count']); }); it('a Find saved without a mode is First, and First leaves the setting off the node', () => { const { find, outputs } = block(); delete find.params[FIND_MODE]; expect(outputs()).toEqual(['found', 'item', 'index', 'notFound']); const hidden = requireNodeDef(FIND).hiddenParams!; expect(hidden(find.params)).toEqual([FIND_MODE]); expect(hidden({ [FIND_MODE]: 'all' })).toEqual([]); }); it('switching the mode drops the Match’s edges on outputs that are gone; undo brings them back', () => { const { behavior, edit, match, setMode, stack } = block(); const hit = edit.addNode('Log'); const other = edit.addNode('Log'); edit.wire([ [match, 'found', hit, 'exec'], [match, 'item', hit, 'message'], ]); const before = behavior.graph.edges.length; setMode('all'); expect(behavior.graph.edges.length).toBe(before - 2); edit.wire([[match, 'done', other, 'exec']]); setMode('count'); // `done` is there in both modes, so its edge stays. expect(behavior.graph.edges.some((e) => e.from.node === match.id && e.from.port === 'done')).toBe(true); stack.undo(); stack.undo(); stack.undo(); expect(behavior.graph.edges.length).toBe(before); expect(behavior.graph.edges.some((e) => e.from.port === 'found')).toBe(true); }); it('an edge that already hung on a gone port elsewhere is left to show as a ghost', () => { const { behavior, edit, find, stack } = block(); const log = edit.addNode('Log'); const ghost = { id: 'ghost', from: { node: log.id, port: 'gone' }, to: { node: log.id, port: 'exec' } }; behavior.graph.edges.push(ghost); stack.execute(new SetNodeParamCommand(behavior.id, find.id, FIND_MODE, 'all')); expect(behavior.graph.edges).toContain(ghost); }); }); describe('the Filter entry', () => { it('is a Find set to All under a name of its own', () => { const { project, behavior } = block(); const filter = paletteEntries(project, behavior).find((e) => e.def.label === 'Filter')!; expect(filter.def.type).toBe(FIND); expect(filter.params).toEqual({ [FIND_MODE]: 'all' }); const add = new AddNodeCommand(behavior.id, filter.def.type, 0, 0, filter.params); expect(add.node.params[FIND_MODE]).toBe('all'); expect(add.closing).not.toBeNull(); }); it('"filter" finds it first, "find" still the plain Find', () => { const { project, behavior } = block(); const first = (query: string) => rankedEntries(paletteEntries(project, behavior), query)[0]!; expect([first('filter').def.label, first('filter').params]).toEqual(['Filter', { [FIND_MODE]: 'all' }]); expect([first('find').def.label, first('find').params]).toEqual(['Find', {}]); }); });