|
| 1 | +import test from 'node:test'; |
| 2 | +import assert from 'node:assert/strict'; |
| 3 | +import { buildSnapshotDiff, snapshotNodeToComparableLine } from '../snapshot-diff.ts'; |
| 4 | +import { attachRefs, type RawSnapshotNode } from '../../utils/snapshot.ts'; |
| 5 | + |
| 6 | +function nodes(raw: RawSnapshotNode[]) { |
| 7 | + return attachRefs(raw); |
| 8 | +} |
| 9 | + |
| 10 | +test('snapshotNodeToComparableLine ignores volatile fields', () => { |
| 11 | + const [node] = nodes([{ |
| 12 | + index: 0, |
| 13 | + type: 'XCUIElementTypeTextField', |
| 14 | + label: 'Email', |
| 15 | + value: 'test@example.com', |
| 16 | + identifier: 'email-input', |
| 17 | + depth: 1, |
| 18 | + rect: { x: 10, y: 20, width: 100, height: 20 }, |
| 19 | + }]); |
| 20 | + assert.equal( |
| 21 | + snapshotNodeToComparableLine(node), |
| 22 | + ' textfield label="Email" value="test@example.com" id="email-input"', |
| 23 | + ); |
| 24 | +}); |
| 25 | + |
| 26 | +test('buildSnapshotDiff returns unchanged lines when snapshots match', () => { |
| 27 | + const previous = nodes([ |
| 28 | + { index: 0, type: 'button', label: 'Submit', depth: 0 }, |
| 29 | + { index: 1, type: 'text', label: 'Create account', depth: 1 }, |
| 30 | + ]); |
| 31 | + const current = nodes([ |
| 32 | + { index: 0, type: 'button', label: 'Submit', depth: 0 }, |
| 33 | + { index: 1, type: 'text', label: 'Create account', depth: 1 }, |
| 34 | + ]); |
| 35 | + const diff = buildSnapshotDiff(previous, current); |
| 36 | + assert.deepEqual(diff.summary, { additions: 0, removals: 0, unchanged: 2 }); |
| 37 | + assert.equal(diff.lines.length, 2); |
| 38 | + assert.equal(diff.lines[0]?.kind, 'unchanged'); |
| 39 | + assert.equal(diff.lines[1]?.kind, 'unchanged'); |
| 40 | +}); |
| 41 | + |
| 42 | +test('buildSnapshotDiff reports removals and additions for value changes', () => { |
| 43 | + const previous = nodes([ |
| 44 | + { index: 0, type: 'textfield', label: 'Email', value: '', depth: 0 }, |
| 45 | + { index: 1, type: 'button', label: 'Submit', depth: 0, enabled: true }, |
| 46 | + ]); |
| 47 | + const current = nodes([ |
| 48 | + { index: 0, type: 'textfield', label: 'Email', value: 'test@example.com', depth: 0 }, |
| 49 | + { index: 1, type: 'button', label: 'Submit', depth: 0, enabled: false }, |
| 50 | + ]); |
| 51 | + const diff = buildSnapshotDiff(previous, current); |
| 52 | + assert.deepEqual(diff.summary, { additions: 2, removals: 2, unchanged: 0 }); |
| 53 | + assert.equal(diff.lines.length, 4); |
| 54 | + const removed = diff.lines.filter((line) => line.kind === 'removed'); |
| 55 | + const added = diff.lines.filter((line) => line.kind === 'added'); |
| 56 | + assert.equal(removed.length, 2); |
| 57 | + assert.equal(added.length, 2); |
| 58 | +}); |
| 59 | + |
| 60 | +test('buildSnapshotDiff keeps stable order with unchanged context', () => { |
| 61 | + const previous = nodes([ |
| 62 | + { index: 0, type: 'heading', label: 'Sign Up', depth: 0 }, |
| 63 | + { index: 1, type: 'text', label: 'Create account', depth: 0 }, |
| 64 | + { index: 2, type: 'button', label: 'Submit', depth: 0, enabled: true }, |
| 65 | + ]); |
| 66 | + const current = nodes([ |
| 67 | + { index: 0, type: 'heading', label: 'Sign Up', depth: 0 }, |
| 68 | + { index: 1, type: 'text', label: 'Create account', depth: 0 }, |
| 69 | + { index: 2, type: 'status', label: 'Sending...', depth: 0 }, |
| 70 | + { index: 3, type: 'button', label: 'Submit', depth: 0, enabled: false }, |
| 71 | + ]); |
| 72 | + const diff = buildSnapshotDiff(previous, current); |
| 73 | + assert.deepEqual(diff.summary, { additions: 2, removals: 1, unchanged: 2 }); |
| 74 | + const kinds = diff.lines.map((line) => line.kind); |
| 75 | + assert.equal(kinds[0], 'unchanged'); |
| 76 | + assert.equal(kinds[1], 'unchanged'); |
| 77 | + assert.equal(diff.lines.filter((line) => line.kind === 'added').length, 2); |
| 78 | + assert.equal(diff.lines.filter((line) => line.kind === 'removed').length, 1); |
| 79 | +}); |
| 80 | + |
| 81 | +test('buildSnapshotDiff uses linear fallback for very large snapshots', () => { |
| 82 | + const previousRaw: RawSnapshotNode[] = []; |
| 83 | + const currentRaw: RawSnapshotNode[] = []; |
| 84 | + for (let index = 0; index < 2_100; index += 1) { |
| 85 | + previousRaw.push({ index, type: 'text', label: `row-${index}`, depth: 0 }); |
| 86 | + currentRaw.push({ index, type: 'text', label: `row-${index}`, depth: 0 }); |
| 87 | + } |
| 88 | + // Change one line so we still exercise add/remove behavior while crossing fallback threshold. |
| 89 | + currentRaw[1_050] = { index: 1_050, type: 'text', label: 'row-1050-updated', depth: 0 }; |
| 90 | + const diff = buildSnapshotDiff(nodes(previousRaw), nodes(currentRaw)); |
| 91 | + assert.equal(diff.summary.additions, 1); |
| 92 | + assert.equal(diff.summary.removals, 1); |
| 93 | + assert.equal(diff.summary.unchanged, 2_099); |
| 94 | +}); |
0 commit comments