diff --git a/ui-tests/tests/fs.spec.ts b/ui-tests/tests/fs.spec.ts new file mode 100644 index 0000000..a740969 --- /dev/null +++ b/ui-tests/tests/fs.spec.ts @@ -0,0 +1,77 @@ +import { expect, test } from '@jupyterlab/galata'; + +import { ContentsHelper } from './utils/contents'; +import { TERMINAL_SELECTOR, decode64, inputLine } from './utils/misc'; + +const MONTHS_TXT = + 'January\nFebruary\nMarch\nApril\nMay\nJune\nJuly\nAugust\nSeptember\nOctober\nNovember\nDecember\n'; +const FACT_LUA = + 'function fact(n, acc)\n' + + ' acc = acc or 1\n' + + ' if n == 0 then\n' + + ' return acc\n' + + ' end\n' + + ' return fact(n-1, n*acc)\n' + + 'end\n' + + 'print(fact(tonumber(arg[1])))\n'; + +test.describe('Filesystem', () => { + test.beforeEach(async ({ page }) => { + await page.goto(); + + // Overwrite the (read-only) page.contents with our own ContentsHelper. + // @ts-ignore + page.contents = new ContentsHelper(page); + + await page.menu.clickMenuItem('File>New>Terminal'); + await page.locator(TERMINAL_SELECTOR).waitFor(); + await page.locator('div.xterm-screen').click(); // sets focus for keyboard input + }); + + test('should have initial files', async ({ page }) => { + // Directory contents. + const content = await page.contents.getContentMetadata('', 'directory'); + expect(content).not.toBeNull(); + const filenames = content?.content.map(item => item.name); + expect(filenames).toEqual( + expect.arrayContaining(['fact.lua', 'months.txt']) + ); + + // File contents. + const months = await page.contents.getContentMetadata('months.txt'); + expect(months?.content).toEqual(MONTHS_TXT); + + // Note fact.lua contents are returned base64 encoded. + const fact = await page.contents.getContentMetadata('fact.lua'); + expect(decode64(fact?.content)).toEqual(FACT_LUA); + }); + + test('should support cp', async ({ page }) => { + await inputLine(page, 'cp months.txt other.txt'); + await page.filebrowser.refresh(); + + expect(await page.contents.fileExists('months.txt')).toBeTruthy(); + expect(await page.contents.fileExists('other.txt')).toBeTruthy(); + + const other = await page.contents.getContentMetadata('other.txt'); + expect(other?.content).toEqual(MONTHS_TXT); + }); + + // rm of files added via --contents is not reliable. + test.skip('should support rm', async ({ page }) => { + await inputLine(page, 'rm fact.lua'); + await page.filebrowser.refresh(); + + expect(await page.contents.fileExists('fact.lua')).toBeFalsy(); + }); + + test('should support touch', async ({ page }) => { + await inputLine(page, 'touch touched.txt'); + await page.filebrowser.refresh(); + + expect(await page.contents.fileExists('touched.txt')).toBeTruthy(); + + const other = await page.contents.getContentMetadata('touched.txt'); + expect(other?.content).toEqual(''); + }); +}); diff --git a/ui-tests/tests/jupyterlite_terminal.spec.ts b/ui-tests/tests/jupyterlite_terminal.spec.ts index 6982652..eac09f6 100644 --- a/ui-tests/tests/jupyterlite_terminal.spec.ts +++ b/ui-tests/tests/jupyterlite_terminal.spec.ts @@ -1,14 +1,5 @@ import { expect, test } from '@jupyterlab/galata'; - -const TERMINAL_SELECTOR = '.jp-Terminal'; - -async function inputLine(page, text: string) { - for (const char of text) { - await page.keyboard.type(char); - await page.waitForTimeout(10); - } - await page.keyboard.press('Enter'); -} +import { TERMINAL_SELECTOR, WAIT_MS, inputLine } from './utils/misc'; test.describe('Terminal extension', () => { test('should emit activation console messages', async ({ page }) => { @@ -82,28 +73,26 @@ test.describe('Images', () => { await page.locator(TERMINAL_SELECTOR).waitFor(); await page.locator('div.xterm-screen').click(); // sets focus for keyboard input - const wait = 100; // milliseconds - await inputLine(page, 'ls'); // avoid timestamps - await page.waitForTimeout(wait); + await page.waitForTimeout(WAIT_MS); await inputLine(page, 'cp months.txt other.txt'); - await page.waitForTimeout(wait); + await page.waitForTimeout(WAIT_MS); await inputLine(page, 'ls'); // avoid timestamps - await page.waitForTimeout(wait); + await page.waitForTimeout(WAIT_MS); await inputLine(page, 'una\t'); // tab complete command name - await page.waitForTimeout(wait); + await page.waitForTimeout(WAIT_MS); await inputLine(page, 'grep ember mon\t'); // tab complete filename - await page.waitForTimeout(wait); + await page.waitForTimeout(WAIT_MS); await page.keyboard.press('Tab'); // list all commands - await page.waitForTimeout(wait); + await page.waitForTimeout(WAIT_MS); await inputLine(page, 'abc'); // no such command - await page.waitForTimeout(wait); + await page.waitForTimeout(WAIT_MS); // Hide modification times. const modified = page.locator('span.jp-DirListing-itemModified'); diff --git a/ui-tests/tests/utils/contents.ts b/ui-tests/tests/utils/contents.ts new file mode 100644 index 0000000..b4969e3 --- /dev/null +++ b/ui-tests/tests/utils/contents.ts @@ -0,0 +1,48 @@ +import type { Contents } from '@jupyterlab/services'; +import type { Page } from '@playwright/test'; + +/** + * Helper class to interact with JupyterLite contents manager. + * + * A subset of the functionality of galata's implementation + */ +export class ContentsHelper { + constructor(readonly page: Page) {} + + async directoryExists(dirPath: string): Promise { + const content = await this._get(dirPath, false, 'directory'); + return content?.type === 'directory'; + } + + async fileExists(filePath: string): Promise { + const content = await this._get(filePath, false); + return content?.type === 'notebook' || content?.type === 'file'; + } + + async getContentMetadata( + path: string, + type: 'file' | 'directory' = 'file' + ): Promise { + return await this._get(path, true, type); + } + + private async _get( + path: string, + wantContents: boolean, + type: 'file' | 'directory' = 'file' + ): Promise { + const model = await this.page.evaluate( + async ({ path, wantContents, type }) => { + const contents = window.galata.app.serviceManager.contents; + const options = { type, content: wantContents }; + try { + return await contents.get(path, options); + } catch (error) { + return null; + } + }, + { path, wantContents, type } + ); + return model; + } +} diff --git a/ui-tests/tests/utils/misc.ts b/ui-tests/tests/utils/misc.ts new file mode 100644 index 0000000..4b042b7 --- /dev/null +++ b/ui-tests/tests/utils/misc.ts @@ -0,0 +1,16 @@ +import { Buffer } from 'node:buffer'; + +export const WAIT_MS = 100; +export const TERMINAL_SELECTOR = '.jp-Terminal'; + +export function decode64(encoded: string): string { + return Buffer.from(encoded, 'base64').toString('binary'); +} + +export async function inputLine(page, text: string) { + for (const char of text) { + await page.keyboard.type(char); + await page.waitForTimeout(10); + } + await page.keyboard.press('Enter'); +}