From 7d072b16d9f4aa2eb9b928c969160988211092f2 Mon Sep 17 00:00:00 2001 From: Nicolas Brichet <nicolas.brichet@quantstack.net> Date: Thu, 19 Sep 2024 11:59:04 +0200 Subject: [PATCH] Add tests --- .../ui-tests/playwright.config.js | 5 + .../ui-tests/tests/code-toolbar.spec.ts | 84 +++++++++----- .../jupyterlab_collaborative_chat.spec.ts | 107 +++++++++++++++++- .../ui-tests/tests/test-utils.ts | 17 +++ 4 files changed, 185 insertions(+), 28 deletions(-) diff --git a/python/jupyterlab-collaborative-chat/ui-tests/playwright.config.js b/python/jupyterlab-collaborative-chat/ui-tests/playwright.config.js index c657f8a..d22e0df 100644 --- a/python/jupyterlab-collaborative-chat/ui-tests/playwright.config.js +++ b/python/jupyterlab-collaborative-chat/ui-tests/playwright.config.js @@ -15,5 +15,10 @@ module.exports = { url: 'http://localhost:8888/lab', timeout: 120 * 1000, reuseExistingServer: !process.env.CI + }, + use: { + contextOptions: { + permissions: ['clipboard-read', 'clipboard-write'] + } } }; diff --git a/python/jupyterlab-collaborative-chat/ui-tests/tests/code-toolbar.spec.ts b/python/jupyterlab-collaborative-chat/ui-tests/tests/code-toolbar.spec.ts index 182acaa..bd11cc9 100644 --- a/python/jupyterlab-collaborative-chat/ui-tests/tests/code-toolbar.spec.ts +++ b/python/jupyterlab-collaborative-chat/ui-tests/tests/code-toolbar.spec.ts @@ -3,14 +3,9 @@ * Distributed under the terms of the Modified BSD License. */ -import { - expect, - galata, - IJupyterLabPageFixture, - test -} from '@jupyterlab/galata'; +import { expect, galata, test } from '@jupyterlab/galata'; -import { openChat, sendMessage, USER } from './test-utils'; +import { openChat, sendMessage, splitMainArea, USER } from './test-utils'; test.use({ mockUser: USER, @@ -26,20 +21,6 @@ const FILENAME = 'toolbar.chat'; const CONTENT = 'print("This is a code cell")'; const MESSAGE = `\`\`\`\n${CONTENT}\n\`\`\``; -async function splitMainArea(page: IJupyterLabPageFixture, name: string) { - // Emulate drag and drop - const viewerHandle = page.activity.getTabLocator(name); - const viewerBBox = await viewerHandle.boundingBox(); - - await page.mouse.move( - viewerBBox!.x + 0.5 * viewerBBox!.width, - viewerBBox!.y + 0.5 * viewerBBox!.height - ); - await page.mouse.down(); - await page.mouse.move(viewerBBox!.x + 0.5 * viewerBBox!.width, 600); - await page.mouse.up(); -} - test.describe('#codeToolbar', () => { test.beforeEach(async ({ page }) => { // Create a chat file @@ -161,19 +142,22 @@ test.describe('#codeToolbar', () => { expect(await page.notebook.getCellTextInput(1)).toBe(`${CONTENT}\n`); }); - test('replace active cell', async ({ page }) => { + test('replace active cell content', async ({ page }) => { const chatPanel = await openChat(page, FILENAME); const message = chatPanel.locator('.jp-chat-message'); - const toolbarButtons = message.locator('.jp-chat-code-toolbar-item button'); + const toolbarButtons = message.locator('.jp-chat-code-toolbar-item'); const notebook = await page.notebook.createNew(); + // write content in the first cell. + const cell = await page.notebook.getCellLocator(0); + await cell?.getByRole('textbox').pressSequentially('initial content'); await sendMessage(page, FILENAME, MESSAGE); await splitMainArea(page, notebook!); - // write content in the first cell. - const cell = await page.notebook.getCellLocator(0); - await cell?.getByRole('textbox').pressSequentially('initial content'); + await expect(toolbarButtons.nth(2)).toHaveAccessibleName( + 'Replace selection (active cell)' + ); await toolbarButtons.nth(2).click(); await page.activity.activateTab(notebook!); @@ -184,10 +168,52 @@ test.describe('#codeToolbar', () => { expect(await page.notebook.getCellTextInput(0)).toBe(`${CONTENT}\n`); }); + test('replace current selection', async ({ page }) => { + const cellContent = 'a = 1\nprint(f"a={a}")'; + const chatPanel = await openChat(page, FILENAME); + const message = chatPanel.locator('.jp-chat-message'); + const toolbarButtons = message.locator('.jp-chat-code-toolbar-item'); + + const notebook = await page.notebook.createNew(); + // write content in the first cell. + const cell = (await page.notebook.getCellLocator(0))!; + await cell.getByRole('textbox').pressSequentially(cellContent); + + // wait for code mirror to be ready. + await expect(cell.locator('.cm-line')).toHaveCount(2); + await expect( + cell.locator('.cm-line').nth(1).locator('.cm-builtin') + ).toBeAttached(); + + // select the 'print' statement in the second line. + const selection = cell + ?.locator('.cm-line') + .nth(1) + .locator('.cm-builtin') + .first(); + await selection.dblclick({ position: { x: 10, y: 10 } }); + + await sendMessage(page, FILENAME, MESSAGE); + await splitMainArea(page, notebook!); + + await expect(toolbarButtons.nth(2)).toHaveAccessibleName( + 'Replace selection (1 line(s))' + ); + await toolbarButtons.nth(2).click(); + + await page.activity.activateTab(notebook!); + await page.waitForCondition( + async () => (await page.notebook.getCellTextInput(0)) !== cellContent + ); + expect(await page.notebook.getCellTextInput(0)).toBe( + `a = 1\n${CONTENT}\n(f"a={a}")` + ); + }); + test('should copy code content', async ({ page }) => { const chatPanel = await openChat(page, FILENAME); const message = chatPanel.locator('.jp-chat-message'); - const toolbarButtons = message.locator('.jp-chat-code-toolbar-item button'); + const toolbarButtons = message.locator('.jp-chat-code-toolbar-item'); const notebook = await page.notebook.createNew(); @@ -196,6 +222,10 @@ test.describe('#codeToolbar', () => { // Copy the message code content to clipboard. await toolbarButtons.last().click(); + expect(await page.evaluate(() => navigator.clipboard.readText())).toBe( + `${CONTENT}\n` + ); + await page.activity.activateTab(notebook!); const cell = await page.notebook.getCellLocator(0); await cell?.getByRole('textbox').press('Control+V'); diff --git a/python/jupyterlab-collaborative-chat/ui-tests/tests/jupyterlab_collaborative_chat.spec.ts b/python/jupyterlab-collaborative-chat/ui-tests/tests/jupyterlab_collaborative_chat.spec.ts index 079211a..ff71929 100644 --- a/python/jupyterlab-collaborative-chat/ui-tests/tests/jupyterlab_collaborative_chat.spec.ts +++ b/python/jupyterlab-collaborative-chat/ui-tests/tests/jupyterlab_collaborative_chat.spec.ts @@ -13,7 +13,7 @@ import { Contents, User } from '@jupyterlab/services'; import { ReadonlyJSONObject, UUID } from '@lumino/coreutils'; import { Locator } from '@playwright/test'; -import { openChat, sendMessage, USER } from './test-utils'; +import { openChat, sendMessage, splitMainArea, USER } from './test-utils'; const FILENAME = 'my-chat.chat'; const MSG_CONTENT = 'Hello World!'; @@ -376,6 +376,111 @@ test.describe('#sendMessages', () => { messages.locator('.jp-chat-message .jp-chat-rendermime-markdown') ).toHaveText(MSG_CONTENT + '\n'); }); + + test('should disable send with selection when there is no notebook', async ({ + page + }) => { + const chatPanel = await openChat(page, FILENAME); + const input = chatPanel + .locator('.jp-chat-input-container') + .getByRole('combobox'); + const openerButton = chatPanel.locator( + '.jp-chat-input-container .jp-chat-send-include-opener' + ); + const sendWithSelection = page.locator('.jp-chat-send-include'); + + await input.pressSequentially(MSG_CONTENT); + await openerButton.click(); + await expect(sendWithSelection).toBeVisible(); + await expect(sendWithSelection).toBeDisabled(); + await expect(sendWithSelection).toContainText( + 'No selection or active cell' + ); + }); + + test('should send with cell content', async ({ page }) => { + const cellContent = 'a = 1\nprint(f"a={a}")'; + const chatPanel = await openChat(page, FILENAME); + const messages = chatPanel.locator('.jp-chat-messages-container'); + const input = chatPanel + .locator('.jp-chat-input-container') + .getByRole('combobox'); + const openerButton = chatPanel.locator( + '.jp-chat-input-container .jp-chat-send-include-opener' + ); + const sendWithSelection = page.locator('.jp-chat-send-include'); + + const notebook = await page.notebook.createNew(); + // write content in the first cell. + const cell = (await page.notebook.getCellLocator(0))!; + await cell.getByRole('textbox').pressSequentially(cellContent); + + await splitMainArea(page, notebook!); + + await input.pressSequentially(MSG_CONTENT); + await openerButton.click(); + await expect(sendWithSelection).toBeVisible(); + await expect(sendWithSelection).toBeEnabled(); + await expect(sendWithSelection).toContainText('Code from 1 active cell'); + await sendWithSelection.click(); + + await expect(messages!.locator('.jp-chat-message')).toHaveCount(1); + + // It seems that the markdown renderer adds a new line, but the '\n' inserter when + // pressing Enter above is trimmed. + await expect( + messages.locator('.jp-chat-message .jp-chat-rendermime-markdown') + ).toHaveText(`${MSG_CONTENT}\n${cellContent}\n`); + }); + + test('should send with text selection', async ({ page }) => { + const cellContent = 'a = 1\nprint(f"a={a}")'; + const chatPanel = await openChat(page, FILENAME); + const messages = chatPanel.locator('.jp-chat-messages-container'); + const input = chatPanel + .locator('.jp-chat-input-container') + .getByRole('combobox'); + const openerButton = chatPanel.locator( + '.jp-chat-input-container .jp-chat-send-include-opener' + ); + const sendWithSelection = page.locator('.jp-chat-send-include'); + + const notebook = await page.notebook.createNew(); + await splitMainArea(page, notebook!); + + // write content in the first cell. + const cell = (await page.notebook.getCellLocator(0))!; + await cell.getByRole('textbox').pressSequentially(cellContent); + + // wait for code mirror to be ready. + await expect(cell.locator('.cm-line')).toHaveCount(2); + await expect( + cell.locator('.cm-line').nth(1).locator('.cm-builtin') + ).toBeAttached(); + + // select the 'print' statement in the second line. + const selection = cell + ?.locator('.cm-line') + .nth(1) + .locator('.cm-builtin') + .first(); + await selection.dblclick({ position: { x: 10, y: 10 } }); + + await input.pressSequentially(MSG_CONTENT); + await openerButton.click(); + await expect(sendWithSelection).toBeVisible(); + await expect(sendWithSelection).toBeEnabled(); + await expect(sendWithSelection).toContainText('1 line(s) selected'); + await sendWithSelection.click(); + + await expect(messages!.locator('.jp-chat-message')).toHaveCount(1); + + // It seems that the markdown renderer adds a new line, but the '\n' inserter when + // pressing Enter above is trimmed. + await expect( + messages.locator('.jp-chat-message .jp-chat-rendermime-markdown') + ).toHaveText(`${MSG_CONTENT}\nprint\n`); + }); }); test.describe('#messagesNavigation', () => { diff --git a/python/jupyterlab-collaborative-chat/ui-tests/tests/test-utils.ts b/python/jupyterlab-collaborative-chat/ui-tests/tests/test-utils.ts index a971833..c6f326e 100644 --- a/python/jupyterlab-collaborative-chat/ui-tests/tests/test-utils.ts +++ b/python/jupyterlab-collaborative-chat/ui-tests/tests/test-utils.ts @@ -54,3 +54,20 @@ export const sendMessage = async ( await input.pressSequentially(content); await sendButton.click(); }; + +export const splitMainArea = async ( + page: IJupyterLabPageFixture, + name: string +) => { + // Emulate drag and drop + const viewerHandle = page.activity.getTabLocator(name); + const viewerBBox = await viewerHandle.boundingBox(); + + await page.mouse.move( + viewerBBox!.x + 0.5 * viewerBBox!.width, + viewerBBox!.y + 0.5 * viewerBBox!.height + ); + await page.mouse.down(); + await page.mouse.move(viewerBBox!.x + 0.5 * viewerBBox!.width, 600); + await page.mouse.up(); +};