Skip to content

Commit 2e54773

Browse files
authored
Merge pull request #112 from RRosio/ui_tests
Add upload tests for jupyterlab file browser and native file browser
2 parents 0cf877d + 0e6d97e commit 2e54773

File tree

2 files changed

+271
-1
lines changed

2 files changed

+271
-1
lines changed

jupyter_fsspec/handlers.py

+7
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ def initialize(self, fs_manager):
101101
self.fs_manager = fs_manager
102102

103103
# POST /jupyter_fsspec/files/action?key=my-key&item_path=/some_directory/file.txt
104+
@tornado.web.authenticated
104105
async def post(self):
105106
"""Move or copy the resource at the input path to destination path.
106107
@@ -178,6 +179,7 @@ def initialize(self, fs_manager):
178179
self.fs_manager = fs_manager
179180

180181
# POST /jupyter_fsspec/files/action?key=my-key&item_path=/some_directory/file.txt
182+
@tornado.web.authenticated
181183
async def post(self):
182184
"""Upload/Download the resource at the input path to destination path.
183185
@@ -271,6 +273,7 @@ class RenameFileHandler(APIHandler):
271273
def initialize(self, fs_manager):
272274
self.fs_manager = fs_manager
273275

276+
@tornado.web.authenticated
274277
async def post(self):
275278
request_data = json.loads(self.request.body.decode("utf-8"))
276279
try:
@@ -335,6 +338,7 @@ async def process_content(self, content):
335338

336339
# GET
337340
# /files
341+
@tornado.web.authenticated
338342
async def get(self):
339343
"""Retrieve list of files for directories or contents for files.
340344
@@ -453,6 +457,7 @@ async def get(self):
453457
# JSON Payload
454458
# item_path=/some_directory/file.txt
455459
# content
460+
@tornado.web.authenticated
456461
async def post(self):
457462
"""Create directories/files or perform other directory/file operations like move and copy
458463
@@ -536,6 +541,7 @@ async def post(self):
536541
# PUT /jupyter_fsspec/files?key=my-key&item_path=/some_directory/file.txt
537542
# JSON Payload
538543
# content
544+
@tornado.web.authenticated
539545
async def put(self):
540546
"""Update ENTIRE content in file.
541547
@@ -606,6 +612,7 @@ async def put(self):
606612
self.finish()
607613

608614
# DELETE /jupyter_fsspec/files?key=my-key&item_path=/some_directory/file.txt
615+
@tornado.web.authenticated
609616
async def delete(self):
610617
"""Delete the resource at the input path.
611618

ui-tests/tests/filesystem_interaction.test.ts

+264-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import { expect, test } from '@jupyterlab/galata';
2+
import path from 'path';
3+
import fs from 'fs';
4+
import os from 'os';
25

36
test.use({
47
autoGoto: false,
@@ -66,6 +69,13 @@ const rootMyMemFs = {
6669
ino: 49648960,
6770
mode: 33188
6871
},
72+
{
73+
name: '/mymemoryfs/myfile2.txt',
74+
type: 'file',
75+
size: 128,
76+
ino: 49638760,
77+
mode: 33188
78+
},
6979
{
7080
name: '/mymemoryfs/otherdocs',
7181
type: 'directory',
@@ -174,7 +184,7 @@ test('test interacting with a filesystem', async ({ page }) => {
174184
for (const element of elements) {
175185
console.log(await element.evaluate(el => el.textContent));
176186
}
177-
expect(countTreeItems).toEqual(3);
187+
expect(countTreeItems).toEqual(4);
178188
});
179189

180190
test('test copy path', async ({ page }) => {
@@ -316,3 +326,256 @@ test('copy open with code block with active notebook cell', async ({
316326
expect(content?.includes(cellText));
317327
expect(content?.includes(copyCodeBlock));
318328
});
329+
330+
test('upload file from the Jupyterlab file browser', async ({ page }) => {
331+
page.on('console', logMsg => console.log('[BROWSER OUTPUT] ', logMsg.text()));
332+
const request_url =
333+
'http://localhost:8888/jupyter_fsspec/files?action=write&key=mymem';
334+
const response_body = {
335+
status: 'success',
336+
desctiption: 'Uploaded file'
337+
};
338+
339+
await page.route(request_url + '**', route => {
340+
route.fulfill({
341+
status: 200,
342+
contentType: 'application/json',
343+
body: JSON.stringify(response_body)
344+
});
345+
});
346+
347+
await page.goto();
348+
await page.getByText('FSSpec', { exact: true }).click();
349+
await page.locator('.jfss-fsitem-root').click();
350+
351+
// open a notebook
352+
await page.notebook.createNew();
353+
await page.waitForTimeout(1000);
354+
await page
355+
.getByRole('button', { name: 'Save and create checkpoint' })
356+
.click();
357+
await page.getByRole('button', { name: 'Rename' }).click();
358+
359+
// right-click on Jupyterlab filebrowser item
360+
await page.getByRole('listitem', { name: 'Name: Untitled.ipynb' }).click({
361+
button: 'right'
362+
});
363+
await page.getByText('Set as fsspec upload target').click();
364+
365+
const file_locator = page
366+
.locator('jp-tree-view')
367+
.locator('jp-tree-item')
368+
.nth(1);
369+
await file_locator.highlight();
370+
await file_locator.click({ button: 'right' });
371+
372+
const requestPromise = page.waitForRequest(
373+
request =>
374+
request.url().includes(request_url) && request.method() === 'POST'
375+
);
376+
377+
// Wait for pop up
378+
const command = 'Upload to path (from integrated file browser)';
379+
await expect.soft(page.getByText(command)).toBeVisible();
380+
await page.getByText(command).highlight();
381+
await page.getByText(command).click();
382+
383+
// input file name and click `Ok`
384+
try {
385+
await page.waitForSelector('.jp-Dialog', { timeout: 3000 });
386+
await page
387+
.locator('.jfss-file-upload-context-popup input')
388+
.fill('test.txt');
389+
await page.getByRole('button', { name: 'Ok' }).click();
390+
} catch (error) {
391+
console.log('Dialog not found, skipping action');
392+
}
393+
394+
// file size information will not be upadated to match the notebook size
395+
// as that information is currenly mocked.
396+
397+
// TODO: ensure HTTP request is made with correct parameters
398+
const request = await requestPromise;
399+
expect.soft(request.method()).toBe('POST');
400+
expect.soft(request.url()).toContain(request_url);
401+
402+
const response = await request.response();
403+
404+
const jsonResponse = await response?.json();
405+
expect.soft(jsonResponse).toEqual(response_body);
406+
});
407+
408+
test('upload file from browser picker', async ({ page }) => {
409+
const request_url =
410+
'http://localhost:8888/jupyter_fsspec/files?action=write&key=mymem';
411+
const response_body = {
412+
status: 'success',
413+
desctiption: 'uploaded file'
414+
};
415+
416+
await page.route(request_url + '**', route => {
417+
route.fulfill({
418+
status: 200,
419+
contentType: 'application/json',
420+
body: JSON.stringify(response_body)
421+
});
422+
});
423+
424+
await page.goto();
425+
await page.getByText('FSSpec', { exact: true }).click();
426+
await page.locator('.jfss-fsitem-root').click();
427+
428+
// open a notebook
429+
await page.notebook.createNew();
430+
await page.waitForTimeout(1000);
431+
await page
432+
.getByRole('button', { name: 'Save and create checkpoint' })
433+
.click();
434+
await page.getByRole('button', { name: 'Rename' }).click();
435+
436+
page.on('request', request =>
437+
console.log('>>', request.method(), request.url())
438+
);
439+
page.on('response', async response =>
440+
console.log('<<', response.status(), response.url(), '<<', response.text())
441+
);
442+
443+
// right-click on browser picker item
444+
const file_locator = page
445+
.locator('jp-tree-view')
446+
.locator('jp-tree-item')
447+
.nth(2);
448+
await file_locator.highlight();
449+
await file_locator.click({ button: 'right' });
450+
451+
// Wait for file picker dialog
452+
const filePickerPromise = page.waitForEvent('filechooser');
453+
454+
// Wait for pop up
455+
const command = 'Upload to path (Browser file picker)';
456+
await expect.soft(page.getByText(command)).toBeVisible();
457+
await page.getByText(command).highlight();
458+
await page.getByText(command).click();
459+
460+
await filePickerPromise;
461+
462+
const tmpFilePath = path.join(os.tmpdir(), 'test-file.txt');
463+
fs.writeFileSync(tmpFilePath, 'This is a test file for Playwright.');
464+
await page.setInputFiles('input[type="file"]', tmpFilePath);
465+
466+
fs.unlinkSync(tmpFilePath);
467+
468+
const requestPromise = page.waitForRequest(
469+
request =>
470+
request.url().includes(request_url) && request.method() === 'POST'
471+
);
472+
473+
// TODO: ensure HTTP request is made with correct parameters
474+
const request = await requestPromise;
475+
expect.soft(request.method()).toBe('POST');
476+
expect.soft(request.url()).toContain(request_url);
477+
478+
const response = await request.response();
479+
expect.soft(response?.status()).toBe(200);
480+
const jsonResponse = await response?.json();
481+
expect.soft(jsonResponse).toEqual(response_body);
482+
});
483+
484+
test('upload file from helper', async ({ page }) => {
485+
page.on('console', logMsg => console.log('[BROWSER OUTPUT] ', logMsg.text()));
486+
const request_url =
487+
'http://localhost:8888/jupyter_fsspec/files/transfer?action=upload';
488+
const response_body = {
489+
status: 'success',
490+
desctiption: 'Uploaded file'
491+
};
492+
493+
await page.route(request_url + '**', route => {
494+
route.fulfill({
495+
status: 200,
496+
contentType: 'application/json',
497+
body: JSON.stringify(response_body)
498+
});
499+
});
500+
501+
await page.goto();
502+
await page.getByText('FSSpec', { exact: true }).click();
503+
await page.locator('.jfss-fsitem-root').click();
504+
505+
// open a notebook
506+
await page.notebook.createNew();
507+
await page.waitForTimeout(1000);
508+
509+
const quote_mark = '"';
510+
const new_bytes = `${quote_mark}Hello there from playwright${quote_mark}.encode()`;
511+
await page.notebook.addCell(
512+
'code',
513+
`from jupyter_fsspec import helper\nhelper.set_user_data(${new_bytes})`
514+
);
515+
await page.notebook.runCell(1);
516+
517+
await page
518+
.getByRole('button', { name: 'Save and create checkpoint' })
519+
.click();
520+
await page.getByRole('button', { name: 'Rename' }).click();
521+
522+
const file_locator = page
523+
.locator('jp-tree-view')
524+
.locator('jp-tree-item')
525+
.nth(1);
526+
await file_locator.highlight();
527+
await file_locator.click({ button: 'right' });
528+
529+
const requestPromise = page.waitForRequest(
530+
request =>
531+
request.url().includes(request_url) && request.method() === 'POST'
532+
);
533+
534+
// Wait for pop up
535+
const command = 'Upload to path (helper.user_data)';
536+
await expect.soft(page.getByText(command)).toBeVisible();
537+
await page.getByText(command).highlight();
538+
await page.getByText(command).click();
539+
540+
// input file name and click `Ok`
541+
try {
542+
await page.waitForSelector('.jp-Dialog', { timeout: 3000 });
543+
await page
544+
.locator('.jfss-file-upload-context-popup input')
545+
.fill('test.txt');
546+
await page.getByRole('button', { name: 'Ok' }).click();
547+
} catch (error) {
548+
console.log('Dialog not found, skipping action');
549+
}
550+
551+
// TODO: ensure HTTP request is made with correct parameters
552+
const request = await requestPromise;
553+
expect.soft(request.method()).toBe('POST');
554+
expect.soft(request.url()).toContain(request_url);
555+
556+
const response = await request.response();
557+
expect.soft(response?.status()).toBe(200);
558+
const jsonResponse = await response?.json();
559+
expect.soft(jsonResponse).toEqual(response_body);
560+
});
561+
562+
test('verify option `send bytes to helper` is present', async ({ page }) => {
563+
page.on('console', logMsg => console.log('[BROWSER OUTPUT] ', logMsg.text()));
564+
565+
await page.goto();
566+
await page.getByText('FSSpec', { exact: true }).click();
567+
await page.locator('.jfss-fsitem-root').click();
568+
569+
// select file to send to helper
570+
const file_locator = page
571+
.locator('jp-tree-view')
572+
.locator('jp-tree-item')
573+
.nth(2);
574+
await file_locator.highlight();
575+
await file_locator.click({ button: 'right' });
576+
577+
// Wait for pop up
578+
const command = 'Send bytes to helper';
579+
await expect.soft(page.getByText(command)).toBeVisible();
580+
await page.getByText(command).click();
581+
});

0 commit comments

Comments
 (0)