@jseeio/jsee 0.3.7 → 0.3.9
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/.claude/settings.local.json +12 -0
- package/.eslintrc.js +38 -0
- package/AGENTS.md +38 -0
- package/CHANGELOG.md +86 -0
- package/CLAUDE.md +5 -0
- package/README.md +60 -42
- package/bin/jsee +1 -1
- package/dist/jsee.js +1 -1
- package/dist/jsee.runtime.js +1 -1
- package/jest-puppeteer.config.js +7 -5
- package/jest.unit.config.js +8 -0
- package/load/index.html +16 -4
- package/package.json +17 -13
- package/src/app.js +35 -11
- package/src/cli.js +591 -330
- package/src/constants.js +12 -0
- package/src/main.js +356 -183
- package/src/utils.js +748 -3
- package/src/worker.js +42 -18
- package/templates/bulma-app.vue +3 -2
- package/templates/bulma-input.vue +23 -18
- package/templates/bulma-output.vue +72 -7
- package/templates/common-inputs.js +2 -13
- package/templates/common-outputs.js +57 -2
- package/templates/file-picker-base.vue +169 -0
- package/templates/file-picker.vue +350 -0
- package/test/fixtures/lodash-like.js +15 -0
- package/test/fixtures/upload-sample.csv +3 -0
- package/test/test-basic.test.js +383 -17
- package/test/test-python.test.js +2 -5
- package/test/unit/cli-fetch.test.js +126 -0
- package/test/unit/utils.test.js +806 -0
- package/webpack.config.js +1 -0
package/test/test-basic.test.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
require('expect-puppeteer')
|
|
2
|
+
const path = require('path')
|
|
2
3
|
|
|
3
4
|
page.setDefaultTimeout(10000)
|
|
4
5
|
|
|
5
|
-
//
|
|
6
|
-
// If you have php installed: php -S localhost:8080
|
|
7
|
-
// Python: python -m http.server 8080
|
|
8
|
-
// Node: npm install -g http-server && http-server -p 8080
|
|
6
|
+
// Server on port 8484 is auto-started by jest-puppeteer (see jest-puppeteer.config.js)
|
|
9
7
|
|
|
10
|
-
const port =
|
|
8
|
+
const port = 8484
|
|
11
9
|
const urlSchema = (name) => `http://localhost:${port}/load/?s=/test/${name}.schema.json`
|
|
12
10
|
const urlHTML = (name) => `http://localhost:${port}/test/${name}.html`
|
|
13
11
|
const urlQuery = (schema) => `http://localhost:${port}/load/?s=${JSON.stringify(schema)}`
|
|
12
|
+
const urlQueryEscaped = (schema) => `http://localhost:${port}/load/?s=${encodeURIComponent(JSON.stringify(schema))}`
|
|
13
|
+
const uploadFixture = path.resolve(__dirname, 'fixtures', 'upload-sample.csv')
|
|
14
14
|
|
|
15
15
|
describe('Initial test', () => {
|
|
16
16
|
beforeAll(async () => {
|
|
@@ -52,12 +52,12 @@ describe('Initial test (worker)', () => {
|
|
|
52
52
|
describe('Minimal examples', () => {
|
|
53
53
|
const schema = {
|
|
54
54
|
'model': {
|
|
55
|
-
'code': 'function (a, b) { return a / b }',
|
|
55
|
+
'code': 'function (a, b) { return a / b }',
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
test('Code only (text) (main window)', async () => {
|
|
59
59
|
schema.model.worker = false
|
|
60
|
-
await page.goto(
|
|
60
|
+
await page.goto(urlQueryEscaped(schema))
|
|
61
61
|
await expect(page).toFill('#a', '100')
|
|
62
62
|
await expect(page).toFill('#b', '4')
|
|
63
63
|
await expect(page).toClick('button', { text: 'Run' })
|
|
@@ -115,6 +115,42 @@ describe('Load code directly', () => {
|
|
|
115
115
|
await expect(page).toClick('button', { text: 'Run' })
|
|
116
116
|
await expect(page).toMatchTextContent('15')
|
|
117
117
|
})
|
|
118
|
+
|
|
119
|
+
test('Window infers function type for URL-loaded JS when type is omitted', async () => {
|
|
120
|
+
const schema = {
|
|
121
|
+
model: {
|
|
122
|
+
name: 'sum',
|
|
123
|
+
container: 'args',
|
|
124
|
+
url: '/test/example-sum.js',
|
|
125
|
+
worker: false
|
|
126
|
+
},
|
|
127
|
+
inputs: [
|
|
128
|
+
{ name: 'a', type: 'int', default: 8 },
|
|
129
|
+
{ name: 'b', type: 'int', default: 7 }
|
|
130
|
+
]
|
|
131
|
+
}
|
|
132
|
+
await page.goto(urlQueryEscaped(schema))
|
|
133
|
+
await expect(page).toClick('button', { text: 'Run' })
|
|
134
|
+
await expect(page).toMatchTextContent('15')
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test('Worker infers function type for URL-loaded JS when type is omitted', async () => {
|
|
138
|
+
const schema = {
|
|
139
|
+
model: {
|
|
140
|
+
name: 'sum',
|
|
141
|
+
container: 'args',
|
|
142
|
+
url: '/test/example-sum.js',
|
|
143
|
+
worker: true
|
|
144
|
+
},
|
|
145
|
+
inputs: [
|
|
146
|
+
{ name: 'a', type: 'int', default: 8 },
|
|
147
|
+
{ name: 'b', type: 'int', default: 7 }
|
|
148
|
+
]
|
|
149
|
+
}
|
|
150
|
+
await page.goto(urlQueryEscaped(schema))
|
|
151
|
+
await expect(page).toClick('button', { text: 'Run' })
|
|
152
|
+
await expect(page).toMatchTextContent('15')
|
|
153
|
+
})
|
|
118
154
|
})
|
|
119
155
|
|
|
120
156
|
describe('Classes', () => {
|
|
@@ -133,14 +169,14 @@ describe('Classes', () => {
|
|
|
133
169
|
|
|
134
170
|
test('Window', async () => {
|
|
135
171
|
schema.model.worker = false
|
|
136
|
-
await page.goto(
|
|
172
|
+
await page.goto(urlQueryEscaped(schema))
|
|
137
173
|
await expect(page).toClick('button', { text: 'Run' })
|
|
138
174
|
await expect(page).toMatchTextContent('200')
|
|
139
175
|
})
|
|
140
176
|
|
|
141
177
|
test('Worker', async () => {
|
|
142
178
|
schema.model.worker = true
|
|
143
|
-
await page.goto(
|
|
179
|
+
await page.goto(urlQueryEscaped(schema))
|
|
144
180
|
await expect(page).toClick('button', { text: 'Run' })
|
|
145
181
|
await expect(page).toMatchTextContent('200')
|
|
146
182
|
})
|
|
@@ -154,7 +190,7 @@ describe('Some edge cases', () => {
|
|
|
154
190
|
}
|
|
155
191
|
}
|
|
156
192
|
schema.model.worker = false
|
|
157
|
-
await page.goto(
|
|
193
|
+
await page.goto(urlQueryEscaped(schema))
|
|
158
194
|
await expect(page).toFill('#a', '0')
|
|
159
195
|
await expect(page).toFill('#b', '0')
|
|
160
196
|
await expect(page).toClick('button', { text: 'Run' })
|
|
@@ -174,21 +210,34 @@ describe('Imports', () => {
|
|
|
174
210
|
}
|
|
175
211
|
`
|
|
176
212
|
},
|
|
177
|
-
'imports':
|
|
213
|
+
'imports': `http://localhost:${port}/test/fixtures/lodash-like.js`,
|
|
178
214
|
'inputs': [
|
|
179
215
|
{ 'name': 'str', 'type': 'string', 'default': 'FooBar' },
|
|
180
216
|
]
|
|
181
217
|
}
|
|
182
218
|
test('Window', async () => {
|
|
183
219
|
schema.model.worker = false
|
|
184
|
-
await page.goto(
|
|
220
|
+
await page.goto(urlQueryEscaped(schema))
|
|
185
221
|
await expect(page).toClick('button', { text: 'Run' })
|
|
186
222
|
await expect(page).toMatchTextContent('foo-bar')
|
|
187
223
|
})
|
|
188
224
|
test('Worker', async () => {
|
|
189
225
|
schema.model.worker = true
|
|
190
|
-
await page.goto(
|
|
191
|
-
|
|
226
|
+
await page.goto(urlQueryEscaped(schema))
|
|
227
|
+
const runUntilReady = async (attempts=3) => {
|
|
228
|
+
if (attempts <= 0) {
|
|
229
|
+
return false
|
|
230
|
+
}
|
|
231
|
+
await expect(page).toClick('button', { text: 'Run' })
|
|
232
|
+
try {
|
|
233
|
+
await page.waitForFunction(() => document.body.innerText.includes('foo-bar'), { timeout: 5000 })
|
|
234
|
+
return true
|
|
235
|
+
} catch (error) {
|
|
236
|
+
return runUntilReady(attempts - 1)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const ready = await runUntilReady()
|
|
240
|
+
expect(ready).toBe(true)
|
|
192
241
|
await expect(page).toMatchTextContent('foo-bar')
|
|
193
242
|
// await (new Promise(resolve => setTimeout(resolve, 1000)))
|
|
194
243
|
})
|
|
@@ -209,14 +258,14 @@ describe('Buttons, button titles and caller', () => {
|
|
|
209
258
|
}
|
|
210
259
|
test('Window', async () => {
|
|
211
260
|
schema.model.worker = false
|
|
212
|
-
await page.goto(
|
|
261
|
+
await page.goto(urlQueryEscaped(schema))
|
|
213
262
|
await expect(page).toMatchTextContent('Test Button')
|
|
214
263
|
await expect(page).toClick('button', { text: 'Test Button' })
|
|
215
264
|
await expect(page).toMatchTextContent('test_button')
|
|
216
265
|
})
|
|
217
266
|
test('Worker', async () => {
|
|
218
267
|
schema.model.worker = true
|
|
219
|
-
await page.goto(
|
|
268
|
+
await page.goto(urlQueryEscaped(schema))
|
|
220
269
|
await expect(page).toClick('button', { text: 'Test Button' })
|
|
221
270
|
await expect(page).toMatchTextContent('test_button')
|
|
222
271
|
})
|
|
@@ -234,4 +283,321 @@ describe('Pipeline', () => {
|
|
|
234
283
|
await expect(page).toClick('button', { text: 'Run' })
|
|
235
284
|
await expect(page).toMatchTextContent((Math.pow((a + b), 2) + 2).toString())
|
|
236
285
|
})
|
|
237
|
-
})
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
describe('File uploads', () => {
|
|
289
|
+
test('default file input reads uploaded text', async () => {
|
|
290
|
+
const schema = {
|
|
291
|
+
model: {
|
|
292
|
+
worker: false,
|
|
293
|
+
code: `function filePreview (inputs) {
|
|
294
|
+
return {
|
|
295
|
+
preview: inputs.file.split('\\n')[0]
|
|
296
|
+
}
|
|
297
|
+
}`
|
|
298
|
+
},
|
|
299
|
+
inputs: [
|
|
300
|
+
{ name: 'file', type: 'file' }
|
|
301
|
+
]
|
|
302
|
+
}
|
|
303
|
+
await page.goto(urlQueryEscaped(schema))
|
|
304
|
+
await page.waitForSelector('#vfp-filePicker')
|
|
305
|
+
const fileInput = await page.$('#vfp-filePicker')
|
|
306
|
+
await fileInput.uploadFile(uploadFixture)
|
|
307
|
+
await page.waitForFunction(() => document.body.innerText.includes('Selected 1 file(s)'))
|
|
308
|
+
await expect(page).toClick('button', { text: 'Run' })
|
|
309
|
+
await expect(page).toMatchTextContent('name,age')
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
test('raw file input passes File object (not text content)', async () => {
|
|
313
|
+
const schema = {
|
|
314
|
+
model: {
|
|
315
|
+
worker: false,
|
|
316
|
+
code: `function fileRawMeta (inputs) {
|
|
317
|
+
const isFileObject = !!inputs.file
|
|
318
|
+
&& (typeof inputs.file === 'object')
|
|
319
|
+
&& (typeof inputs.file.name === 'string')
|
|
320
|
+
&& (typeof inputs.file.text === 'function')
|
|
321
|
+
return {
|
|
322
|
+
is_file_object: isFileObject,
|
|
323
|
+
is_string: typeof inputs.file === 'string',
|
|
324
|
+
file_name: inputs.file && inputs.file.name ? inputs.file.name : '',
|
|
325
|
+
content_prefix: typeof inputs.file === 'string' ? inputs.file.slice(0, 10) : 'NONE'
|
|
326
|
+
}
|
|
327
|
+
}`
|
|
328
|
+
},
|
|
329
|
+
inputs: [
|
|
330
|
+
{ name: 'file', type: 'file', raw: true }
|
|
331
|
+
]
|
|
332
|
+
}
|
|
333
|
+
await page.goto(urlQueryEscaped(schema))
|
|
334
|
+
await page.waitForSelector('#vfp-filePicker')
|
|
335
|
+
const fileInput = await page.$('#vfp-filePicker')
|
|
336
|
+
await fileInput.uploadFile(uploadFixture)
|
|
337
|
+
await page.waitForFunction(() => document.body.innerText.includes('Selected 1 file(s)'))
|
|
338
|
+
await expect(page).toClick('button', { text: 'Run' })
|
|
339
|
+
await expect(page).toMatchTextContent('is_file_object')
|
|
340
|
+
await expect(page).toMatchTextContent('true')
|
|
341
|
+
await expect(page).toMatchTextContent('file_name')
|
|
342
|
+
await expect(page).toMatchTextContent('upload-sample.csv')
|
|
343
|
+
await expect(page).toMatchTextContent('content_prefix')
|
|
344
|
+
await expect(page).toMatchTextContent('NONE')
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
test('raw url input passes URL handle (not fetched text content)', async () => {
|
|
348
|
+
const schema = {
|
|
349
|
+
model: {
|
|
350
|
+
worker: false,
|
|
351
|
+
code: `function fileRawUrlMeta (inputs) {
|
|
352
|
+
const isUrlHandle = !!inputs.file
|
|
353
|
+
&& (typeof inputs.file === 'object')
|
|
354
|
+
&& (inputs.file.kind === 'url')
|
|
355
|
+
&& (typeof inputs.file.url === 'string')
|
|
356
|
+
return {
|
|
357
|
+
is_url_handle: isUrlHandle,
|
|
358
|
+
is_string: typeof inputs.file === 'string',
|
|
359
|
+
url_value: isUrlHandle ? inputs.file.url : '',
|
|
360
|
+
content_prefix: typeof inputs.file === 'string' ? inputs.file.slice(0, 10) : 'NONE'
|
|
361
|
+
}
|
|
362
|
+
}`
|
|
363
|
+
},
|
|
364
|
+
inputs: [
|
|
365
|
+
{ name: 'file', type: 'file', raw: true }
|
|
366
|
+
]
|
|
367
|
+
}
|
|
368
|
+
const rawUrl = 'http://127.0.0.1:9999/nope.csv'
|
|
369
|
+
await page.goto(urlQueryEscaped(schema))
|
|
370
|
+
await expect(page).toClick('button', { text: 'From URL' })
|
|
371
|
+
await page.waitForSelector('input.vfp-urlInput[type="text"]')
|
|
372
|
+
await page.click('input.vfp-urlInput[type="text"]', { clickCount: 3 })
|
|
373
|
+
await page.type('input.vfp-urlInput[type="text"]', rawUrl)
|
|
374
|
+
await page.evaluate(() => {
|
|
375
|
+
const loadButton = Array.from(document.querySelectorAll('button'))
|
|
376
|
+
.find(btn => btn.textContent.trim() === 'Load')
|
|
377
|
+
if (!loadButton) throw new Error('Load button not found')
|
|
378
|
+
loadButton.click()
|
|
379
|
+
})
|
|
380
|
+
await expect(page).toClick('button', { text: 'Run' })
|
|
381
|
+
await page.waitForFunction((expectedUrl) => {
|
|
382
|
+
return document.querySelector('#outputs').innerText.includes(expectedUrl)
|
|
383
|
+
}, {}, rawUrl)
|
|
384
|
+
await expect(page).toMatchTextContent('is_url_handle')
|
|
385
|
+
await expect(page).toMatchTextContent('true')
|
|
386
|
+
await expect(page).toMatchTextContent('url_value')
|
|
387
|
+
await expect(page).toMatchTextContent(rawUrl)
|
|
388
|
+
await expect(page).toMatchTextContent('content_prefix')
|
|
389
|
+
await expect(page).toMatchTextContent('NONE')
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
test('file query param auto-loads URL without clicking Load', async () => {
|
|
393
|
+
const schema = {
|
|
394
|
+
model: {
|
|
395
|
+
worker: false,
|
|
396
|
+
code: `function filePreviewFromQuery (inputs) {
|
|
397
|
+
return {
|
|
398
|
+
preview: inputs.file.split('\\n')[0]
|
|
399
|
+
}
|
|
400
|
+
}`
|
|
401
|
+
},
|
|
402
|
+
inputs: [
|
|
403
|
+
{ name: 'file', type: 'file' }
|
|
404
|
+
]
|
|
405
|
+
}
|
|
406
|
+
const fileUrl = `http://localhost:${port}/test/fixtures/upload-sample.csv`
|
|
407
|
+
await page.goto(`${urlQueryEscaped(schema)}&file=${encodeURIComponent(fileUrl)}`)
|
|
408
|
+
await page.waitForFunction(() => document.body.innerText.includes('Loaded from URL:'), { timeout: 5000 })
|
|
409
|
+
await expect(page).toClick('button', { text: 'Run' })
|
|
410
|
+
await expect(page).toMatchTextContent('preview')
|
|
411
|
+
await expect(page).toMatchTextContent('name,age')
|
|
412
|
+
})
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
describe('Streamed file inputs', () => {
|
|
416
|
+
test('main thread receives uploaded file as async iterable stream', async () => {
|
|
417
|
+
const schema = {
|
|
418
|
+
model: {
|
|
419
|
+
worker: false,
|
|
420
|
+
code: `async function streamMainFile (inputs) {
|
|
421
|
+
const reader = inputs.file
|
|
422
|
+
const isIterable = !!reader
|
|
423
|
+
&& typeof reader[Symbol.asyncIterator] === 'function'
|
|
424
|
+
&& typeof reader.text === 'function'
|
|
425
|
+
&& typeof reader.bytes === 'function'
|
|
426
|
+
&& typeof reader.lines === 'function'
|
|
427
|
+
if (!isIterable) {
|
|
428
|
+
return { is_iterable: false, header: 'NONE' }
|
|
429
|
+
}
|
|
430
|
+
const text = await reader.text()
|
|
431
|
+
return {
|
|
432
|
+
is_iterable: true,
|
|
433
|
+
header: text.split('\\n')[0]
|
|
434
|
+
}
|
|
435
|
+
}`
|
|
436
|
+
},
|
|
437
|
+
inputs: [
|
|
438
|
+
{ name: 'file', type: 'file', raw: true, stream: true }
|
|
439
|
+
]
|
|
440
|
+
}
|
|
441
|
+
await page.goto(urlQueryEscaped(schema))
|
|
442
|
+
await page.waitForSelector('#vfp-filePicker')
|
|
443
|
+
const fileInput = await page.$('#vfp-filePicker')
|
|
444
|
+
await fileInput.uploadFile(uploadFixture)
|
|
445
|
+
await expect(page).toClick('button', { text: 'Run' })
|
|
446
|
+
await expect(page).toMatchTextContent('is_iterable')
|
|
447
|
+
await expect(page).toMatchTextContent('true')
|
|
448
|
+
await expect(page).toMatchTextContent('header')
|
|
449
|
+
await expect(page).toMatchTextContent('name,age')
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
test('stream metadata is preserved for downstream pipeline models', async () => {
|
|
453
|
+
const schema = {
|
|
454
|
+
model: [
|
|
455
|
+
{
|
|
456
|
+
worker: false,
|
|
457
|
+
code: `function streamStageOne (inputs) {
|
|
458
|
+
return {
|
|
459
|
+
stage1_name: inputs.file && inputs.file.name ? inputs.file.name : 'NONE'
|
|
460
|
+
}
|
|
461
|
+
}`
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
worker: false,
|
|
465
|
+
code: `function streamStageTwo (inputs) {
|
|
466
|
+
return {
|
|
467
|
+
stage2_name: inputs.file && inputs.file.name ? inputs.file.name : 'NONE',
|
|
468
|
+
stage2_size: inputs.file && typeof inputs.file.size === 'number' ? inputs.file.size : -1
|
|
469
|
+
}
|
|
470
|
+
}`
|
|
471
|
+
}
|
|
472
|
+
],
|
|
473
|
+
inputs: [
|
|
474
|
+
{ name: 'file', type: 'file', raw: true, stream: true }
|
|
475
|
+
]
|
|
476
|
+
}
|
|
477
|
+
await page.goto(urlQueryEscaped(schema))
|
|
478
|
+
await page.waitForSelector('#vfp-filePicker')
|
|
479
|
+
const fileInput = await page.$('#vfp-filePicker')
|
|
480
|
+
await fileInput.uploadFile(uploadFixture)
|
|
481
|
+
await expect(page).toClick('button', { text: 'Run' })
|
|
482
|
+
await expect(page).toMatchTextContent('stage1_name')
|
|
483
|
+
await expect(page).toMatchTextContent('stage2_name')
|
|
484
|
+
await expect(page).toMatchTextContent('upload-sample.csv')
|
|
485
|
+
await expect(page).toMatchTextContent('stage2_size')
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
test('main thread receives URL source as async iterable stream', async () => {
|
|
489
|
+
const schema = {
|
|
490
|
+
model: {
|
|
491
|
+
worker: false,
|
|
492
|
+
code: `async function streamMainUrl (inputs) {
|
|
493
|
+
const reader = inputs.file
|
|
494
|
+
const text = await reader.text()
|
|
495
|
+
return {
|
|
496
|
+
is_iterable: typeof reader[Symbol.asyncIterator] === 'function',
|
|
497
|
+
header: text.split('\\n')[0]
|
|
498
|
+
}
|
|
499
|
+
}`
|
|
500
|
+
},
|
|
501
|
+
inputs: [
|
|
502
|
+
{ name: 'file', type: 'file', raw: true, stream: true }
|
|
503
|
+
]
|
|
504
|
+
}
|
|
505
|
+
const sampleUrl = `http://localhost:${port}/test/fixtures/upload-sample.csv`
|
|
506
|
+
await page.goto(urlQueryEscaped(schema))
|
|
507
|
+
await expect(page).toClick('button', { text: 'From URL' })
|
|
508
|
+
await page.waitForSelector('input.vfp-urlInput[type="text"]')
|
|
509
|
+
await page.click('input.vfp-urlInput[type="text"]', { clickCount: 3 })
|
|
510
|
+
await page.type('input.vfp-urlInput[type="text"]', sampleUrl)
|
|
511
|
+
await page.evaluate(() => {
|
|
512
|
+
const loadButton = Array.from(document.querySelectorAll('button'))
|
|
513
|
+
.find(btn => btn.textContent.trim() === 'Load')
|
|
514
|
+
if (!loadButton) throw new Error('Load button not found')
|
|
515
|
+
loadButton.click()
|
|
516
|
+
})
|
|
517
|
+
await expect(page).toClick('button', { text: 'Run' })
|
|
518
|
+
await page.waitForFunction(() => {
|
|
519
|
+
const outputs = document.querySelector('#outputs')
|
|
520
|
+
return outputs && outputs.innerText.includes('header')
|
|
521
|
+
}, { timeout: 5000 })
|
|
522
|
+
await expect(page).toMatchTextContent('is_iterable')
|
|
523
|
+
await expect(page).toMatchTextContent('true')
|
|
524
|
+
await expect(page).toMatchTextContent('header')
|
|
525
|
+
await expect(page).toMatchTextContent('name,age')
|
|
526
|
+
})
|
|
527
|
+
|
|
528
|
+
test('worker receives uploaded file as async iterable stream', async () => {
|
|
529
|
+
const schema = {
|
|
530
|
+
model: {
|
|
531
|
+
worker: true,
|
|
532
|
+
code: `async function streamWorkerFile (inputs) {
|
|
533
|
+
const reader = inputs.file
|
|
534
|
+
const lines = []
|
|
535
|
+
for await (const line of reader.lines()) {
|
|
536
|
+
lines.push(line)
|
|
537
|
+
}
|
|
538
|
+
return {
|
|
539
|
+
is_iterable: typeof reader.text === 'function',
|
|
540
|
+
header: lines[0] || 'NONE'
|
|
541
|
+
}
|
|
542
|
+
}`
|
|
543
|
+
},
|
|
544
|
+
inputs: [
|
|
545
|
+
{ name: 'file', type: 'file', raw: true, stream: true }
|
|
546
|
+
]
|
|
547
|
+
}
|
|
548
|
+
await page.goto(urlQueryEscaped(schema))
|
|
549
|
+
await page.waitForSelector('#vfp-filePicker')
|
|
550
|
+
const fileInput = await page.$('#vfp-filePicker')
|
|
551
|
+
await fileInput.uploadFile(uploadFixture)
|
|
552
|
+
await expect(page).toClick('button', { text: 'Run' })
|
|
553
|
+
await expect(page).toMatchTextContent('is_iterable')
|
|
554
|
+
await expect(page).toMatchTextContent('true')
|
|
555
|
+
await expect(page).toMatchTextContent('header')
|
|
556
|
+
await expect(page).toMatchTextContent('name,age')
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
test('worker receives URL source as async iterable stream', async () => {
|
|
560
|
+
const schema = {
|
|
561
|
+
model: {
|
|
562
|
+
worker: true,
|
|
563
|
+
code: `async function streamWorkerUrl (inputs) {
|
|
564
|
+
const reader = inputs.file
|
|
565
|
+
const decoder = new TextDecoder()
|
|
566
|
+
let text = ''
|
|
567
|
+
for await (const chunk of reader) {
|
|
568
|
+
text += decoder.decode(chunk, { stream: true })
|
|
569
|
+
}
|
|
570
|
+
text += decoder.decode()
|
|
571
|
+
return {
|
|
572
|
+
is_iterable: typeof reader.bytes === 'function',
|
|
573
|
+
header: text.split('\\n')[0]
|
|
574
|
+
}
|
|
575
|
+
}`
|
|
576
|
+
},
|
|
577
|
+
inputs: [
|
|
578
|
+
{ name: 'file', type: 'file', raw: true, stream: true }
|
|
579
|
+
]
|
|
580
|
+
}
|
|
581
|
+
const sampleUrl = `http://localhost:${port}/test/fixtures/upload-sample.csv`
|
|
582
|
+
await page.goto(urlQueryEscaped(schema))
|
|
583
|
+
await expect(page).toClick('button', { text: 'From URL' })
|
|
584
|
+
await page.waitForSelector('input.vfp-urlInput[type="text"]')
|
|
585
|
+
await page.click('input.vfp-urlInput[type="text"]', { clickCount: 3 })
|
|
586
|
+
await page.type('input.vfp-urlInput[type="text"]', sampleUrl)
|
|
587
|
+
await page.evaluate(() => {
|
|
588
|
+
const loadButton = Array.from(document.querySelectorAll('button'))
|
|
589
|
+
.find(btn => btn.textContent.trim() === 'Load')
|
|
590
|
+
if (!loadButton) throw new Error('Load button not found')
|
|
591
|
+
loadButton.click()
|
|
592
|
+
})
|
|
593
|
+
await expect(page).toClick('button', { text: 'Run' })
|
|
594
|
+
await page.waitForFunction(() => {
|
|
595
|
+
const outputs = document.querySelector('#outputs')
|
|
596
|
+
return outputs && outputs.innerText.includes('header')
|
|
597
|
+
}, { timeout: 5000 })
|
|
598
|
+
await expect(page).toMatchTextContent('is_iterable')
|
|
599
|
+
await expect(page).toMatchTextContent('true')
|
|
600
|
+
await expect(page).toMatchTextContent('header')
|
|
601
|
+
await expect(page).toMatchTextContent('name,age')
|
|
602
|
+
})
|
|
603
|
+
})
|
package/test/test-python.test.js
CHANGED
|
@@ -2,12 +2,9 @@ require('expect-puppeteer')
|
|
|
2
2
|
|
|
3
3
|
page.setDefaultTimeout(30000)
|
|
4
4
|
|
|
5
|
-
//
|
|
6
|
-
// If you have php installed: php -S localhost:8080
|
|
7
|
-
// Python: python -m http.server 8080
|
|
8
|
-
// Node: npm install -g http-server && http-server -p 8080
|
|
5
|
+
// Server on port 8484 is auto-started by jest-puppeteer (see jest-puppeteer.config.js)
|
|
9
6
|
|
|
10
|
-
const port =
|
|
7
|
+
const port = 8484
|
|
11
8
|
const urlSchema = (name) => `http://localhost:${port}/load/?s=/test/${name}.schema.json`
|
|
12
9
|
const urlHTML = (name) => `http://localhost:${port}/test/${name}.html`
|
|
13
10
|
const urlQuery = (schema) => `http://localhost:${port}/load/?s=${JSON.stringify(schema)}`
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const os = require('os')
|
|
3
|
+
const path = require('path')
|
|
4
|
+
const gen = require('../../src/cli')
|
|
5
|
+
const { collectFetchBundleBlocks, resolveFetchImport, resolveRuntimeMode } = gen
|
|
6
|
+
|
|
7
|
+
describe('collectFetchBundleBlocks', () => {
|
|
8
|
+
test('collects model, view and render blocks', () => {
|
|
9
|
+
const schema = {
|
|
10
|
+
model: { name: 'model', url: './model.js' },
|
|
11
|
+
view: { name: 'view', url: './view.js' },
|
|
12
|
+
render: { name: 'render', url: './render.js' }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const blocks = collectFetchBundleBlocks(schema)
|
|
16
|
+
|
|
17
|
+
expect(blocks.map(b => b.name)).toEqual(['model', 'view', 'render'])
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('supports arrays and skips missing sections', () => {
|
|
21
|
+
const schema = {
|
|
22
|
+
model: [
|
|
23
|
+
{ name: 'm1', url: './m1.js' },
|
|
24
|
+
{ name: 'm2', url: './m2.js' }
|
|
25
|
+
],
|
|
26
|
+
view: [{ name: 'v1', url: './v1.js' }]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const blocks = collectFetchBundleBlocks(schema)
|
|
30
|
+
|
|
31
|
+
expect(blocks.map(b => b.name)).toEqual(['m1', 'm2', 'v1'])
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
describe('resolveFetchImport', () => {
|
|
36
|
+
test('resolves local relative import paths against model location', () => {
|
|
37
|
+
const cwd = '/tmp/jsee-workspace'
|
|
38
|
+
const result = resolveFetchImport('./helpers/math.js', 'apps/qrcode/model.js', cwd)
|
|
39
|
+
|
|
40
|
+
expect(result.schemaImport).toBe('apps/qrcode/helpers/math.js')
|
|
41
|
+
expect(result.importUrl).toBe('https://cdn.jsdelivr.net/npm/apps/qrcode/helpers/math.js')
|
|
42
|
+
expect(result.localFilePath).toBe(path.join(cwd, 'apps/qrcode/helpers/math.js'))
|
|
43
|
+
expect(result.remoteUrl).toBeNull()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('keeps package imports as remote URLs', () => {
|
|
47
|
+
const result = resolveFetchImport('chart.js', 'apps/qrcode/model.js', '/tmp/jsee-workspace')
|
|
48
|
+
|
|
49
|
+
expect(result.schemaImport).toBe('chart.js')
|
|
50
|
+
expect(result.importUrl).toBe('https://cdn.jsdelivr.net/npm/chart.js')
|
|
51
|
+
expect(result.localFilePath).toBeNull()
|
|
52
|
+
expect(result.remoteUrl).toBe('https://cdn.jsdelivr.net/npm/chart.js')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('supports object imports and preserves extra fields', () => {
|
|
56
|
+
const result = resolveFetchImport(
|
|
57
|
+
{ url: './helpers/math.js', integrity: 'sha-123' },
|
|
58
|
+
'apps/qrcode/model.js',
|
|
59
|
+
'/tmp/jsee-workspace'
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
expect(result.schemaEntry).toEqual({
|
|
63
|
+
url: 'apps/qrcode/helpers/math.js',
|
|
64
|
+
integrity: 'sha-123'
|
|
65
|
+
})
|
|
66
|
+
expect(result.importUrl).toBe('https://cdn.jsdelivr.net/npm/apps/qrcode/helpers/math.js')
|
|
67
|
+
expect(result.localFilePath).toBe(path.join('/tmp/jsee-workspace', 'apps/qrcode/helpers/math.js'))
|
|
68
|
+
expect(result.remoteUrl).toBeNull()
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
describe('resolveRuntimeMode', () => {
|
|
73
|
+
test('defaults to cdn for generated output when fetch is disabled', () => {
|
|
74
|
+
expect(resolveRuntimeMode('auto', false, true)).toBe('cdn')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('defaults to local for served apps when fetch is disabled', () => {
|
|
78
|
+
expect(resolveRuntimeMode('auto', false, false)).toBe('local')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('uses inline mode when fetch is enabled', () => {
|
|
82
|
+
expect(resolveRuntimeMode('auto', true, true)).toBe('inline')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('honors explicit runtime mode', () => {
|
|
86
|
+
expect(resolveRuntimeMode('local', true, true)).toBe('local')
|
|
87
|
+
expect(resolveRuntimeMode('inline', false, false)).toBe('inline')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('throws on invalid runtime mode', () => {
|
|
91
|
+
expect(() => resolveRuntimeMode('invalid', false, false)).toThrow('Invalid runtime mode')
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
describe('output writes', () => {
|
|
96
|
+
test('writes absolute output paths and keeps json output intact', async () => {
|
|
97
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jsee-cli-output-'))
|
|
98
|
+
const schemaPath = path.join(tmpDir, 'schema.json')
|
|
99
|
+
const jsonOutputPath = path.join(tmpDir, 'result.json')
|
|
100
|
+
const htmlOutputPath = path.join(tmpDir, 'result.html')
|
|
101
|
+
|
|
102
|
+
fs.writeFileSync(schemaPath, JSON.stringify({
|
|
103
|
+
model: [
|
|
104
|
+
{
|
|
105
|
+
name: 'demo',
|
|
106
|
+
type: 'function',
|
|
107
|
+
code: 'function demo () { return 1 }'
|
|
108
|
+
}
|
|
109
|
+
],
|
|
110
|
+
inputs: [],
|
|
111
|
+
outputs: []
|
|
112
|
+
}, null, 2))
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
await gen(['--inputs', schemaPath, '--outputs', `${jsonOutputPath},${htmlOutputPath}`])
|
|
116
|
+
|
|
117
|
+
const jsonContent = fs.readFileSync(jsonOutputPath, 'utf8')
|
|
118
|
+
const htmlContent = fs.readFileSync(htmlOutputPath, 'utf8')
|
|
119
|
+
|
|
120
|
+
expect(() => JSON.parse(jsonContent)).not.toThrow()
|
|
121
|
+
expect(htmlContent).toContain('<!DOCTYPE html>')
|
|
122
|
+
} finally {
|
|
123
|
+
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
})
|