diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 536b87c786875..96be450becff5 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -67,4 +67,4 @@ jobs: path: | examples/playwright/test-results/ examples/playwright/playwright-report/ - retention-days: 2 + retention-days: 7 diff --git a/examples/playwright/src/tests/theia-notebook-editor.test.ts b/examples/playwright/src/tests/theia-notebook-editor.test.ts index 29b77f594479d..3045d25421b87 100644 --- a/examples/playwright/src/tests/theia-notebook-editor.test.ts +++ b/examples/playwright/src/tests/theia-notebook-editor.test.ts @@ -14,7 +14,7 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { PlaywrightWorkerArgs, expect, test } from '@playwright/test'; +import { Locator, PlaywrightWorkerArgs, expect, test } from '@playwright/test'; import { TheiaApp } from '../theia-app'; import { TheiaAppLoader, TheiaPlaywrightTestConfig } from '../theia-app-loader'; import { TheiaNotebookCell } from '../theia-notebook-cell'; @@ -188,8 +188,121 @@ test.describe('Theia Notebook Cell interaction', () => { expect(await cell.executionCount()).toBe('3'); }); + test('Check arrow up and down works', async () => { + const cell = await firstCell(editor); + await editor.addCodeCell(); + const secondCell = (await editor.cells())[1]; + // second cell is selected after creation + expect(await secondCell.isSelected()).toBe(true); + // select cell above + await editor.page.keyboard.type('second cell'); + await secondCell.editor.page.keyboard.press('ArrowUp'); + expect(await cell.isSelected()).toBe(true); + + // select cell below + await cell.app.page.keyboard.press('ArrowDown'); + expect(await secondCell.isSelected()).toBe(true); + }); + + test('Check k(up)/j(down) selection works', async () => { + const cell = await firstCell(editor); + await editor.addCodeCell(); + const secondCell = (await editor.cells())[1]; + // second cell is selected after creation + expect(await secondCell.isSelected()).toBe(true); + await secondCell.selectCell(); // deselect editor focus + + // select cell above + await secondCell.editor.page.keyboard.press('k'); + expect(await cell.isSelected()).toBe(true); + + // select cell below + await cell.app.page.keyboard.press('j'); + expect(await secondCell.isSelected()).toBe(true); + }); + + test('Check x/c/v works', async () => { + const cell = await firstCell(editor); + await cell.addEditorText('print("First cell")'); + await editor.addCodeCell(); + const secondCell = (await editor.cells())[1]; + await secondCell.addEditorText('print("Second cell")'); + await secondCell.selectCell(); // deselect editor focus + + // cut second cell + await secondCell.page.keyboard.press('x'); + await editor.waitForCellCountChanged(2); + expect((await editor.cells()).length).toBe(1); + + // paste second cell + await cell.selectCell(); + await cell.page.keyboard.press('v'); + await editor.waitForCellCountChanged(1); + expect((await editor.cells()).length).toBe(2); + const pastedCell = (await editor.cells())[1]; + expect(await pastedCell.isSelected()).toBe(true); + + // copy first cell + await cell.selectCell(); // deselect editor focus + await cell.page.keyboard.press('c'); + // paste copied cell + await cell.page.keyboard.press('v'); + await editor.waitForCellCountChanged(2); + expect((await editor.cells()).length).toBe(3); + expect(await (await editor.cells())[0].editorText()).toBe('print("First cell")'); + expect(await (await editor.cells())[1].editorText()).toBe('print("First cell")'); + expect(await (await editor.cells())[2].editorText()).toBe('print("Second cell")'); + }); + + test('Check LineNumber switch `l` works', async () => { + const cell = await firstCell(editor); + await cell.addEditorText('print("First cell")'); + await cell.selectCell(); + await cell.page.keyboard.press('l'); + // NOTE: div.line-numbers is not visible + await cell.editor.locator.locator('.overflow-guard > div.line-numbers').waitFor({ state: 'attached' }); + }); + + test('Check Collapse output switch `o` works', async () => { + const cell = await firstCell(editor); + await cell.addEditorText('print("Check output collapse")'); + await cell.selectCell(); + await cell.execute(); // produce output + expect(await cell.outputText()).toBe('Check output collapse'); + + await cell.page.keyboard.press('o'); + await (await cell.outputContainer()).waitFor({ state: 'hidden' }); + await cell.page.keyboard.press('o'); + await (await cell.outputContainer()).waitFor({ state: 'visible' }); + + expect(await cell.outputText()).toBe('Check output collapse'); + }); + + test('Check arrow-up/arrow-down/escape with code completion', async () => { + await editor.addMarkdownCell(); + const mdCell = (await editor.cells())[1]; + await mdCell.addEditorText('h'); + + await editor.page.keyboard.press('Control+Space'); // call CC (suggestWidgetVisible=true) + await ensureCodeCompletionVisible(mdCell.editor.locator); + await editor.page.keyboard.press('Escape'); // close CC + // check the same cell still selected and not lose the edit mode + expect(await mdCell.editor.isFocused()).toBe(true); + + await editor.page.keyboard.press('Control+Space'); // call CC (suggestWidgetVisible=true) + await ensureCodeCompletionVisible(mdCell.editor.locator); + await editor.page.keyboard.press('ArrowUp'); // select next entry in CC list + await editor.page.keyboard.press('Enter'); // apply completion + // check the same cell still selected and not the second one due to 'ArrowDown' being pressed + expect(await mdCell.isSelected()).toBe(true); + + }); }); +async function ensureCodeCompletionVisible(parent: Locator): Promise { + await parent.locator('div.monaco-editor div.suggest-widget').waitFor({ timeout: 5000 }); +} + async function firstCell(editor: TheiaNotebookEditor): Promise { return (await editor.cells())[0]; } diff --git a/examples/playwright/src/theia-monaco-editor.ts b/examples/playwright/src/theia-monaco-editor.ts index 7e290df0509e5..7cfbf4b492b65 100644 --- a/examples/playwright/src/theia-monaco-editor.ts +++ b/examples/playwright/src/theia-monaco-editor.ts @@ -104,6 +104,19 @@ export class TheiaMonacoEditor extends TheiaPageObject { await this.page.keyboard.type(text); } + /** + * @returns `true` if the editor is focused, `false` otherwise. + */ + async isFocused(): Promise { + const viewElement = await this.viewElement(); + const monacoEditor = await viewElement?.$('div.monaco-editor'); + if (!monacoEditor) { + throw new Error('Couldn\'t retrieve monaco editor element.'); + } + const editorClass = await monacoEditor.getAttribute('class'); + return editorClass?.includes('focused') ?? false; + } + protected replaceEditorSymbolsWithSpace(content: string): string | Promise { // [ ]   => \u00a0 -- NO-BREAK SPACE // [ยท] · => \u00b7 -- MIDDLE DOT diff --git a/examples/playwright/src/theia-notebook-cell.ts b/examples/playwright/src/theia-notebook-cell.ts index 22859950f63fd..0478a680ac8c4 100644 --- a/examples/playwright/src/theia-notebook-cell.ts +++ b/examples/playwright/src/theia-notebook-cell.ts @@ -177,6 +177,14 @@ export class TheiaNotebookCell extends TheiaPageObject { return text?.substring(1, text.length - 1); } + /** + * @returns `true` if the cell is selected (blue vertical line), `false` otherwise. + */ + async isSelected(): Promise { + const markerClass = await this.locator.locator('div.theia-notebook-cell-marker').getAttribute('class'); + return markerClass?.includes('theia-notebook-cell-marker-selected') ?? false; + } + /** * @returns The output text of the cell. */ @@ -190,7 +198,14 @@ export class TheiaNotebookCell extends TheiaPageObject { return spanTexts.join(''); } - protected async outputContainer(): Promise { + /** + * Selects the cell itself not it's editor. Important for shortcut usage like copy-, cut-, paste-cell. + */ + async selectCell(): Promise { + await this.sidebar().click(); + } + + async outputContainer(): Promise { const outFrame = await this.outputFrame(); // each cell has it's own output div with a unique id = cellHandle const cellOutput = outFrame.locator(`div#cellHandle${await this.cellHandle()}`); diff --git a/examples/playwright/src/theia-notebook-editor.ts b/examples/playwright/src/theia-notebook-editor.ts index d07964edfe718..31ddbd7c3ef42 100644 --- a/examples/playwright/src/theia-notebook-editor.ts +++ b/examples/playwright/src/theia-notebook-editor.ts @@ -127,7 +127,7 @@ export class TheiaNotebookEditor extends TheiaEditor { await this.waitForCellCountChanged(currentCellsCount); } - protected async waitForCellCountChanged(prevCount: number): Promise { + async waitForCellCountChanged(prevCount: number): Promise { await this.viewLocator().locator('li.theia-notebook-cell').evaluateAll( (elements, currentCount) => elements.length !== currentCount, prevCount ); diff --git a/packages/notebook/src/browser/contributions/notebook-cell-actions-contribution.ts b/packages/notebook/src/browser/contributions/notebook-cell-actions-contribution.ts index 87d656cb7cb44..38c50a753c21a 100644 --- a/packages/notebook/src/browser/contributions/notebook-cell-actions-contribution.ts +++ b/packages/notebook/src/browser/contributions/notebook-cell-actions-contribution.ts @@ -516,7 +516,7 @@ export class NotebookCellActionContribution implements MenuContribution, Command { command: NotebookCellCommands.STOP_EDIT_COMMAND.id, keybinding: 'esc', - when: `editorTextFocus && ${NOTEBOOK_EDITOR_FOCUSED}`, + when: `editorTextFocus && ${NOTEBOOK_EDITOR_FOCUSED} && !suggestWidgetVisible`, }, { command: NotebookCellCommands.EXECUTE_SINGLE_CELL_COMMAND.id,