Files
codegraph/__tests__/ui-conditions.test.ts
T
Colby McHenry e288d7645b feat(expo-router): add Expo Router support for Screens and navigations and introduce Steps API
- Adds Expo Router integration with a new Screens view and a Steps API to surface screens and their transitions.
- Extends codegraph extraction/resolution to handle namespace objects, React hook bindings for handlers, and Swift RN bridge evidence; introduces per-site guard arguments and trigger metadata, enabling richer flow analysis across JS ↔ native boundaries.
- Introduces UI and data-model changes to represent conditions as words (WHEN/AND/OR/NOT), display per-site call arguments, and show what fires a site (triggers). Adds new utilities (ui/conditions.ts) and updates ScreensView and StepsView to render scenarios with multiple sites and “ways” counts.
- Implements site readers for WHEN/ARGS/TRIGGER, and wiring to expose steps via API endpoints (including /api/steps); enhances tests to cover namespace resolution, useCallback-driven handlers, and inline RN event listeners.
- Updates styling and templates to reflect the new wording, scenario rows, and per-site details, including NOT instead of leading negation strings and multi-way links.
- Documents and reflects changes in changelog and design docs to describe Expo Router integration and the Steps surface.
2026-08-28 10:21:46 -05:00

70 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Conditions as a reader says them: the joins we add (`&&` between guards,
* `||` between a link's scenarios, `!(…)` around a negated guard) become
* and / or / not, the code inside a guard stays code, and a link with several
* call sites is several scenarios with their shared clauses said once.
*/
import { describe, it, expect } from 'vitest';
import { clauseWords, clauses, restWords, scenarios, splitTop, whenWords } from '../ui/src/lib/conditions';
describe('conditions', () => {
it('splits at the top level only, respecting brackets and strings', () => {
expect(splitTop('a && (b || c) && "x && y" && d', ' && ')).toEqual(['a', '(b || c)', '"x && y"', 'd']);
expect(splitTop('a && b || c && d', ' || ')).toEqual(['a && b', 'c && d']);
expect(clauses('!busy && isCollected')).toEqual(['!busy', 'isCollected']);
// A merged condition has no single innermost clause: it comes back whole.
expect(clauses('a && b || c')).toEqual(['a && b || c']);
});
it('says NOT for our negations and leaves the code inside alone', () => {
expect(clauseWords('!busy')).toBe('NOT busy');
expect(clauseWords('!user?.organization_id')).toBe('NOT user?.organization_id');
expect(clauseWords('!(isUploadInProgress || elapsed < 5000)')).toBe('NOT (isUploadInProgress || elapsed < 5000)');
// `!(a) || b` is not a negated whole: untouched.
expect(clauseWords('!(a) || b')).toBe('!(a) || b');
expect(clauseWords('(!object?.id || !object?.name)')).toBe('(!object?.id || !object?.name)');
expect(clauseWords('selectedDetectionItems.length === 1')).toBe('selectedDetectionItems.length === 1');
});
it('words a whole condition: AND within a scenario, OR between scenarios', () => {
expect(whenWords('!(busy || late) && user?.organization_id && !object?.id')).toBe(
'NOT (busy || late) AND user?.organization_id AND NOT object?.id'
);
expect(whenWords('!x && y || !x && !y')).toBe('NOT x AND y OR NOT x AND NOT y');
// The same guard met twice along a chain is said once.
expect(whenWords('ctl && !(!ctl || done) && !(!ctl || done) && ready')).toBe('ctl AND NOT (!ctl || done) AND ready');
expect(scenarios([{ when: 'a && a && b' }]).common).toEqual(['a', 'b']);
expect(whenWords('')).toBe('');
});
it('factors the clauses every scenario shares, and keeps each rows own tail', () => {
const sites = [
{ line: 248, when: '!(busy || late) && !user?.organization_id' },
{ line: 257, when: '!(busy || late) && user?.organization_id && (!object?.id || !object?.name)' },
{ line: 292, when: '!(busy || late) && user?.organization_id && !(!object?.id || !object?.name) && items.length === 1' },
{ line: 306, when: '!(busy || late) && user?.organization_id && !(!object?.id || !object?.name) && !items.length' },
];
const sc = scenarios(sites);
expect(sc.common).toEqual(['!(busy || late)']);
expect(sc.rows.map((r) => r.rest)).toEqual([
['!user?.organization_id'],
['user?.organization_id', '(!object?.id || !object?.name)'],
['user?.organization_id', '!(!object?.id || !object?.name)', 'items.length === 1'],
['user?.organization_id', '!(!object?.id || !object?.name)', '!items.length'],
]);
expect(restWords(sc.rows[0]!.rest, true)).toBe('AND NOT user?.organization_id');
expect(restWords(sc.rows[2]!.rest, true)).toBe(
'AND user?.organization_id AND NOT (!object?.id || !object?.name) AND items.length === 1'
);
});
it('one site is one scenario with nothing left to say; no shared prefix says when', () => {
expect(scenarios([{ when: 'a && b' }])).toEqual({ common: ['a', 'b'], rows: [{ site: { when: 'a && b' }, rest: [] }] });
const sc = scenarios([{ when: 'a' }, { when: 'b' }, { when: '' }]);
expect(sc.common).toEqual([]);
expect(restWords(sc.rows[0]!.rest, false)).toBe('WHEN a');
expect(restWords(sc.rows[2]!.rest, false)).toBe('always');
expect(scenarios([])).toEqual({ common: [], rows: [] });
});
});