@jseeio/jsee 0.3.7 → 0.3.8
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 +9 -0
- package/AGENTS.md +37 -0
- package/CHANGELOG.md +71 -0
- package/CLAUDE.md +5 -0
- package/README.md +20 -41
- package/bin/jsee +1 -1
- package/dist/jsee.js +1 -1
- package/dist/jsee.runtime.js +1 -1
- package/jest-puppeteer.config.js +2 -1
- package/jest.unit.config.js +8 -0
- package/load/index.html +9 -4
- package/package.json +15 -13
- package/src/app.js +35 -11
- package/src/cli.js +662 -548
- package/src/main.js +348 -152
- package/src/utils.js +590 -3
- package/src/worker.js +42 -18
- package/templates/bulma-app.vue +3 -2
- package/templates/bulma-input.vue +22 -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 +318 -0
- package/test/fixtures/lodash-like.js +15 -0
- package/test/fixtures/upload-sample.csv +3 -0
- package/test/test-basic.test.js +286 -11
- package/test/unit/utils.test.js +519 -0
- package/webpack.config.js +1 -0
package/test/test-basic.test.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require('expect-puppeteer')
|
|
2
|
+
const path = require('path')
|
|
2
3
|
|
|
3
4
|
page.setDefaultTimeout(10000)
|
|
4
5
|
|
|
@@ -11,6 +12,8 @@ const port = 8080
|
|
|
11
12
|
const urlSchema = (name) => `http://localhost:${port}/load/?s=/test/${name}.schema.json`
|
|
12
13
|
const urlHTML = (name) => `http://localhost:${port}/test/${name}.html`
|
|
13
14
|
const urlQuery = (schema) => `http://localhost:${port}/load/?s=${JSON.stringify(schema)}`
|
|
15
|
+
const urlQueryEscaped = (schema) => `http://localhost:${port}/load/?s=${encodeURIComponent(JSON.stringify(schema))}`
|
|
16
|
+
const uploadFixture = path.resolve(__dirname, 'fixtures', 'upload-sample.csv')
|
|
14
17
|
|
|
15
18
|
describe('Initial test', () => {
|
|
16
19
|
beforeAll(async () => {
|
|
@@ -57,7 +60,7 @@ describe('Minimal examples', () => {
|
|
|
57
60
|
}
|
|
58
61
|
test('Code only (text) (main window)', async () => {
|
|
59
62
|
schema.model.worker = false
|
|
60
|
-
await page.goto(
|
|
63
|
+
await page.goto(urlQueryEscaped(schema))
|
|
61
64
|
await expect(page).toFill('#a', '100')
|
|
62
65
|
await expect(page).toFill('#b', '4')
|
|
63
66
|
await expect(page).toClick('button', { text: 'Run' })
|
|
@@ -133,14 +136,14 @@ describe('Classes', () => {
|
|
|
133
136
|
|
|
134
137
|
test('Window', async () => {
|
|
135
138
|
schema.model.worker = false
|
|
136
|
-
await page.goto(
|
|
139
|
+
await page.goto(urlQueryEscaped(schema))
|
|
137
140
|
await expect(page).toClick('button', { text: 'Run' })
|
|
138
141
|
await expect(page).toMatchTextContent('200')
|
|
139
142
|
})
|
|
140
143
|
|
|
141
144
|
test('Worker', async () => {
|
|
142
145
|
schema.model.worker = true
|
|
143
|
-
await page.goto(
|
|
146
|
+
await page.goto(urlQueryEscaped(schema))
|
|
144
147
|
await expect(page).toClick('button', { text: 'Run' })
|
|
145
148
|
await expect(page).toMatchTextContent('200')
|
|
146
149
|
})
|
|
@@ -154,7 +157,7 @@ describe('Some edge cases', () => {
|
|
|
154
157
|
}
|
|
155
158
|
}
|
|
156
159
|
schema.model.worker = false
|
|
157
|
-
await page.goto(
|
|
160
|
+
await page.goto(urlQueryEscaped(schema))
|
|
158
161
|
await expect(page).toFill('#a', '0')
|
|
159
162
|
await expect(page).toFill('#b', '0')
|
|
160
163
|
await expect(page).toClick('button', { text: 'Run' })
|
|
@@ -174,21 +177,34 @@ describe('Imports', () => {
|
|
|
174
177
|
}
|
|
175
178
|
`
|
|
176
179
|
},
|
|
177
|
-
'imports':
|
|
180
|
+
'imports': `http://localhost:${port}/test/fixtures/lodash-like.js`,
|
|
178
181
|
'inputs': [
|
|
179
182
|
{ 'name': 'str', 'type': 'string', 'default': 'FooBar' },
|
|
180
183
|
]
|
|
181
184
|
}
|
|
182
185
|
test('Window', async () => {
|
|
183
186
|
schema.model.worker = false
|
|
184
|
-
await page.goto(
|
|
187
|
+
await page.goto(urlQueryEscaped(schema))
|
|
185
188
|
await expect(page).toClick('button', { text: 'Run' })
|
|
186
189
|
await expect(page).toMatchTextContent('foo-bar')
|
|
187
190
|
})
|
|
188
191
|
test('Worker', async () => {
|
|
189
192
|
schema.model.worker = true
|
|
190
|
-
await page.goto(
|
|
191
|
-
|
|
193
|
+
await page.goto(urlQueryEscaped(schema))
|
|
194
|
+
const runUntilReady = async (attempts=3) => {
|
|
195
|
+
if (attempts <= 0) {
|
|
196
|
+
return false
|
|
197
|
+
}
|
|
198
|
+
await expect(page).toClick('button', { text: 'Run' })
|
|
199
|
+
try {
|
|
200
|
+
await page.waitForFunction(() => document.body.innerText.includes('foo-bar'), { timeout: 5000 })
|
|
201
|
+
return true
|
|
202
|
+
} catch (error) {
|
|
203
|
+
return runUntilReady(attempts - 1)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const ready = await runUntilReady()
|
|
207
|
+
expect(ready).toBe(true)
|
|
192
208
|
await expect(page).toMatchTextContent('foo-bar')
|
|
193
209
|
// await (new Promise(resolve => setTimeout(resolve, 1000)))
|
|
194
210
|
})
|
|
@@ -209,14 +225,14 @@ describe('Buttons, button titles and caller', () => {
|
|
|
209
225
|
}
|
|
210
226
|
test('Window', async () => {
|
|
211
227
|
schema.model.worker = false
|
|
212
|
-
await page.goto(
|
|
228
|
+
await page.goto(urlQueryEscaped(schema))
|
|
213
229
|
await expect(page).toMatchTextContent('Test Button')
|
|
214
230
|
await expect(page).toClick('button', { text: 'Test Button' })
|
|
215
231
|
await expect(page).toMatchTextContent('test_button')
|
|
216
232
|
})
|
|
217
233
|
test('Worker', async () => {
|
|
218
234
|
schema.model.worker = true
|
|
219
|
-
await page.goto(
|
|
235
|
+
await page.goto(urlQueryEscaped(schema))
|
|
220
236
|
await expect(page).toClick('button', { text: 'Test Button' })
|
|
221
237
|
await expect(page).toMatchTextContent('test_button')
|
|
222
238
|
})
|
|
@@ -234,4 +250,263 @@ describe('Pipeline', () => {
|
|
|
234
250
|
await expect(page).toClick('button', { text: 'Run' })
|
|
235
251
|
await expect(page).toMatchTextContent((Math.pow((a + b), 2) + 2).toString())
|
|
236
252
|
})
|
|
237
|
-
})
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
describe('File uploads', () => {
|
|
256
|
+
test('default file input reads uploaded text', async () => {
|
|
257
|
+
const schema = {
|
|
258
|
+
model: {
|
|
259
|
+
worker: false,
|
|
260
|
+
code: `function filePreview (inputs) {
|
|
261
|
+
return {
|
|
262
|
+
preview: inputs.file.split('\\n')[0]
|
|
263
|
+
}
|
|
264
|
+
}`
|
|
265
|
+
},
|
|
266
|
+
inputs: [
|
|
267
|
+
{ name: 'file', type: 'file' }
|
|
268
|
+
]
|
|
269
|
+
}
|
|
270
|
+
await page.goto(urlQueryEscaped(schema))
|
|
271
|
+
await page.waitForSelector('#vfp-filePicker')
|
|
272
|
+
const fileInput = await page.$('#vfp-filePicker')
|
|
273
|
+
await fileInput.uploadFile(uploadFixture)
|
|
274
|
+
await page.waitForFunction(() => document.body.innerText.includes('Selected 1 file(s)'))
|
|
275
|
+
await expect(page).toClick('button', { text: 'Run' })
|
|
276
|
+
await expect(page).toMatchTextContent('name,age')
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
test('raw file input passes File object (not text content)', async () => {
|
|
280
|
+
const schema = {
|
|
281
|
+
model: {
|
|
282
|
+
worker: false,
|
|
283
|
+
code: `function fileRawMeta (inputs) {
|
|
284
|
+
const isFileObject = !!inputs.file
|
|
285
|
+
&& (typeof inputs.file === 'object')
|
|
286
|
+
&& (typeof inputs.file.name === 'string')
|
|
287
|
+
&& (typeof inputs.file.text === 'function')
|
|
288
|
+
return {
|
|
289
|
+
is_file_object: isFileObject,
|
|
290
|
+
is_string: typeof inputs.file === 'string',
|
|
291
|
+
file_name: inputs.file && inputs.file.name ? inputs.file.name : '',
|
|
292
|
+
content_prefix: typeof inputs.file === 'string' ? inputs.file.slice(0, 10) : 'NONE'
|
|
293
|
+
}
|
|
294
|
+
}`
|
|
295
|
+
},
|
|
296
|
+
inputs: [
|
|
297
|
+
{ name: 'file', type: 'file', raw: true }
|
|
298
|
+
]
|
|
299
|
+
}
|
|
300
|
+
await page.goto(urlQueryEscaped(schema))
|
|
301
|
+
await page.waitForSelector('#vfp-filePicker')
|
|
302
|
+
const fileInput = await page.$('#vfp-filePicker')
|
|
303
|
+
await fileInput.uploadFile(uploadFixture)
|
|
304
|
+
await page.waitForFunction(() => document.body.innerText.includes('Selected 1 file(s)'))
|
|
305
|
+
await expect(page).toClick('button', { text: 'Run' })
|
|
306
|
+
await expect(page).toMatchTextContent('is_file_object')
|
|
307
|
+
await expect(page).toMatchTextContent('true')
|
|
308
|
+
await expect(page).toMatchTextContent('file_name')
|
|
309
|
+
await expect(page).toMatchTextContent('upload-sample.csv')
|
|
310
|
+
await expect(page).toMatchTextContent('content_prefix')
|
|
311
|
+
await expect(page).toMatchTextContent('NONE')
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
test('raw url input passes URL handle (not fetched text content)', async () => {
|
|
315
|
+
const schema = {
|
|
316
|
+
model: {
|
|
317
|
+
worker: false,
|
|
318
|
+
code: `function fileRawUrlMeta (inputs) {
|
|
319
|
+
const isUrlHandle = !!inputs.file
|
|
320
|
+
&& (typeof inputs.file === 'object')
|
|
321
|
+
&& (inputs.file.kind === 'url')
|
|
322
|
+
&& (typeof inputs.file.url === 'string')
|
|
323
|
+
return {
|
|
324
|
+
is_url_handle: isUrlHandle,
|
|
325
|
+
is_string: typeof inputs.file === 'string',
|
|
326
|
+
url_value: isUrlHandle ? inputs.file.url : '',
|
|
327
|
+
content_prefix: typeof inputs.file === 'string' ? inputs.file.slice(0, 10) : 'NONE'
|
|
328
|
+
}
|
|
329
|
+
}`
|
|
330
|
+
},
|
|
331
|
+
inputs: [
|
|
332
|
+
{ name: 'file', type: 'file', raw: true }
|
|
333
|
+
]
|
|
334
|
+
}
|
|
335
|
+
const rawUrl = 'http://127.0.0.1:9999/nope.csv'
|
|
336
|
+
await page.goto(urlQueryEscaped(schema))
|
|
337
|
+
await expect(page).toClick('button', { text: 'From URL' })
|
|
338
|
+
await page.waitForSelector('input.vfp-urlInput[type="text"]')
|
|
339
|
+
await page.click('input.vfp-urlInput[type="text"]', { clickCount: 3 })
|
|
340
|
+
await page.type('input.vfp-urlInput[type="text"]', rawUrl)
|
|
341
|
+
await page.evaluate(() => {
|
|
342
|
+
const loadButton = Array.from(document.querySelectorAll('button'))
|
|
343
|
+
.find(btn => btn.textContent.trim() === 'Load')
|
|
344
|
+
if (!loadButton) throw new Error('Load button not found')
|
|
345
|
+
loadButton.click()
|
|
346
|
+
})
|
|
347
|
+
await expect(page).toClick('button', { text: 'Run' })
|
|
348
|
+
await page.waitForFunction((expectedUrl) => {
|
|
349
|
+
return document.querySelector('#outputs').innerText.includes(expectedUrl)
|
|
350
|
+
}, {}, rawUrl)
|
|
351
|
+
await expect(page).toMatchTextContent('is_url_handle')
|
|
352
|
+
await expect(page).toMatchTextContent('true')
|
|
353
|
+
await expect(page).toMatchTextContent('url_value')
|
|
354
|
+
await expect(page).toMatchTextContent(rawUrl)
|
|
355
|
+
await expect(page).toMatchTextContent('content_prefix')
|
|
356
|
+
await expect(page).toMatchTextContent('NONE')
|
|
357
|
+
})
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
describe('Streamed file inputs', () => {
|
|
361
|
+
test('main thread receives uploaded file as async iterable stream', async () => {
|
|
362
|
+
const schema = {
|
|
363
|
+
model: {
|
|
364
|
+
worker: false,
|
|
365
|
+
code: `async function streamMainFile (inputs) {
|
|
366
|
+
const reader = inputs.file
|
|
367
|
+
const isIterable = !!reader
|
|
368
|
+
&& typeof reader[Symbol.asyncIterator] === 'function'
|
|
369
|
+
&& typeof reader.text === 'function'
|
|
370
|
+
&& typeof reader.bytes === 'function'
|
|
371
|
+
&& typeof reader.lines === 'function'
|
|
372
|
+
if (!isIterable) {
|
|
373
|
+
return { is_iterable: false, header: 'NONE' }
|
|
374
|
+
}
|
|
375
|
+
const text = await reader.text()
|
|
376
|
+
return {
|
|
377
|
+
is_iterable: true,
|
|
378
|
+
header: text.split('\\n')[0]
|
|
379
|
+
}
|
|
380
|
+
}`
|
|
381
|
+
},
|
|
382
|
+
inputs: [
|
|
383
|
+
{ name: 'file', type: 'file', raw: true, stream: true }
|
|
384
|
+
]
|
|
385
|
+
}
|
|
386
|
+
await page.goto(urlQueryEscaped(schema))
|
|
387
|
+
await page.waitForSelector('#vfp-filePicker')
|
|
388
|
+
const fileInput = await page.$('#vfp-filePicker')
|
|
389
|
+
await fileInput.uploadFile(uploadFixture)
|
|
390
|
+
await expect(page).toClick('button', { text: 'Run' })
|
|
391
|
+
await expect(page).toMatchTextContent('is_iterable')
|
|
392
|
+
await expect(page).toMatchTextContent('true')
|
|
393
|
+
await expect(page).toMatchTextContent('header')
|
|
394
|
+
await expect(page).toMatchTextContent('name,age')
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
test('main thread receives URL source as async iterable stream', async () => {
|
|
398
|
+
const schema = {
|
|
399
|
+
model: {
|
|
400
|
+
worker: false,
|
|
401
|
+
code: `async function streamMainUrl (inputs) {
|
|
402
|
+
const reader = inputs.file
|
|
403
|
+
const text = await reader.text()
|
|
404
|
+
return {
|
|
405
|
+
is_iterable: typeof reader[Symbol.asyncIterator] === 'function',
|
|
406
|
+
header: text.split('\\n')[0]
|
|
407
|
+
}
|
|
408
|
+
}`
|
|
409
|
+
},
|
|
410
|
+
inputs: [
|
|
411
|
+
{ name: 'file', type: 'file', raw: true, stream: true }
|
|
412
|
+
]
|
|
413
|
+
}
|
|
414
|
+
const sampleUrl = `http://localhost:${port}/test/fixtures/upload-sample.csv`
|
|
415
|
+
await page.goto(urlQueryEscaped(schema))
|
|
416
|
+
await expect(page).toClick('button', { text: 'From URL' })
|
|
417
|
+
await page.waitForSelector('input.vfp-urlInput[type="text"]')
|
|
418
|
+
await page.click('input.vfp-urlInput[type="text"]', { clickCount: 3 })
|
|
419
|
+
await page.type('input.vfp-urlInput[type="text"]', sampleUrl)
|
|
420
|
+
await page.evaluate(() => {
|
|
421
|
+
const loadButton = Array.from(document.querySelectorAll('button'))
|
|
422
|
+
.find(btn => btn.textContent.trim() === 'Load')
|
|
423
|
+
if (!loadButton) throw new Error('Load button not found')
|
|
424
|
+
loadButton.click()
|
|
425
|
+
})
|
|
426
|
+
await expect(page).toClick('button', { text: 'Run' })
|
|
427
|
+
await page.waitForFunction(() => {
|
|
428
|
+
const outputs = document.querySelector('#outputs')
|
|
429
|
+
return outputs && outputs.innerText.includes('header')
|
|
430
|
+
}, { timeout: 5000 })
|
|
431
|
+
await expect(page).toMatchTextContent('is_iterable')
|
|
432
|
+
await expect(page).toMatchTextContent('true')
|
|
433
|
+
await expect(page).toMatchTextContent('header')
|
|
434
|
+
await expect(page).toMatchTextContent('name,age')
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
test('worker receives uploaded file as async iterable stream', async () => {
|
|
438
|
+
const schema = {
|
|
439
|
+
model: {
|
|
440
|
+
worker: true,
|
|
441
|
+
code: `async function streamWorkerFile (inputs) {
|
|
442
|
+
const reader = inputs.file
|
|
443
|
+
const lines = []
|
|
444
|
+
for await (const line of reader.lines()) {
|
|
445
|
+
lines.push(line)
|
|
446
|
+
}
|
|
447
|
+
return {
|
|
448
|
+
is_iterable: typeof reader.text === 'function',
|
|
449
|
+
header: lines[0] || 'NONE'
|
|
450
|
+
}
|
|
451
|
+
}`
|
|
452
|
+
},
|
|
453
|
+
inputs: [
|
|
454
|
+
{ name: 'file', type: 'file', raw: true, stream: true }
|
|
455
|
+
]
|
|
456
|
+
}
|
|
457
|
+
await page.goto(urlQueryEscaped(schema))
|
|
458
|
+
await page.waitForSelector('#vfp-filePicker')
|
|
459
|
+
const fileInput = await page.$('#vfp-filePicker')
|
|
460
|
+
await fileInput.uploadFile(uploadFixture)
|
|
461
|
+
await expect(page).toClick('button', { text: 'Run' })
|
|
462
|
+
await expect(page).toMatchTextContent('is_iterable')
|
|
463
|
+
await expect(page).toMatchTextContent('true')
|
|
464
|
+
await expect(page).toMatchTextContent('header')
|
|
465
|
+
await expect(page).toMatchTextContent('name,age')
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
test('worker receives URL source as async iterable stream', async () => {
|
|
469
|
+
const schema = {
|
|
470
|
+
model: {
|
|
471
|
+
worker: true,
|
|
472
|
+
code: `async function streamWorkerUrl (inputs) {
|
|
473
|
+
const reader = inputs.file
|
|
474
|
+
const decoder = new TextDecoder()
|
|
475
|
+
let text = ''
|
|
476
|
+
for await (const chunk of reader) {
|
|
477
|
+
text += decoder.decode(chunk, { stream: true })
|
|
478
|
+
}
|
|
479
|
+
text += decoder.decode()
|
|
480
|
+
return {
|
|
481
|
+
is_iterable: typeof reader.bytes === 'function',
|
|
482
|
+
header: text.split('\\n')[0]
|
|
483
|
+
}
|
|
484
|
+
}`
|
|
485
|
+
},
|
|
486
|
+
inputs: [
|
|
487
|
+
{ name: 'file', type: 'file', raw: true, stream: true }
|
|
488
|
+
]
|
|
489
|
+
}
|
|
490
|
+
const sampleUrl = `http://localhost:${port}/test/fixtures/upload-sample.csv`
|
|
491
|
+
await page.goto(urlQueryEscaped(schema))
|
|
492
|
+
await expect(page).toClick('button', { text: 'From URL' })
|
|
493
|
+
await page.waitForSelector('input.vfp-urlInput[type="text"]')
|
|
494
|
+
await page.click('input.vfp-urlInput[type="text"]', { clickCount: 3 })
|
|
495
|
+
await page.type('input.vfp-urlInput[type="text"]', sampleUrl)
|
|
496
|
+
await page.evaluate(() => {
|
|
497
|
+
const loadButton = Array.from(document.querySelectorAll('button'))
|
|
498
|
+
.find(btn => btn.textContent.trim() === 'Load')
|
|
499
|
+
if (!loadButton) throw new Error('Load button not found')
|
|
500
|
+
loadButton.click()
|
|
501
|
+
})
|
|
502
|
+
await expect(page).toClick('button', { text: 'Run' })
|
|
503
|
+
await page.waitForFunction(() => {
|
|
504
|
+
const outputs = document.querySelector('#outputs')
|
|
505
|
+
return outputs && outputs.innerText.includes('header')
|
|
506
|
+
}, { timeout: 5000 })
|
|
507
|
+
await expect(page).toMatchTextContent('is_iterable')
|
|
508
|
+
await expect(page).toMatchTextContent('true')
|
|
509
|
+
await expect(page).toMatchTextContent('header')
|
|
510
|
+
await expect(page).toMatchTextContent('name,age')
|
|
511
|
+
})
|
|
512
|
+
})
|