diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 656cf27..9ec0162 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -140,7 +140,7 @@ jobs: - name: Execute integration tests working-directory: ui-tests run: | - jlpm playwright test --retries=2 + jlpm test --retries=2 - name: Upload Playwright Test report if: always() @@ -151,6 +151,63 @@ jobs: ui-tests/test-results ui-tests/playwright-report + integration-tests-notebook: + name: Integration tests notebook + needs: build_extension + runs-on: ubuntu-latest + + env: + PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/pw-browsers + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Base Setup + uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 + + - name: Download extension package + uses: actions/download-artifact@v3 + with: + name: jupyterlab_chat-artifacts + + - name: Install the extension + run: | + set -eux + python -m pip install "notebook>=7.0.0,<8" jupyterlab_chat*.whl + + - name: Install dependencies + working-directory: ui-tests + env: + YARN_ENABLE_IMMUTABLE_INSTALLS: 0 + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + run: jlpm install + + - name: Set up browser cache + uses: actions/cache@v3 + with: + path: | + ${{ github.workspace }}/pw-browsers + key: ${{ runner.os }}-${{ hashFiles('ui-tests/yarn.lock') }} + + - name: Install browser + run: jlpm playwright install chromium + working-directory: ui-tests + + - name: Execute integration tests + working-directory: ui-tests + run: | + jlpm test:notebook --retries=2 + + - name: Upload Playwright Test report + if: always() + uses: actions/upload-artifact@v3 + with: + name: jupyterlab_chat-notebook-playwright-tests + path: | + ui-tests/test-results + ui-tests/playwright-report + typing-tests: name: Typing test runs-on: ubuntu-latest diff --git a/python/jupyterlab-chat/package.json b/python/jupyterlab-chat/package.json index 9977369..3f28bff 100644 --- a/python/jupyterlab-chat/package.json +++ b/python/jupyterlab-chat/package.json @@ -57,6 +57,7 @@ "watch:labextension": "jupyter labextension watch ." }, "dependencies": { + "@jupyter-notebook/application": "^7.2.0", "@jupyter/docprovider": "^2.1.4", "@jupyter/ydoc": "^1.1.1", "@jupyterlab/application": "^4.2.0", diff --git a/python/jupyterlab-chat/src/index.ts b/python/jupyterlab-chat/src/index.ts index 4a25326..94a047e 100644 --- a/python/jupyterlab-chat/src/index.ts +++ b/python/jupyterlab-chat/src/index.ts @@ -3,6 +3,7 @@ * Distributed under the terms of the Modified BSD License. */ +import { NotebookShell } from '@jupyter-notebook/application'; import { ActiveCellManager, AutocompletionRegistry, @@ -517,8 +518,24 @@ const chatCommands: JupyterFrontEndPlugin = { } if (inSidePanel && chatPanel) { - // The chat is opened in the chat panel. - app.shell.activateById(chatPanel.id); + /** + * The chat is opened in the chat panel, ensure the chat panel is opened. + * + * NOTES: In Notebook application, the panel is collapsed when using the + * `activateById` when it is already opened. + * See https://github.com/jupyter/notebook/issues/7534 + */ + if (app.shell instanceof NotebookShell) { + const shell: NotebookShell = app.shell; + if ( + shell.leftHandler?.currentWidget?.id !== chatPanel.id || + !shell.leftHandler.isVisible + ) { + shell.activateById(chatPanel.id); + } + } else { + app.shell.activateById(chatPanel.id); + } if (chatPanel.openIfExists(filepath)) { return; diff --git a/ui-tests/jupyter_server_test_config_notebook.py b/ui-tests/jupyter_server_test_config_notebook.py new file mode 100644 index 0000000..3d7d497 --- /dev/null +++ b/ui-tests/jupyter_server_test_config_notebook.py @@ -0,0 +1,27 @@ +# Copyright (c) Jupyter Development Team. +# Distributed under the terms of the Modified BSD License. + +"""Server configuration for integration tests. +!! Never use this configuration in production because it +opens the server to the world and provide access to JupyterLab +JavaScript objects through the global window variable. +""" +from pathlib import Path +import jupyterlab + +c.ServerApp.port = 8888 +c.ServerApp.port_retries = 0 +c.ServerApp.open_browser = False + +c.ServerApp.token = "" +c.ServerApp.password = "" +c.ServerApp.disable_check_xsrf = True + +c.JupyterNotebookApp.expose_app_in_browser = True + +c.LabServerApp.extra_labextensions_path = str(Path(jupyterlab.__file__).parent / "galata") + +c.FileContentsManager.delete_to_trash = False + +# Uncomment to set server log level to debug level +# c.ServerApp.log_level = "DEBUG" diff --git a/ui-tests/package.json b/ui-tests/package.json index 42797e4..ce3c3cc 100644 --- a/ui-tests/package.json +++ b/ui-tests/package.json @@ -5,7 +5,9 @@ "private": true, "scripts": { "start": "jupyter lab --config jupyter_server_test_config.py", + "start:notebook": "jupyter notebook --config jupyter_server_test_config_notebook.py", "test": "jlpm playwright test", + "test:notebook": "jlpm playwright test --config=playwright.notebook.config.js", "test:update": "jlpm playwright test --update-snapshots" }, "devDependencies": { diff --git a/ui-tests/playwright.config.js b/ui-tests/playwright.config.js index d22e0df..a392819 100644 --- a/ui-tests/playwright.config.js +++ b/ui-tests/playwright.config.js @@ -16,6 +16,7 @@ module.exports = { timeout: 120 * 1000, reuseExistingServer: !process.env.CI }, + testIgnore: 'tests/notebook-application.spec.ts', use: { contextOptions: { permissions: ['clipboard-read', 'clipboard-write'] diff --git a/ui-tests/playwright.notebook.config.js b/ui-tests/playwright.notebook.config.js new file mode 100644 index 0000000..28c7ef0 --- /dev/null +++ b/ui-tests/playwright.notebook.config.js @@ -0,0 +1,20 @@ +/* + * Copyright (c) Jupyter Development Team. + * Distributed under the terms of the Modified BSD License. + */ + +/** + * Configuration for Playwright using default from @jupyterlab/galata + */ +const baseConfig = require('@jupyterlab/galata/lib/playwright-config'); + +module.exports = { + ...baseConfig, + webServer: { + command: 'jlpm start:notebook', + url: 'http://localhost:8888/tree', + timeout: 120 * 1000, + reuseExistingServer: !process.env.CI + }, + testMatch: 'tests/notebook-application.spec.ts' +}; diff --git a/ui-tests/tests/notebook-application.spec.ts b/ui-tests/tests/notebook-application.spec.ts new file mode 100644 index 0000000..36ceb6b --- /dev/null +++ b/ui-tests/tests/notebook-application.spec.ts @@ -0,0 +1,65 @@ +/* + * Copyright (c) Jupyter Development Team. + * Distributed under the terms of the Modified BSD License. + */ + +import { expect, test as base } from '@jupyterlab/galata'; + +export const test = base.extend({ + waitForApplication: async ({ baseURL }, use, testInfo) => { + const waitIsReady = async (page): Promise => { + await page.waitForSelector('#main-panel'); + }; + await use(waitIsReady); + }, +}); + +test.use({ + autoGoto: false, + appPath: '', + viewport: { width: 1024, height: 900 }, +}); + +const NAME = 'my_chat'; +const FILENAME = `${NAME}.chat`; + +test.describe('#NotebookApp', () => { + test.beforeEach(async ({ page }) => { + // Create a chat file + await page.filebrowser.contents.uploadContent('{}', 'text', FILENAME); + }); + + test.afterEach(async ({ page }) => { + if (await page.filebrowser.contents.fileExists(FILENAME)) { + await page.filebrowser.contents.deleteFile(FILENAME); + } + }); + + test('Should open side panel and list existing chats', async ({ page }) => { + await page.goto('tree'); + await page.menu.clickMenuItem('View>Left Sidebar>Show Jupyter Chat'); + const panel = page.locator('#jp-left-stack'); + await expect(panel).toBeVisible(); + await expect(panel.locator('.jp-lab-chat-sidepanel')).toBeVisible(); + + const select = panel.locator( + '.jp-SidePanel-toolbar .jp-Toolbar-item.jp-lab-chat-open select' + ); + + await expect(select.locator('option')).toHaveCount(2); + await expect(select.locator('option').last()).toHaveText(NAME); + }); + + test('Should open main panel in a separate tab', async ({ page, context }) => { + await page.goto('tree'); + + const pagePromise = context.waitForEvent('page'); + await page.dblclick(`.jp-FileBrowser-listing >> text=${FILENAME}`); + + const newPage = await pagePromise; + //wait for Load + // await newPage.waitForLoadState(); + + await expect(newPage.locator('.jp-MainAreaWidget')).toHaveClass(/jp-lab-chat-main-panel/); + }); +}); diff --git a/yarn.lock b/yarn.lock index 60d0c1c..137b263 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2430,6 +2430,25 @@ __metadata: languageName: node linkType: hard +"@jupyter-notebook/application@npm:^7.2.0": + version: 7.2.1 + resolution: "@jupyter-notebook/application@npm:7.2.1" + dependencies: + "@jupyterlab/application": ~4.2.0 + "@jupyterlab/coreutils": ~6.2.0 + "@jupyterlab/docregistry": ~4.2.0 + "@jupyterlab/rendermime-interfaces": ~3.10.0 + "@jupyterlab/ui-components": ~4.2.0 + "@lumino/algorithm": ^2.0.1 + "@lumino/coreutils": ^2.1.2 + "@lumino/messaging": ^2.0.1 + "@lumino/polling": ^2.1.2 + "@lumino/signaling": ^2.1.2 + "@lumino/widgets": ^2.3.2 + checksum: 14114a2f7144e2a7bbf5e80476ef9f4b229bd9ca65d5620a97aba2e9d666cd4ae2a04cb4c96f6b0575e6383a930073945fae580eb441e3b3d48fc0ac61629376 + languageName: node + linkType: hard + "@jupyter/chat@^0.6.2, @jupyter/chat@workspace:packages/jupyter-chat": version: 0.0.0-use.local resolution: "@jupyter/chat@workspace:packages/jupyter-chat" @@ -2546,7 +2565,7 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/application@npm:^4.2.0": +"@jupyterlab/application@npm:^4.2.0, @jupyterlab/application@npm:~4.2.0": version: 4.2.3 resolution: "@jupyterlab/application@npm:4.2.3" dependencies: @@ -2788,6 +2807,20 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/coreutils@npm:~6.2.0": + version: 6.2.6 + resolution: "@jupyterlab/coreutils@npm:6.2.6" + dependencies: + "@lumino/coreutils": ^2.1.2 + "@lumino/disposable": ^2.1.2 + "@lumino/signaling": ^2.1.2 + minimist: ~1.2.0 + path-browserify: ^1.0.0 + url-parse: ~1.5.4 + checksum: edb955a1d96a96546dce96a04329fe3ee38be301805337b9fd6ae7f21227625aab76f77ce5adacb80675831aed9b869525af787cbbbd64acf415ed67256e693a + languageName: node + linkType: hard + "@jupyterlab/docmanager@npm:^4.2.3": version: 4.2.3 resolution: "@jupyterlab/docmanager@npm:4.2.3" @@ -2813,7 +2846,7 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/docregistry@npm:^4.2.0, @jupyterlab/docregistry@npm:^4.2.3": +"@jupyterlab/docregistry@npm:^4.2.0, @jupyterlab/docregistry@npm:^4.2.3, @jupyterlab/docregistry@npm:~4.2.0": version: 4.2.3 resolution: "@jupyterlab/docregistry@npm:4.2.3" dependencies: @@ -3045,6 +3078,16 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/rendermime-interfaces@npm:~3.10.0": + version: 3.10.6 + resolution: "@jupyterlab/rendermime-interfaces@npm:3.10.6" + dependencies: + "@lumino/coreutils": ^1.11.0 || ^2.1.2 + "@lumino/widgets": ^1.37.2 || ^2.3.2 + checksum: c3fed69c4df34ad203870522af9741662029b59aacfff07f19e4725750746d18195b4f41123fa1280c13aa8303e2a1a30697a50fc98bdba53bb4cbbbdda2d2b1 + languageName: node + linkType: hard + "@jupyterlab/rendermime@npm:^4.2.0, @jupyterlab/rendermime@npm:^4.2.3": version: 4.2.3 resolution: "@jupyterlab/rendermime@npm:4.2.3" @@ -3190,7 +3233,7 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/ui-components@npm:^4.2.0, @jupyterlab/ui-components@npm:^4.2.3": +"@jupyterlab/ui-components@npm:^4.2.0, @jupyterlab/ui-components@npm:^4.2.3, @jupyterlab/ui-components@npm:~4.2.0": version: 4.2.3 resolution: "@jupyterlab/ui-components@npm:4.2.3" dependencies: @@ -10020,6 +10063,7 @@ __metadata: version: 0.0.0-use.local resolution: "jupyterlab-chat-extension@workspace:python/jupyterlab-chat" dependencies: + "@jupyter-notebook/application": ^7.2.0 "@jupyter/docprovider": ^2.1.4 "@jupyter/ydoc": ^1.1.1 "@jupyterlab/application": ^4.2.0