@kaiko.io/rescript-deser 6.0.0 → 7.0.0-alpha.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/README.md +3 -3
- package/lib/bs/.bsdeps +4 -4
- package/lib/bs/.compiler.log +2 -2
- package/lib/bs/.ninja_log +55 -43
- package/lib/bs/.project-files-cache +0 -0
- package/lib/bs/___incremental/Deser.cmi +0 -0
- package/lib/bs/___incremental/Deser.cmj +0 -0
- package/lib/bs/___incremental/Deser.cmt +0 -0
- package/lib/bs/___incremental/Deser.res +351 -0
- package/lib/bs/___incremental/index.cmi +0 -0
- package/lib/bs/___incremental/index.cmj +0 -0
- package/lib/bs/___incremental/index.cmt +0 -0
- package/lib/bs/___incremental/index.res +189 -0
- package/lib/bs/src/Deser.ast +0 -0
- package/lib/bs/src/Deser.cmi +0 -0
- package/lib/bs/src/Deser.cmj +0 -0
- package/lib/bs/src/Deser.cmt +0 -0
- package/lib/bs/tests/QUnit.cmt +0 -0
- package/lib/bs/tests/index.ast +0 -0
- package/lib/bs/tests/index.cmi +0 -0
- package/lib/bs/tests/index.cmt +0 -0
- package/lib/es6/src/Deser.js +133 -237
- package/lib/js/src/Deser.js +133 -237
- package/package.json +2 -1
- package/src/Deser.res +80 -74
- package/tests/index.res +10 -10
- package/tests/run-tests.js +192 -0
package/src/Deser.res
CHANGED
|
@@ -26,7 +26,7 @@ exception TypeError(string)
|
|
|
26
26
|
module type Deserializer = {
|
|
27
27
|
type t
|
|
28
28
|
let name: string
|
|
29
|
-
let fromJSON:
|
|
29
|
+
let fromJSON: JSON.t => result<t, string>
|
|
30
30
|
let checkFieldsSanity: unit => result<unit, string>
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -39,11 +39,11 @@ module Field = {
|
|
|
39
39
|
| Float
|
|
40
40
|
| Boolean
|
|
41
41
|
| Array(t)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
// These SHOULD strings in ISO format, but we only validate the string
|
|
43
|
+
// can be represented in Js.Date without spewing NaN all over the place;
|
|
44
|
+
// Js.Date.fromString("xxx") returns an object that is mostly unusable.
|
|
45
45
|
///
|
|
46
|
-
|
|
46
|
+
// We also allow floats and then use Date.fromTime
|
|
47
47
|
| Date
|
|
48
48
|
| Datetime // alias of Date
|
|
49
49
|
|
|
@@ -51,17 +51,17 @@ module Field = {
|
|
|
51
51
|
| Object(array<(string, t)>)
|
|
52
52
|
| Optional(t)
|
|
53
53
|
| OptionalWithDefault(t, FieldValue.t)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
// An arbitrary mapping from names to other arbitrary fields. The
|
|
55
|
+
// difference with Object, is that you don't know the names of the
|
|
56
|
+
// expected entries.
|
|
57
57
|
| Mapping(t)
|
|
58
58
|
|
|
59
59
|
| Deserializer(module(Deserializer))
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
// A specialized Array of deserialized items that ignores unparsable
|
|
62
|
+
// items and returns the valid collection. This saves the user from
|
|
63
|
+
// writing 'Array(DefaultWhenInvalid(Optional(Deserializer(module(M)))))'
|
|
64
|
+
// and then post-process the list of items with 'Array.keepSome'
|
|
65
65
|
| Collection(module(Deserializer))
|
|
66
66
|
| DefaultWhenInvalid(t, FieldValue.t)
|
|
67
67
|
|
|
@@ -135,20 +135,20 @@ module Field = {
|
|
|
135
135
|
| DefaultWhenInvalid(t, _) => `Protected ${t->toString}`
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
let _taggedToString = (tagged:
|
|
138
|
+
let _taggedToString = (tagged: JSON.t) => {
|
|
139
139
|
switch tagged {
|
|
140
|
-
|
|
|
141
|
-
|
|
|
142
|
-
|
|
|
143
|
-
|
|
|
144
|
-
|
|
|
145
|
-
|
|
|
146
|
-
|
|
|
140
|
+
| Boolean(false) => "Boolean(false)"
|
|
141
|
+
| Boolean(true) => "Boolean(true)"
|
|
142
|
+
| Null => "Null"
|
|
143
|
+
| String(text) => `String("${text}")`
|
|
144
|
+
| Number(number) => `Number(${number->Float.toString})`
|
|
145
|
+
| Object(obj) => `Object(${obj->JSON.stringifyAny->Option.getOr("...")})`
|
|
146
|
+
| Array(array) => `Array(${array->JSON.stringifyAny->Option.getOr("...")})`
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
let rec extractValue = (
|
|
151
|
-
values: Dict.t<
|
|
151
|
+
values: Dict.t<JSON.t>,
|
|
152
152
|
field: string,
|
|
153
153
|
shape: t,
|
|
154
154
|
self: t,
|
|
@@ -157,28 +157,30 @@ module Field = {
|
|
|
157
157
|
| Some(value) => value->fromUntagged(shape, self)
|
|
158
158
|
| None =>
|
|
159
159
|
switch shape {
|
|
160
|
-
| DefaultWhenInvalid(_, _) =>
|
|
161
|
-
| Optional(_) =>
|
|
160
|
+
| DefaultWhenInvalid(_, _) => JSON.Null->fromUntagged(shape, self)
|
|
161
|
+
| Optional(_) => JSON.Null->fromUntagged(shape, self)
|
|
162
162
|
| OptionalWithDefault(_, default) => default
|
|
163
163
|
| _ => raise(TypeError(`Missing non-optional field '${field}'`))
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
|
-
and fromUntagged
|
|
168
|
-
switch (shape, untagged
|
|
167
|
+
and fromUntagged: (JSON.t, t, t) => FieldValue.t = (untagged, shape, self) => {
|
|
168
|
+
switch (shape, untagged) {
|
|
169
169
|
| (Any, _) => untagged->FieldValue.any
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
| (Int,
|
|
178
|
-
|
|
179
|
-
| (
|
|
180
|
-
|
|
181
|
-
| (
|
|
170
|
+
|
|
171
|
+
| (Literal(expected), String(text)) if text == expected => FieldValue.string(text)
|
|
172
|
+
| (Literal(expected), String(text)) =>
|
|
173
|
+
raise(TypeError(`Expecting literal ${expected}, got ${text}`))
|
|
174
|
+
|
|
175
|
+
| (String, String(text)) => FieldValue.string(text)
|
|
176
|
+
|
|
177
|
+
| (Int, Number(number)) => FieldValue.int(number->Float.toInt)
|
|
178
|
+
|
|
179
|
+
| (Float, Number(number)) => FieldValue.float(number)
|
|
180
|
+
|
|
181
|
+
| (Boolean, Boolean(v)) => FieldValue.boolean(v)
|
|
182
|
+
|
|
183
|
+
| (Tuple(bases), Array(items)) => {
|
|
182
184
|
let lenbases = bases->Array.length
|
|
183
185
|
let lenitems = items->Array.length
|
|
184
186
|
if lenbases == lenitems {
|
|
@@ -191,52 +193,56 @@ module Field = {
|
|
|
191
193
|
}
|
|
192
194
|
}
|
|
193
195
|
|
|
194
|
-
| (Datetime,
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
raise(TypeError(`Invalid date ${s}`))
|
|
199
|
-
}
|
|
200
|
-
r->FieldValue.any
|
|
196
|
+
| (Datetime | Date, String(s)) =>
|
|
197
|
+
let r = Date.fromString(s)
|
|
198
|
+
if r->Date.getDate->Int.toFloat->Float.isNaN {
|
|
199
|
+
raise(TypeError(`Invalid date ${s}`))
|
|
201
200
|
}
|
|
201
|
+
r->FieldValue.any
|
|
202
202
|
|
|
203
|
-
| (Datetime,
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
raise(TypeError(`Invalid date ${f->Js.Float.toString}`))
|
|
208
|
-
}
|
|
209
|
-
r->FieldValue.any
|
|
203
|
+
| (Datetime | Date, Number(f)) =>
|
|
204
|
+
let r = Date.fromTime(f)
|
|
205
|
+
if r->Date.getDate->Int.toFloat->Float.isNaN {
|
|
206
|
+
raise(TypeError(`Invalid date ${f->Float.toString}`))
|
|
210
207
|
}
|
|
208
|
+
r->FieldValue.any
|
|
211
209
|
|
|
212
|
-
| (Array(shape),
|
|
210
|
+
| (Array(shape), Array(items)) =>
|
|
213
211
|
FieldValue.array(items->Array.map(item => item->fromUntagged(shape, self)))
|
|
214
|
-
|
|
212
|
+
|
|
213
|
+
| (Mapping(f), Object(values)) =>
|
|
215
214
|
values->Dict.mapValues(v => v->fromUntagged(f, self))->FieldValue.mapping
|
|
216
|
-
| (Object(fields), Js.Json.JSONObject(values)) =>
|
|
217
|
-
FieldValue.object(
|
|
218
|
-
fields
|
|
219
|
-
->Array.map(((field, shape)) => {
|
|
220
|
-
let value = switch extractValue(values, field, shape, self) {
|
|
221
|
-
| value => value
|
|
222
|
-
| exception TypeError(msg) => raise(TypeError(`Field "${field}": ${msg}`))
|
|
223
|
-
}
|
|
224
|
-
(field, value)
|
|
225
|
-
})
|
|
226
|
-
->Dict.fromArray,
|
|
227
|
-
)
|
|
228
215
|
|
|
229
|
-
| (
|
|
216
|
+
| (Object(fields), Object(values)) =>
|
|
217
|
+
fields
|
|
218
|
+
->Array.map(((field, shape)) => {
|
|
219
|
+
let value = switch extractValue(values, field, shape, self) {
|
|
220
|
+
| value => value
|
|
221
|
+
| exception TypeError(msg) => raise(TypeError(`Field "${field}": ${msg}`))
|
|
222
|
+
}
|
|
223
|
+
(field, value)
|
|
224
|
+
})
|
|
225
|
+
->Dict.fromArray
|
|
226
|
+
->FieldValue.object
|
|
227
|
+
|
|
228
|
+
| (OptionalWithDefault(_, value), Null) => value
|
|
230
229
|
| (OptionalWithDefault(shape, _), _) => untagged->fromUntagged(shape, self)
|
|
231
|
-
| (Optional(_),
|
|
230
|
+
| (Optional(_), Null) => FieldValue.null
|
|
232
231
|
| (Optional(shape), _) => untagged->fromUntagged(shape, self)
|
|
233
232
|
| (Morphism(shape, f), _) => untagged->fromUntagged(shape, self)->f->FieldValue.any
|
|
234
233
|
|
|
235
|
-
| (Collection(m),
|
|
234
|
+
| (Collection(m), Array(items)) => {
|
|
236
235
|
module M = unpack(m: Deserializer)
|
|
237
236
|
items
|
|
238
237
|
->Array.map(M.fromJSON)
|
|
239
|
-
->Array.
|
|
238
|
+
->Array.filterMap(x =>
|
|
239
|
+
switch x {
|
|
240
|
+
| Ok(v) => Some(v)
|
|
241
|
+
| Error(msg) =>
|
|
242
|
+
Console.warn3(__MODULE__, "Could not deserialize value in the collection", msg)
|
|
243
|
+
None
|
|
244
|
+
}
|
|
245
|
+
)
|
|
240
246
|
->Array.map(FieldValue.any)
|
|
241
247
|
->FieldValue.array
|
|
242
248
|
}
|
|
@@ -253,7 +259,7 @@ module Field = {
|
|
|
253
259
|
switch untagged->fromUntagged(t, self) {
|
|
254
260
|
| res => res
|
|
255
261
|
| exception TypeError(msg) => {
|
|
256
|
-
|
|
262
|
+
Console.warn2("Detected and ignore (with default): ", msg)
|
|
257
263
|
default
|
|
258
264
|
}
|
|
259
265
|
}
|
|
@@ -296,8 +302,8 @@ module Field = {
|
|
|
296
302
|
)
|
|
297
303
|
->Result.map(_ => ())
|
|
298
304
|
|
|
299
|
-
|
|
300
|
-
|
|
305
|
+
// Mappings and arrays can be empty, so their payloads are
|
|
306
|
+
// automatically optional.
|
|
301
307
|
| (Mapping(field), _) | (Array(field), _) => checkFieldsSanity(name, field, true)
|
|
302
308
|
|
|
303
309
|
| (Tuple(fields), optional) =>
|
|
@@ -335,8 +341,8 @@ module MakeDeserializer = (S: Serializable): (Deserializer with type t = S.t) =>
|
|
|
335
341
|
")
|
|
336
342
|
let checkFieldsSanity = () => Field.checkFieldsSanity(name, fields, false)
|
|
337
343
|
|
|
338
|
-
@doc("Parse a `
|
|
339
|
-
let fromJSON
|
|
344
|
+
@doc("Parse a `'a` into `result<t, string>`")
|
|
345
|
+
let fromJSON: JSON.t => result<t, _> = json => {
|
|
340
346
|
switch json->Field.fromUntagged(fields, fields) {
|
|
341
347
|
| res => Ok(res->_toNativeType)
|
|
342
348
|
| exception TypeError(e) => Error(e)
|
package/tests/index.res
CHANGED
|
@@ -4,7 +4,7 @@ open RescriptCore
|
|
|
4
4
|
module Appointment = {
|
|
5
5
|
type t = {
|
|
6
6
|
note: string,
|
|
7
|
-
date:
|
|
7
|
+
date: Date.t,
|
|
8
8
|
extra: option<string>,
|
|
9
9
|
}
|
|
10
10
|
module Deserializer = Deser.MakeDeserializer({
|
|
@@ -21,7 +21,7 @@ module_("Basic deserializer", _ => {
|
|
|
21
21
|
%raw(`{"note": "Bicentennial doctor's appointment", "date": "2100-01-01"}`),
|
|
22
22
|
{
|
|
23
23
|
note: "Bicentennial doctor's appointment",
|
|
24
|
-
date:
|
|
24
|
+
date: Date.fromString("2100-01-01"),
|
|
25
25
|
extra: None,
|
|
26
26
|
},
|
|
27
27
|
),
|
|
@@ -33,7 +33,7 @@ module_("Basic deserializer", _ => {
|
|
|
33
33
|
}`),
|
|
34
34
|
{
|
|
35
35
|
note: "Bicentennial doctor's appointment",
|
|
36
|
-
date:
|
|
36
|
+
date: Date.fromString("2100-01-01"),
|
|
37
37
|
extra: Some("Don't take your stop-aging pills the night before the appointment"),
|
|
38
38
|
},
|
|
39
39
|
),
|
|
@@ -43,10 +43,10 @@ module_("Basic deserializer", _ => {
|
|
|
43
43
|
qunit->expect(valid->Array.length)
|
|
44
44
|
valid->Array.forEach(
|
|
45
45
|
((data, expected)) => {
|
|
46
|
-
|
|
46
|
+
Console.log2("Running sample", data)
|
|
47
47
|
switch Appointment.Deserializer.fromJSON(data) {
|
|
48
48
|
| Ok(result) => qunit->deepEqual(result, expected, "result == expected")
|
|
49
|
-
| Error(msg) =>
|
|
49
|
+
| Error(msg) => Console.error(msg)
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
52
|
)
|
|
@@ -63,11 +63,11 @@ module_("Basic deserializer", _ => {
|
|
|
63
63
|
qunit->expect(invalid->Array.length)
|
|
64
64
|
invalid->Array.forEach(
|
|
65
65
|
((message, data)) => {
|
|
66
|
-
|
|
66
|
+
Console.log3("Running sample", message, data)
|
|
67
67
|
switch Appointment.Deserializer.fromJSON(data) {
|
|
68
|
-
| Ok(result) =>
|
|
68
|
+
| Ok(result) => Console.error2("Invalid being accepted: ", result)
|
|
69
69
|
| Error(msg) => {
|
|
70
|
-
|
|
70
|
+
Console.log2("Correctly detected:", msg)
|
|
71
71
|
qunit->ok(true, true)
|
|
72
72
|
}
|
|
73
73
|
}
|
|
@@ -147,10 +147,10 @@ module_("Recursive deserializer", _ => {
|
|
|
147
147
|
qunit->expect(valid->Array.length)
|
|
148
148
|
valid->Array.forEach(
|
|
149
149
|
((data, expected)) => {
|
|
150
|
-
|
|
150
|
+
Console.log2("Running sample", data)
|
|
151
151
|
switch TrivialTree.Deserializer.fromJSON(data) {
|
|
152
152
|
| Ok(result) => qunit->deepEqual(result, expected, "result == expected")
|
|
153
|
-
| Error(msg) =>
|
|
153
|
+
| Error(msg) => Console.error(msg)
|
|
154
154
|
}
|
|
155
155
|
},
|
|
156
156
|
)
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Automated test runner for rescript-deser using Playwright
|
|
5
|
+
*
|
|
6
|
+
* This script starts a Vite dev server, runs QUnit tests in a headless browser,
|
|
7
|
+
* and reports the results to the console.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { chromium } = require('@playwright/test');
|
|
11
|
+
const { spawn } = require('child_process');
|
|
12
|
+
const http = require('http');
|
|
13
|
+
|
|
14
|
+
const TEST_PORT = process.env.TEST_PORT || 4370;
|
|
15
|
+
const TEST_URL = `http://localhost:${TEST_PORT}`;
|
|
16
|
+
const STARTUP_TIMEOUT = 30000; // 30 seconds for server startup
|
|
17
|
+
const TEST_TIMEOUT = 120000; // 2 minutes for all tests to complete
|
|
18
|
+
|
|
19
|
+
async function waitForServer(url, timeout) {
|
|
20
|
+
const start = Date.now();
|
|
21
|
+
|
|
22
|
+
while (Date.now() - start < timeout) {
|
|
23
|
+
try {
|
|
24
|
+
await new Promise((resolve, reject) => {
|
|
25
|
+
const req = http.get(url, (res) => {
|
|
26
|
+
resolve();
|
|
27
|
+
});
|
|
28
|
+
req.on('error', reject);
|
|
29
|
+
req.setTimeout(1000);
|
|
30
|
+
});
|
|
31
|
+
return true;
|
|
32
|
+
} catch (err) {
|
|
33
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
throw new Error(`Server did not start within ${timeout}ms`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function runTests() {
|
|
41
|
+
let viteProcess = null;
|
|
42
|
+
let browser = null;
|
|
43
|
+
let exitCode = 1;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
// Start Vite dev server
|
|
47
|
+
console.log(`Starting Vite server on port ${TEST_PORT}...`);
|
|
48
|
+
viteProcess = spawn('npx', ['vite', '--port', String(TEST_PORT), '--strictPort'], {
|
|
49
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
50
|
+
env: { ...process.env }
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Capture server output for debugging
|
|
54
|
+
viteProcess.stdout.on('data', (data) => {
|
|
55
|
+
if (process.env.DEBUG) {
|
|
56
|
+
console.log('[vite]', data.toString());
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
viteProcess.stderr.on('data', (data) => {
|
|
61
|
+
if (process.env.DEBUG) {
|
|
62
|
+
console.error('[vite error]', data.toString());
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Wait for server to be ready
|
|
67
|
+
console.log('Waiting for server to start...');
|
|
68
|
+
await waitForServer(TEST_URL, STARTUP_TIMEOUT);
|
|
69
|
+
console.log('Server is ready!');
|
|
70
|
+
|
|
71
|
+
// Launch browser
|
|
72
|
+
console.log('Launching browser...');
|
|
73
|
+
browser = await chromium.launch({
|
|
74
|
+
headless: true,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const context = await browser.newContext();
|
|
78
|
+
const page = await context.newPage();
|
|
79
|
+
|
|
80
|
+
// Set up console message handling to show test output
|
|
81
|
+
page.on('console', msg => {
|
|
82
|
+
const type = msg.type();
|
|
83
|
+
if (process.env.DEBUG) {
|
|
84
|
+
console.log(`[browser ${type}]`, msg.text());
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Navigate to test page
|
|
89
|
+
console.log(`Navigating to ${TEST_URL}...`);
|
|
90
|
+
await page.goto(TEST_URL, { waitUntil: 'networkidle' });
|
|
91
|
+
|
|
92
|
+
// Wait for QUnit to complete and get results
|
|
93
|
+
console.log('Running tests...\n');
|
|
94
|
+
const results = await page.waitForFunction(
|
|
95
|
+
() => {
|
|
96
|
+
if (typeof QUnit !== 'undefined' && QUnit.config && QUnit.config.stats) {
|
|
97
|
+
const done = QUnit.config.current === null || QUnit.config.current === undefined;
|
|
98
|
+
if (done) {
|
|
99
|
+
return {
|
|
100
|
+
passed: QUnit.config.stats.all - QUnit.config.stats.bad,
|
|
101
|
+
failed: QUnit.config.stats.bad,
|
|
102
|
+
total: QUnit.config.stats.all,
|
|
103
|
+
runtime: QUnit.config.stats.runtime
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
},
|
|
109
|
+
{ timeout: TEST_TIMEOUT }
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const testResults = await results.jsonValue();
|
|
113
|
+
|
|
114
|
+
// Print results
|
|
115
|
+
console.log('========================================');
|
|
116
|
+
console.log(' Test Results');
|
|
117
|
+
console.log('========================================');
|
|
118
|
+
console.log(`Total: ${testResults.total}`);
|
|
119
|
+
console.log(`Passed: ${testResults.passed} ✓`);
|
|
120
|
+
console.log(`Failed: ${testResults.failed} ${testResults.failed > 0 ? '✗' : ''}`);
|
|
121
|
+
console.log(`Runtime: ${testResults.runtime}ms`);
|
|
122
|
+
console.log('========================================');
|
|
123
|
+
|
|
124
|
+
if (testResults.failed > 0) {
|
|
125
|
+
// Get detailed failure information
|
|
126
|
+
const failures = await page.evaluate(() => {
|
|
127
|
+
const failedTests = [];
|
|
128
|
+
const testItems = document.querySelectorAll('#qunit-tests > li.fail');
|
|
129
|
+
|
|
130
|
+
testItems.forEach(item => {
|
|
131
|
+
const testName = item.querySelector('.test-name')?.textContent || 'Unknown test';
|
|
132
|
+
const moduleName = item.querySelector('.module-name')?.textContent || '';
|
|
133
|
+
const assertions = [];
|
|
134
|
+
|
|
135
|
+
item.querySelectorAll('.fail').forEach(assertion => {
|
|
136
|
+
const message = assertion.querySelector('.test-message')?.textContent || '';
|
|
137
|
+
const expected = assertion.querySelector('.test-expected')?.textContent || '';
|
|
138
|
+
const actual = assertion.querySelector('.test-actual')?.textContent || '';
|
|
139
|
+
|
|
140
|
+
assertions.push({ message, expected, actual });
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
failedTests.push({ module: moduleName, test: testName, assertions });
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
return failedTests;
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
console.log('\nFailed Tests:');
|
|
150
|
+
failures.forEach((failure, index) => {
|
|
151
|
+
console.log(`\n${index + 1}. ${failure.module ? failure.module + ' > ' : ''}${failure.test}`);
|
|
152
|
+
failure.assertions.forEach(assertion => {
|
|
153
|
+
if (assertion.message) console.log(` ${assertion.message}`);
|
|
154
|
+
if (assertion.expected) console.log(` Expected: ${assertion.expected}`);
|
|
155
|
+
if (assertion.actual) console.log(` Actual: ${assertion.actual}`);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
exitCode = 1;
|
|
160
|
+
} else {
|
|
161
|
+
console.log('\n✓ All tests passed!');
|
|
162
|
+
exitCode = 0;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
} catch (error) {
|
|
166
|
+
console.error('\n✗ Test run failed:');
|
|
167
|
+
console.error(error.message);
|
|
168
|
+
exitCode = 1;
|
|
169
|
+
} finally {
|
|
170
|
+
// Cleanup
|
|
171
|
+
if (browser) {
|
|
172
|
+
await browser.close();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (viteProcess) {
|
|
176
|
+
viteProcess.kill('SIGTERM');
|
|
177
|
+
// Give it a moment to shut down gracefully
|
|
178
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
179
|
+
if (!viteProcess.killed) {
|
|
180
|
+
viteProcess.kill('SIGKILL');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
process.exit(exitCode);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Run the tests
|
|
189
|
+
runTests().catch(err => {
|
|
190
|
+
console.error('Unexpected error:', err);
|
|
191
|
+
process.exit(1);
|
|
192
|
+
});
|