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();
+};