ui-program-model.test.ts 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. /**
  2. * What the rail SAYS. The block tree the server sends is turned into rows of
  3. * boxes and words here (`ui/src/lib/program-model.ts`); this pins the words —
  4. * which is the part a reader actually meets.
  5. */
  6. import { describe, it, expect } from 'vitest';
  7. import { joinTokens } from '../ui/src/lib/conditions';
  8. import { armWords, buildRailModel, endWords, groupLabel } from '../ui/src/lib/program-model';
  9. import type { WireArm, WireBlock, WireItem, WireStep, WireStepsPayload } from '../ui/src/lib/wire';
  10. const step = (id: string, over: Partial<WireStep> = {}): WireStep => ({
  11. id,
  12. kind: 'effect',
  13. anchor: false,
  14. node: null,
  15. label: id,
  16. sub: `response · handler`,
  17. depth: 1,
  18. cut: null,
  19. ...over,
  20. });
  21. function payload(steps: WireStep[], root: WireBlock): WireStepsPayload {
  22. return {
  23. anchor: { id: 'a', kind: 'route', name: 'POST /login', qualifiedName: 'POST /login', file: 'r.js', line: 1, endLine: 1, language: 'javascript', test: false },
  24. ambiguous: [],
  25. project: 'api',
  26. steps,
  27. links: [],
  28. program: { root, truncated: 0 },
  29. defaultView: 'order',
  30. depth: 8,
  31. limit: 120,
  32. through: false,
  33. truncated: { steps: 0, hubs: 0, chrome: 0 },
  34. index: { lastIndexedAt: null, edges: 0, files: 0 },
  35. timing: { elapsedMs: 1 },
  36. };
  37. }
  38. const arm = (when: string, over: Partial<WireArm> = {}): WireArm => ({ when, ends: null, body: [], ...over });
  39. describe('the rail’s words', () => {
  40. it('says the decision once, and each arm only which side it is', () => {
  41. const on = 'user && (await user.matchPassword(password))';
  42. const fork: WireItem = {
  43. kind: 'fork',
  44. form: 'if',
  45. on,
  46. arms: [arm(on, { ends: 'reply', body: [{ kind: 'step', step: '200' }] }), arm(`!(${on})`, { not: true, ends: 'reply', body: [{ kind: 'step', step: '401' }] })],
  47. };
  48. const model = buildRailModel(payload([step('200'), step('401')], [fork]));
  49. expect(model).toHaveLength(1);
  50. const rail = model[0]!;
  51. if (rail.kind !== 'fork') throw new Error('expected a fork');
  52. expect(joinTokens(rail.words)).toBe('user AND (await user.matchPassword(password))');
  53. expect(rail.arms.map((a) => joinTokens(a.words))).toEqual(['WHEN', 'WHEN NOT']);
  54. expect(rail.arms.map((a) => a.ends)).toEqual(['answers here', 'answers here']);
  55. });
  56. it('keeps a disjunction whole rather than reading it as two ways of arriving', () => {
  57. // `!image || unlimitedCollection` is ONE condition, and the parentheses
  58. // `guardLabel` puts round it are what stop the OR from splitting it.
  59. const on = '(!image || unlimitedCollection)';
  60. const model = buildRailModel(payload([], [{ kind: 'fork', form: 'if', on, arms: [arm(on)] }]));
  61. const rail = model[0]!;
  62. if (rail.kind !== 'fork') throw new Error('expected a fork');
  63. expect(joinTokens(rail.words)).toBe('(!image || unlimitedCollection)');
  64. });
  65. it('gives a switch’s arms their own conditions', () => {
  66. const fork: WireItem = {
  67. kind: 'fork',
  68. form: 'switch',
  69. on: 'kind',
  70. arms: [arm("kind === 'a'"), arm('kind: default')],
  71. };
  72. const model = buildRailModel(payload([], [fork]));
  73. const rail = model[0]!;
  74. if (rail.kind !== 'fork') throw new Error('expected a fork');
  75. expect(joinTokens(rail.words)).toBe('kind');
  76. expect(rail.arms.map((a) => joinTokens(a.words))).toEqual(["WHEN kind === 'a'", 'WHEN kind: default']);
  77. });
  78. it('lets a try say `on error` once', () => {
  79. expect(armWords('try', 'on error', arm('on error'))).toEqual([]);
  80. const model = buildRailModel(payload([], [{ kind: 'fork', form: 'try', on: 'on error', arms: [arm('on error')] }]));
  81. const rail = model[0]!;
  82. if (rail.kind !== 'fork') throw new Error('expected a fork');
  83. expect(joinTokens(rail.words)).toBe('on error');
  84. });
  85. it('says how each arm leaves', () => {
  86. expect(endWords('reply')).toBe('answers here');
  87. expect(endWords('return')).toBe('returns here');
  88. expect(endWords('throw')).toBe('throws here');
  89. expect(endWords('exit')).toBe('leaves here');
  90. });
  91. it('names each kind of bracketed run', () => {
  92. const via = { id: 'f', kind: 'function' as const, name: 'generateToken', qualifiedName: 'generateToken', file: 'a.js', line: 1, endLine: 2, language: 'javascript', test: false };
  93. expect(groupLabel({ kind: 'block', block: 'inline', via, body: [] })).toBe('via generateToken');
  94. expect(groupLabel({ kind: 'block', block: 'inline', body: [] })).toBe('via a helper');
  95. expect(groupLabel({ kind: 'block', block: 'later', by: 'then', body: [] })).toBe('later · then');
  96. expect(groupLabel({ kind: 'block', block: 'loop', by: 'item of items', body: [] })).toBe('for each item of items');
  97. expect(groupLabel({ kind: 'block', block: 'together', by: 'Promise.all', body: [] })).toBe('together · Promise.all');
  98. });
  99. it('carries a box’s two lines and where the call is written', () => {
  100. const model = buildRailModel(
  101. payload([step('200', { label: '200', sub: 'response · authUser' })], [{ kind: 'step', step: '200', within: 'res.json' }])
  102. );
  103. const rail = model[0]!;
  104. if (rail.kind !== 'step') throw new Error('expected a step');
  105. expect(rail.info?.label).toBe('200');
  106. expect(rail.info?.sub).toBe('response · authUser');
  107. expect(rail.within).toBe('res.json');
  108. });
  109. it('says where the reading stopped', () => {
  110. const model = buildRailModel(payload([], [{ kind: 'cut', why: 'folded' }, { kind: 'cut', why: 'depth' }]));
  111. expect(model.map((i) => (i.kind === 'cut' ? i.text : ''))).toEqual([
  112. 'reads back into itself — the rest is the same code again',
  113. 'as deep as this reading goes — start at a step below to read on',
  114. ]);
  115. });
  116. it('is empty when there is no body to read', () => {
  117. expect(buildRailModel({ ...payload([], []), program: null })).toEqual([]);
  118. });
  119. });