@nudojs/cli 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +34 -0
- package/dist/chunk-3HCPXPEW.js +5286 -0
- package/dist/evaluator-api.d.ts +94 -2
- package/dist/evaluator-api.js +187 -3
- package/dist/index.js +595 -130
- package/examples/real-world.js +66 -0
- package/examples/validate-real-world.ts +160 -0
- package/package.json +17 -2
- package/src/__tests__/builtin-advanced.test.ts +292 -0
- package/src/__tests__/builtin-api.test.ts +308 -0
- package/src/__tests__/builtin-functions.test.ts +313 -0
- package/src/__tests__/builtin-map.test.ts +129 -0
- package/src/__tests__/builtin-promise.test.ts +150 -0
- package/src/__tests__/builtin-regexp.test.ts +133 -0
- package/src/__tests__/builtin-set.test.ts +163 -0
- package/src/__tests__/builtin-url.test.ts +256 -0
- package/src/__tests__/builtin-web.test.ts +340 -0
- package/src/__tests__/combination-scenarios.test.ts +384 -0
- package/src/__tests__/edge-cases.test.ts +749 -0
- package/src/__tests__/evaluator-loops.test.ts +120 -0
- package/src/__tests__/evaluator-nullish-coalescing.test.ts +86 -0
- package/src/__tests__/evaluator-optional-chaining.test.ts +37 -0
- package/src/__tests__/evaluator-phase3.test.ts +353 -2
- package/src/__tests__/evaluator.test.ts +702 -2
- package/src/__tests__/mock-arrow-function.test.ts +169 -0
- package/src/__tests__/mock-comparison.test.ts +221 -0
- package/src/__tests__/mock-comprehensive.test.ts +250 -0
- package/src/__tests__/mock-final-integration.test.ts +263 -0
- package/src/__tests__/mock-helpers.test.ts +174 -0
- package/src/__tests__/mock-realworld.test.ts +191 -0
- package/src/__tests__/mock-rest-debug.test.ts +110 -0
- package/src/__tests__/mock-rest-edge.test.ts +176 -0
- package/src/__tests__/mock-sinon-runtime.test.ts +168 -0
- package/src/__tests__/narrowing-discriminated.test.ts +35 -0
- package/src/__tests__/narrowing-in-operator.test.ts +56 -0
- package/src/__tests__/narrowing-switch.test.ts +56 -0
- package/src/__tests__/narrowing-truthiness.test.ts +52 -0
- package/src/__tests__/nudo-mock-syntax.test.ts +245 -0
- package/src/__tests__/realworld-patterns.test.ts +430 -0
- package/src/__tests__/syntax-sugar.test.ts +345 -0
- package/src/builtins/builtin-intl.ts +17 -0
- package/src/builtins/builtin-map.ts +174 -0
- package/src/builtins/builtin-promise.ts +116 -0
- package/src/builtins/builtin-reflect.ts +30 -0
- package/src/builtins/builtin-regexp.ts +34 -0
- package/src/builtins/builtin-set.ts +131 -0
- package/src/builtins/builtin-symbol.ts +16 -0
- package/src/builtins/builtin-url.ts +58 -0
- package/src/builtins/builtin-weak.ts +22 -0
- package/src/builtins/builtin-web.ts +111 -0
- package/src/builtins/index.ts +61 -0
- package/src/config.ts +32 -0
- package/src/env-loader.ts +174 -0
- package/src/evaluator-api.ts +20 -0
- package/src/evaluator.ts +4018 -342
- package/src/index.ts +776 -137
- package/src/narrowing.ts +230 -15
- package/src/resolve-npm.ts +63 -0
- package/dist/chunk-LGWVGEMB.js +0 -1817
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { parse, extractDirectives, type CaseDirective, type MockDirective, parseTypeValueExpr } from "@nudojs/parser";
|
|
3
|
+
import { typeValueToString, createEnvironment, T, mockHelperToTypeValue } from "@nudojs/core";
|
|
4
|
+
import { evaluateFunctionFull } from "../evaluator.js";
|
|
5
|
+
|
|
6
|
+
function runNudoTest(source: string): { name: string; caseName: string; result: string }[] {
|
|
7
|
+
const ast = parse(source);
|
|
8
|
+
const directives = extractDirectives(ast);
|
|
9
|
+
const env = createEnvironment();
|
|
10
|
+
const results: { name: string; caseName: string; result: string }[] = [];
|
|
11
|
+
|
|
12
|
+
for (const fn of directives) {
|
|
13
|
+
for (const d of fn.directives) {
|
|
14
|
+
if (d.kind === "mock") {
|
|
15
|
+
if (d.arrowFn) {
|
|
16
|
+
const fnType = T.fn(d.arrowFn.params, d.arrowFn.body, env);
|
|
17
|
+
(fnType as any)._paramPatterns = d.arrowFn.paramPatterns;
|
|
18
|
+
env.bind(d.name, fnType);
|
|
19
|
+
} else if (d.nudoMock) {
|
|
20
|
+
const typeVal = mockHelperToTypeValue(d.nudoMock, env);
|
|
21
|
+
env.bind(d.name, typeVal);
|
|
22
|
+
} else if (d.sinonExpr) {
|
|
23
|
+
// Handle sinon expressions
|
|
24
|
+
const body = { type: "BlockStatement", body: [] } as any;
|
|
25
|
+
const fn = T.fn(["...args"], body, env);
|
|
26
|
+
if (d.sinonExpr.returnValue) {
|
|
27
|
+
(fn as any)._directReturn = d.sinonExpr.returnValue;
|
|
28
|
+
} else if (d.sinonExpr.resolvedValue) {
|
|
29
|
+
(fn as any)._directReturn = T.promise(d.sinonExpr.resolvedValue);
|
|
30
|
+
} else if (d.sinonExpr.rejectedValue) {
|
|
31
|
+
(fn as any)._directReturn = T.never;
|
|
32
|
+
} else {
|
|
33
|
+
(fn as any)._directReturn = T.unknown;
|
|
34
|
+
}
|
|
35
|
+
env.bind(d.name, fn);
|
|
36
|
+
} else if (d.expression) {
|
|
37
|
+
env.bind(d.name, parseTypeValueExpr(d.expression));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const caseDirectives = fn.directives.filter((d): d is CaseDirective => d.kind === "case");
|
|
43
|
+
for (const dir of caseDirectives) {
|
|
44
|
+
const result = evaluateFunctionFull(fn.node, dir.args, env);
|
|
45
|
+
results.push({
|
|
46
|
+
name: fn.name,
|
|
47
|
+
caseName: dir.name,
|
|
48
|
+
result: typeValueToString(result.value),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return results;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe("Nudo Mock Helper Syntax", () => {
|
|
56
|
+
it("stub() should bind as a function returning unknown", () => {
|
|
57
|
+
const results = runNudoTest(`
|
|
58
|
+
// @nudo:mock handler = stub()
|
|
59
|
+
// @nudo:case "test" (42)
|
|
60
|
+
function testFn(x) {
|
|
61
|
+
return handler(x);
|
|
62
|
+
}
|
|
63
|
+
`);
|
|
64
|
+
expect(results[0].result).toBe("unknown");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("stub().returns({ status: 200 }) should return the specified value", () => {
|
|
68
|
+
const results = runNudoTest(`
|
|
69
|
+
// @nudo:mock fetch = stub().returns({ status: 200, data: { ok: true } })
|
|
70
|
+
// @nudo:case "api" ("/users")
|
|
71
|
+
function fetchUsers(url) {
|
|
72
|
+
return fetch(url);
|
|
73
|
+
}
|
|
74
|
+
`);
|
|
75
|
+
expect(results[0].result).toContain("status: 200");
|
|
76
|
+
expect(results[0].result).toContain("ok: true");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("stub().resolves('hello') should return Promise<\"hello\">", () => {
|
|
80
|
+
const results = runNudoTest(`
|
|
81
|
+
// @nudo:mock fetchData = stub().resolves("hello")
|
|
82
|
+
// @nudo:case "async" ()
|
|
83
|
+
function getData() {
|
|
84
|
+
return fetchData();
|
|
85
|
+
}
|
|
86
|
+
`);
|
|
87
|
+
expect(results[0].result).toBe('Promise<"hello">');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("spy() should bind as a function returning unknown", () => {
|
|
91
|
+
const results = runNudoTest(`
|
|
92
|
+
// @nudo:mock listener = spy()
|
|
93
|
+
// @nudo:case "event" ({ type: "click" })
|
|
94
|
+
function handleEvent(event) {
|
|
95
|
+
return listener(event);
|
|
96
|
+
}
|
|
97
|
+
`);
|
|
98
|
+
expect(results[0].result).toBe("unknown");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("mock() should bind as a function returning unknown", () => {
|
|
102
|
+
const results = runNudoTest(`
|
|
103
|
+
// @nudo:mock service = mock()
|
|
104
|
+
// @nudo:case "call" ()
|
|
105
|
+
function useService() {
|
|
106
|
+
return service();
|
|
107
|
+
}
|
|
108
|
+
`);
|
|
109
|
+
expect(results[0].result).toBe("unknown");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("stub().returns with complex object", () => {
|
|
113
|
+
const results = runNudoTest(`
|
|
114
|
+
// @nudo:mock httpClient = stub().returns({ status: 200, data: { users: [{ id: 1, name: "Alice" }] } })
|
|
115
|
+
// @nudo:case "users" ()
|
|
116
|
+
function getUsers() {
|
|
117
|
+
const response = httpClient();
|
|
118
|
+
return response.data.users;
|
|
119
|
+
}
|
|
120
|
+
`);
|
|
121
|
+
expect(results[0].result).toContain("id: 1");
|
|
122
|
+
expect(results[0].result).toContain("name: \"Alice\"");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("mixed nudo mocks and arrow function mocks", () => {
|
|
126
|
+
const results = runNudoTest(`
|
|
127
|
+
// @nudo:mock validate = (x) => x !== null
|
|
128
|
+
// @nudo:mock transform = stub().returns({ processed: true })
|
|
129
|
+
// @nudo:case "mix" (42)
|
|
130
|
+
function process(x) {
|
|
131
|
+
if (!validate(x)) return null;
|
|
132
|
+
return transform(x);
|
|
133
|
+
}
|
|
134
|
+
`);
|
|
135
|
+
expect(results[0].result).toContain("processed: true");
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe("Chain call syntax", () => {
|
|
140
|
+
it("stub().returns(v).onFirstCall() should parse and use returns value", () => {
|
|
141
|
+
const results = runNudoTest(`
|
|
142
|
+
// @nudo:mock handler = stub().returns(42).onFirstCall()
|
|
143
|
+
// @nudo:case "test" ()
|
|
144
|
+
function fn() {
|
|
145
|
+
return handler();
|
|
146
|
+
}
|
|
147
|
+
`);
|
|
148
|
+
expect(results[0].result).toBe("42");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("stub().callsFake(expr) should parse", () => {
|
|
152
|
+
const results = runNudoTest(`
|
|
153
|
+
// @nudo:mock transform = stub().callsFake((x) => x)
|
|
154
|
+
// @nudo:case "test" (21)
|
|
155
|
+
function fn(x) {
|
|
156
|
+
return transform(x);
|
|
157
|
+
}
|
|
158
|
+
`);
|
|
159
|
+
expect(results[0].result).toBeDefined();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("spy().returns(v) should parse", () => {
|
|
163
|
+
const results = runNudoTest(`
|
|
164
|
+
// @nudo:mock listener = spy().returns({ handled: true })
|
|
165
|
+
// @nudo:case "test" ()
|
|
166
|
+
function fn() {
|
|
167
|
+
return listener();
|
|
168
|
+
}
|
|
169
|
+
`);
|
|
170
|
+
expect(results[0].result).toContain("handled: true");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("stub().withArgs(a, b).returns(v) should parse", () => {
|
|
174
|
+
const results = runNudoTest(`
|
|
175
|
+
// @nudo:mock fetch = stub().withArgs("GET", "/api").returns({ status: 200 })
|
|
176
|
+
// @nudo:case "test" ()
|
|
177
|
+
function fn() {
|
|
178
|
+
return fetch("GET", "/api");
|
|
179
|
+
}
|
|
180
|
+
`);
|
|
181
|
+
expect(results[0].result).toContain("status: 200");
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe("callsFake execution semantics (unified stub/sinon paths)", () => {
|
|
186
|
+
it("stub().callsFake lifts the fake onto arrowFn (same binding as plain arrow mocks)", () => {
|
|
187
|
+
const ast = parse(`
|
|
188
|
+
// @nudo:mock transform = stub().callsFake((x) => ({ v: x }))
|
|
189
|
+
function applyTransform(x) {
|
|
190
|
+
return transform(x);
|
|
191
|
+
}
|
|
192
|
+
`);
|
|
193
|
+
const directives = extractDirectives(ast);
|
|
194
|
+
const mockDir = directives[0].directives.find((d): d is MockDirective => d.kind === "mock")!;
|
|
195
|
+
// 与 @nudo:mock f = (x) => ... 相同的 arrowFn 形态:T.fn + _paramPatterns + 全局 env
|
|
196
|
+
expect(mockDir.arrowFn).toBeDefined();
|
|
197
|
+
expect(mockDir.arrowFn!.params).toEqual(["x"]);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("stub().callsFake((x) => ({ v: x })) direct call with 21 yields { v: 21 }", () => {
|
|
201
|
+
const results = runNudoTest(`
|
|
202
|
+
// @nudo:mock transform = stub().callsFake((x) => ({ v: x }))
|
|
203
|
+
// @nudo:case "direct" (21)
|
|
204
|
+
function applyTransform(x) {
|
|
205
|
+
return transform(x);
|
|
206
|
+
}
|
|
207
|
+
`);
|
|
208
|
+
expect(results[0].result).toBe("{ v: 21 }");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("sinon.stub().callsFake((x) => ({ v: x })) direct call with 21 yields { v: 21 }", () => {
|
|
212
|
+
const results = runNudoTest(`
|
|
213
|
+
// @nudo:mock transform = sinon.stub().callsFake((x) => ({ v: x }))
|
|
214
|
+
// @nudo:case "direct" (21)
|
|
215
|
+
function applyTransform(x) {
|
|
216
|
+
return transform(x);
|
|
217
|
+
}
|
|
218
|
+
`);
|
|
219
|
+
expect(results[0].result).toBe("{ v: 21 }");
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("callsFake propagates precisely through arr.map(transform) HOF", () => {
|
|
223
|
+
const results = runNudoTest(`
|
|
224
|
+
// @nudo:mock transform = stub().callsFake((x) => ({ v: x }))
|
|
225
|
+
// @nudo:case "hof" ([1, 2, 3])
|
|
226
|
+
function mapAll(arr) {
|
|
227
|
+
return arr.map(transform);
|
|
228
|
+
}
|
|
229
|
+
`);
|
|
230
|
+
expect(results[0].result).toBe("[{ v: 1 }, { v: 2 }, { v: 3 }]");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("withArgs(...).returns(...) mixed chain: matched args win, unmatched use default stub behavior", () => {
|
|
234
|
+
const results = runNudoTest(`
|
|
235
|
+
// @nudo:mock handler = stub().withArgs(21).returns("hit")
|
|
236
|
+
// @nudo:case "match" (21)
|
|
237
|
+
// @nudo:case "miss" (22)
|
|
238
|
+
function call(x) {
|
|
239
|
+
return handler(x);
|
|
240
|
+
}
|
|
241
|
+
`);
|
|
242
|
+
expect(results[0].result).toBe('"hit"');
|
|
243
|
+
expect(results[1].result).toBe("unknown");
|
|
244
|
+
});
|
|
245
|
+
});
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { parse, extractDirectives, type CaseDirective } from "@nudojs/parser";
|
|
3
|
+
import { typeValueToString, createEnvironment } from "@nudojs/core";
|
|
4
|
+
import { evaluateFunctionFull } from "../evaluator.js";
|
|
5
|
+
|
|
6
|
+
function runTest(source: string): { name: string; caseName: string; result: string }[] {
|
|
7
|
+
const ast = parse(source);
|
|
8
|
+
const directives = extractDirectives(ast);
|
|
9
|
+
const env = createEnvironment();
|
|
10
|
+
const results: { name: string; caseName: string; result: string }[] = [];
|
|
11
|
+
|
|
12
|
+
for (const fn of directives) {
|
|
13
|
+
const caseDirectives = fn.directives.filter((d): d is CaseDirective => d.kind === "case");
|
|
14
|
+
for (const dir of caseDirectives) {
|
|
15
|
+
const result = evaluateFunctionFull(fn.node, dir.args, env);
|
|
16
|
+
results.push({
|
|
17
|
+
name: fn.name,
|
|
18
|
+
caseName: dir.name,
|
|
19
|
+
result: typeValueToString(result.value),
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return results;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("Real-World Pattern Tests", () => {
|
|
27
|
+
describe("Data Processing Pipelines", () => {
|
|
28
|
+
it("user data transformation", () => {
|
|
29
|
+
const results = runTest(`
|
|
30
|
+
// @nudo:case "transform-users" ([{ id: 1, firstName: "Alice", lastName: "Smith", active: true, isAdmin: false }, { id: 2, firstName: "Bob", lastName: "Jones", active: true, isAdmin: true }])
|
|
31
|
+
function transformUsers(users) {
|
|
32
|
+
return users
|
|
33
|
+
.filter(u => u.active)
|
|
34
|
+
.map(u => ({
|
|
35
|
+
id: u.id,
|
|
36
|
+
displayName: u.firstName + " " + u.lastName,
|
|
37
|
+
role: u.isAdmin ? "admin" : "user"
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
`);
|
|
41
|
+
expect(results[0].result).toContain("[");
|
|
42
|
+
expect(results[0].result).toContain("displayName");
|
|
43
|
+
expect(results[0].result).toContain("role");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("group by key", () => {
|
|
47
|
+
const results = runTest(`
|
|
48
|
+
// @nudo:case "group-by" ([{ type: "a", value: 1 }, { type: "b", value: 2 }, { type: "a", value: 3 }])
|
|
49
|
+
function groupByType(items) {
|
|
50
|
+
const groups = new Map();
|
|
51
|
+
for (const item of items) {
|
|
52
|
+
if (!groups.has(item.type)) {
|
|
53
|
+
groups.set(item.type, []);
|
|
54
|
+
}
|
|
55
|
+
groups.get(item.type).push(item.value);
|
|
56
|
+
}
|
|
57
|
+
return groups;
|
|
58
|
+
}
|
|
59
|
+
`);
|
|
60
|
+
expect(results[0].result).toContain("Map");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("flatten and deduplicate", () => {
|
|
64
|
+
const results = runTest(`
|
|
65
|
+
// @nudo:case "flatten-dedup" ([[1, 2], [2, 3], [3, 4]])
|
|
66
|
+
function flattenDedup(arrays) {
|
|
67
|
+
const flat = arrays.reduce((acc, arr) => acc.concat(arr), []);
|
|
68
|
+
return Array.from(new Set(flat));
|
|
69
|
+
}
|
|
70
|
+
`);
|
|
71
|
+
expect(results[0].result).toContain("[");
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("Error Handling Patterns", () => {
|
|
76
|
+
it("Result type pattern", () => {
|
|
77
|
+
const results = runTest(`
|
|
78
|
+
// @nudo:case "result-type" (10, 2)
|
|
79
|
+
function safeDivide(a, b) {
|
|
80
|
+
try {
|
|
81
|
+
if (b === 0) throw new Error("Division by zero");
|
|
82
|
+
return { ok: true, value: a / b };
|
|
83
|
+
} catch (e) {
|
|
84
|
+
return { ok: false, error: e.message };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
`);
|
|
88
|
+
expect(results[0].result).toContain("ok");
|
|
89
|
+
expect(results[0].result).toContain("value");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("retry pattern", () => {
|
|
93
|
+
const results = runTest(`
|
|
94
|
+
// @nudo:case "retry" ()
|
|
95
|
+
async function withRetry(fn, maxRetries) {
|
|
96
|
+
let lastError;
|
|
97
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
98
|
+
try {
|
|
99
|
+
return { success: true, data: await fn() };
|
|
100
|
+
} catch (e) {
|
|
101
|
+
lastError = e;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return { success: false, error: "max retries exceeded" };
|
|
105
|
+
}
|
|
106
|
+
`);
|
|
107
|
+
// Async function returns Promise
|
|
108
|
+
expect(results[0].result).toContain("Promise");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("validation pattern", () => {
|
|
112
|
+
const results = runTest(`
|
|
113
|
+
// @nudo:case "validate" ({ email: "test@example.com", age: 25 })
|
|
114
|
+
function validateUser(user) {
|
|
115
|
+
const errors = [];
|
|
116
|
+
if (!user.email || !user.email.includes("@")) {
|
|
117
|
+
errors.push("Invalid email");
|
|
118
|
+
}
|
|
119
|
+
if (user.age < 0 || user.age > 150) {
|
|
120
|
+
errors.push("Invalid age");
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
valid: errors.length === 0,
|
|
124
|
+
errors,
|
|
125
|
+
user: errors.length === 0 ? user : undefined
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
`);
|
|
129
|
+
expect(results[0].result).toContain("valid");
|
|
130
|
+
expect(results[0].result).toContain("errors");
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("Event Emitter Pattern", () => {
|
|
135
|
+
it("basic event emitter", () => {
|
|
136
|
+
const results = runTest(`
|
|
137
|
+
// @nudo:case "emitter" ()
|
|
138
|
+
function createEmitter() {
|
|
139
|
+
const listeners = new Map();
|
|
140
|
+
return {
|
|
141
|
+
on(event, fn) {
|
|
142
|
+
if (!listeners.has(event)) {
|
|
143
|
+
listeners.set(event, []);
|
|
144
|
+
}
|
|
145
|
+
listeners.get(event).push(fn);
|
|
146
|
+
},
|
|
147
|
+
off(event, fn) {
|
|
148
|
+
const fns = listeners.get(event);
|
|
149
|
+
if (fns) {
|
|
150
|
+
const idx = fns.indexOf(fn);
|
|
151
|
+
if (idx >= 0) fns.splice(idx, 1);
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
emit(event, ...args) {
|
|
155
|
+
const fns = listeners.get(event);
|
|
156
|
+
if (fns) {
|
|
157
|
+
for (const fn of fns) fn(...args);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
`);
|
|
163
|
+
expect(results[0].result).toContain("on");
|
|
164
|
+
expect(results[0].result).toContain("off");
|
|
165
|
+
expect(results[0].result).toContain("emit");
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe("Configuration Patterns", () => {
|
|
170
|
+
it("config with defaults", () => {
|
|
171
|
+
const results = runTest(`
|
|
172
|
+
// @nudo:case "config" ({ host: "localhost" })
|
|
173
|
+
function createConfig(userConfig) {
|
|
174
|
+
const defaults = {
|
|
175
|
+
port: 3000,
|
|
176
|
+
debug: false,
|
|
177
|
+
logLevel: "info"
|
|
178
|
+
};
|
|
179
|
+
return { ...defaults, ...userConfig };
|
|
180
|
+
}
|
|
181
|
+
`);
|
|
182
|
+
expect(results[0].result).toContain("host");
|
|
183
|
+
expect(results[0].result).toContain("port");
|
|
184
|
+
expect(results[0].result).toContain("debug");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("config validation", () => {
|
|
188
|
+
const results = runTest(`
|
|
189
|
+
// @nudo:case "validate-config" ({ host: "localhost", port: 3000 })
|
|
190
|
+
function validateConfig(config) {
|
|
191
|
+
const errors = [];
|
|
192
|
+
if (!config.host) errors.push("host is required");
|
|
193
|
+
if (config.port < 1 || config.port > 65535) errors.push("port must be 1-65535");
|
|
194
|
+
return {
|
|
195
|
+
valid: errors.length === 0,
|
|
196
|
+
errors,
|
|
197
|
+
config: errors.length === 0 ? config : undefined
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
`);
|
|
201
|
+
expect(results[0].result).toContain("valid");
|
|
202
|
+
expect(results[0].result).toContain("config");
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe("API Response Patterns", () => {
|
|
207
|
+
it("API response handler", () => {
|
|
208
|
+
const results = runTest(`
|
|
209
|
+
// @nudo:case "api-response" ({ statusCode: 200, body: { data: "test" }, headers: { "x-request-id": "123" } })
|
|
210
|
+
function handleResponse(response) {
|
|
211
|
+
if (!response) {
|
|
212
|
+
return { status: "error", message: "No response" };
|
|
213
|
+
}
|
|
214
|
+
if (response.statusCode >= 200 && response.statusCode < 300) {
|
|
215
|
+
return {
|
|
216
|
+
status: "success",
|
|
217
|
+
data: response.body,
|
|
218
|
+
meta: { requestId: response.headers["x-request-id"] }
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
if (response.statusCode >= 400) {
|
|
222
|
+
return {
|
|
223
|
+
status: "error",
|
|
224
|
+
code: response.statusCode,
|
|
225
|
+
message: response.body?.error || "Unknown error"
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
return { status: "unknown", code: response.statusCode };
|
|
229
|
+
}
|
|
230
|
+
`);
|
|
231
|
+
expect(results[0].result).toContain("success");
|
|
232
|
+
expect(results[0].result).toContain("data");
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("paginated response", () => {
|
|
236
|
+
const results = runTest(`
|
|
237
|
+
// @nudo:case "paginate" ([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 1, 3)
|
|
238
|
+
function paginate(items, page, pageSize) {
|
|
239
|
+
const start = (page - 1) * pageSize;
|
|
240
|
+
const end = start + pageSize;
|
|
241
|
+
const pageItems = items.slice(start, end);
|
|
242
|
+
return {
|
|
243
|
+
items: pageItems,
|
|
244
|
+
page,
|
|
245
|
+
pageSize,
|
|
246
|
+
total: items.length,
|
|
247
|
+
totalPages: Math.ceil(items.length / pageSize)
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
`);
|
|
251
|
+
expect(results[0].result).toContain("items");
|
|
252
|
+
expect(results[0].result).toContain("page");
|
|
253
|
+
expect(results[0].result).toContain("total");
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe("State Management Patterns", () => {
|
|
258
|
+
it("counter with actions", () => {
|
|
259
|
+
const results = runTest(`
|
|
260
|
+
// @nudo:case "counter" ()
|
|
261
|
+
function createCounter(initial = 0) {
|
|
262
|
+
let count = initial;
|
|
263
|
+
return {
|
|
264
|
+
increment() { return ++count; },
|
|
265
|
+
decrement() { return --count; },
|
|
266
|
+
reset() { count = initial; return count; },
|
|
267
|
+
get() { return count; }
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
`);
|
|
271
|
+
expect(results[0].result).toContain("increment");
|
|
272
|
+
expect(results[0].result).toContain("decrement");
|
|
273
|
+
expect(results[0].result).toContain("reset");
|
|
274
|
+
expect(results[0].result).toContain("get");
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("todo list", () => {
|
|
278
|
+
const results = runTest(`
|
|
279
|
+
// @nudo:case "todo-list" ()
|
|
280
|
+
function createTodoList() {
|
|
281
|
+
const todos = [];
|
|
282
|
+
let nextId = 1;
|
|
283
|
+
return {
|
|
284
|
+
add(text) {
|
|
285
|
+
const todo = { id: nextId++, text, completed: false };
|
|
286
|
+
todos.push(todo);
|
|
287
|
+
return todo;
|
|
288
|
+
},
|
|
289
|
+
complete(id) {
|
|
290
|
+
const todo = todos.find(t => t.id === id);
|
|
291
|
+
if (todo) todo.completed = true;
|
|
292
|
+
return todo;
|
|
293
|
+
},
|
|
294
|
+
getAll() { return todos; },
|
|
295
|
+
getActive() { return todos.filter(t => !t.completed); }
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
`);
|
|
299
|
+
expect(results[0].result).toContain("add");
|
|
300
|
+
expect(results[0].result).toContain("complete");
|
|
301
|
+
expect(results[0].result).toContain("getAll");
|
|
302
|
+
expect(results[0].result).toContain("getActive");
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
describe("Utility Functions", () => {
|
|
307
|
+
it("deep clone", () => {
|
|
308
|
+
const results = runTest(`
|
|
309
|
+
// @nudo:case "deep-clone" ({ a: { b: { c: 1 } }, d: [1, 2, 3] })
|
|
310
|
+
function deepClone(obj) {
|
|
311
|
+
if (obj === null || typeof obj !== "object") return obj;
|
|
312
|
+
if (Array.isArray(obj)) return obj.map(deepClone);
|
|
313
|
+
const clone = {};
|
|
314
|
+
for (const key in obj) {
|
|
315
|
+
clone[key] = deepClone(obj[key]);
|
|
316
|
+
}
|
|
317
|
+
return clone;
|
|
318
|
+
}
|
|
319
|
+
`);
|
|
320
|
+
expect(results[0].result).toContain("a");
|
|
321
|
+
expect(results[0].result).toContain("b");
|
|
322
|
+
expect(results[0].result).toContain("d");
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("debounce", () => {
|
|
326
|
+
const results = runTest(`
|
|
327
|
+
// @nudo:case "debounce" ()
|
|
328
|
+
function createDebounce(fn, delay) {
|
|
329
|
+
let timeoutId;
|
|
330
|
+
return function(...args) {
|
|
331
|
+
clearTimeout(timeoutId);
|
|
332
|
+
timeoutId = setTimeout(() => fn(...args), delay);
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
`);
|
|
336
|
+
// Functions are displayed as arrow function syntax
|
|
337
|
+
expect(results[0].result).toContain("=>");
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it("memoize", () => {
|
|
341
|
+
const results = runTest(`
|
|
342
|
+
// @nudo:case "memoize" ()
|
|
343
|
+
function memoize(fn) {
|
|
344
|
+
const cache = new Map();
|
|
345
|
+
return function(...args) {
|
|
346
|
+
const key = JSON.stringify(args);
|
|
347
|
+
if (cache.has(key)) return cache.get(key);
|
|
348
|
+
const result = fn(...args);
|
|
349
|
+
cache.set(key, result);
|
|
350
|
+
return result;
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
`);
|
|
354
|
+
// Functions are displayed as arrow function syntax
|
|
355
|
+
expect(results[0].result).toContain("=>");
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("escape dispatch through a string-keyed method table", () => {
|
|
359
|
+
const results = runTest(`
|
|
360
|
+
// @nudo:case "table-dispatch" (T.string)
|
|
361
|
+
function escapeViaTable(char) {
|
|
362
|
+
const Strings = {
|
|
363
|
+
'<': () => 'LT',
|
|
364
|
+
'>': () => 'GT',
|
|
365
|
+
'&': () => 'AMP'
|
|
366
|
+
};
|
|
367
|
+
return Strings[char]();
|
|
368
|
+
}
|
|
369
|
+
`);
|
|
370
|
+
// Strings[char] unions the table's functions; calling the union runs
|
|
371
|
+
// each member, so the result is the union of their results.
|
|
372
|
+
const r = results[0].result;
|
|
373
|
+
expect(r).toContain("LT");
|
|
374
|
+
expect(r).toContain("GT");
|
|
375
|
+
expect(r).toContain("AMP");
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
describe("Named Function Callbacks through builtin HOFs", () => {
|
|
381
|
+
it("arr.map(double) with a named function declaration yields precise literals", () => {
|
|
382
|
+
const results = runTest(`
|
|
383
|
+
// @nudo:case "xs" ([1, 2, 3])
|
|
384
|
+
function doubleAll(xs) {
|
|
385
|
+
function double(x) { return x * 2; }
|
|
386
|
+
return xs.map(double);
|
|
387
|
+
}
|
|
388
|
+
`);
|
|
389
|
+
// 具名函数回调从其 TypeValue 闭包求值,而非降级 unknown
|
|
390
|
+
expect(results[0].result).toBe("[2, 4, 6]");
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it("arr.reduce(sum, 0) with a named reducer yields number", () => {
|
|
394
|
+
const results = runTest(`
|
|
395
|
+
// @nudo:case "xs" ([1, 2, 3])
|
|
396
|
+
function sumAll(xs) {
|
|
397
|
+
function sum(a, b) { return a + b; }
|
|
398
|
+
return xs.reduce(sum, 0);
|
|
399
|
+
}
|
|
400
|
+
`);
|
|
401
|
+
expect(results[0].result).toBe("6");
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it("arr.filter(big) with a named predicate keeps the element type", () => {
|
|
405
|
+
const results = runTest(`
|
|
406
|
+
// @nudo:case "xs" ([1, 2, 3, 4])
|
|
407
|
+
function keepBig(xs) {
|
|
408
|
+
function big(x) { return x > 2; }
|
|
409
|
+
return xs.filter(big);
|
|
410
|
+
}
|
|
411
|
+
`);
|
|
412
|
+
// 过滤结果元素类型不丢失:保留 3、4 字面量成员
|
|
413
|
+
expect(results[0].result).toContain("3");
|
|
414
|
+
expect(results[0].result).toContain("4");
|
|
415
|
+
expect(results[0].result).toContain("[");
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it("named callbacks over a union receiver distribute per member", () => {
|
|
419
|
+
const results = runTest(`
|
|
420
|
+
// @nudo:case "go" ()
|
|
421
|
+
function pickThenMap() {
|
|
422
|
+
const xs = Math.random() > 0.5 ? [1, 2] : [3, 4, 5];
|
|
423
|
+
function double(x) { return x * 2; }
|
|
424
|
+
return xs.map(double);
|
|
425
|
+
}
|
|
426
|
+
`);
|
|
427
|
+
// union 接收者:每个数组成员分别经回调求值后 union
|
|
428
|
+
expect(results[0].result).toBe("[2, 4] | [6, 8, 10]");
|
|
429
|
+
});
|
|
430
|
+
});
|