ui-steps-model.test.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. /**
  2. * The Steps view's model, without a browser: rows by the server's depth, the
  3. * words in a box by kind, one edge per pair with the Screens view's label
  4. * rule, and the panel's two lists.
  5. */
  6. import { describe, it, expect } from 'vitest';
  7. import { buildStepsModel, kindWord, stepLabel, stepNeighbourhood, stepSub, stepViaText } from '../ui/src/lib/steps-model';
  8. import { placeLabels } from '../ui/src/lib/screens-model';
  9. import type { WireNodeRef, WireStep, WireStepLink, WireStepsPayload } from '../ui/src/lib/wire';
  10. function ref(name: string, file = 'src/a.tsx', language: WireNodeRef['language'] = 'tsx'): WireNodeRef {
  11. return { id: `function:${name}`, kind: 'function', name, qualifiedName: name, file, line: 1, endLine: 9, language, test: false };
  12. }
  13. function step(label: string, kind: WireStep['kind'], depth: number, extra: Partial<WireStep> = {}): WireStep {
  14. const node = kind === 'effect' ? null : ref(label, extra.node?.file ?? 'src/a.tsx');
  15. return { id: node?.id ?? `effect:fn:${label}`, kind, anchor: depth === 0, node, label, sub: 'src/a.tsx', depth, cut: null, ...extra };
  16. }
  17. function link(from: WireStep, to: WireStep, extra: Partial<WireStepLink> = {}): WireStepLink {
  18. return { id: `${from.id} ${to.id}`, from: from.id, to: to.id, kind: 'calls', via: [], when: '', label: '', synthesized: false, uncertain: false, sites: [], ...extra };
  19. }
  20. function payload(steps: WireStep[], links: WireStepLink[]): WireStepsPayload {
  21. return {
  22. anchor: steps[0]!.node!,
  23. ambiguous: [],
  24. steps,
  25. links,
  26. depth: 8,
  27. limit: 120,
  28. through: false,
  29. truncated: { steps: 0, hubs: 0, chrome: 0 },
  30. index: { lastIndexedAt: null, edges: 0, files: 0 },
  31. timing: { elapsedMs: 1 },
  32. };
  33. }
  34. describe('steps model', () => {
  35. const screen = step('/capture/review', 'screen', 0, { screen: { path: '/capture/review', component: ref('ReviewScreen') } });
  36. const handler = step('handleApprove', 'trigger', 1);
  37. const bridge = step('finalizeCaptureSession', 'bridge', 2, { node: ref('finalizeCaptureSession', 'ios/CaptureView.swift', 'swift') });
  38. const event = step('handleZipComplete', 'event', 3, { event: 'onZipComplete' });
  39. const effect = step('client.post', 'effect', 4, { sub: 'network · uploadARCapture', effect: { api: 'client.post', apis: ['client.post'], category: 'network', by: ref('uploadARCapture'), line: 3 } });
  40. const store = step('setZipUri', 'store', 4, { node: ref('setZipUri', 'src/storage/capture.storage.ts') });
  41. const home = step('/', 'screen', 4, { screen: { path: '/', component: null } });
  42. const links = [
  43. link(screen, handler, { kind: 'handler' }),
  44. link(handler, bridge, { kind: 'bridge', when: '!busy' }),
  45. link(bridge, event, { kind: 'event', synthesized: true, via: [ref('emitZipComplete', 'ios/CaptureEvents.swift', 'swift')], when: 'result', label: 'via rn-event-channel · event onZipComplete' }),
  46. link(event, effect, { kind: 'effect', via: [ref('uploadARCapture')] }),
  47. link(event, store, { kind: 'store' }),
  48. link(event, home, { kind: 'navigates', when: 'unlimited' }),
  49. // A second way from the event to the store, unconditional: the pair is one edge saying "2 ways".
  50. { ...link(event, store, { kind: 'store', when: 'retry' }), id: 'second' },
  51. ];
  52. const model = buildStepsModel(payload([screen, handler, bridge, event, effect, store, home], links));
  53. it('puts the anchor on top and each row one step further away', () => {
  54. const y = (id: string) => model.layout.nodes.find((n) => n.id === id)!.y;
  55. expect(y(screen.id)).toBeLessThan(y(handler.id));
  56. expect(y(handler.id)).toBeLessThan(y(bridge.id));
  57. expect(y(bridge.id)).toBeLessThan(y(event.id));
  58. expect(y(event.id)).toBeLessThan(y(effect.id));
  59. expect(y(effect.id)).toBe(y(store.id));
  60. expect(y(effect.id)).toBe(y(home.id));
  61. });
  62. it('one edge per pair, labelled with the innermost condition or a count', () => {
  63. const edges = [...model.edges.values()];
  64. expect(edges).toHaveLength(6);
  65. const toBridge = edges.find((e) => e.to === bridge.id)!;
  66. expect(toBridge.label).toBe('NOT busy');
  67. expect(toBridge.kind).toBe('bridge');
  68. const toEvent = edges.find((e) => e.to === event.id)!;
  69. expect(toEvent.synthesized).toBe(true);
  70. expect(toEvent.label).toBe('result');
  71. const toStore = edges.find((e) => e.to === store.id)!;
  72. expect(toStore.links).toHaveLength(2);
  73. expect(toStore.label).toBe('2 ways · 1 conditional');
  74. expect(toStore.kind).toBe('store');
  75. });
  76. it('counts steps per kind', () => {
  77. expect(model.counts).toEqual({ anchor: 0, screen: 2, trigger: 1, bridge: 1, event: 1, store: 1, effect: 1 });
  78. });
  79. it('words a box by its kind', () => {
  80. expect(stepLabel(bridge)).toBe('⇢ finalizeCaptureSession');
  81. expect(stepLabel(event)).toBe('⇠ onZipComplete');
  82. expect(stepLabel({ ...event, events: ['onZipComplete', 'onZipError', 'onCameraReady'] })).toBe('⇠ onZipComplete +2');
  83. expect(stepLabel(screen)).toBe('/capture/review');
  84. expect(stepSub(event)).toBe('handleZipComplete · a.tsx');
  85. expect(stepSub(bridge)).toBe('native · CaptureView.swift');
  86. expect(stepSub(store)).toBe('store · capture.storage.ts');
  87. expect(stepSub(effect)).toBe('network · uploadARCapture');
  88. expect(kindWord('effect')).toBe('outside the index');
  89. expect(stepViaText(links[2]!)).toBe('emitZipComplete');
  90. });
  91. it('labels a selected step at the far end of each line, and lists its links', () => {
  92. const pills = placeLabels(model, event.id);
  93. expect(pills.hidden).toBe(0);
  94. const words = [...pills.pills.values()].map((p) => p.text).sort();
  95. expect(words).toEqual(['← result', '→ 2 ways · 1 conditional', '→ unlimited']);
  96. const lists = stepNeighbourhood(payload([screen, handler, bridge, event, effect, store, home], links), event.id);
  97. expect(lists.arrivesFrom.map((l) => l.from)).toEqual([bridge.id]);
  98. expect(lists.leadsTo.map((l) => l.to)).toEqual([effect.id, store.id, home.id, store.id]);
  99. });
  100. });