OILS / web / search.test.js View on Github | oils.pub

160 lines, 141 significant
1const assert = require('node:assert/strict');
2const test = require('node:test');
3const path = require('node:path');
4const fs = require('node:fs/promises');
5const vm = require('node:vm');
6
7/**
8 * Load web/search.js in a context which sets window.test to true.
9 *
10 * Without this, search.js will try to interact with the DOM and then fail
11 * because node.js doesn't provide DOM APIs.
12 *
13 * Returns the module (an object with all defined functions).
14 *
15 * Scaffolded with some help from ChatGPT.
16 */
17async function loadSearchModule() {
18 const sandbox = {
19 window: { test: true },
20 };
21 sandbox.globalThis = sandbox;
22
23 const scriptPath = path.join(__dirname, 'search.js');
24 const source = await fs.readFile(scriptPath, 'utf8');
25 const context = vm.createContext(sandbox);
26 new vm.Script(source, { filename: scriptPath }).runInContext(context);
27 return context;
28}
29
30/**
31 * Because we execute in a node.js VM context [0], you cannot assert.deepStrictEqual
32 * because array objects inside the vm context are different from those outside
33 * the context.
34 *
35 * If we do a (de)serialize loop through JSON, we reconstruct the objects using
36 * the main test context, which can then be compared using assert.deepStrictEqual.
37 *
38 * Without this, we get "Values have same structure but are not reference-equal"
39 * errors when running this test.
40 *
41 * See the "filterAndRank keeps matching descendants only" test for usage.
42 *
43 * [0]: https://nodejs.org/api/vm.html#vmcreatecontextcontextobject-options
44 */
45function pullFromContext(value) {
46 return JSON.parse(JSON.stringify(value));
47}
48
49let search;
50test.before(async () => {
51 search = await loadSearchModule();
52});
53
54test('damerauLevenshteinDistance handles common edits', () => {
55 const cases = [
56 // Empty string is fine
57 { a: '', b: '', expected: 0 },
58
59 // Exact match
60 { a: 'abc', b: 'abc', expected: 0 },
61
62 // 1 Deletion
63 { a: 'abc', b: 'ab', expected: 1 },
64
65 // 3 Additions
66 { a: '', b: 'abc', expected: 3 },
67
68 // 1 replacement
69 { a: 'abc', b: 'adc', expected: 1 },
70
71 // 1 Transposition
72 { a: 'ca', b: 'ac', expected: 1 },
73
74 // 2 replacements, 1 addition
75 { a: 'kitten', b: 'sitting', expected: 3 },
76 ];
77
78 for (const { a, b, expected } of cases) {
79 assert.strictEqual(
80 search.damerauLevenshteinDistance(a, b),
81 expected,
82 `distance between "${a}" and "${b}" should be ${expected}`
83 );
84 assert.strictEqual(
85 search.damerauLevenshteinDistance(b, a),
86 expected,
87 `distance should be symmetric for "${b}" vs "${a}"`
88 );
89 }
90});
91
92test('rankSymbol orders exact and fuzzy matches', () => {
93 assert.strictEqual(search.rankSymbol('method', 'method'), 0);
94 assert.strictEqual(search.rankSymbol('method', 'methd'), 1);
95 assert.strictEqual(search.rankSymbol('method', 'xxxxxxxxxxxx'), Infinity);
96});
97
98test('filterAndRank keeps matching descendants only', () => {
99 const index = [
100 {
101 symbol: 'Root',
102 anchor: 'root',
103 children: [
104 { symbol: 'ChildMatch', anchor: 'child-match', children: [] },
105 { symbol: 'ChildOther', anchor: 'child-other', children: [] },
106 ],
107 },
108 { symbol: 'Sibling', anchor: 'sibling', children: [] },
109 ];
110
111 const pruned = search.filterAndRank(index, 'match');
112
113 assert.deepStrictEqual(pullFromContext(pruned), [
114 {
115 _rank: 0,
116 symbol: 'Root',
117 anchor: 'root',
118 children: [
119 { _rank: 0, symbol: 'ChildMatch', anchor: 'child-match', children: [] },
120 ],
121 },
122 ]);
123});
124
125test('trimResults enforces a render limit while keeping parents', () => {
126 const results = [
127 {
128 symbol: 'Root',
129 anchor: 'root',
130 children: [
131 { symbol: 'ChildOne', anchor: 'c1', children: [] },
132 { symbol: 'ChildTwo', anchor: 'c2', children: [] },
133 ],
134 },
135 { symbol: 'Sibling', anchor: 'sibling', children: [] },
136 ];
137
138 const trimmedThree = search.trimResults(results, 3);
139 assert.deepStrictEqual(pullFromContext(trimmedThree), [
140 {
141 symbol: 'Root',
142 anchor: 'root',
143 children: [
144 { symbol: 'ChildOne', anchor: 'c1', children: [] },
145 { symbol: 'ChildTwo', anchor: 'c2', children: [] },
146 ],
147 },
148 ]);
149
150 const trimmedTwo = search.trimResults(results, 2);
151 assert.deepStrictEqual(pullFromContext(trimmedTwo), [
152 {
153 symbol: 'Root',
154 anchor: 'root',
155 children: [
156 { symbol: 'ChildOne', anchor: 'c1', children: [] },
157 ],
158 },
159 ]);
160});