@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.
@@ -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(urlQuery(schema))
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(urlQuery(schema))
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(urlQuery(schema))
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(urlQuery(schema))
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': 'lodash@4.17.21/lodash.min.js',
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(urlQuery(schema))
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(urlQuery(schema))
191
- await expect(page).toClick('button', { text: 'Run' })
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(urlQuery(schema))
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(urlQuery(schema))
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
+ })