From 5a9335ad7506b0d9438c3d64643cee57c4d0e275 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Mon, 6 Oct 2025 16:01:33 -0500 Subject: [PATCH 01/22] update clipboard test --- test/e2e/pages/notebooksPositron.ts | 198 +++++++------- .../positron-notebook-copy-paste.test.ts | 247 ++++++++---------- 2 files changed, 217 insertions(+), 228 deletions(-) diff --git a/test/e2e/pages/notebooksPositron.ts b/test/e2e/pages/notebooksPositron.ts index 897133162dbd..4c2703ff4fd9 100644 --- a/test/e2e/pages/notebooksPositron.ts +++ b/test/e2e/pages/notebooksPositron.ts @@ -9,6 +9,7 @@ import { QuickInput } from './quickInput'; import { QuickAccess } from './quickaccess'; import test, { expect, Locator } from '@playwright/test'; import { HotKeys } from './hotKeys.js'; +import { time } from 'console'; const DEFAULT_TIMEOUT = 10000; @@ -16,25 +17,34 @@ const DEFAULT_TIMEOUT = 10000; * Notebooks functionality exclusive to Positron notebooks. */ export class PositronNotebooks extends Notebooks { - positronNotebook: Locator; + positronNotebook = this.code.driver.page.locator('.positron-notebook').first(); + cell = this.code.driver.page.locator('[data-testid="notebook-cell"]'); + newCellButton = this.code.driver.page.getByLabel(/new code cell/i); + editorAtIndex = (index: number) => this.cell.nth(index).locator('.positron-cell-editor-monaco-widget textarea'); + runCellAtIndex = (index: number) => this.cell.nth(index).getByLabel(/execute cell/i); + spinnerAtIndex = (index: number) => this.cell.nth(index).getByLabel(/cell is executing/i); + cellExecutionInfoAtIndex = (index: number) => this.cell.nth(index).getByLabel(/cell execution info/i); + detectingKernelsText = this.code.driver.page.getByText(/detecting kernels/i); + cellStatusSyncIcon = this.code.driver.page.locator('.cell-status-item-has-runnable .codicon-sync'); + kernelStatusBadge = this.code.driver.page.getByTestId('notebook-kernel-status'); + deleteCellButton = this.cell.getByRole('button', { name: /delete the selected cell/i }); + // Selector constants for Positron notebook elements - private static readonly RUN_CELL_LABEL = /execute cell/i; - private static readonly NOTEBOOK_CELL_SELECTOR = '[data-testid="notebook-cell"]'; - private static readonly NEW_CODE_CELL_LABEL = /new code cell/i; - private static readonly MONACO_EDITOR_SELECTOR = '.positron-cell-editor-monaco-widget textarea'; - private static readonly CELL_EXECUTING_LABEL = /cell is executing/i; - private static readonly CELL_EXECUTION_INFO_LABEL = /cell execution info/i; - private static readonly NOTEBOOK_KERNEL_STATUS_TESTID = 'notebook-kernel-status'; - private static readonly DELETE_CELL_LABEL = /delete the selected cell/i; - private static readonly POSITRON_NOTEBOOK_SELECTOR = '.positron-notebook'; - private static readonly CELL_STATUS_SYNC_SELECTOR = '.cell-status-item-has-runnable .codicon-sync'; - private static readonly DETECTING_KERNELS_TEXT = /detecting kernels/i; + // private static readonly RUN_CELL_LABEL = /execute cell/i; + // private static readonly NOTEBOOK_CELL_SELECTOR = '[data-testid="notebook-cell"]'; + // private static readonly NEW_CODE_CELL_LABEL = /new code cell/i; + // private static readonly MONACO_EDITOR_SELECTOR = '.positron-cell-editor-monaco-widget textarea'; + // private static readonly CELL_EXECUTING_LABEL = /cell is executing/i; + // private static readonly CELL_EXECUTION_INFO_LABEL = /cell execution info/i; + // private static readonly NOTEBOOK_KERNEL_STATUS_TESTID = 'notebook-kernel-status'; + // private static readonly DELETE_CELL_LABEL = /delete the selected cell/i; + // private static readonly POSITRON_NOTEBOOK_SELECTOR = '.positron-notebook'; + // private static readonly CELL_STATUS_SYNC_SELECTOR = '.cell-status-item-has-runnable .codicon-sync'; + // private static readonly DETECTING_KERNELS_TEXT = /detecting kernels/i; constructor(code: Code, quickinput: QuickInput, quickaccess: QuickAccess, hotKeys: HotKeys) { super(code, quickinput, quickaccess, hotKeys); - - this.positronNotebook = this.code.driver.page.locator(PositronNotebooks.POSITRON_NOTEBOOK_SELECTOR).first(); } // -- Actions -- @@ -51,51 +61,47 @@ export class PositronNotebooks extends Notebooks { } /** - * Override selectCellAtIndex to use Positron-specific selectors + * Action: Select a cell at the specified index. */ async selectCellAtIndex(cellIndex: number): Promise { await test.step(`Select cell at index: ${cellIndex}`, async () => { - // Use semantic selector - await this.code.driver.page.locator(PositronNotebooks.NOTEBOOK_CELL_SELECTOR).nth(cellIndex).click(); + await this.cell.nth(cellIndex).click(); }); } /** - * Get the current number of cells in the notebook + * Action: Create a new code cell at the END of the notebook. */ - private async getCellCount(): Promise { - return await this.code.driver.page.locator(PositronNotebooks.NOTEBOOK_CELL_SELECTOR).count(); - } + private async createNewCodeCell(): Promise { + await test.step(`Create new code cell`, async () => { + const newCellButtonCount = await this.newCellButton.count(); - /** - * Create a new code cell at the specified index - */ - private async createNewCodeCell(index: number): Promise { - await test.step(`Create new code cell at index ${index}`, async () => { - // Find all "New Code Cell" buttons - they appear between cells - const addCellButtons = this.code.driver.page.getByLabel(PositronNotebooks.NEW_CODE_CELL_LABEL); - const buttonCount = await addCellButtons.count(); - - if (buttonCount === 0) { + if (newCellButtonCount === 0) { throw new Error('No "New Code Cell" buttons found'); } // Click the last button (which adds a cell at the end) // Note: This assumes we're always adding at the end, which matches the validation in addCodeToCellAtIndex - await addCellButtons.last().click(); + await this.newCellButton.last().click(); // Wait for the new cell to appear - await expect(this.code.driver.page.locator(PositronNotebooks.NOTEBOOK_CELL_SELECTOR).nth(index)).toBeVisible({ timeout: DEFAULT_TIMEOUT }); + await expect(this.cell).toHaveCount(newCellButtonCount + 1, { timeout: DEFAULT_TIMEOUT }); }); } /** - * Override addCodeToCellAtIndex to use Positron-specific selectors and Monaco editor + * Action: Add code to a cell at the specified index. + * If the cell does not exist, it creates it at the end of the notebook. + * Throws an error if trying to create a cell beyond the next sequential index. + * + * @param code - The code to add to the cell. + * @param cellIndex - The index of the cell to add code to (default: 0). + * @param delay - Optional delay between keystrokes for typing simulation (default: 0, meaning no delay). */ async addCodeToCellAtIndex(code: string, cellIndex = 0, delay = 0): Promise { await test.step('Add code to Positron cell', async () => { // Check if the cell exists - const currentCellCount = await this.getCellCount(); + const currentCellCount = await this.cell.count(); if (cellIndex >= currentCellCount) { // Cell doesn't exist, need to create it @@ -105,22 +111,18 @@ export class PositronNotebooks extends Notebooks { } // Create the new cell - await this.createNewCodeCell(cellIndex); + await this.createNewCodeCell(); } // Now select and fill the cell (existing logic) await this.selectCellAtIndex(cellIndex); - // Get the specific cell's editor - const cell = this.code.driver.page.locator(PositronNotebooks.NOTEBOOK_CELL_SELECTOR).nth(cellIndex); - const editor = cell.locator(PositronNotebooks.MONACO_EDITOR_SELECTOR); - // Ensure editor is focused and type/fill the code - await editor.focus(); + await this.editorAtIndex(cellIndex).focus(); if (delay) { - await editor.pressSequentially(code, { delay }); + await this.editorAtIndex(cellIndex).pressSequentially(code, { delay }); } else { - await editor.fill(code); + await this.editorAtIndex(cellIndex).fill(code); } }); } @@ -130,17 +132,12 @@ export class PositronNotebooks extends Notebooks { */ async executeCodeInCell(cellIndex = 0): Promise { await test.step('Execute code in Positron notebook cell', async () => { - // Select the cell first - await this.selectCellAtIndex(cellIndex); - // Find and click the run button for this specific cell - const cell = this.code.driver.page.locator(PositronNotebooks.NOTEBOOK_CELL_SELECTOR).nth(cellIndex); - const runButton = cell.getByLabel(PositronNotebooks.RUN_CELL_LABEL); - - await runButton.click(); + await this.selectCellAtIndex(cellIndex); + await this.runCellAtIndex(cellIndex).click(); // Wait for execution to complete by checking the execution spinner is gone - const spinner = cell.getByLabel(PositronNotebooks.CELL_EXECUTING_LABEL); + const spinner = this.spinnerAtIndex(cellIndex); // Wait for spinner to appear (cell is executing) await expect(spinner).toBeVisible({ timeout: DEFAULT_TIMEOUT }).catch(() => { @@ -157,14 +154,8 @@ export class PositronNotebooks extends Notebooks { */ async startExecutingCodeInCell(cellIndex = 0): Promise { await test.step('Start executing code in Positron notebook cell', async () => { - // Select the cell first await this.selectCellAtIndex(cellIndex); - - // Find and click the run button for this specific cell - const cell = this.code.driver.page.locator(PositronNotebooks.NOTEBOOK_CELL_SELECTOR).nth(cellIndex); - const runButton = cell.getByLabel(PositronNotebooks.RUN_CELL_LABEL); - - await runButton.click(); + await this.runCellAtIndex(cellIndex).click(); }); } @@ -176,7 +167,7 @@ export class PositronNotebooks extends Notebooks { async addCodeToCellAndRun(code: string, cellIndex = 0, delay = 0): Promise { return await test.step(`Add code and run cell ${cellIndex}`, async () => { // Check if the cell exists - const currentCellCount = await this.getCellCount(); + const currentCellCount = await this.cell.count(); if (cellIndex >= currentCellCount) { // Cell doesn't exist, need to create it @@ -185,18 +176,13 @@ export class PositronNotebooks extends Notebooks { throw new Error(`Cannot create cell at index ${cellIndex}. Current cell count is ${currentCellCount}. Can only add cells sequentially.`); } - // Create the new cell - await this.createNewCodeCell(cellIndex); + await this.createNewCodeCell(); } - // Get the cell once and reuse the reference - const cell = this.code.driver.page.locator(PositronNotebooks.NOTEBOOK_CELL_SELECTOR).nth(cellIndex); - - // Select the cell - await cell.click(); + await this.cell.nth(cellIndex).click() // Find and fill the Monaco editor - const editor = cell.locator(PositronNotebooks.MONACO_EDITOR_SELECTOR); + const editor = this.editorAtIndex(cellIndex); await editor.focus(); if (delay) { @@ -206,8 +192,7 @@ export class PositronNotebooks extends Notebooks { } // Find and click the run button - const runButton = cell.getByLabel(PositronNotebooks.RUN_CELL_LABEL); - await runButton.click(); + await this.runCellAtIndex(cellIndex).click(); // // Wait for execution to complete // const spinner = cell.getByLabel('Cell is executing'); @@ -219,16 +204,53 @@ export class PositronNotebooks extends Notebooks { // // Wait for spinner to disappear (execution complete) // await expect(spinner).toHaveCount(0, { timeout: DEFAULT_TIMEOUT }); - return cell; + return this.cell.nth(cellIndex); }); } + /** + * Helper function to copy cells using keyboard shortcut + */ + async copyCellsWithKeyboard(): Promise { + // We need to press escape to get the focus out of the cell editor itself + await this.code.driver.page.keyboard.press('Escape'); + await this.hotKeys.copy(); + } + + async cutCellsWithKeyboard(): Promise { + // We need to press escape to get the focus out of the cell editor itself + await this.code.driver.page.keyboard.press('Escape'); + await this.hotKeys.cut(); + } + + async pasteCellsWithKeyboard(): Promise { + // We need to press escape to get the focus out of the cell editor itself + await this.code.driver.page.keyboard.press('Escape'); + await this.hotKeys.paste(); + } + + async expectCellCountToBe(expectedCount: number): Promise { + await test.step(`Expect cell count to be ${expectedCount}`, async () => { + await expect(this.cell).toHaveCount(expectedCount, { timeout: DEFAULT_TIMEOUT }); + }); + } + + async expectCellContentAtIndexToBe(cellIndex: number, expectedContent: string): Promise { + await test.step(`Expect cell ${cellIndex} content to be: ${expectedContent}`, async () => { + const actualContent = await this.getCellContent(cellIndex); + await expect(async () => { + expect(actualContent).toBe(expectedContent); + }).toPass({ timeout: DEFAULT_TIMEOUT }); + }); + } + + /** * Get execution info icon for a specific cell */ getExecutionInfoIcon(cellIndex = 0): Locator { - const cell = this.code.driver.page.locator(PositronNotebooks.NOTEBOOK_CELL_SELECTOR).nth(cellIndex); - return cell.getByLabel(PositronNotebooks.CELL_EXECUTION_INFO_LABEL); + return this.cellExecutionInfoAtIndex(cellIndex); + // return this.cell.nth(cellIndex).getByLabel(/cell execution info/i); } @@ -243,7 +265,7 @@ export class PositronNotebooks extends Notebooks { /** * Wait for execution info icon to be visible after cell execution */ - async waitForExecutionInfoIcon(cellIndex = 0, timeout = DEFAULT_TIMEOUT): Promise { + async expectExecutionInfoIconToBeVisible(cellIndex = 0, timeout = DEFAULT_TIMEOUT): Promise { await test.step(`Wait for execution info icon in cell ${cellIndex}`, async () => { const icon = this.getExecutionInfoIcon(cellIndex); await expect(icon).toBeVisible({ timeout }); @@ -266,16 +288,15 @@ export class PositronNotebooks extends Notebooks { await this.expectToBeVisible(); // Wait for kernel detection to complete - await expect(this.code.driver.page.locator(PositronNotebooks.CELL_STATUS_SYNC_SELECTOR)).not.toBeVisible({ timeout: 30000 }); - await expect(this.code.driver.page.getByText(PositronNotebooks.DETECTING_KERNELS_TEXT)).not.toBeVisible({ timeout: 30000 }); + await expect(this.cellStatusSyncIcon).not.toBeVisible({ timeout: 30000 }); + await expect(this.detectingKernelsText).not.toBeVisible({ timeout: 30000 }); // Get the kernel status badge using data-testid - const kernelStatusBadge = this.code.driver.page.getByTestId(PositronNotebooks.NOTEBOOK_KERNEL_STATUS_TESTID); - await expect(kernelStatusBadge).toBeVisible({ timeout: 5000 }); + await expect(this.kernelStatusBadge).toBeVisible({ timeout: 5000 }); try { // Check if the desired kernel is already selected - const currentKernelText = await kernelStatusBadge.textContent(); + const currentKernelText = await this.kernelStatusBadge.textContent(); if (currentKernelText && currentKernelText.includes(desiredKernel) && currentKernelText.includes('Connected')) { this.code.logger.log(`Kernel already selected and connected: ${desiredKernel}`); return; @@ -288,7 +309,7 @@ export class PositronNotebooks extends Notebooks { try { // Click on kernel status badge to open selection this.code.logger.log(`Clicking kernel status badge to select: ${desiredKernel}`); - await kernelStatusBadge.click(); + await this.kernelStatusBadge.click(); // Wait for kernel selection UI to appear await this.quickinput.waitForQuickInputOpened(); @@ -304,7 +325,7 @@ export class PositronNotebooks extends Notebooks { } // Wait for the kernel status to show "Connected" - await expect(kernelStatusBadge).toContainText('Connected', { timeout: 30000 }); + await expect(this.kernelStatusBadge).toContainText('Connected', { timeout: 30000 }); this.code.logger.log('Kernel is connected and ready'); }); } @@ -362,25 +383,16 @@ export class PositronNotebooks extends Notebooks { async deleteCellWithActionBar(cellIndex = 0): Promise { await test.step(`Delete cell ${cellIndex} using action bar`, async () => { // Get the current cell count before deletion - const initialCount = await this.code.driver.page.locator(PositronNotebooks.NOTEBOOK_CELL_SELECTOR).count(); - - // Get the specific cell - const cell = this.code.driver.page.locator(PositronNotebooks.NOTEBOOK_CELL_SELECTOR).nth(cellIndex); + const initialCount = await this.cell.count(); // Click on the cell to make the action bar visible - await cell.click(); - - // Find and click the delete button in the action bar - const deleteButton = cell.getByRole('button', { name: PositronNotebooks.DELETE_CELL_LABEL }); - - // Wait for the delete button to be visible - await expect(deleteButton).toBeVisible({ timeout: DEFAULT_TIMEOUT }); + await this.cell.nth(cellIndex).click(); // Click the delete button - await deleteButton.click(); + await this.deleteCellButton.click(); // Wait for the deletion to complete by checking cell count decreased - await expect(this.code.driver.page.locator(PositronNotebooks.NOTEBOOK_CELL_SELECTOR)).toHaveCount(initialCount - 1, { timeout: DEFAULT_TIMEOUT }); + await expect(this.cell).toHaveCount(initialCount - 1, { timeout: DEFAULT_TIMEOUT }); // Give a small delay for focus to settle await this.code.driver.page.waitForTimeout(100); diff --git a/test/e2e/tests/notebook/positron-notebook-copy-paste.test.ts b/test/e2e/tests/notebook/positron-notebook-copy-paste.test.ts index 635bb770b08d..2ce597d6a680 100644 --- a/test/e2e/tests/notebook/positron-notebook-copy-paste.test.ts +++ b/test/e2e/tests/notebook/positron-notebook-copy-paste.test.ts @@ -3,7 +3,6 @@ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ -import { Application } from '../../infra/index.js'; import { test, tags } from '../_test.setup'; import { expect } from '@playwright/test'; @@ -11,191 +10,169 @@ test.use({ suiteId: __filename }); -/** - * Helper function to get cell count - */ -async function getCellCount(app: Application): Promise { - return await app.code.driver.page.locator('[data-testid="notebook-cell"]').count(); -} - -/** - * Helper function to copy cells using keyboard shortcut - */ -async function copyCellsWithKeyboard(app: Application): Promise { - // We need to press escape to get the focus out of the cell editor itself - await app.code.driver.page.keyboard.press('Escape'); - const modifierKey = process.platform === 'darwin' ? 'Meta' : 'Control'; - await app.code.driver.page.keyboard.press(`${modifierKey}+KeyC`); -} - -/** - * Helper function to cut cells using keyboard shortcut - */ -async function cutCellsWithKeyboard(app: Application): Promise { - // We need to press escape to get the focus out of the cell editor itself - await app.code.driver.page.keyboard.press('Escape'); - const modifierKey = process.platform === 'darwin' ? 'Meta' : 'Control'; - await app.code.driver.page.keyboard.press(`${modifierKey}+KeyX`); -} - -/** - * Helper function to paste cells using keyboard shortcut - */ -async function pasteCellsWithKeyboard(app: Application): Promise { - // We need to press escape to get the focus out of the cell editor itself - await app.code.driver.page.keyboard.press('Escape'); - const modifierKey = process.platform === 'darwin' ? 'Meta' : 'Control'; - await app.code.driver.page.keyboard.press(`${modifierKey}+KeyV`); -} - // Not running on web due to https://github.com/posit-dev/positron/issues/9193 test.describe('Notebook Cell Copy-Paste Behavior', { tag: [tags.CRITICAL, tags.WIN, tags.NOTEBOOKS] }, () => { - test.beforeAll(async function ({ app, settings }) { - await app.workbench.notebooksPositron.enablePositronNotebooks(settings); - // Configure Positron as the notebook editor - await app.workbench.notebooksPositron.setNotebookEditor(settings, 'positron'); + + test.beforeAll(async function ({ settings }) { + await settings.set({ + 'positron.notebook.enabled': true, + 'workbench.editorAssociations': { '*.ipynb': 'workbench.editor.positronNotebook' } + }, { reload: true }) }); + test.afterEach(async function ({ hotKeys }) { + await hotKeys.closeAllEditors(); + }) + test('Cell copy-paste behavior - comprehensive test', async function ({ app }) { - // Setup: Create notebook and select kernel once - await app.workbench.notebooks.createNewNotebook(); - await app.workbench.notebooksPositron.expectToBeVisible(); + const { notebooksPositron } = app.workbench; // ======================================== - // Setup: Create 5 cells with distinct content + // Setup: Create notebook with 5 cells and distinct content // ======================================== - await app.workbench.notebooksPositron.addCodeToCellAtIndex('# Cell 0', 0); - await app.workbench.notebooksPositron.addCodeToCellAtIndex('# Cell 1', 1); - await app.workbench.notebooksPositron.addCodeToCellAtIndex('# Cell 2', 2); - await app.workbench.notebooksPositron.addCodeToCellAtIndex('# Cell 3', 3); - await app.workbench.notebooksPositron.addCodeToCellAtIndex('# Cell 4', 4); - - // Verify we have 5 cells - expect(await getCellCount(app)).toBe(5); + await test.step(' Test Setup: Create notebook and add cells', async () => { + // Setup: Create notebook and select kernel once + await notebooksPositron.createNewNotebook(); + await notebooksPositron.expectToBeVisible(); + + // Setup: Create 5 cells with distinct content + await notebooksPositron.addCodeToCellAtIndex('# Cell 0', 0); + await notebooksPositron.addCodeToCellAtIndex('# Cell 1', 1); + await notebooksPositron.addCodeToCellAtIndex('# Cell 2', 2); + await notebooksPositron.addCodeToCellAtIndex('# Cell 3', 3); + await notebooksPositron.addCodeToCellAtIndex('# Cell 4', 4); + + // Verify we have 5 cells + await notebooksPositron.expectCellCountToBe(5); + }); // ======================================== // Test 1: Copy single cell and paste at end // ======================================== - await app.workbench.notebooksPositron.selectCellAtIndex(2); + await test.step('Test 1: Copy single cell and paste at end', async () => { + await notebooksPositron.selectCellAtIndex(2); - // Verify cell 2 has correct content - expect(await app.workbench.notebooksPositron.getCellContent(2)).toBe('# Cell 2'); + // Verify cell 2 has correct content + await notebooksPositron.expectCellContentAtIndexToBe(2, '# Cell 2'); - // Copy the cell - await copyCellsWithKeyboard(app); + // Copy the cell + await notebooksPositron.copyCellsWithKeyboard(); - // Move to last cell and paste after it - await app.workbench.notebooksPositron.selectCellAtIndex(4); - await pasteCellsWithKeyboard(app); + // Move to last cell and paste after it + await notebooksPositron.selectCellAtIndex(4); + await notebooksPositron.pasteCellsWithKeyboard(); - // Verify cell count increased - expect(await getCellCount(app)).toBe(6); + // Verify cell count increased + await notebooksPositron.expectCellCountToBe(6); - // Verify the pasted cell has the correct content (should be at index 5) - expect(await app.workbench.notebooksPositron.getCellContent(5)).toBe('# Cell 2'); + // Verify the pasted cell has the correct content (should be at index 5) + expect(await notebooksPositron.getCellContent(5)).toBe('# Cell 2'); + }); // ======================================== // Test 2: Cut single cell and paste at different position // ======================================== - await app.workbench.notebooksPositron.selectCellAtIndex(1); + await test.step('Test 2: Cut single cell and paste at different position', async () => { + await notebooksPositron.selectCellAtIndex(1); - // Verify we're at cell 1 with correct content - expect(await app.workbench.notebooksPositron.getCellContent(1)).toBe('# Cell 1'); + // Verify we're at cell 1 with correct content + await notebooksPositron.expectCellContentAtIndexToBe(1, '# Cell 1'); - // Cut the cell - await cutCellsWithKeyboard(app); + // Cut the cell + await notebooksPositron.cutCellsWithKeyboard(); - // Verify cell count decreased - expect(await getCellCount(app)).toBe(5); + // Verify cell count decreased + await notebooksPositron.expectCellCountToBe(5); - // Verify what was cell 2 is now at index 1 - expect(await app.workbench.notebooksPositron.getCellContent(1)).toBe('# Cell 2'); + // Verify what was cell 2 is now at index 1 + await notebooksPositron.expectCellContentAtIndexToBe(1, '# Cell 2'); - // Move to index 3 and paste - await app.workbench.notebooksPositron.selectCellAtIndex(3); - await pasteCellsWithKeyboard(app); + // Move to index 3 and paste + await notebooksPositron.selectCellAtIndex(3); + await notebooksPositron.pasteCellsWithKeyboard(); - // Verify cell count is back to 6 - expect(await getCellCount(app)).toBe(6); + // Verify cell count is back to 6 + await notebooksPositron.expectCellCountToBe(6); - // Verify the pasted cell has correct content at index 4 - expect(await app.workbench.notebooksPositron.getCellContent(4)).toBe('# Cell 1'); + // Verify the pasted cell has correct content at index 4 + await notebooksPositron.expectCellContentAtIndexToBe(4, '# Cell 1'); + }); // ======================================== // Test 3: Copy cell and paste multiple times (clipboard persistence) // ======================================== - await app.workbench.notebooksPositron.selectCellAtIndex(0); + await test.step('Test 3: Copy cell and paste multiple times (clipboard persistence)', async () => { + await notebooksPositron.selectCellAtIndex(0); + + // Copy cell 0 + await notebooksPositron.expectCellContentAtIndexToBe(0, '# Cell 0'); + await notebooksPositron.copyCellsWithKeyboard(); - // Copy cell 0 - expect(await app.workbench.notebooksPositron.getCellContent(0)).toBe('# Cell 0'); - await copyCellsWithKeyboard(app); + // Paste at position 2 + await notebooksPositron.selectCellAtIndex(2); + await notebooksPositron.pasteCellsWithKeyboard(); - // Paste at position 2 - await app.workbench.notebooksPositron.selectCellAtIndex(2); - await pasteCellsWithKeyboard(app); + // Verify first paste + await notebooksPositron.expectCellCountToBe(7); + await notebooksPositron.expectCellContentAtIndexToBe(3, '# Cell 0'); - // Verify first paste - expect(await getCellCount(app)).toBe(7); - expect(await app.workbench.notebooksPositron.getCellContent(3)).toBe('# Cell 0'); + // Paste again at position 5 + await notebooksPositron.selectCellAtIndex(5); + await notebooksPositron.pasteCellsWithKeyboard(); - // Paste again at position 5 - await app.workbench.notebooksPositron.selectCellAtIndex(5); - await pasteCellsWithKeyboard(app); + // Verify second paste + await notebooksPositron.expectCellCountToBe(8); + await notebooksPositron.expectCellContentAtIndexToBe(6, '# Cell 0'); + }); - // Verify second paste - expect(await getCellCount(app)).toBe(8); - expect(await app.workbench.notebooksPositron.getCellContent(6)).toBe('# Cell 0'); // ======================================== // Test 4: Cut and paste at beginning of notebook // ======================================== - // Select a middle cell to cut - await app.workbench.notebooksPositron.selectCellAtIndex(4); - const cellToMoveContent = await app.workbench.notebooksPositron.getCellContent(4); + await test.step('Test 4: Cut and paste at beginning of notebook', async () => { + // Select a middle cell to cut + await notebooksPositron.selectCellAtIndex(4); + const cellToMoveContent = await notebooksPositron.getCellContent(4); - // Cut the cell - await cutCellsWithKeyboard(app); + // Cut the cell + await notebooksPositron.cutCellsWithKeyboard(); - // Verify cell removed - expect(await getCellCount(app)).toBe(7); + // Verify cell removed + await notebooksPositron.expectCellCountToBe(7); - // Move to first cell and paste - // Note: Paste typically inserts after the current cell - await app.workbench.notebooksPositron.selectCellAtIndex(0); - await pasteCellsWithKeyboard(app); + // Move to first cell and paste + // Note: Paste typically inserts after the current cell + await notebooksPositron.selectCellAtIndex(0); + await notebooksPositron.pasteCellsWithKeyboard(); - // Verify cell count restored - expect(await getCellCount(app)).toBe(8); + // Verify cell count restored + await notebooksPositron.expectCellCountToBe(8); - // Verify pasted cell is at index 1 (pasted after cell 0) - expect(await app.workbench.notebooksPositron.getCellContent(1)).toBe(cellToMoveContent); + // Verify pasted cell is at index 1 (pasted after cell 0) + await notebooksPositron.expectCellContentAtIndexToBe(1, cellToMoveContent); + }) // ======================================== // Test 5: Cut all cells and verify notebook can be empty // ======================================== - // Delete cells until only one remains - while (await getCellCount(app) > 1) { - await app.workbench.notebooksPositron.selectCellAtIndex(0); - await cutCellsWithKeyboard(app); - } - - // Verify we have exactly one cell - expect(await getCellCount(app)).toBe(1); - - // Cut the last cell - in Positron notebooks, this may be allowed - await cutCellsWithKeyboard(app); - - // Check if notebook can be empty (Positron may allow 0 cells) - const finalCount = await getCellCount(app); - expect(finalCount).toBeLessThanOrEqual(1); - - // ======================================== - // Cleanup - // ======================================== - // Close the notebook without saving - await app.workbench.notebooks.closeNotebookWithoutSaving(); + await test.step('Verify other cells shifted down correctly', async () => { + // Delete cells until only one remains + while (await notebooksPositron.cell.count() > 1) { + await notebooksPositron.selectCellAtIndex(0); + await notebooksPositron.cutCellsWithKeyboard(); + } + + // Verify we have exactly one cell + await notebooksPositron.expectCellCountToBe(1); + + // Cut the last cell - in Positron notebooks, this may be allowed + await notebooksPositron.cutCellsWithKeyboard(); + + // Check if notebook can be empty (Positron may allow 0 cells) + const finalCount = await notebooksPositron.cell.count(); + expect(finalCount).toBeLessThanOrEqual(1); + }) }); - }); From 08dae88636cc49a4a39ea5d4e8939f2fb731c395 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Tue, 7 Oct 2025 15:10:30 -0500 Subject: [PATCH 02/22] 1st pass --- test/e2e/pages/hotKeys.ts | 6 +- test/e2e/pages/notebooks.ts | 2 +- test/e2e/pages/notebooksPositron.ts | 583 ++++++++++++------ .../notebook/cell-deletion-action-bar.test.ts | 178 +++--- .../notebook/cell-execution-info.test.ts | 191 +++--- .../tests/notebook/notebook-create.test.ts | 12 +- .../e2e/tests/notebook/notebook-debug.test.ts | 2 +- .../notebook-focus-and-selection.test.ts | 412 ++++--------- .../notebook/notebook-integration.test.ts | 14 +- .../notebook-raises-exception.test.ts | 48 +- .../positron-notebook-copy-paste.test.ts | 55 +- .../notebook/positron-notebook-editor.test.ts | 36 +- .../positron-notebook-undo-redo.test.ts | 170 ++--- .../variables/variables-notebook.test.ts | 6 +- 14 files changed, 836 insertions(+), 879 deletions(-) diff --git a/test/e2e/pages/hotKeys.ts b/test/e2e/pages/hotKeys.ts index 970e4c35c972..6e91947666a3 100644 --- a/test/e2e/pages/hotKeys.ts +++ b/test/e2e/pages/hotKeys.ts @@ -201,8 +201,12 @@ export class HotKeys { await this.pressHotKeys('Cmd+J K', 'Open workspace settings JSON', true); } - public async reloadWindow() { + public async reloadWindow(waitForReady = false) { await this.pressHotKeys('Cmd+R R', 'Reload window'); + if (waitForReady) { + await expect(this.code.driver.page.locator('text=/^Starting up|^Starting|^Preparing|^Discovering( \\w+)? interpreters|starting\\.$/i')).toHaveCount(0, { timeout: 90000 }); + + } } public async openWelcomeWalkthrough() { diff --git a/test/e2e/pages/notebooks.ts b/test/e2e/pages/notebooks.ts index 5e57271bd634..5229d8e720b0 100644 --- a/test/e2e/pages/notebooks.ts +++ b/test/e2e/pages/notebooks.ts @@ -119,7 +119,7 @@ export class Notebooks { }); } - async addCodeToCellAtIndex(code: string, cellIndex = 0, delay = 0) { + async addCodeToCellAtIndex(cellIndex: number, code: string, delay = 0) { await test.step('Add code to first cell', async () => { await this.selectCellAtIndex(cellIndex); await this.typeInEditor(code, delay); diff --git a/test/e2e/pages/notebooksPositron.ts b/test/e2e/pages/notebooksPositron.ts index 4c2703ff4fd9..cc307e44061a 100644 --- a/test/e2e/pages/notebooksPositron.ts +++ b/test/e2e/pages/notebooksPositron.ts @@ -9,7 +9,21 @@ import { QuickInput } from './quickInput'; import { QuickAccess } from './quickaccess'; import test, { expect, Locator } from '@playwright/test'; import { HotKeys } from './hotKeys.js'; -import { time } from 'console'; +import { app } from 'electron'; + +type SettingsFixture = { + set: ( + settings: Record, + options?: { reload?: boolean | 'web'; waitMs?: number; waitForReady?: boolean; keepOpen?: boolean } + ) => Promise; +}; + +type ConfigureNotebookEditorOptions = { + editor: 'positron' | 'default'; + reload?: boolean | 'web'; + waitMs?: number; +}; + const DEFAULT_TIMEOUT = 10000; @@ -21,27 +35,20 @@ export class PositronNotebooks extends Notebooks { cell = this.code.driver.page.locator('[data-testid="notebook-cell"]'); newCellButton = this.code.driver.page.getByLabel(/new code cell/i); editorAtIndex = (index: number) => this.cell.nth(index).locator('.positron-cell-editor-monaco-widget textarea'); - runCellAtIndex = (index: number) => this.cell.nth(index).getByLabel(/execute cell/i); + runCellButtonAtIndex = (index: number) => this.cell.nth(index).getByLabel(/execute cell/i); + spinner = this.code.driver.page.getByLabel(/cell is executing/i); spinnerAtIndex = (index: number) => this.cell.nth(index).getByLabel(/cell is executing/i); cellExecutionInfoAtIndex = (index: number) => this.cell.nth(index).getByLabel(/cell execution info/i); + executionStatusAtIndex = (index: number) => this.cell.nth(index).locator('[data-execution-status]'); detectingKernelsText = this.code.driver.page.getByText(/detecting kernels/i); cellStatusSyncIcon = this.code.driver.page.locator('.cell-status-item-has-runnable .codicon-sync'); kernelStatusBadge = this.code.driver.page.getByTestId('notebook-kernel-status'); deleteCellButton = this.cell.getByRole('button', { name: /delete the selected cell/i }); - - - // Selector constants for Positron notebook elements - // private static readonly RUN_CELL_LABEL = /execute cell/i; - // private static readonly NOTEBOOK_CELL_SELECTOR = '[data-testid="notebook-cell"]'; - // private static readonly NEW_CODE_CELL_LABEL = /new code cell/i; - // private static readonly MONACO_EDITOR_SELECTOR = '.positron-cell-editor-monaco-widget textarea'; - // private static readonly CELL_EXECUTING_LABEL = /cell is executing/i; - // private static readonly CELL_EXECUTION_INFO_LABEL = /cell execution info/i; - // private static readonly NOTEBOOK_KERNEL_STATUS_TESTID = 'notebook-kernel-status'; - // private static readonly DELETE_CELL_LABEL = /delete the selected cell/i; - // private static readonly POSITRON_NOTEBOOK_SELECTOR = '.positron-notebook'; - // private static readonly CELL_STATUS_SYNC_SELECTOR = '.cell-status-item-has-runnable .codicon-sync'; - // private static readonly DETECTING_KERNELS_TEXT = /detecting kernels/i; + cellInfoToolTip = this.code.driver.page.getByRole('tooltip', { name: /cell execution details/i }); + cellInfoToolTipStatus = this.cellInfoToolTip.getByLabel('Execution status'); + cellInfoToolTipDuration = this.cellInfoToolTip.getByLabel('Execution duration'); + cellInfoToolTipOrder = this.cellInfoToolTip.getByLabel('Execution order'); + cellInfoToolTipCompleted = this.cellInfoToolTip.getByLabel('Execution completed'); constructor(code: Code, quickinput: QuickInput, quickaccess: QuickAccess, hotKeys: HotKeys) { super(code, quickinput, quickaccess, hotKeys); @@ -49,10 +56,30 @@ export class PositronNotebooks extends Notebooks { // -- Actions -- + /** + * Action: Enable the Positron Notebooks feature + * @param settings - The settings fixture. + * @param options - Configuration options for enabling the feature. + */ + async enableFeature( + settings: SettingsFixture, + { editor, reload = false, waitMs = 800 }: ConfigureNotebookEditorOptions + ): Promise { + const associations = editor === 'positron' + ? { '*.ipynb': 'workbench.editor.positronNotebook' } + : {}; + + await settings.set( + { + 'positron.notebook.enabled': true, + 'workbench.editorAssociations': associations, + }, + { reload, waitMs, waitForReady: true } + ); + } + /** * Action: Open a Positron notebook. - * It does not check for an active cell, as Positron notebooks do not have the same cell structure as VS Code notebooks. - * * @param path - The path to the notebook to open. */ async openNotebook(path: string): Promise { @@ -61,80 +88,48 @@ export class PositronNotebooks extends Notebooks { } /** + * @override * Action: Select a cell at the specified index. + * @param cellIndex - The index of the cell to select. */ - async selectCellAtIndex(cellIndex: number): Promise { + async selectCellAtIndex(cellIndex: number, { exitEditMode = false }: { exitEditMode?: boolean } = {}): Promise { await test.step(`Select cell at index: ${cellIndex}`, async () => { await this.cell.nth(cellIndex).click(); + + await this.expectCellIndexToBeSelected(cellIndex, { isSelected: true, inEditMode: true }); + + if (exitEditMode) { + await this.code.driver.page.keyboard.press('Escape'); + await this.expectCellIndexToBeSelected(cellIndex, { isSelected: true, inEditMode: false }); + } }); } /** * Action: Create a new code cell at the END of the notebook. */ - private async createNewCodeCell(): Promise { - await test.step(`Create new code cell`, async () => { + private async addCodeCellToEnd(): Promise { + await test.step(`Create new code cell at end:`, async () => { const newCellButtonCount = await this.newCellButton.count(); if (newCellButtonCount === 0) { throw new Error('No "New Code Cell" buttons found'); } - // Click the last button (which adds a cell at the end) - // Note: This assumes we're always adding at the end, which matches the validation in addCodeToCellAtIndex + // Click the last "New Code Cell" button to add a cell at the end await this.newCellButton.last().click(); - - // Wait for the new cell to appear await expect(this.cell).toHaveCount(newCellButtonCount + 1, { timeout: DEFAULT_TIMEOUT }); }); } /** - * Action: Add code to a cell at the specified index. - * If the cell does not exist, it creates it at the end of the notebook. - * Throws an error if trying to create a cell beyond the next sequential index. - * - * @param code - The code to add to the cell. - * @param cellIndex - The index of the cell to add code to (default: 0). - * @param delay - Optional delay between keystrokes for typing simulation (default: 0, meaning no delay). - */ - async addCodeToCellAtIndex(code: string, cellIndex = 0, delay = 0): Promise { - await test.step('Add code to Positron cell', async () => { - // Check if the cell exists - const currentCellCount = await this.cell.count(); - - if (cellIndex >= currentCellCount) { - // Cell doesn't exist, need to create it - // Verify we're only adding one cell at the end - if (cellIndex > currentCellCount) { - throw new Error(`Cannot create cell at index ${cellIndex}. Current cell count is ${currentCellCount}. Can only add cells sequentially.`); - } - - // Create the new cell - await this.createNewCodeCell(); - } - - // Now select and fill the cell (existing logic) - await this.selectCellAtIndex(cellIndex); - - // Ensure editor is focused and type/fill the code - await this.editorAtIndex(cellIndex).focus(); - if (delay) { - await this.editorAtIndex(cellIndex).pressSequentially(code, { delay }); - } else { - await this.editorAtIndex(cellIndex).fill(code); - } - }); - } - - /** - * Execute code in the current cell by clicking the run button + * Action: Run the code in the cell at the specified index. */ - async executeCodeInCell(cellIndex = 0): Promise { + async runCodeAtIndex(cellIndex = 0): Promise { await test.step('Execute code in Positron notebook cell', async () => { await this.selectCellAtIndex(cellIndex); - await this.runCellAtIndex(cellIndex).click(); + await this.runCellButtonAtIndex(cellIndex).click(); // Wait for execution to complete by checking the execution spinner is gone const spinner = this.spinnerAtIndex(cellIndex); @@ -150,38 +145,41 @@ export class PositronNotebooks extends Notebooks { } /** - * Start executing code in the current cell by clicking the run button without waiting for completion + * Action: Move the mouse away from the notebook area to close any open tooltips/popups. */ - async startExecutingCodeInCell(cellIndex = 0): Promise { - await test.step('Start executing code in Positron notebook cell', async () => { - await this.selectCellAtIndex(cellIndex); - await this.runCellAtIndex(cellIndex).click(); - }); - } + async moveMouseAway(): Promise { + await this.code.driver.page.mouse.move(0, 0); + }; /** - * Add code to a cell and run it - combined operation for efficiency. - * This avoids repeatedly finding the same cell and provides better performance. - * Returns the cell locator for further operations. + * Action: Add code to a cell at the specified index and run it. + * + * @param code - The code to add to the cell. + * @param cellIndex - The index of the cell to add code to (default: 0). + * @param options - Options to control behavior: + * delay: Optional delay between keystrokes for typing simulation (default: 0, meaning no delay). + * run: Whether to run the cell after adding code (default: false). + * waitForSpinner: Whether to wait for the execution spinner to appear and disappear (default: false). + * waitForPopup: Whether to wait for the execution info popup to appear after running (default: false). */ - async addCodeToCellAndRun(code: string, cellIndex = 0, delay = 0): Promise { + async addCodeToCell( + cellIndex: number, + code: string, + options?: { delay?: number; run?: boolean; waitForSpinner?: boolean; waitForPopup?: boolean } + ): Promise { + const { delay = 0, run = false, waitForSpinner = false, waitForPopup = false } = options ?? {}; return await test.step(`Add code and run cell ${cellIndex}`, async () => { - // Check if the cell exists const currentCellCount = await this.cell.count(); if (cellIndex >= currentCellCount) { - // Cell doesn't exist, need to create it - // Verify we're only adding one cell at the end if (cellIndex > currentCellCount) { throw new Error(`Cannot create cell at index ${cellIndex}. Current cell count is ${currentCellCount}. Can only add cells sequentially.`); } - - await this.createNewCodeCell(); + await this.addCodeCellToEnd(); } - await this.cell.nth(cellIndex).click() + await this.cell.nth(cellIndex).click(); - // Find and fill the Monaco editor const editor = this.editorAtIndex(cellIndex); await editor.focus(); @@ -191,85 +189,94 @@ export class PositronNotebooks extends Notebooks { await editor.fill(code); } - // Find and click the run button - await this.runCellAtIndex(cellIndex).click(); + if (run) { + await this.runCellButtonAtIndex(cellIndex).click(); - // // Wait for execution to complete - // const spinner = cell.getByLabel('Cell is executing'); + if (waitForSpinner) { + const spinner = this.spinnerAtIndex(cellIndex); + await expect(spinner).toBeVisible({ timeout: DEFAULT_TIMEOUT }).catch(() => { + // Spinner might not appear for very fast executions, that's okay + }); + await expect(spinner).toHaveCount(0, { timeout: DEFAULT_TIMEOUT }); + } - // // Wait for spinner to appear (cell is executing) - // await expect(spinner).toBeVisible({ timeout: DEFAULT_TIMEOUT }).catch(() => { - // // Spinner might not appear for very fast executions, that's okay - // }); + if (waitForPopup) { + // const infoPopup = this.cell.nth(cellIndex).getByRole('tooltip', { name: /cell execution details/i }); + await expect(this.cellInfoToolTip).toBeVisible(); + } + } - // // Wait for spinner to disappear (execution complete) - // await expect(spinner).toHaveCount(0, { timeout: DEFAULT_TIMEOUT }); return this.cell.nth(cellIndex); }); } /** - * Helper function to copy cells using keyboard shortcut + * Action: Perform a cell action using keyboard shortcuts. + * @param action - The action to perform: 'copy', 'cut', 'paste', 'undo', 'redo', 'addCellBelow'. */ - async copyCellsWithKeyboard(): Promise { - // We need to press escape to get the focus out of the cell editor itself + async performCellAction(action: 'copy' | 'cut' | 'paste' | 'undo' | 'redo' | 'delete' | 'addCellBelow'): Promise { + // Press escape to ensure focus is out of the cell editor await this.code.driver.page.keyboard.press('Escape'); - await this.hotKeys.copy(); - } - async cutCellsWithKeyboard(): Promise { - // We need to press escape to get the focus out of the cell editor itself - await this.code.driver.page.keyboard.press('Escape'); - await this.hotKeys.cut(); - } - - async pasteCellsWithKeyboard(): Promise { - // We need to press escape to get the focus out of the cell editor itself - await this.code.driver.page.keyboard.press('Escape'); - await this.hotKeys.paste(); - } - - async expectCellCountToBe(expectedCount: number): Promise { - await test.step(`Expect cell count to be ${expectedCount}`, async () => { - await expect(this.cell).toHaveCount(expectedCount, { timeout: DEFAULT_TIMEOUT }); - }); - } - - async expectCellContentAtIndexToBe(cellIndex: number, expectedContent: string): Promise { - await test.step(`Expect cell ${cellIndex} content to be: ${expectedContent}`, async () => { - const actualContent = await this.getCellContent(cellIndex); - await expect(async () => { - expect(actualContent).toBe(expectedContent); - }).toPass({ timeout: DEFAULT_TIMEOUT }); - }); + switch (action) { + case 'copy': + await this.hotKeys.copy(); + break; + case 'cut': + await this.hotKeys.cut(); + break; + case 'paste': + await this.hotKeys.paste(); + break; + case 'undo': + await this.hotKeys.undo(); + break; + case 'redo': + await this.hotKeys.redo(); + break; + case 'delete': + await this.code.driver.page.keyboard.press('Backspace'); + break; + case 'addCellBelow': + await this.code.driver.page.keyboard.press('KeyB'); + break; + default: + throw new Error(`Unknown cell action: ${action}`); + } } /** - * Get execution info icon for a specific cell + * Helper function to delete cell using action bar delete button */ - getExecutionInfoIcon(cellIndex = 0): Locator { - return this.cellExecutionInfoAtIndex(cellIndex); - // return this.cell.nth(cellIndex).getByLabel(/cell execution info/i); - } + async deleteCellWithActionBar(cellIndex = 0): Promise { + await test.step(`Delete cell ${cellIndex} using action bar`, async () => { + // Get the current cell count before deletion + const initialCount = await this.cell.count(); + // Click on the cell to make the action bar visible + await this.cell.nth(cellIndex).click(); - /** - * Get the execution status for a specific cell - */ - async getExecutionStatus(cellIndex = 0): Promise { - const icon = this.getExecutionInfoIcon(cellIndex); - return await icon.getAttribute('data-execution-status'); + // Click the delete button + await this.deleteCellButton.click(); + + // Wait for the deletion to complete by checking cell count decreased + await expect(this.cell).toHaveCount(initialCount - 1, { timeout: DEFAULT_TIMEOUT }); + + // Give a small delay for focus to settle + await this.code.driver.page.waitForTimeout(100); + }); } /** - * Wait for execution info icon to be visible after cell execution + * Get cell content for identification */ - async expectExecutionInfoIconToBeVisible(cellIndex = 0, timeout = DEFAULT_TIMEOUT): Promise { - await test.step(`Wait for execution info icon in cell ${cellIndex}`, async () => { - const icon = this.getExecutionInfoIcon(cellIndex); - await expect(icon).toBeVisible({ timeout }); - }); + async getCellContent(cellIndex: number): Promise { + const cell = this.code.driver.page.locator('[data-testid="notebook-cell"]').nth(cellIndex); + const editor = cell.locator('.positron-cell-editor-monaco-widget .view-lines'); + const content = await editor.textContent() ?? ''; + // Replace the weird ascii space with a proper space + return content.replace(/\u00a0/g, ' '); } /** @@ -341,72 +348,272 @@ export class PositronNotebooks extends Notebooks { }); } + /** + * Verify: Cell count matches expected count. + * @param expectedCount - The expected number of cells. + */ + async expectCellCountToBe(expectedCount: number): Promise { + await test.step(`Expect cell count to be ${expectedCount}`, async () => { + await expect(this.cell).toHaveCount(expectedCount, { timeout: DEFAULT_TIMEOUT }); + }); + } /** - * Helper function to set notebook editor associations - * @param settings - The settings fixture - * @param editor - 'positron' to use Positron notebook editor, 'default' to clear associations - * @param waitMs - The number of milliseconds to wait for the settings to be applied + * Verify: Cell content at specified index matches expected content. + * @param cellIndex - The index of the cell to check. + * @param expectedContent - The expected content of the cell. */ - async setNotebookEditor( - settings: { - set: (settings: Record, options?: { reload?: boolean | 'web'; waitMs?: number; waitForReady?: boolean; keepOpen?: boolean }) => Promise; - }, - editor: 'positron' | 'default', - waitMs = 800 - ) { - await settings.set({ - 'positron.notebook.enabled': true, - 'workbench.editorAssociations': editor === 'positron' - ? { '*.ipynb': 'workbench.editor.positronNotebook' } - : {} - }, { waitMs }); + async expectCellContentAtIndexToBe(cellIndex: number, expectedContent: string): Promise { + await test.step(`Expect cell ${cellIndex} content to be: ${expectedContent}`, async () => { + const actualContent = await this.getCellContent(cellIndex); + await expect(async () => { + expect(actualContent).toBe(expectedContent); + }).toPass({ timeout: DEFAULT_TIMEOUT }); + }); } /** - * Helper function to enable Positron notebooks with reload - * @param settings - The settings fixture + * Verify: Cell content at specified index contains expected substring. + * @param cellIndex - The index of the cell to check. + * @param expectedSubstring - The substring expected to be contained in the cell content. */ - async enablePositronNotebooks( - settings: { - set: (settings: Record, options?: { reload?: boolean | 'web'; waitMs?: number; waitForReady?: boolean; keepOpen?: boolean }) => Promise; - } - ) { - await settings.set({ - 'positron.notebook.enabled': true, - }, { reload: true }); + /** + * Verify: Cell content at specified index contains expected substring or matches RegExp. + * @param cellIndex - The index of the cell to check. + * @param expected - The substring or RegExp expected to be contained in the cell content. + */ + async expectCellContentAtIndexToContain(cellIndex: number, expected: string | RegExp): Promise { + await test.step( + `Expect cell ${cellIndex} content to contain: ${expected instanceof RegExp ? expected.toString() : expected}`, + async () => { + const actualContent = await this.getCellContent(cellIndex); + await expect(async () => { + if (expected instanceof RegExp) { + expect(actualContent).toMatch(expected); + } else { + expect(actualContent).toContain(expected); + } + }).toPass({ timeout: DEFAULT_TIMEOUT }); + } + ); } /** - * Helper function to delete cell using action bar delete button + * Verify: Cell info tooltip contains expected content. + * @param expectedContent - Object with expected content to verify. + * Use RegExp for fields where exact match is not feasible (e.g., duration, completed time). */ - async deleteCellWithActionBar(cellIndex = 0): Promise { - await test.step(`Delete cell ${cellIndex} using action bar`, async () => { - // Get the current cell count before deletion - const initialCount = await this.cell.count(); + async expectToolTipToContain(expectedContent: { order?: number; duration?: RegExp; status?: 'Success' | 'Failed' | 'Currently running...'; completed?: RegExp }): Promise { + await test.step(`Expect cell info tooltip to contain: ${JSON.stringify(expectedContent)}`, async () => { + await expect(this.cellInfoToolTip).toBeVisible({ timeout: DEFAULT_TIMEOUT }); + + const labelMap: Record = { + order: 'Execution Order', + duration: 'Duration', + status: 'Status', + completed: 'Completed' + }; + + const getValueLocator = (label: string) => + this.code.driver.page + .locator('.popup-label-text', { hasText: label }) + .locator('..') + .locator('.popup-value-text'); + + for (const key of Object.keys(expectedContent) as (keyof typeof expectedContent)[]) { + const expectedValue = expectedContent[key]; + if (expectedValue !== undefined) { + if (key === 'status' && expectedValue === 'Currently running...') { + // Special case when cell is actively running: check for label, not value + const labelLocator = this.code.driver.page.locator('.popup-label', { hasText: 'Currently running...' }); + await expect(labelLocator).toBeVisible({ timeout: DEFAULT_TIMEOUT }); + } else { + const valueLocator = getValueLocator(labelMap[key]); + const expectedText = expectedValue instanceof RegExp ? expectedValue : expectedValue.toString(); + await expect(valueLocator).toContainText(expectedText, { timeout: DEFAULT_TIMEOUT }); + } + } + } + }); + } - // Click on the cell to make the action bar visible - await this.cell.nth(cellIndex).click(); - // Click the delete button - await this.deleteCellButton.click(); + /** + * Verify: Cell execution status matches expected status. + * @param cellIndex - The index of the cell to check. + * @param expectedStatus - The expected execution status of the cell. + * @param timeout - The timeout for the expectation. + */ + async expectExecutionStatusToBe(cellIndex: number, expectedStatus: 'running' | 'idle' | 'failed' | 'success', timeout = DEFAULT_TIMEOUT): Promise { + await test.step(`Expect execution status to be: ${expectedStatus}`, async () => { + await expect(this.executionStatusAtIndex(cellIndex)).toHaveAttribute('data-execution-status', expectedStatus, { timeout }); + }); + } - // Wait for the deletion to complete by checking cell count decreased - await expect(this.cell).toHaveCount(initialCount - 1, { timeout: DEFAULT_TIMEOUT }); - // Give a small delay for focus to settle - await this.code.driver.page.waitForTimeout(100); + async expectSpinnerAtIndex(cellIndex: number, visible = true, timeout = DEFAULT_TIMEOUT): Promise { + await test.step(`Expect spinner to be ${visible ? 'visible' : 'hidden'} in cell ${cellIndex}`, async () => { + if (visible) { + await expect(this.spinnerAtIndex(cellIndex)).toBeVisible({ timeout }); + } else { + await expect(this.spinnerAtIndex(cellIndex)).toHaveCount(0, { timeout }); + } + }); + } + + async expectNoActiveSpinners(timeout = DEFAULT_TIMEOUT): Promise { + await test.step('Expect no active spinners in notebook', async () => { + await expect(this.spinner).toHaveCount(0, { timeout }); }); } /** - * Get cell content for identification + * Verify: Cell info tooltip visibility. + * @param visible - Whether the tooltip should be visible. + * @param timeout - Timeout for the expectation. */ - async getCellContent(cellIndex: number): Promise { - const cell = this.code.driver.page.locator('[data-testid="notebook-cell"]').nth(cellIndex); - const editor = cell.locator('.positron-cell-editor-monaco-widget .view-lines'); - const content = await editor.textContent() ?? ''; - // Replace the weird ascii space with a proper space - return content.replace(/\u00a0/g, ' '); + async expectToolTipVisible(visible: boolean, timeout = DEFAULT_TIMEOUT): Promise { + await test.step(`Expect cell info tooltip to be ${visible ? 'visible' : 'hidden'}`, async () => { + const assertion = expect(this.cellInfoToolTip); + if (visible) { + await assertion.toBeVisible({ timeout }); + } else { + await assertion.not.toBeVisible({ timeout }); + } + }); + } + + /** + * Get the index of the currently focused cell. + * @returns The index of the focused cell, or null if no cell is focused. + */ + async getFocusedCellIndex(): Promise { + const cells = this.cell; + const cellCount = await cells.count(); + + for (let i = 0; i < cellCount; i++) { + const cell = cells.nth(i); + const isFocused = await cell.evaluate((element) => { + // Check if this cell or any descendant has focus + return element.contains(document.activeElement) || + element === document.activeElement; + }); + + if (isFocused) { + return i; + } + } + return null; } + + // /** + // * Verify: the focused cell index is (or is not) the expected index. + // * @param expectedIndex - The expected index of the focused cell, or null if no cell should be focused. + // * @param timeout - Timeout for the expectation. + // * @param shouldBeFocused - If true, checks that the cell is focused; if false, checks that it is not focused. + // */ + // async expectCellIndexToBeFocused( + // expectedIndex: number | null, + // shouldBeFocused = true, + // timeout = 1000000, + // ): Promise { + // await test.step( + // `Expect focused cell index to be${shouldBeFocused ? '' : ' not'}: ${expectedIndex}`, + // async () => { + // await expect(async () => { + // const actualIndex = await this.getFocusedCellIndex(); + // shouldBeFocused + // ? expect(actualIndex).toBe(expectedIndex) + // : expect(actualIndex).not.toBe(expectedIndex); + // }).toPass({ timeout }); + // } + // ); + // } + + /** + * Verify: the cell at the specified index is (or is not) selected, + * and optionally, whether it is in edit mode. + * @param expectedIndex - The index of the cell to check. + * @param options - Options to specify selection and edit mode expectations. + */ + async expectCellIndexToBeSelected( + expectedIndex: number, + options?: { isSelected?: boolean; inEditMode?: boolean; timeout?: number } + ): Promise { + const { + isSelected = true, + inEditMode = undefined, + timeout = DEFAULT_TIMEOUT + } = options ?? {}; + + await test.step( + `Expect cell at index ${expectedIndex} to be${isSelected ? '' : ' not'} selected` + + (inEditMode !== undefined ? ` and${inEditMode ? '' : ' not'} in edit mode` : ''), + async () => { + await expect(async () => { + const cells = this.cell; + const cellCount = await cells.count(); + const selectedIndices: number[] = []; + + for (let i = 0; i < cellCount; i++) { + const cell = cells.nth(i); + const isSelected = (await cell.getAttribute('aria-selected')) === 'true'; + if (isSelected) { + selectedIndices.push(i); + } + } + + isSelected + ? expect(selectedIndices).toContain(expectedIndex) + : expect(selectedIndices).not.toContain(expectedIndex); + + if (inEditMode !== undefined) { + const ta = this.editorAtIndex(expectedIndex); + const isEditing = await ta.evaluate(el => el === document.activeElement); + inEditMode + ? expect(isEditing).toBe(true) + : expect(isEditing).toBe(false); + } + }).toPass({ timeout }); + } + ); + } + + // /** + // * Return the index of the cell that is in EDIT MODE (i.e., Monaco textarea is focused). + // * @returns index or null if none are in edit mode. + // */ + // async getEditingCellIndex(): Promise { + // const cells = this.cell; + // const count = await cells.count(); + + // for (let i = 0; i < count; i++) { + // const ta = this.editorAtIndex(i); // '.positron-cell-editor-monaco-widget textarea' + // // Check the textarea is the active element (edit mode). + // const isEditing = await ta.evaluate((el) => el === document.activeElement); + // if (isEditing) return i; + // } + // return null; + // } + + // /** + // * Verify a specific cell IS / IS NOT in edit mode. + // * Edit mode is defined as the Monaco textarea being focused. + // */ + // async expectCellEditModeAtIndex( + // index: number, + // shouldBeEditing = true, + // timeout = DEFAULT_TIMEOUT + // ): Promise { + // await test.step(`Expect cell ${index} to be${shouldBeEditing ? '' : ' not'} in edit mode`, async () => { + // const ta = this.editorAtIndex(index); + // if (shouldBeEditing) { + // await expect(ta).toBeFocused({ timeout }); + // } else { + // await expect(ta).not.toBeFocused({ timeout }); + // } + // }); + // } } + + diff --git a/test/e2e/tests/notebook/cell-deletion-action-bar.test.ts b/test/e2e/tests/notebook/cell-deletion-action-bar.test.ts index 5ee3dbe82df0..7b4d4920579c 100644 --- a/test/e2e/tests/notebook/cell-deletion-action-bar.test.ts +++ b/test/e2e/tests/notebook/cell-deletion-action-bar.test.ts @@ -3,7 +3,6 @@ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ -import { Application } from '../../infra/index.js'; import { test, tags } from '../_test.setup'; import { expect } from '@playwright/test'; @@ -11,133 +10,140 @@ test.use({ suiteId: __filename }); -/** - * Helper function to get cell count - */ -async function getCellCount(app: Application): Promise { - return await app.code.driver.page.locator('[data-testid="notebook-cell"]').count(); -} - - - - -test.describe('Cell Deletion Action Bar Behavior', { +test.describe('Positorn Notebooks: Cell Deletion Action Bar Behavior', { tag: [tags.CRITICAL, tags.WIN, tags.NOTEBOOKS] }, () => { - test('Cell deletion using action bar', async function ({ app, settings }) { - // Enable Positron notebooks - await app.workbench.notebooksPositron.enablePositronNotebooks(settings); - await app.workbench.notebooksPositron.setNotebookEditor(settings, 'positron'); + test.beforeAll(async function ({ app, settings }) { + await app.workbench.notebooksPositron.enableFeature(settings, { + editor: 'positron', + reload: true, + }); + }); + + test.afterEach(async function ({ hotKeys }) { + await hotKeys.closeAllEditors(); + }); - await app.workbench.notebooks.createNewNotebook(); - await app.workbench.notebooksPositron.expectToBeVisible(); + test('Cell deletion using action bar', async function ({ app, settings }) { + const { notebooks, notebooksPositron } = app.workbench; // ======================================== // Setup: Create 6 cells with distinct content // ======================================== - await app.workbench.notebooksPositron.addCodeToCellAtIndex('# Cell 0', 0); - await app.workbench.notebooksPositron.addCodeToCellAtIndex('# Cell 1', 1); - await app.workbench.notebooksPositron.addCodeToCellAtIndex('# Cell 2', 2); - await app.workbench.notebooksPositron.addCodeToCellAtIndex('# Cell 3', 3); - await app.workbench.notebooksPositron.addCodeToCellAtIndex('# Cell 4', 4); - await app.workbench.notebooksPositron.addCodeToCellAtIndex('# Cell 5', 5); + await test.step(' Test Setup: Create notebook', async () => { + await notebooks.createNewNotebook(); + await notebooksPositron.expectToBeVisible(); + + await notebooksPositron.addCodeToCell(0, '# Cell 0'); + await notebooksPositron.addCodeToCell(1, '# Cell 1'); + await notebooksPositron.addCodeToCell(2, '# Cell 2'); + await notebooksPositron.addCodeToCell(3, '# Cell 3'); + await notebooksPositron.addCodeToCell(4, '# Cell 4'); + await notebooksPositron.addCodeToCell(5, '# Cell 5'); + + // Verify we have 6 cells + await notebooksPositron.expectCellCountToBe(6); + }); - // Verify we have 6 cells - expect(await getCellCount(app)).toBe(6); // ======================================== // Test 1: Delete a selected cell (cell 2) // ======================================== - // Select cell 2 explicitly - await app.workbench.notebooksPositron.selectCellAtIndex(2); + await test.step('Test 1: Delete selected cell (cell 2)', async () => { + // Select cell 2 explicitly + await notebooksPositron.selectCellAtIndex(2); - // Verify cell 2 has correct content before deletion - expect(await app.workbench.notebooksPositron.getCellContent(2)).toBe('# Cell 2'); + // Verify cell 2 has correct content before deletion + expect(await notebooksPositron.getCellContent(2)).toBe('# Cell 2'); - // Delete the selected cell using action bar - await app.workbench.notebooksPositron.deleteCellWithActionBar(2); + // Delete the selected cell using action bar + await notebooksPositron.deleteCellWithActionBar(2); - // Verify cell count decreased - expect(await getCellCount(app)).toBe(5); + // Verify cell count decreased + await notebooksPositron.expectCellCountToBe(5); - // Verify what was cell 3 is now at index 2 - expect(await app.workbench.notebooksPositron.getCellContent(2)).toBe('# Cell 3'); + // Verify what was cell 3 is now at index 2 + expect(await notebooksPositron.getCellContent(2)).toBe('# Cell 3'); + }); // ======================================== // Test 2: Delete another cell (cell 3, originally cell 4) // ======================================== - // Select cell 3 for deletion - await app.workbench.notebooksPositron.selectCellAtIndex(3); - // Verify cell 3 has correct content before deletion - expect(await app.workbench.notebooksPositron.getCellContent(3)).toBe('# Cell 4'); + await test.step('Test 2: Delete another cell (cell 3, originally cell 4)', async () => { + // Select cell 3 for deletion + await notebooksPositron.selectCellAtIndex(3); - // Delete the selected cell using action bar - await app.workbench.notebooksPositron.deleteCellWithActionBar(3); + // Verify cell 3 has correct content before deletion + await notebooksPositron.expectCellContentAtIndexToBe(3, '# Cell 4'); - // Verify cell count decreased - expect(await getCellCount(app)).toBe(4); + // Delete the selected cell using action bar + await notebooksPositron.deleteCellWithActionBar(3); - // Verify the remaining cells are correct - expect(await app.workbench.notebooksPositron.getCellContent(0)).toBe('# Cell 0'); - expect(await app.workbench.notebooksPositron.getCellContent(1)).toBe('# Cell 1'); - expect(await app.workbench.notebooksPositron.getCellContent(2)).toBe('# Cell 3'); - expect(await app.workbench.notebooksPositron.getCellContent(3)).toBe('# Cell 5'); + // Verify cell count decreased + await notebooksPositron.expectCellCountToBe(4); + + // Verify the remaining cells are correct + await notebooksPositron.expectCellContentAtIndexToBe(0, '# Cell 0'); + await notebooksPositron.expectCellContentAtIndexToBe(1, '# Cell 1'); + await notebooksPositron.expectCellContentAtIndexToBe(2, '# Cell 3'); + await notebooksPositron.expectCellContentAtIndexToBe(3, '# Cell 5'); + }) // ======================================== // Test 3: Delete last cell (cell 3, contains '# Cell 5') // ======================================== - // Verify we're at the last cell with correct content - expect(await app.workbench.notebooksPositron.getCellContent(3)).toBe('# Cell 5'); + await test.step('Test 3: Delete last cell (cell 3, contains \'# Cell 5\')', async () => { + // Verify we're at the last cell with correct content + expect(await notebooksPositron.getCellContent(3)).toBe('# Cell 5'); - // Delete the last cell using action bar - await app.workbench.notebooksPositron.deleteCellWithActionBar(3); + // Delete the last cell using action bar + await notebooksPositron.deleteCellWithActionBar(3); - // Verify cell count decreased - expect(await getCellCount(app)).toBe(3); + // Verify cell count decreased + await notebooksPositron.expectCellCountToBe(3); - // Verify remaining cells - expect(await app.workbench.notebooksPositron.getCellContent(0)).toBe('# Cell 0'); - expect(await app.workbench.notebooksPositron.getCellContent(1)).toBe('# Cell 1'); - expect(await app.workbench.notebooksPositron.getCellContent(2)).toBe('# Cell 3'); + // Verify remaining cells + await notebooksPositron.expectCellContentAtIndexToBe(0, '# Cell 0'); + await notebooksPositron.expectCellContentAtIndexToBe(1, '# Cell 1'); + await notebooksPositron.expectCellContentAtIndexToBe(2, '# Cell 3'); + }) // ======================================== // Test 4: Delete first cell (cell 0) // ======================================== - // Verify we're at the first cell with correct content - expect(await app.workbench.notebooksPositron.getCellContent(0)).toBe('# Cell 0'); + await test.step('Test 4: Delete first cell (cell 0)', async () => { + // Verify we're at the first cell with correct content + expect(await notebooksPositron.getCellContent(0)).toBe('# Cell 0'); - // Delete the first cell using action bar - await app.workbench.notebooksPositron.deleteCellWithActionBar(0); + // Delete the first cell using action bar + await notebooksPositron.deleteCellWithActionBar(0); - // Verify cell count decreased - expect(await getCellCount(app)).toBe(2); + // Verify cell count decreased + await notebooksPositron.expectCellCountToBe(2); - // Verify what was cell 1 is now at index 0 - expect(await app.workbench.notebooksPositron.getCellContent(0)).toBe('# Cell 1'); - expect(await app.workbench.notebooksPositron.getCellContent(1)).toBe('# Cell 3'); + // Verify what was cell 1 is now at index 0 + expect(await notebooksPositron.getCellContent(0)).toBe('# Cell 1'); + expect(await notebooksPositron.getCellContent(1)).toBe('# Cell 3'); + }) // ======================================== // Test 5: Delete remaining cells // ======================================== - // Delete until only one cell remains - while (await getCellCount(app) > 1) { - const currentCount = await getCellCount(app); - await app.workbench.notebooksPositron.deleteCellWithActionBar(0); - - // Verify count decreased - expect(await getCellCount(app)).toBe(currentCount - 1); - } - - // Verify we have exactly one cell remaining with the correct content - expect(await getCellCount(app)).toBe(1); - expect(await app.workbench.notebooksPositron.getCellContent(0)).toBe('# Cell 3'); - - // ======================================== - // Cleanup - // ======================================== - // Close the notebook without saving - await app.workbench.notebooks.closeNotebookWithoutSaving(); + await test.step('Test 5: Delete remaining cells', async () => { + // Delete until only one cell remains + while (await notebooksPositron.cell.count() > 1) { + const currentCount = await notebooksPositron.cell.count(); + await notebooksPositron.deleteCellWithActionBar(0); + + // Verify count decreased + await notebooksPositron.expectCellCountToBe(currentCount - 1); + } + + // Verify we have exactly one cell remaining with the correct content + await notebooksPositron.expectCellCountToBe(1); + await notebooksPositron.expectCellContentAtIndexToBe(0, '# Cell 3'); + }) }); }); diff --git a/test/e2e/tests/notebook/cell-execution-info.test.ts b/test/e2e/tests/notebook/cell-execution-info.test.ts index 9e26294bb472..0839a10ed733 100644 --- a/test/e2e/tests/notebook/cell-execution-info.test.ts +++ b/test/e2e/tests/notebook/cell-execution-info.test.ts @@ -3,152 +3,119 @@ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ -import { Application } from '../../infra/index.js'; import { test, tags } from '../_test.setup'; -import { expect, Locator } from '@playwright/test'; test.use({ suiteId: __filename }); -/** - * Helper function to execute code in a cell and wait for the execution info icon to appear - */ -async function executeCodeAndWaitForCompletion(app: Application, code: string, cellIndex: number = 0, waitForPopup: boolean = true) { - const cell = await app.workbench.notebooksPositron.addCodeToCellAndRun(code, cellIndex); - const infoPopup = cell.getByRole('tooltip', { name: /cell execution details/i }); - // Wait for the popup to have the execution order field indicating the cell has run. - if (waitForPopup) { - await expect(infoPopup).toContainText(/execution order/i); - } - return infoPopup; -} - -async function activateInfoPopup({ app, icon }: { app: Application; icon: Locator }): Promise { - icon.hover({ force: true }); - const popup = app.code.driver.page.getByRole('tooltip', { name: 'Cell execution details' }); - await expect(popup).toBeVisible(); - return popup; -} - -test.describe('Cell Execution Info Popup', { +test.describe('Positron Notebooks: Cell Execution Tooltip', { tag: [tags.CRITICAL, tags.WIN, tags.NOTEBOOKS] }, () => { + test.beforeAll(async function ({ app, settings }) { - await app.workbench.notebooksPositron.enablePositronNotebooks(settings); - // Configure Positron as the notebook editor - await app.workbench.notebooksPositron.setNotebookEditor(settings, 'positron'); + await app.workbench.notebooksPositron.enableFeature(settings, { + editor: 'positron', + reload: true, + }); }); - test('Comprehensive cell execution info test - all scenarios in one notebook', async function ({ app }) { - // Setup: Create notebook and select kernel once - await app.workbench.notebooks.createNewNotebook(); + test.afterEach(async function ({ app, hotKeys }) { + const { notebooksPositron } = app.workbench; + + await notebooksPositron.moveMouseAway(); + await notebooksPositron.expectNoActiveSpinners(); + + await hotKeys.closeAllEditors(); + }) - // Wait for the first cell to be created and visible - // This is important on CI where timing differences can cause race conditions - const firstCell = app.code.driver.page.locator('[data-testid="notebook-cell"]').first(); - await expect(firstCell).toBeVisible({ timeout: 5000 }); + test('Cell Execution Tooltip - Basic Functionality', async function ({ app }) { + const { notebooks, notebooksPositron } = app.workbench; - await app.workbench.notebooksPositron.selectAndWaitForKernel('Python'); + await test.step('Test Setup: Create notebook and select kernel', async () => { + await notebooks.createNewNotebook(); + await notebooksPositron.expectCellCountToBe(1); // Important for CI stability + await notebooksPositron.selectAndWaitForKernel('Python'); + }) // ======================================== // Cell 0: Basic popup display with successful execution // ======================================== - const popup0 = await executeCodeAndWaitForCompletion(app, 'print("hello world")', 0); - - // Verify popup content shows execution info - await expect(popup0.getByLabel('Execution order')).toBeVisible(); - await expect(popup0.getByLabel('Execution order')).toContainText('1'); - await expect(popup0.getByLabel('Execution duration')).toBeVisible(); - await expect(popup0.getByLabel('Execution status')).toContainText('Success'); - - // Verify auto-close behavior - await app.code.driver.page.mouse.move(0, 0); // Move mouse away - await expect(popup0).toBeHidden(); + await test.step('Cell 0 - Successful execution info display', async () => { + // Verify popup shows success status + await notebooksPositron.addCodeToCell(0, 'print("hello world")', { run: true }); + await notebooksPositron.expectToolTipToContain({ + order: 1, + duration: /\d+ms/, + status: 'Success' + }); + + // Verify auto-close behavior + await notebooksPositron.moveMouseAway(); + await notebooksPositron.expectToolTipVisible(false); + }); // ======================================== // Cell 1: Failed execution state display // ======================================== - // Create and execute a new cell with failing code - // const icon1 = await executeCodeAndWaitForCompletion(app, 'raise Exception("test error")', 1); - // await expect(icon1).toBeVisible(); - - // // Verify failed execution status - // await expect(icon1).toHaveAttribute('data-execution-status', 'failed'); - - // Verify popup shows failed status - const popup1 = await executeCodeAndWaitForCompletion(app, 'raise Exception("test error")', 1); - await expect(popup1).toContainText(/Failed/i); - - // Move mouse away to close popup - await app.code.driver.page.mouse.move(0, 0); - await expect(popup1).toBeHidden(); + await test.step('Cell 1 - Failed execution info display', async () => { + // Verify popup shows failed status + await notebooksPositron.addCodeToCell(1, 'raise Exception("test error")', { + run: true, + }); + await notebooksPositron.expectToolTipToContain({ + order: 2, + duration: /\d+ms/, + status: 'Failed' + }); + + // Verify auto-close behavior + await notebooksPositron.moveMouseAway(); + await notebooksPositron.expectToolTipVisible(false); + }); // ======================================== // Cell 2: Running execution state display // ======================================== - const popup2 = await executeCodeAndWaitForCompletion(app, 'import time; time.sleep(3)', 2, false); - - // Wait for execution to start - spinner should appear in button area - const cell2 = app.code.driver.page.locator('[data-testid="notebook-cell"]').nth(2); - const spinner = cell2.getByLabel(/cell is executing/i); - await expect(spinner).toBeVisible({ timeout: 5000 }); - - // Wait for execution info icon to appear during execution and verify running state - await expect(cell2.locator('[data-execution-status="running"]')).toBeVisible(); - // const icon2 = app.workbench.notebooksPositron.getExecutionInfoIcon(2); - // await expect(icon2).toBeVisible({ timeout: 3000 }); - // await expect(icon2).toHaveAttribute('data-execution-status', 'running'); - - // Verify popup shows running status - await expect(popup2).toContainText('Currently running...'); - - // Move mouse away and wait for execution to complete - await app.code.driver.page.mouse.move(0, 0); - await expect(spinner).toHaveCount(0, { timeout: 10000 }); + await test.step('Cell 2 - Running execution info display', async () => { + // Verify popup shows running status while cell is executing + await notebooksPositron.addCodeToCell(2, 'import time; time.sleep(3)', { run: true }); + await notebooksPositron.expectSpinnerAtIndex(2); + await notebooksPositron.expectExecutionStatusToBe(2, 'running'); + await notebooksPositron.expectToolTipToContain({ + status: 'Currently running...' + }); + + // Verify auto-close behavior + await notebooksPositron.moveMouseAway(); + await notebooksPositron.expectSpinnerAtIndex(2, false) + }); // ======================================== // Cell 3: Relative time display // ======================================== - // Execute code in a new cell and get the execution info icon - const icon3 = await executeCodeAndWaitForCompletion(app, 'print("relative time test")', 3); - const popup3 = await activateInfoPopup({ app, icon: icon3 }); - - // Verify relative time is displayed (should show recent execution) - // Some renderers may insert non-breaking spaces between words. Use \s to match any whitespace. - // Some renderers may insert non-breaking spaces between words. Check for either phrase. - const popupText = await popup3.textContent(); - expect( - popupText?.toLowerCase().includes('seconds ago') || - popupText?.toLowerCase().includes('just now') - ).toBeTruthy(); - - // Move mouse away to close popup - await app.code.driver.page.mouse.move(0, 0); - await expect(popup3).toBeHidden(); + await test.step('Cell 3 - Relative time display', async () => { + await notebooksPositron.addCodeToCell(3, 'print("relative time test")', { run: true }); + await notebooksPositron.expectToolTipToContain({ + completed: /Just now|seconds ago/, + }) + + await notebooksPositron.moveMouseAway(); + }) // ======================================== // Cell 4: Hover timing and interaction // ======================================== - // Execute code in a new cell and get the execution info icon - const popup4 = await executeCodeAndWaitForCompletion(app, 'print("hover test")', 4); + await test.step('Cell 4 - Hover timing and interaction', async () => { + await notebooksPositron.addCodeToCell(4, 'print("hover test")', { run: true }); - // Test popup closes when mouse moves away - await app.code.driver.page.mouse.move(0, 0); - await expect(popup4).toBeHidden(); - - // Test that hovering again after closing still works - await app.code.driver.page.getByRole('button', { name: 'Execute cell' }).hover(); - await expect(popup4).toBeVisible(); - - // ======================================== - // Cleanup - // ======================================== - // Move mouse away to ensure tooltip is hidden before closing - await app.code.driver.page.mouse.move(0, 0); - await expect(app.code.driver.page.getByRole('tooltip', { name: 'Cell execution details' })).toBeHidden(); + // Test popup closes when mouse moves away + await notebooksPositron.moveMouseAway(); + await notebooksPositron.expectToolTipVisible(false) - // Close the notebook without saving - await app.workbench.notebooks.closeNotebookWithoutSaving(); + // Test that hovering again after closing still works + await notebooksPositron.runCellButtonAtIndex(4).hover(); + await notebooksPositron.expectToolTipVisible(true); + }) }); }); diff --git a/test/e2e/tests/notebook/notebook-create.test.ts b/test/e2e/tests/notebook/notebook-create.test.ts index a23af24413e9..9cc8f4076455 100644 --- a/test/e2e/tests/notebook/notebook-create.test.ts +++ b/test/e2e/tests/notebook/notebook-create.test.ts @@ -40,7 +40,7 @@ test.describe('Notebooks', { }); test('Python - Verify code cell execution in notebook', async function ({ app }) { - await app.workbench.notebooks.addCodeToCellAtIndex('eval("8**2")'); + await app.workbench.notebooks.addCodeToCellAtIndex(0, 'eval("8**2")'); await app.workbench.notebooks.executeCodeInCell(); await app.workbench.notebooks.assertCellOutput('64'); }); @@ -62,7 +62,7 @@ test.describe('Notebooks', { await runCommand('workbench.action.toggleAuxiliaryBar'); // First, create and execute a cell to verify initial session - await notebooks.addCodeToCellAtIndex('foo = "bar"'); + await notebooks.addCodeToCellAtIndex(0, 'foo = "bar"'); await expect.poll( async () => { @@ -102,7 +102,7 @@ test.describe('Notebooks', { // Create a new variable using the now saved notebook // Add code to the new cell (using typeInEditor since addCodeToLastCell isn't available) - await notebooks.addCodeToCellAtIndex('baz = "baz"', 1); + await notebooks.addCodeToCellAtIndex(1, 'baz = "baz"'); await expect(async () => { await notebooks.selectCellAtIndex(1); await notebooks.executeActiveCell(); @@ -114,11 +114,11 @@ test.describe('Notebooks', { await app.workbench.notebooks.insertNotebookCell('code'); - await app.workbench.notebooks.addCodeToCellAtIndex('import torch'); + await app.workbench.notebooks.addCodeToCellAtIndex(0, 'import torch'); await app.workbench.notebooks.insertNotebookCell('code'); - await app.workbench.notebooks.addCodeToCellAtIndex('torch.rand(10)', 1); + await app.workbench.notebooks.addCodeToCellAtIndex(1, 'torch.rand(10)'); // toPass block seems to be needed on Ubuntu await expect(async () => { @@ -148,7 +148,7 @@ test.describe('Notebooks', { }); test('R - Verify code cell execution in notebook', async function ({ app }) { - await app.workbench.notebooks.addCodeToCellAtIndex('eval(parse(text="8**2"))'); + await app.workbench.notebooks.addCodeToCellAtIndex(0, 'eval(parse(text="8**2"))'); await app.workbench.notebooks.executeCodeInCell(); await app.workbench.notebooks.assertCellOutput('[1] 64'); }); diff --git a/test/e2e/tests/notebook/notebook-debug.test.ts b/test/e2e/tests/notebook/notebook-debug.test.ts index 9ce9a7df298a..0018897e4993 100644 --- a/test/e2e/tests/notebook/notebook-debug.test.ts +++ b/test/e2e/tests/notebook/notebook-debug.test.ts @@ -56,7 +56,7 @@ test.describe('Notebook Debugging', { 'print(message)' ].join('\n'); - await app.workbench.notebooks.addCodeToCellAtIndex(code, 0); + await app.workbench.notebooks.addCodeToCellAtIndex(0, code); // Set BPs await app.workbench.debug.setBreakpointOnLine(5); // intermediate calculation diff --git a/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts b/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts index a2b2b5c26657..ed8916d86d77 100644 --- a/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts +++ b/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts @@ -3,7 +3,6 @@ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ -import { Application } from '../../infra/index.js'; import { test, tags } from '../_test.setup'; import { expect } from '@playwright/test'; @@ -11,343 +10,146 @@ test.use({ suiteId: __filename }); -/** - * Helper function to get the currently focused cell index - * Checks if the cell or any of its children contain the active element - */ -async function getFocusedCellIndex(app: Application): Promise { - const cells = app.code.driver.page.locator('[data-testid="notebook-cell"]'); - const cellCount = await cells.count(); - - for (let i = 0; i < cellCount; i++) { - const cell = cells.nth(i); - const isFocused = await cell.evaluate((element) => { - // Check if this cell or any descendant has focus - return element.contains(document.activeElement) || - element === document.activeElement; - }); - - if (isFocused) { - return i; - } - } - return null; -} - -/** - * Helper function to check if a cell is selected (has selection styling) - */ -async function isCellSelected(app: Application, index: number): Promise { - const cell = app.code.driver.page.locator('[data-testid="notebook-cell"]').nth(index); - const ariaSelected = await cell.getAttribute('aria-selected'); - return ariaSelected === 'true'; -} - -/** - * Helper function to get cell count - */ -async function getCellCount(app: Application): Promise { - return await app.code.driver.page.locator('[data-testid="notebook-cell"]').count(); -} - -/** - * Helper function to wait for focus to settle (useful after DOM changes) - * Waits until any notebook cell has focus (or until timeout) - */ -async function waitForFocusSettle(app: Application, timeoutMs: number = 2000): Promise { - const page = app.code.driver.page; - await page.waitForFunction(() => { - const cells = Array.from(document.querySelectorAll('[data-testid="notebook-cell"]')); - return cells.some(cell => - cell.contains(document.activeElement) || cell === document.activeElement - ); - }, { timeout: timeoutMs }); -} - -/** - * Helper function to check if the Monaco editor in a cell is focused - */ -async function isEditorFocused(app: Application, cellIndex: number): Promise { - const cell = app.code.driver.page.locator('[data-testid="notebook-cell"]').nth(cellIndex); - const editor = cell.locator('.monaco-editor'); - - // Check if the monaco editor or any of its children has focus - return await editor.evaluate((element) => { - return element.contains(document.activeElement); - }); -} - -/** - * Helper function to normalize cell content by replacing non-breaking spaces with regular spaces - */ -function normalizeCellContent(content: string): string { - // Replace non-breaking spaces (U+00A0) with regular spaces - return content.replace(/\u00A0/g, ' ').replace(/ /g, ' '); -} - // Not running on web due to Positron notebooks being desktop-only test.describe('Notebook Focus and Selection', { tag: [tags.CRITICAL, tags.WIN, tags.NOTEBOOKS] }, () => { test.beforeAll(async function ({ app, settings }) { - await app.workbench.notebooksPositron.enablePositronNotebooks(settings); - await app.workbench.notebooksPositron.setNotebookEditor(settings, 'positron'); + await app.workbench.notebooksPositron.enableFeature(settings, { + editor: 'positron', + reload: true, + }); }); test.beforeEach(async function ({ app }) { - // Create a fresh notebook with 5 cells for each test - await app.workbench.notebooks.createNewNotebook(); - await app.workbench.notebooksPositron.expectToBeVisible(); + const { notebooksPositron } = app.workbench; - // Add content to cells - await app.workbench.notebooksPositron.addCodeToCellAtIndex('print("Cell 0")', 0); - await app.workbench.notebooksPositron.addCodeToCellAtIndex('print("Cell 1")', 1); - await app.workbench.notebooksPositron.addCodeToCellAtIndex('print("Cell 2")', 2); - await app.workbench.notebooksPositron.addCodeToCellAtIndex('print("Cell 3")', 3); - await app.workbench.notebooksPositron.addCodeToCellAtIndex('print("Cell 4")', 4); - - expect(await getCellCount(app)).toBe(5); + // Create a fresh notebook with 5 cells for each test + await notebooksPositron.createNewNotebook(); + await notebooksPositron.expectToBeVisible(); + + // Add 5 cells with distinct content + await notebooksPositron.addCodeToCell(0, 'print("Cell 0")'); + await notebooksPositron.addCodeToCell(1, 'print("Cell 1")'); + await notebooksPositron.addCodeToCell(2, 'print("Cell 2")'); + await notebooksPositron.addCodeToCell(3, 'print("Cell 3")'); + await notebooksPositron.addCodeToCell(4, 'print("Cell 4")'); + await notebooksPositron.expectCellCountToBe(5); }); - test.afterEach(async function ({ app, hotKeys }) { + test.afterEach(async function ({ hotKeys }) { await hotKeys.closeAllEditors(); }); - test('Cell selection via click focuses cell and adds selection styling', async function ({ app }) { - // Click on cell 2 - await app.workbench.notebooksPositron.selectCellAtIndex(2); - await waitForFocusSettle(app, 200); - - // Verify cell is focused - expect(await getFocusedCellIndex(app)).toBe(2); - - // Verify cell is selected (has aria-selected="true") - expect(await isCellSelected(app, 2)).toBe(true); - - // Verify other cells are not selected - expect(await isCellSelected(app, 0)).toBe(false); - expect(await isCellSelected(app, 1)).toBe(false); - }); - - // NOTE: Clicking a selected cell currently does NOT deselect it in the current implementation - // This test is commented out as the behavior may change during refactoring - // test('Clicking a selected cell deselects it', async function ({ app }) { ... }); - - test('Arrow Down navigation moves focus to next cell', async function ({ app }) { - // Select cell 1 - await app.workbench.notebooksPositron.selectCellAtIndex(1); - await waitForFocusSettle(app, 200); - expect(await getFocusedCellIndex(app)).toBe(1); - - // Press Escape to ensure we're not in edit mode - await app.code.driver.page.keyboard.press('Escape'); - await waitForFocusSettle(app, 100); - - // Press Arrow Down - await app.code.driver.page.keyboard.press('ArrowDown'); - await waitForFocusSettle(app, 200); - - // Focus should move to cell 2 - expect(await getFocusedCellIndex(app)).toBe(2); - expect(await isCellSelected(app, 2)).toBe(true); - }); - - test('Arrow Up navigation moves focus to previous cell', async function ({ app }) { - // Select cell 3 - await app.workbench.notebooksPositron.selectCellAtIndex(3); - await waitForFocusSettle(app, 200); - expect(await getFocusedCellIndex(app)).toBe(3); - - // Press Escape to ensure we're not in edit mode - await app.code.driver.page.keyboard.press('Escape'); - await waitForFocusSettle(app, 100); - - // Press Arrow Up - await app.code.driver.page.keyboard.press('ArrowUp'); - await waitForFocusSettle(app, 200); - - // Focus should move to cell 2 - expect(await getFocusedCellIndex(app)).toBe(2); - expect(await isCellSelected(app, 2)).toBe(true); - }); - - test('Arrow Down at last cell does not change selection', async function ({ app }) { - // Select last cell (index 4) - await app.workbench.notebooksPositron.selectCellAtIndex(4); - await waitForFocusSettle(app, 200); - expect(await getFocusedCellIndex(app)).toBe(4); + test('Keyboard behavior with notebook cells', async function ({ app }) { + const { notebooksPositron } = app.workbench; - // Press Escape to ensure we're not in edit mode - await app.code.driver.page.keyboard.press('Escape'); - await waitForFocusSettle(app, 100); - - // Press Arrow Down - await app.code.driver.page.keyboard.press('ArrowDown'); - await waitForFocusSettle(app, 200); - - // Focus should remain on cell 4 - expect(await getFocusedCellIndex(app)).toBe(4); - }); - - test('Arrow Up at first cell does not change selection', async function ({ app }) { - // Select first cell (index 0) - await app.workbench.notebooksPositron.selectCellAtIndex(0); - await waitForFocusSettle(app, 200); - expect(await getFocusedCellIndex(app)).toBe(0); + await test.step('Test 1: Arrow Down navigation moves focus to next cell', async () => { + await notebooksPositron.selectCellAtIndex(1, { exitEditMode: true }); + await app.code.driver.page.keyboard.press('ArrowDown'); + await notebooksPositron.expectCellIndexToBeSelected(2, { inEditMode: false }); + }); - // Press Escape to ensure we're not in edit mode - await app.code.driver.page.keyboard.press('Escape'); - await waitForFocusSettle(app, 100); + await test.step('Test 2: Arrow Up navigation moves focus to previous cell', async () => { + await notebooksPositron.selectCellAtIndex(3, { exitEditMode: true }); + await app.code.driver.page.keyboard.press('ArrowUp'); + await notebooksPositron.expectCellIndexToBeSelected(2, { inEditMode: false }); + }); - // Press Arrow Up - await app.code.driver.page.keyboard.press('ArrowUp'); - await waitForFocusSettle(app, 200); + await test.step('Test 3: Arrow Down at last cell does not change selection', async () => { + await notebooksPositron.selectCellAtIndex(4, { exitEditMode: true }); + await app.code.driver.page.keyboard.press('ArrowDown'); + await notebooksPositron.expectCellIndexToBeSelected(4, { inEditMode: false }); + }); - // Focus should remain on cell 0 - expect(await getFocusedCellIndex(app)).toBe(0); - }); + await test.step('Test 4: Arrow Up at first cell does not change selection', async () => { + await notebooksPositron.selectCellAtIndex(0, { exitEditMode: true }); + await app.code.driver.page.keyboard.press('ArrowUp'); + await notebooksPositron.expectCellIndexToBeSelected(0, { inEditMode: false }); + }); - test('Shift+Arrow Down adds next cell to selection', async function ({ app }) { - // Select cell 1 - await app.workbench.notebooksPositron.selectCellAtIndex(1); - await waitForFocusSettle(app, 200); + await test.step('Test 5: Shift+Arrow Down adds next cell to selection', async () => { + await notebooksPositron.selectCellAtIndex(1, { exitEditMode: true }); + await app.code.driver.page.keyboard.press('Shift+ArrowDown'); + await notebooksPositron.expectCellIndexToBeSelected(1, { inEditMode: false }); + await notebooksPositron.expectCellIndexToBeSelected(2, { inEditMode: false }); + }); - // Press Escape to ensure we're not in edit mode - await app.code.driver.page.keyboard.press('Escape'); - await waitForFocusSettle(app, 100); + await test.step('Test 6: Focus is maintained across multiple navigation operations', async () => { + await notebooksPositron.selectCellAtIndex(0, { exitEditMode: true }); - // Shift+Arrow Down - await app.code.driver.page.keyboard.press('Shift+ArrowDown'); - await waitForFocusSettle(app, 200); + // Navigate down multiple times + await app.code.driver.page.keyboard.press('ArrowDown'); + await notebooksPositron.expectCellIndexToBeSelected(1, { inEditMode: false }); - // Both cell 1 and cell 2 should be selected - expect(await isCellSelected(app, 1)).toBe(true); - expect(await isCellSelected(app, 2)).toBe(true); - }); + await app.code.driver.page.keyboard.press('ArrowDown'); + await notebooksPositron.expectCellIndexToBeSelected(2, { inEditMode: false }); - test('Focus is maintained across multiple navigation operations', async function ({ app }) { - // Start at cell 0 - await app.workbench.notebooksPositron.selectCellAtIndex(0); - await waitForFocusSettle(app, 200); - await app.code.driver.page.keyboard.press('Escape'); - await waitForFocusSettle(app, 100); - - // Navigate down twice - await app.code.driver.page.keyboard.press('ArrowDown'); - await waitForFocusSettle(app, 150); - expect(await getFocusedCellIndex(app)).toBe(1); - - await app.code.driver.page.keyboard.press('ArrowDown'); - await waitForFocusSettle(app, 150); - expect(await getFocusedCellIndex(app)).toBe(2); - - // Navigate down once more - await app.code.driver.page.keyboard.press('ArrowDown'); - await waitForFocusSettle(app, 150); - expect(await getFocusedCellIndex(app)).toBe(3); - - // Navigate up - await app.code.driver.page.keyboard.press('ArrowUp'); - await waitForFocusSettle(app, 150); - expect(await getFocusedCellIndex(app)).toBe(2); - }); + // Navigate down once more + await app.code.driver.page.keyboard.press('ArrowDown'); + await notebooksPositron.expectCellIndexToBeSelected(3, { inEditMode: false }); - test('Sanity check: clicking editor focuses it (validates isEditorFocused helper)', async function ({ app }) { - // Select cell 1 - await app.workbench.notebooksPositron.selectCellAtIndex(1); - await waitForFocusSettle(app, 200); - - // Press Escape to exit edit mode (selectCellAtIndex may enter edit mode) - await app.code.driver.page.keyboard.press('Escape'); - await waitForFocusSettle(app, 200); - - // Editor should not be focused after pressing Escape - expect(await isEditorFocused(app, 1)).toBe(false); - - // Click directly into the Monaco editor - const cell = app.code.driver.page.locator('[data-testid="notebook-cell"]').nth(1); - const editor = cell.locator('.monaco-editor'); - await editor.click(); - await waitForFocusSettle(app, 200); - - // Now editor should be focused - expect(await isEditorFocused(app, 1)).toBe(true); - - // Type some text to confirm editor is really focused - await app.code.driver.page.keyboard.type('# editor good'); - const cellContent = await app.workbench.notebooksPositron.getCellContent(1); - // Normalize content to handle non-breaking spaces - const normalizedContent = normalizeCellContent(cellContent); - expect(normalizedContent).toContain('# editor good'); - }); - - test('Enter key on selected cell enters edit mode', async function ({ app }) { - // Select cell 2 - await app.workbench.notebooksPositron.selectCellAtIndex(2); - await waitForFocusSettle(app, 200); + // Navigate up + await app.code.driver.page.keyboard.press('ArrowUp'); + await notebooksPositron.expectCellIndexToBeSelected(2, { inEditMode: false }); + }); + }) + + test('Editor mode behavior with notebook cells', async function ({ app }) { + const { notebooksPositron } = app.workbench; + + await test.step('Test 1: Clicking into cell focuses editor and enters edit mode', async () => { + // Clicking on cell should focus and enter edit mode + await notebooksPositron.selectCellAtIndex(1); + await notebooksPositron.expectCellIndexToBeSelected(0, { isSelected: false, inEditMode: false }); + await notebooksPositron.expectCellIndexToBeSelected(1, { isSelected: true, inEditMode: true }); + await notebooksPositron.expectCellIndexToBeSelected(2, { isSelected: false, inEditMode: false }); + await notebooksPositron.expectCellIndexToBeSelected(3, { isSelected: false, inEditMode: false }); + await notebooksPositron.expectCellIndexToBeSelected(4, { isSelected: false, inEditMode: false }) + + // Verify we can type into the editor after clicking + await app.code.driver.page.keyboard.type('# editor good'); + await notebooksPositron.expectCellContentAtIndexToContain(1, '# editor good'); + }); - // Press Escape to ensure we're not in edit mode - await app.code.driver.page.keyboard.press('Escape'); - await waitForFocusSettle(app, 200); + await test.step('Test 2: Enter key on selected cell enters edit mode and doesn\'t add new lines', async () => { + // Verify pressing Enter enters edit mode + await notebooksPositron.selectCellAtIndex(2, { exitEditMode: true }); + await app.code.driver.page.keyboard.press('Enter'); + await notebooksPositron.expectCellIndexToBeSelected(2, { + isSelected: true, + inEditMode: true + }); + + // Verify we can type into the editor after pressing Enter + await app.code.driver.page.keyboard.type('# test'); + await notebooksPositron.expectCellContentAtIndexToContain(2, /^print\("Cell 2"\)# test/); + }); - // Verify cell is selected (not in edit mode) - expect(await isCellSelected(app, 2)).toBe(true); - expect(await isEditorFocused(app, 2)).toBe(false); + await test.step('Test 3: Shift+Enter on last cell creates new cell and enters edit mode', async () => { + // Select last cell (index 4) + await notebooksPositron.selectCellAtIndex(4); - // Press Enter to enter edit mode - await app.code.driver.page.keyboard.press('Enter'); - await waitForFocusSettle(app, 300); + // Get initial cell count + const initialCount = await notebooksPositron.cell.count(); + expect(initialCount).toBe(5); - // Verify Monaco editor is now focused - expect(await isEditorFocused(app, 2)).toBe(true); + // Press Shift+Enter to add a new cell below + await app.code.driver.page.keyboard.press('Shift+Enter'); - // Verify we can type in the editor - await app.code.driver.page.keyboard.type('# test'); - await waitForFocusSettle(app, 100); + // Verify new cell was added + const newCount = await notebooksPositron.cell.count(); + expect(newCount).toBe(6); - // Verify content was added (cell should contain original + new text) - const cellContent = await app.workbench.notebooksPositron.getCellContent(2); - const normalizedContent = normalizeCellContent(cellContent); - expect(normalizedContent).toContain('# test'); + // Verify the NEW cell (index 5) is now in edit mode with focus + await notebooksPositron.expectCellIndexToBeSelected(5, { inEditMode: true }); - // Verify no extra newline was added at the beginning (Enter key didn't bleed through) - // The content should start with the original content, not a newline - expect(normalizedContent).toMatch(/^print\("Cell 2"\)/); + // Verify we can type immediately in the new cell + await app.code.driver.page.keyboard.type('new cell content'); + await notebooksPositron.expectCellContentAtIndexToContain(5, 'new cell content'); + }); }); - test('Shift+Enter on last cell creates new cell and enters edit mode', async function ({ app }) { - // Select last cell (index 4) - await app.workbench.notebooksPositron.selectCellAtIndex(4); - await waitForFocusSettle(app, 200); - - // Enter edit mode on the last cell - await app.code.driver.page.keyboard.press('Enter'); - await waitForFocusSettle(app, 300); - expect(await isEditorFocused(app, 4)).toBe(true); - // Get initial cell count - const initialCount = await getCellCount(app); - expect(initialCount).toBe(5); - - // Press Shift+Enter to add a new cell below - await app.code.driver.page.keyboard.press('Shift+Enter'); - await waitForFocusSettle(app, 500); - - // Verify new cell was added - const newCount = await getCellCount(app); - expect(newCount).toBe(6); - - // Verify the NEW cell (index 5) is now in edit mode with focus - expect(await isEditorFocused(app, 5)).toBe(true); - expect(await isCellSelected(app, 5)).toBe(true); - - // Verify we can type immediately in the new cell - await app.code.driver.page.keyboard.type('new cell content'); - await waitForFocusSettle(app, 100); - - const newCellContent = await app.workbench.notebooksPositron.getCellContent(5); - const normalizedContent = normalizeCellContent(newCellContent); - expect(normalizedContent).toContain('new cell content'); - }); }); diff --git a/test/e2e/tests/notebook/notebook-integration.test.ts b/test/e2e/tests/notebook/notebook-integration.test.ts index 2be564f4043a..9d1e7cf19903 100644 --- a/test/e2e/tests/notebook/notebook-integration.test.ts +++ b/test/e2e/tests/notebook/notebook-integration.test.ts @@ -74,7 +74,7 @@ env_vars = ['PATH', 'HOME', 'USER'] for var in env_vars: value = os.environ.get(var, 'Not set') print(f"{var}: {value[:50]}..." if len(str(value)) > 50 else f"{var}: {value}")`; - await app.workbench.notebooks.addCodeToCellAtIndex(envCode); + await app.workbench.notebooks.addCodeToCellAtIndex(0, envCode); await app.workbench.notebooks.executeCodeInCell(); await app.workbench.notebooks.assertCellOutput('Python version:'); await app.workbench.notebooks.assertCellOutput('Platform:'); @@ -90,7 +90,7 @@ data_dict = {'name': 'test', 'value': 42} print(f"Global variable: {global_var}") print(f"Numbers list: {numbers}") print(f"Data dictionary: {data_dict}")`; - await app.workbench.notebooks.addCodeToCellAtIndex(variableCode); + await app.workbench.notebooks.addCodeToCellAtIndex(0, variableCode); await app.workbench.notebooks.executeCodeInCell(); await app.workbench.notebooks.insertNotebookCell('code'); const useVariableCode = `# Use variables from previous cell @@ -104,7 +104,7 @@ data_dict['new_key'] = 'new_value' print(f"Modified numbers: {numbers}") print(f"Modified dictionary: {data_dict}")`; - await app.workbench.notebooks.addCodeToCellAtIndex(useVariableCode, 1); + await app.workbench.notebooks.addCodeToCellAtIndex(1, useVariableCode); await app.workbench.notebooks.executeCodeInCell(); await app.workbench.notebooks.assertCellOutput('Hello from first cell', 0); await app.workbench.notebooks.assertCellOutput('Sum of numbers: 15'); @@ -151,7 +151,7 @@ print(f"Trigonometry (45 degrees):") print(f"sin: {sin_val:.3f}") print(f"cos: {cos_val:.3f}") print(f"tan: {tan_val:.3f}")`; - await app.workbench.notebooks.addCodeToCellAtIndex(mathCode); + await app.workbench.notebooks.addCodeToCellAtIndex(0, mathCode); await app.workbench.notebooks.executeCodeInCell(); await app.workbench.notebooks.insertNotebookCell('code'); const advancedMathCode = `# Logarithms and exponentials @@ -172,7 +172,7 @@ square_root = math.sqrt(16) cube_root = 27 ** (1/3) print(f"sqrt(16) = {square_root}") print(f"cbrt(27) = {cube_root:.1f}")`; - await app.workbench.notebooks.addCodeToCellAtIndex(advancedMathCode, 1); + await app.workbench.notebooks.addCodeToCellAtIndex(1, advancedMathCode); await app.workbench.notebooks.executeCodeInCell(); await app.workbench.notebooks.assertCellOutput('Sum: 55'); await app.workbench.notebooks.assertCellOutput('Mean: 5.5'); @@ -210,7 +210,7 @@ df <- data.frame( cat("Dataset dimensions:", dim(df), "\n") cat("Column names:", names(df), "\n") print(head(df, 3))`; - await app.workbench.notebooks.addCodeToCellAtIndex(rDataCode); + await app.workbench.notebooks.addCodeToCellAtIndex(0, rDataCode); await app.workbench.notebooks.executeCodeInCell(); await app.workbench.notebooks.insertNotebookCell('code'); const aggregationCode = `# Group by operations @@ -222,7 +222,7 @@ print(agg_stats) max_values <- aggregate(cbind(value1, value2) ~ group, data = df, FUN = max) cat("\nMax values per group:\n") print(max_values)`; - await app.workbench.notebooks.addCodeToCellAtIndex(aggregationCode, 1); + await app.workbench.notebooks.addCodeToCellAtIndex(1, aggregationCode); await app.workbench.notebooks.executeCodeInCell(); await app.workbench.notebooks.assertCellOutput('Dataset dimensions: 20 4'); await app.workbench.notebooks.assertCellOutput('Column names: id group value1 value2'); diff --git a/test/e2e/tests/notebook/notebook-raises-exception.test.ts b/test/e2e/tests/notebook/notebook-raises-exception.test.ts index 84396e80f8ac..151fe8f7878c 100644 --- a/test/e2e/tests/notebook/notebook-raises-exception.test.ts +++ b/test/e2e/tests/notebook/notebook-raises-exception.test.ts @@ -9,7 +9,7 @@ test.use({ suiteId: __filename }); -test.describe('Notebook Cell Execution with raises-exception tag', { +test.describe('Notebooks: Cell Execution with raises-exception tag', { tag: [tags.NOTEBOOKS, tags.WIN, tags.WEB] }, () => { @@ -20,29 +20,31 @@ test.describe('Notebook Cell Execution with raises-exception tag', { await app.workbench.notebooks.selectInterpreter('Python'); }); - test.afterEach(async function ({ app }) { - await app.workbench.notebooks.closeNotebookWithoutSaving(); + test.afterEach(async function ({ hotKeys }) { + await hotKeys.closeAllEditors(); }); test('Python - Execution stops at exception without raises-exception tag', async function ({ app, page, hotKeys }) { + const { notebooks } = app.workbench; + // Cell 1: Normal execution await hotKeys.scrollToTop(); - await app.workbench.notebooks.addCodeToCellAtIndex('print("Cell 1 executed")'); + await notebooks.addCodeToCellAtIndex(0, 'print("Cell 1 executed")'); // Cell 2: Exception without tag (should stop execution) - await app.workbench.notebooks.insertNotebookCell('code'); - await app.workbench.notebooks.addCodeToCellAtIndex('raise ValueError("This should stop execution")', 1); + await notebooks.insertNotebookCell('code'); + await notebooks.addCodeToCellAtIndex(1, 'raise ValueError("This should stop execution")'); // Cell 3: Should NOT execute - await app.workbench.notebooks.insertNotebookCell('code'); - await app.workbench.notebooks.addCodeToCellAtIndex('print("Cell 3 should not execute")', 2); + await notebooks.insertNotebookCell('code'); + await notebooks.addCodeToCellAtIndex(2, 'print("Cell 3 should not execute")'); // Run all cells - await app.workbench.notebooks.runAllCells(); + await notebooks.runAllCells(); // Verify outputs - await app.workbench.notebooks.assertCellOutput('Cell 1 executed'); - await app.workbench.notebooks.assertCellOutput('ValueError: This should stop execution'); + await notebooks.assertCellOutput('Cell 1 executed'); + await notebooks.assertCellOutput('ValueError: This should stop execution'); // Cell 3 should have no output const cell3Output = page.locator('.cell-inner-container > .cell').nth(2).locator('.output'); @@ -50,13 +52,15 @@ test.describe('Notebook Cell Execution with raises-exception tag', { }); test('Python - Execution continues after exception with raises-exception tag', async function ({ app, page, hotKeys }) { + const { notebooks, quickInput } = app.workbench; + // Cell 1: Normal execution await hotKeys.scrollToTop(); - await app.workbench.notebooks.addCodeToCellAtIndex('print("Cell 1 executed")'); + await notebooks.addCodeToCellAtIndex(0, 'print("Cell 1 executed")'); // Cell 2: Exception with raises-exception tag - await app.workbench.notebooks.insertNotebookCell('code'); - await app.workbench.notebooks.addCodeToCellAtIndex('raise ValueError("Expected error - execution should continue")', 1); + await notebooks.insertNotebookCell('code'); + await notebooks.addCodeToCellAtIndex(1, 'raise ValueError("Expected error - execution should continue")'); // Add raises-exception tag to Cell 2 // First, ensure the cell is selected @@ -66,22 +70,22 @@ test.describe('Notebook Cell Execution with raises-exception tag', { await hotKeys.jupyterCellAddTag(); // Type the tag name in the quick input - await app.workbench.quickInput.waitForQuickInputOpened(); - await app.workbench.quickInput.type('raises-exception'); + await quickInput.waitForQuickInputOpened(); + await quickInput.type('raises-exception'); // Press Enter key to submit (there's no okay button to press) await page.keyboard.press('Enter'); // Cell 3: Should execute despite Cell 2 error - await app.workbench.notebooks.insertNotebookCell('code'); - await app.workbench.notebooks.addCodeToCellAtIndex('print("Cell 3 executed successfully!")', 2); + await notebooks.insertNotebookCell('code'); + await notebooks.addCodeToCellAtIndex(2, 'print("Cell 3 executed successfully!")'); // Run all cells by clicking the "Run All" button - await app.workbench.notebooks.runAllCells(); + await notebooks.runAllCells(); // Verify outputs - await app.workbench.notebooks.assertCellOutput('Cell 1 executed'); - await app.workbench.notebooks.assertCellOutput('ValueError: Expected error - execution should continue'); - await app.workbench.notebooks.assertCellOutput('Cell 3 executed successfully!'); + await notebooks.assertCellOutput('Cell 1 executed'); + await notebooks.assertCellOutput('ValueError: Expected error - execution should continue'); + await notebooks.assertCellOutput('Cell 3 executed successfully!'); }); }); }); diff --git a/test/e2e/tests/notebook/positron-notebook-copy-paste.test.ts b/test/e2e/tests/notebook/positron-notebook-copy-paste.test.ts index 2ce597d6a680..879c9fa13618 100644 --- a/test/e2e/tests/notebook/positron-notebook-copy-paste.test.ts +++ b/test/e2e/tests/notebook/positron-notebook-copy-paste.test.ts @@ -11,38 +11,38 @@ test.use({ }); // Not running on web due to https://github.com/posit-dev/positron/issues/9193 -test.describe('Notebook Cell Copy-Paste Behavior', { +test.describe('Positron Notebooks: Cell Copy-Paste Behavior', { tag: [tags.CRITICAL, tags.WIN, tags.NOTEBOOKS] }, () => { - test.beforeAll(async function ({ settings }) { - await settings.set({ - 'positron.notebook.enabled': true, - 'workbench.editorAssociations': { '*.ipynb': 'workbench.editor.positronNotebook' } - }, { reload: true }) + test.beforeAll(async ({ app, settings }) => { + await app.workbench.notebooksPositron.enableFeature(settings, { + editor: 'positron', + reload: true, + }); }); test.afterEach(async function ({ hotKeys }) { await hotKeys.closeAllEditors(); - }) + }); - test('Cell copy-paste behavior - comprehensive test', async function ({ app }) { + test('Should correctly copy and paste cell content in various scenarios', async function ({ app }) { const { notebooksPositron } = app.workbench; // ======================================== // Setup: Create notebook with 5 cells and distinct content // ======================================== - await test.step(' Test Setup: Create notebook and add cells', async () => { + await test.step('Test Setup: Create notebook and add cells', async () => { // Setup: Create notebook and select kernel once await notebooksPositron.createNewNotebook(); await notebooksPositron.expectToBeVisible(); // Setup: Create 5 cells with distinct content - await notebooksPositron.addCodeToCellAtIndex('# Cell 0', 0); - await notebooksPositron.addCodeToCellAtIndex('# Cell 1', 1); - await notebooksPositron.addCodeToCellAtIndex('# Cell 2', 2); - await notebooksPositron.addCodeToCellAtIndex('# Cell 3', 3); - await notebooksPositron.addCodeToCellAtIndex('# Cell 4', 4); + await notebooksPositron.addCodeToCell(0, '# Cell 0'); + await notebooksPositron.addCodeToCell(1, '# Cell 1'); + await notebooksPositron.addCodeToCell(2, '# Cell 2'); + await notebooksPositron.addCodeToCell(3, '# Cell 3'); + await notebooksPositron.addCodeToCell(4, '# Cell 4'); // Verify we have 5 cells await notebooksPositron.expectCellCountToBe(5); @@ -58,11 +58,11 @@ test.describe('Notebook Cell Copy-Paste Behavior', { await notebooksPositron.expectCellContentAtIndexToBe(2, '# Cell 2'); // Copy the cell - await notebooksPositron.copyCellsWithKeyboard(); + await notebooksPositron.performCellAction('copy'); // Move to last cell and paste after it await notebooksPositron.selectCellAtIndex(4); - await notebooksPositron.pasteCellsWithKeyboard(); + await notebooksPositron.performCellAction('paste'); // Verify cell count increased await notebooksPositron.expectCellCountToBe(6); @@ -81,7 +81,7 @@ test.describe('Notebook Cell Copy-Paste Behavior', { await notebooksPositron.expectCellContentAtIndexToBe(1, '# Cell 1'); // Cut the cell - await notebooksPositron.cutCellsWithKeyboard(); + await notebooksPositron.performCellAction('cut'); // Verify cell count decreased await notebooksPositron.expectCellCountToBe(5); @@ -91,7 +91,7 @@ test.describe('Notebook Cell Copy-Paste Behavior', { // Move to index 3 and paste await notebooksPositron.selectCellAtIndex(3); - await notebooksPositron.pasteCellsWithKeyboard(); + await notebooksPositron.performCellAction('paste'); // Verify cell count is back to 6 await notebooksPositron.expectCellCountToBe(6); @@ -108,11 +108,11 @@ test.describe('Notebook Cell Copy-Paste Behavior', { // Copy cell 0 await notebooksPositron.expectCellContentAtIndexToBe(0, '# Cell 0'); - await notebooksPositron.copyCellsWithKeyboard(); + await notebooksPositron.performCellAction('copy'); // Paste at position 2 await notebooksPositron.selectCellAtIndex(2); - await notebooksPositron.pasteCellsWithKeyboard(); + await notebooksPositron.performCellAction('paste'); // Verify first paste await notebooksPositron.expectCellCountToBe(7); @@ -120,14 +120,13 @@ test.describe('Notebook Cell Copy-Paste Behavior', { // Paste again at position 5 await notebooksPositron.selectCellAtIndex(5); - await notebooksPositron.pasteCellsWithKeyboard(); + await notebooksPositron.performCellAction('paste'); // Verify second paste await notebooksPositron.expectCellCountToBe(8); await notebooksPositron.expectCellContentAtIndexToBe(6, '# Cell 0'); }); - // ======================================== // Test 4: Cut and paste at beginning of notebook // ======================================== @@ -137,7 +136,7 @@ test.describe('Notebook Cell Copy-Paste Behavior', { const cellToMoveContent = await notebooksPositron.getCellContent(4); // Cut the cell - await notebooksPositron.cutCellsWithKeyboard(); + await notebooksPositron.performCellAction('cut'); // Verify cell removed await notebooksPositron.expectCellCountToBe(7); @@ -145,14 +144,14 @@ test.describe('Notebook Cell Copy-Paste Behavior', { // Move to first cell and paste // Note: Paste typically inserts after the current cell await notebooksPositron.selectCellAtIndex(0); - await notebooksPositron.pasteCellsWithKeyboard(); + await notebooksPositron.performCellAction('paste'); // Verify cell count restored await notebooksPositron.expectCellCountToBe(8); // Verify pasted cell is at index 1 (pasted after cell 0) await notebooksPositron.expectCellContentAtIndexToBe(1, cellToMoveContent); - }) + }); // ======================================== // Test 5: Cut all cells and verify notebook can be empty @@ -161,18 +160,18 @@ test.describe('Notebook Cell Copy-Paste Behavior', { // Delete cells until only one remains while (await notebooksPositron.cell.count() > 1) { await notebooksPositron.selectCellAtIndex(0); - await notebooksPositron.cutCellsWithKeyboard(); + await notebooksPositron.performCellAction('cut'); } // Verify we have exactly one cell await notebooksPositron.expectCellCountToBe(1); // Cut the last cell - in Positron notebooks, this may be allowed - await notebooksPositron.cutCellsWithKeyboard(); + await notebooksPositron.performCellAction('cut'); // Check if notebook can be empty (Positron may allow 0 cells) const finalCount = await notebooksPositron.cell.count(); expect(finalCount).toBeLessThanOrEqual(1); - }) + }); }); }); diff --git a/test/e2e/tests/notebook/positron-notebook-editor.test.ts b/test/e2e/tests/notebook/positron-notebook-editor.test.ts index 24f56e451d21..a06aa65884b1 100644 --- a/test/e2e/tests/notebook/positron-notebook-editor.test.ts +++ b/test/e2e/tests/notebook/positron-notebook-editor.test.ts @@ -12,21 +12,24 @@ test.use({ suiteId: __filename }); - -test.describe('Positron notebook opening and saving', { +test.describe('Positron Notebooks: Open & Save', { tag: [tags.CRITICAL, tags.WIN, tags.NOTEBOOKS] }, () => { test.beforeAll(async function ({ app, settings }) { - await app.workbench.notebooksPositron.enablePositronNotebooks(settings); + await app.workbench.notebooksPositron.enableFeature(settings, { + editor: 'default', + reload: true, + }); }); test.beforeEach(async function ({ app, settings }) { // Reset editor associations to default state before each test - await app.workbench.notebooksPositron.setNotebookEditor(settings, 'default'); + await app.workbench.notebooksPositron.enableFeature(settings, { + editor: 'default', + }); }); - test.afterEach(async function ({ app, hotKeys, settings }) { - await app.workbench.notebooksPositron.setNotebookEditor(settings, 'default'); + test.afterEach(async function ({ hotKeys }) { await hotKeys.closeAllEditors(); }); @@ -40,7 +43,9 @@ test.describe('Positron notebook opening and saving', { // Configure Positron as the default notebook editor // This sets workbench.editorAssociations to map *.ipynb files to the Positron notebook editor - await app.workbench.notebooksPositron.setNotebookEditor(settings, 'positron'); + await app.workbench.notebooksPositron.enableFeature(settings, { + editor: 'positron', + }); // Verify that newly opened notebooks now use the Positron editor // The same notebook file should now open with the Positron interface instead of VS Code @@ -50,7 +55,9 @@ test.describe('Positron notebook opening and saving', { // Reset to default configuration and verify VS Code editor is used again // Close all editors first to ensure a clean state for the next test await hotKeys.closeAllEditors(); - await app.workbench.notebooksPositron.setNotebookEditor(settings, 'default'); + await app.workbench.notebooksPositron.enableFeature(settings, { + editor: 'default', + }); // Confirm that removing the association restores VS Code notebook editor // This ensures the configuration change is properly applied and the fallback works @@ -63,7 +70,9 @@ test.describe('Positron notebook opening and saving', { const { notebooks, notebooksPositron, quickInput, editors } = app.workbench; // Configure Positron as the default notebook editor - await app.workbench.notebooksPositron.setNotebookEditor(settings, 'positron'); + await app.workbench.notebooksPositron.enableFeature(settings, { + editor: 'positron', + }); // Create a new untitled notebook await notebooks.createNewNotebook(); @@ -95,7 +104,9 @@ test.describe('Positron notebook opening and saving', { const { notebooks, notebooksPositron, editors } = app.workbench; // Configure Positron as the default notebook editor - await notebooksPositron.setNotebookEditor(settings, 'positron'); + await app.workbench.notebooksPositron.enableFeature(settings, { + editor: 'positron', + }); // Create a new notebook (which starts dirty) await notebooks.createNewNotebook(); @@ -109,10 +120,7 @@ test.describe('Positron notebook opening and saving', { test.expect(tabsBefore).toBe(1); // Reload the window to simulate restart - await hotKeys.reloadWindow(); - // Wait for the reload to complete - await app.code.driver.page.waitForTimeout(3000); - await app.code.driver.page.locator('.monaco-workbench').waitFor({ state: 'visible' }); + await hotKeys.reloadWindow(true); // After reload, check for the ghost editor issue // The bug would cause both a Positron notebook AND a VS Code notebook to be visible diff --git a/test/e2e/tests/notebook/positron-notebook-undo-redo.test.ts b/test/e2e/tests/notebook/positron-notebook-undo-redo.test.ts index 758d8bcbb495..0e01b8db72a9 100644 --- a/test/e2e/tests/notebook/positron-notebook-undo-redo.test.ts +++ b/test/e2e/tests/notebook/positron-notebook-undo-redo.test.ts @@ -3,130 +3,90 @@ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ -import { Application } from '../../infra/index.js'; import { test, tags } from '../_test.setup'; -import { expect } from '@playwright/test'; test.use({ suiteId: __filename }); -/** - * Helper function to get cell count - */ -async function getCellCount(app: Application): Promise { - return await app.code.driver.page.locator('[data-testid="notebook-cell"]').count(); -} - -/** - * Helper function to perform undo using keyboard shortcut - */ -async function undoWithKeyboard(app: Application): Promise { - // We need to press escape to get the focus out of the cell editor itself - await app.code.driver.page.keyboard.press('Escape'); - const modifierKey = process.platform === 'darwin' ? 'Meta' : 'Control'; - await app.code.driver.page.keyboard.press(`${modifierKey}+KeyZ`); -} - -/** - * Helper function to perform redo using keyboard shortcut - */ -async function redoWithKeyboard(app: Application): Promise { - // We need to press escape to get the focus out of the cell editor itself - await app.code.driver.page.keyboard.press('Escape'); - const modifierKey = process.platform === 'darwin' ? 'Meta' : 'Control'; - await app.code.driver.page.keyboard.press(`${modifierKey}+Shift+KeyZ`); -} - -/** - * Helper function to delete a cell using keyboard shortcut - */ -async function deleteCellWithKeyboard(app: Application): Promise { - // We need to press escape to get the focus out of the cell editor itself - await app.code.driver.page.keyboard.press('Escape'); - await app.code.driver.page.keyboard.press('Backspace'); -} - -/** - * Helper function to add a code cell below using keyboard shortcut - */ -async function addCodeCellBelowWithKeyboard(app: Application): Promise { - // We need to press escape to get the focus out of the cell editor itself - await app.code.driver.page.keyboard.press('Escape'); - await app.code.driver.page.keyboard.press('KeyB'); -} - // Not running on web due to https://github.com/posit-dev/positron/issues/9193 -test.describe('Notebook Cell Undo-Redo Behavior', { +test.describe('Postiron Notebooks: Cell Undo-Redo Behavior', { tag: [tags.CRITICAL, tags.WIN, tags.NOTEBOOKS] }, () => { + test.beforeAll(async function ({ app, settings }) { - await app.workbench.notebooksPositron.enablePositronNotebooks(settings); - // Configure Positron as the notebook editor - await app.workbench.notebooksPositron.setNotebookEditor(settings, 'positron'); + await app.workbench.notebooksPositron.enableFeature(settings, { + editor: 'positron', + reload: true, + }); }); - test('Cell undo-redo behavior - comprehensive test', async function ({ app }) { - // Setup: Create notebook - await app.workbench.notebooks.createNewNotebook(); - await app.workbench.notebooksPositron.expectToBeVisible(); - - // ======================================== - // Test 1: Basic add cell and undo/redo - // ======================================== - // Start with initial cell - await app.workbench.notebooksPositron.addCodeToCellAtIndex('# Initial Cell', 0); - expect(await getCellCount(app)).toBe(1); - expect(await app.workbench.notebooksPositron.getCellContent(0)).toBe('# Initial Cell'); - - // Add a second cell - await app.workbench.notebooksPositron.selectCellAtIndex(0); - await addCodeCellBelowWithKeyboard(app); - await app.workbench.notebooksPositron.addCodeToCellAtIndex('# Second Cell', 1); - expect(await getCellCount(app)).toBe(2); - expect(await app.workbench.notebooksPositron.getCellContent(1)).toBe('# Second Cell'); + test.afterEach(async function ({ hotKeys }) { + await hotKeys.closeAllEditors(); + }); - // Undo the add cell operation - await undoWithKeyboard(app); - expect(await getCellCount(app)).toBe(1); - expect(await app.workbench.notebooksPositron.getCellContent(0)).toBe('# Initial Cell'); + test('Should correctly undo and redo cell actions', async function ({ app }) { + const { notebooks, notebooksPositron } = app.workbench; - // Redo the add cell operation to add back cell - await redoWithKeyboard(app); - expect(await getCellCount(app)).toBe(2); - expect(await app.workbench.notebooksPositron.getCellContent(1)).toBe('# Second Cell'); + await test.step('Test Setup: Create notebook', async () => { + await notebooks.createNewNotebook(); + await notebooksPositron.expectToBeVisible(); + }); // ======================================== - // Test 2: Delete cell and undo/redo + // Test 1: Basic add cell and undo/redo // ======================================== - // Add a third cell for deletion test - await app.workbench.notebooksPositron.selectCellAtIndex(1); - await addCodeCellBelowWithKeyboard(app); - await app.workbench.notebooksPositron.addCodeToCellAtIndex('# Cell to Delete', 2); - expect(await getCellCount(app)).toBe(3); - - // Delete the middle cell - await app.workbench.notebooksPositron.selectCellAtIndex(1); - await deleteCellWithKeyboard(app); - expect(await getCellCount(app)).toBe(2); - expect(await app.workbench.notebooksPositron.getCellContent(0)).toBe('# Initial Cell'); - expect(await app.workbench.notebooksPositron.getCellContent(1)).toBe('# Cell to Delete'); - - // Undo the delete - await undoWithKeyboard(app); - expect(await getCellCount(app)).toBe(3); - expect(await app.workbench.notebooksPositron.getCellContent(1)).toBe('# Second Cell'); - expect(await app.workbench.notebooksPositron.getCellContent(2)).toBe('# Cell to Delete'); - - // Redo the delete - await redoWithKeyboard(app); - expect(await getCellCount(app)).toBe(2); - expect(await app.workbench.notebooksPositron.getCellContent(1)).toBe('# Cell to Delete'); + await test.step('Test 1: Add cell and undo/redo', async () => { + // Start with initial cell + await notebooksPositron.addCodeToCell(0, '# Initial Cell'); + await notebooksPositron.expectCellCountToBe(1); + await notebooksPositron.expectCellContentAtIndexToBe(0, '# Initial Cell'); + + // Add a second cell + await notebooksPositron.selectCellAtIndex(0); + await notebooksPositron.performCellAction('addCellBelow'); + await notebooksPositron.addCodeToCell(1, '# Second Cell'); + await notebooksPositron.expectCellCountToBe(2); + await notebooksPositron.expectCellContentAtIndexToBe(1, '# Second Cell'); + + // Undo the add cell operation + await notebooksPositron.performCellAction('undo'); + await notebooksPositron.expectCellCountToBe(1); + await notebooksPositron.expectCellContentAtIndexToBe(0, '# Initial Cell'); + + // Redo the add cell operation to add back cell + await notebooksPositron.performCellAction('redo'); + await notebooksPositron.expectCellCountToBe(2); + await notebooksPositron.expectCellContentAtIndexToBe(1, '# Second Cell'); + }); // ======================================== - // Cleanup + // Test 2: Delete cell and undo/redo // ======================================== - // Close the notebook without saving - await app.workbench.notebooks.closeNotebookWithoutSaving(); + await test.step('Test 2: Delete cell and undo/redo', async () => { + // Add a third cell for deletion test + await notebooksPositron.selectCellAtIndex(1); + await notebooksPositron.performCellAction('addCellBelow'); + await notebooksPositron.addCodeToCell(2, '# Cell to Delete'); + await notebooksPositron.expectCellCountToBe(3); + + // Delete the middle cell + await notebooksPositron.selectCellAtIndex(1); + await notebooksPositron.performCellAction('delete'); + await notebooksPositron.expectCellCountToBe(2); + await notebooksPositron.expectCellContentAtIndexToBe(0, '# Initial Cell'); + await notebooksPositron.expectCellContentAtIndexToBe(1, '# Cell to Delete'); + + // Undo the delete + await notebooksPositron.performCellAction('undo'); + await notebooksPositron.expectCellCountToBe(3); + await notebooksPositron.expectCellContentAtIndexToBe(1, '# Second Cell'); + await notebooksPositron.expectCellContentAtIndexToBe(2, '# Cell to Delete'); + + // Redo the delete + await notebooksPositron.performCellAction('redo'); + await notebooksPositron.expectCellCountToBe(2); + await notebooksPositron.expectCellContentAtIndexToBe(1, '# Cell to Delete'); + }) }); }); diff --git a/test/e2e/tests/variables/variables-notebook.test.ts b/test/e2e/tests/variables/variables-notebook.test.ts index 03cc2ac705cb..5cf182befddb 100644 --- a/test/e2e/tests/variables/variables-notebook.test.ts +++ b/test/e2e/tests/variables/variables-notebook.test.ts @@ -26,7 +26,7 @@ test.describe('Variables Pane - Notebook', { // Create a variable via a notebook await notebooks.createNewNotebook(); await notebooks.selectInterpreter('R'); - await notebooks.addCodeToCellAtIndex('y <- c(2, 3, 4, 5)'); + await notebooks.addCodeToCellAtIndex(0, 'y <- c(2, 3, 4, 5)'); await notebooks.executeCodeInCell(); // Verify the interpreter and var in the variable pane @@ -41,7 +41,7 @@ test.describe('Variables Pane - Notebook', { // Create a variable via a notebook await notebooks.createNewNotebook(); await notebooks.selectInterpreter('Python'); - await notebooks.addCodeToCellAtIndex('y = [2, 3, 4, 5]'); + await notebooks.addCodeToCellAtIndex(0, 'y = [2, 3, 4, 5]'); await notebooks.executeCodeInCell(); // Verify the interpreter and var in the variable pane @@ -56,7 +56,7 @@ test.describe('Variables Pane - Notebook', { // Create a variable via a notebook await notebooks.createNewNotebook(); await notebooks.selectInterpreter('Python'); - await notebooks.addCodeToCellAtIndex('dict = [{"a":1,"b":2},{"a":3,"b":4}]'); + await notebooks.addCodeToCellAtIndex(0, 'dict = [{"a":1,"b":2},{"a":3,"b":4}]'); await notebooks.executeCodeInCell(); // Verify the interpreter and var in the variable pane From e35b8218d75faa86a6e8cf1ba7571ee883a0694b Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Tue, 7 Oct 2025 15:48:01 -0500 Subject: [PATCH 03/22] cleanup --- test/e2e/pages/notebooksPositron.ts | 181 ++++++------------ .../notebook/cell-deletion-action-bar.test.ts | 1 - .../tests/notebook/notebook-create.test.ts | 74 +++---- .../notebook-focus-and-selection.test.ts | 1 - .../notebook/notebook-large-python.test.ts | 15 +- .../tests/notebook/notebook-large-r.test.ts | 14 +- 6 files changed, 115 insertions(+), 171 deletions(-) diff --git a/test/e2e/pages/notebooksPositron.ts b/test/e2e/pages/notebooksPositron.ts index cc307e44061a..26714813e5fa 100644 --- a/test/e2e/pages/notebooksPositron.ts +++ b/test/e2e/pages/notebooksPositron.ts @@ -9,7 +9,6 @@ import { QuickInput } from './quickInput'; import { QuickAccess } from './quickaccess'; import test, { expect, Locator } from '@playwright/test'; import { HotKeys } from './hotKeys.js'; -import { app } from 'electron'; type SettingsFixture = { set: ( @@ -54,7 +53,45 @@ export class PositronNotebooks extends Notebooks { super(code, quickinput, quickaccess, hotKeys); } - // -- Actions -- + // #region GETTERS + + /** + * Get cell content for identification + */ + async getCellContent(cellIndex: number): Promise { + const cell = this.code.driver.page.locator('[data-testid="notebook-cell"]').nth(cellIndex); + const editor = cell.locator('.positron-cell-editor-monaco-widget .view-lines'); + const content = await editor.textContent() ?? ''; + // Replace the weird ascii space with a proper space + return content.replace(/\u00a0/g, ' '); + } + + /** + * Get the index of the currently focused cell. + * @returns The index of the focused cell, or null if no cell is focused. + */ + async getFocusedCellIndex(): Promise { + const cells = this.cell; + const cellCount = await cells.count(); + + for (let i = 0; i < cellCount; i++) { + const cell = cells.nth(i); + const isFocused = await cell.evaluate((element) => { + // Check if this cell or any descendant has focus + return element.contains(document.activeElement) || + element === document.activeElement; + }); + + if (isFocused) { + return i; + } + } + return null; + } + + // #endregion + + // #region ACTIONS /** * Action: Enable the Positron Notebooks feature @@ -109,7 +146,7 @@ export class PositronNotebooks extends Notebooks { * Action: Create a new code cell at the END of the notebook. */ private async addCodeCellToEnd(): Promise { - await test.step(`Create new code cell at end:`, async () => { + await test.step(`Create new code cell at end`, async () => { const newCellButtonCount = await this.newCellButton.count(); if (newCellButtonCount === 0) { @@ -126,20 +163,15 @@ export class PositronNotebooks extends Notebooks { * Action: Run the code in the cell at the specified index. */ async runCodeAtIndex(cellIndex = 0): Promise { - await test.step('Execute code in Positron notebook cell', async () => { - + await test.step(`Run code in cell ${cellIndex}`, async () => { await this.selectCellAtIndex(cellIndex); await this.runCellButtonAtIndex(cellIndex).click(); - // Wait for execution to complete by checking the execution spinner is gone + // Wait for spinner to appear (cell is executing) and disappear (execution complete) const spinner = this.spinnerAtIndex(cellIndex); - - // Wait for spinner to appear (cell is executing) await expect(spinner).toBeVisible({ timeout: DEFAULT_TIMEOUT }).catch(() => { // Spinner might not appear for very fast executions, that's okay }); - - // Wait for spinner to disappear (execution complete) await expect(spinner).toHaveCount(0, { timeout: DEFAULT_TIMEOUT }); }); } @@ -212,7 +244,7 @@ export class PositronNotebooks extends Notebooks { /** * Action: Perform a cell action using keyboard shortcuts. - * @param action - The action to perform: 'copy', 'cut', 'paste', 'undo', 'redo', 'addCellBelow'. + * @param action - The action to perform: 'copy', 'cut', 'paste', 'undo', 'redo', 'delete', 'addCellBelow'. */ async performCellAction(action: 'copy' | 'cut' | 'paste' | 'undo' | 'redo' | 'delete' | 'addCellBelow'): Promise { // Press escape to ensure focus is out of the cell editor @@ -245,9 +277,8 @@ export class PositronNotebooks extends Notebooks { } } - /** - * Helper function to delete cell using action bar delete button + * Action: Delete a cell using the action bar button. */ async deleteCellWithActionBar(cellIndex = 0): Promise { await test.step(`Delete cell ${cellIndex} using action bar`, async () => { @@ -269,18 +300,7 @@ export class PositronNotebooks extends Notebooks { } /** - * Get cell content for identification - */ - async getCellContent(cellIndex: number): Promise { - const cell = this.code.driver.page.locator('[data-testid="notebook-cell"]').nth(cellIndex); - const editor = cell.locator('.positron-cell-editor-monaco-widget .view-lines'); - const content = await editor.textContent() ?? ''; - // Replace the weird ascii space with a proper space - return content.replace(/\u00a0/g, ' '); - } - - /** - * Select interpreter and wait for the kernel to be ready. + * Action: Select interpreter and wait for the kernel to be ready. * This combines selecting the interpreter with waiting for kernel connection to prevent flakiness. * Directly implements Positron-specific logic without unnecessary notebook type detection. */ @@ -337,7 +357,9 @@ export class PositronNotebooks extends Notebooks { }); } - // -- Verifications -- + // #endregion + + // #region VERIFICATIONS /** * Verify: a Positron notebook is visible on the page. @@ -349,9 +371,9 @@ export class PositronNotebooks extends Notebooks { } /** - * Verify: Cell count matches expected count. - * @param expectedCount - The expected number of cells. - */ + * Verify: Cell count matches expected count. + * @param expectedCount - The expected number of cells. + */ async expectCellCountToBe(expectedCount: number): Promise { await test.step(`Expect cell count to be ${expectedCount}`, async () => { await expect(this.cell).toHaveCount(expectedCount, { timeout: DEFAULT_TIMEOUT }); @@ -372,11 +394,6 @@ export class PositronNotebooks extends Notebooks { }); } - /** - * Verify: Cell content at specified index contains expected substring. - * @param cellIndex - The index of the cell to check. - * @param expectedSubstring - The substring expected to be contained in the cell content. - */ /** * Verify: Cell content at specified index contains expected substring or matches RegExp. * @param cellIndex - The index of the cell to check. @@ -437,7 +454,6 @@ export class PositronNotebooks extends Notebooks { }); } - /** * Verify: Cell execution status matches expected status. * @param cellIndex - The index of the cell to check. @@ -450,7 +466,12 @@ export class PositronNotebooks extends Notebooks { }); } - + /** + * Verify: Spinner visibility in a cell. + * @param cellIndex - The index of the cell to check. + * @param visible - Whether the spinner should be visible (true) or not (false). + * @param timeout - The timeout for the expectation. + */ async expectSpinnerAtIndex(cellIndex: number, visible = true, timeout = DEFAULT_TIMEOUT): Promise { await test.step(`Expect spinner to be ${visible ? 'visible' : 'hidden'} in cell ${cellIndex}`, async () => { if (visible) { @@ -461,6 +482,10 @@ export class PositronNotebooks extends Notebooks { }); } + /** + * Verify: No active spinners are present. + * @param timeout - Timeout for the expectation. + */ async expectNoActiveSpinners(timeout = DEFAULT_TIMEOUT): Promise { await test.step('Expect no active spinners in notebook', async () => { await expect(this.spinner).toHaveCount(0, { timeout }); @@ -483,53 +508,6 @@ export class PositronNotebooks extends Notebooks { }); } - /** - * Get the index of the currently focused cell. - * @returns The index of the focused cell, or null if no cell is focused. - */ - async getFocusedCellIndex(): Promise { - const cells = this.cell; - const cellCount = await cells.count(); - - for (let i = 0; i < cellCount; i++) { - const cell = cells.nth(i); - const isFocused = await cell.evaluate((element) => { - // Check if this cell or any descendant has focus - return element.contains(document.activeElement) || - element === document.activeElement; - }); - - if (isFocused) { - return i; - } - } - return null; - } - - // /** - // * Verify: the focused cell index is (or is not) the expected index. - // * @param expectedIndex - The expected index of the focused cell, or null if no cell should be focused. - // * @param timeout - Timeout for the expectation. - // * @param shouldBeFocused - If true, checks that the cell is focused; if false, checks that it is not focused. - // */ - // async expectCellIndexToBeFocused( - // expectedIndex: number | null, - // shouldBeFocused = true, - // timeout = 1000000, - // ): Promise { - // await test.step( - // `Expect focused cell index to be${shouldBeFocused ? '' : ' not'}: ${expectedIndex}`, - // async () => { - // await expect(async () => { - // const actualIndex = await this.getFocusedCellIndex(); - // shouldBeFocused - // ? expect(actualIndex).toBe(expectedIndex) - // : expect(actualIndex).not.toBe(expectedIndex); - // }).toPass({ timeout }); - // } - // ); - // } - /** * Verify: the cell at the specified index is (or is not) selected, * and optionally, whether it is in edit mode. @@ -578,42 +556,7 @@ export class PositronNotebooks extends Notebooks { } ); } - - // /** - // * Return the index of the cell that is in EDIT MODE (i.e., Monaco textarea is focused). - // * @returns index or null if none are in edit mode. - // */ - // async getEditingCellIndex(): Promise { - // const cells = this.cell; - // const count = await cells.count(); - - // for (let i = 0; i < count; i++) { - // const ta = this.editorAtIndex(i); // '.positron-cell-editor-monaco-widget textarea' - // // Check the textarea is the active element (edit mode). - // const isEditing = await ta.evaluate((el) => el === document.activeElement); - // if (isEditing) return i; - // } - // return null; - // } - - // /** - // * Verify a specific cell IS / IS NOT in edit mode. - // * Edit mode is defined as the Monaco textarea being focused. - // */ - // async expectCellEditModeAtIndex( - // index: number, - // shouldBeEditing = true, - // timeout = DEFAULT_TIMEOUT - // ): Promise { - // await test.step(`Expect cell ${index} to be${shouldBeEditing ? '' : ' not'} in edit mode`, async () => { - // const ta = this.editorAtIndex(index); - // if (shouldBeEditing) { - // await expect(ta).toBeFocused({ timeout }); - // } else { - // await expect(ta).not.toBeFocused({ timeout }); - // } - // }); - // } + // #endregion } diff --git a/test/e2e/tests/notebook/cell-deletion-action-bar.test.ts b/test/e2e/tests/notebook/cell-deletion-action-bar.test.ts index 7b4d4920579c..49f31f66e549 100644 --- a/test/e2e/tests/notebook/cell-deletion-action-bar.test.ts +++ b/test/e2e/tests/notebook/cell-deletion-action-bar.test.ts @@ -70,7 +70,6 @@ test.describe('Positorn Notebooks: Cell Deletion Action Bar Behavior', { // ======================================== // Test 2: Delete another cell (cell 3, originally cell 4) // ======================================== - await test.step('Test 2: Delete another cell (cell 3, originally cell 4)', async () => { // Select cell 3 for deletion await notebooksPositron.selectCellAtIndex(3); diff --git a/test/e2e/tests/notebook/notebook-create.test.ts b/test/e2e/tests/notebook/notebook-create.test.ts index 9cc8f4076455..f4a07236545b 100644 --- a/test/e2e/tests/notebook/notebook-create.test.ts +++ b/test/e2e/tests/notebook/notebook-create.test.ts @@ -39,27 +39,31 @@ test.describe('Notebooks', { await cleanup.removeTestFiles([newFileName]); }); - test('Python - Verify code cell execution in notebook', async function ({ app }) { - await app.workbench.notebooks.addCodeToCellAtIndex(0, 'eval("8**2")'); - await app.workbench.notebooks.executeCodeInCell(); - await app.workbench.notebooks.assertCellOutput('64'); - }); - - test('Python - Verify markdown formatting in notebook', async function ({ app }) { - const randomText = Math.random().toString(36).substring(7); - - await app.workbench.notebooks.insertNotebookCell('markdown'); - await app.workbench.notebooks.typeInEditor(`## ${randomText} `); - await app.workbench.notebooks.stopEditingCell(); - await app.workbench.notebooks.assertMarkdownText('h2', randomText); + test('Python - Verify code cell execution and markdown formatting in notebook', async function ({ app }) { + const { notebooks } = app.workbench; + + await test.step('Verify code cell execution in notebook', async () => { + await notebooks.addCodeToCellAtIndex(0, 'eval("8**2")'); + await notebooks.executeCodeInCell(); + await notebooks.assertCellOutput('64'); + }); + + await test.step('Verify markdown formatting in notebook', async () => { + const randomText = Math.random().toString(36).substring(7); + + await notebooks.insertNotebookCell('markdown'); + await notebooks.typeInEditor(`## ${randomText} `); + await notebooks.stopEditingCell(); + await notebooks.assertMarkdownText('h2', randomText); + }); }); test('Python - Save untitled notebook and preserve session', async function ({ app, runCommand }) { - const { notebooks, variables, layouts, quickInput } = app.workbench; + const { notebooks, variables, layouts, quickInput, hotKeys } = app.workbench; // Ensure auxiliary sidebar is open to see variables pane await layouts.enterLayout('notebook'); - await runCommand('workbench.action.toggleAuxiliaryBar'); + await hotKeys.showSecondarySidebar(); // First, create and execute a cell to verify initial session await notebooks.addCodeToCellAtIndex(0, 'foo = "bar"'); @@ -111,14 +115,12 @@ test.describe('Notebooks', { }); test('Python - Ensure LSP works across cells', async function ({ app }) { + const { notebooks } = app.workbench; - await app.workbench.notebooks.insertNotebookCell('code'); - - await app.workbench.notebooks.addCodeToCellAtIndex(0, 'import torch'); - - await app.workbench.notebooks.insertNotebookCell('code'); - - await app.workbench.notebooks.addCodeToCellAtIndex(1, 'torch.rand(10)'); + await notebooks.insertNotebookCell('code'); + await notebooks.addCodeToCellAtIndex(0, 'import torch'); + await notebooks.insertNotebookCell('code'); + await notebooks.addCodeToCellAtIndex(1, 'torch.rand(10)'); // toPass block seems to be needed on Ubuntu await expect(async () => { @@ -147,19 +149,21 @@ test.describe('Notebooks', { await app.workbench.notebooks.closeNotebookWithoutSaving(); }); - test('R - Verify code cell execution in notebook', async function ({ app }) { - await app.workbench.notebooks.addCodeToCellAtIndex(0, 'eval(parse(text="8**2"))'); - await app.workbench.notebooks.executeCodeInCell(); - await app.workbench.notebooks.assertCellOutput('[1] 64'); - }); - - test('R - Verify markdown formatting in notebook', async function ({ app }) { - const randomText = Math.random().toString(36).substring(7); - - await app.workbench.notebooks.insertNotebookCell('markdown'); - await app.workbench.notebooks.typeInEditor(`## ${randomText} `); - await app.workbench.notebooks.stopEditingCell(); - await app.workbench.notebooks.assertMarkdownText('h2', randomText); + test('R - Verify code cell execution and markdown formatting in notebook', async function ({ app }) { + await test.step('Verify code cell execution in notebook', async () => { + await app.workbench.notebooks.addCodeToCellAtIndex(0, 'eval(parse(text="8**2"))'); + await app.workbench.notebooks.executeCodeInCell(); + await app.workbench.notebooks.assertCellOutput('[1] 64'); + }); + + await test.step('Verify markdown formatting in notebook', async () => { + const randomText = Math.random().toString(36).substring(7); + + await app.workbench.notebooks.insertNotebookCell('markdown'); + await app.workbench.notebooks.typeInEditor(`## ${randomText} `); + await app.workbench.notebooks.stopEditingCell(); + await app.workbench.notebooks.assertMarkdownText('h2', randomText); + }); }); }); }); diff --git a/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts b/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts index ed8916d86d77..ab85b1658d32 100644 --- a/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts +++ b/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts @@ -85,7 +85,6 @@ test.describe('Notebook Focus and Selection', { await app.code.driver.page.keyboard.press('ArrowDown'); await notebooksPositron.expectCellIndexToBeSelected(2, { inEditMode: false }); - // Navigate down once more await app.code.driver.page.keyboard.press('ArrowDown'); await notebooksPositron.expectCellIndexToBeSelected(3, { inEditMode: false }); diff --git a/test/e2e/tests/notebook/notebook-large-python.test.ts b/test/e2e/tests/notebook/notebook-large-python.test.ts index 4786bfbde5d0..f1c1623f59fc 100644 --- a/test/e2e/tests/notebook/notebook-large-python.test.ts +++ b/test/e2e/tests/notebook/notebook-large-python.test.ts @@ -16,22 +16,21 @@ test.describe('Large Python Notebook', { }, () => { test.afterAll(async function ({ hotKeys }) { - // If we don't close the editor, the test teardown fails await hotKeys.closeAllEditors(); }); - test('Python - Large notebook execution', async function ({ app, python }) { + test('Python - Large notebook execution', async function ({ app, openDataFile, runCommand, python }) { test.slow(); - const notebooks = app.workbench.notebooks; + const { notebooks, layouts } = app.workbench; - await app.workbench.quickaccess.openDataFile(join(app.workspacePathOrFolder, 'workspaces', 'large_py_notebook', 'spotify.ipynb')); + // open the large Python notebook and run all cells + await openDataFile(join(app.workspacePathOrFolder, 'workspaces', 'large_py_notebook', 'spotify.ipynb')); await notebooks.selectInterpreter('Python'); - await notebooks.runAllCells(120000); - await app.workbench.layouts.enterLayout('notebook'); - - await app.workbench.quickaccess.runCommand('notebook.focusTop'); + // scroll through the notebook and count unique plot outputs + await layouts.enterLayout('notebook'); + await runCommand('notebook.focusTop'); await app.code.driver.page.locator('span').filter({ hasText: 'import pandas as pd' }).locator('span').first().click(); const allFigures: any[] = []; diff --git a/test/e2e/tests/notebook/notebook-large-r.test.ts b/test/e2e/tests/notebook/notebook-large-r.test.ts index 8b6b2979f9cc..1fe2ca604569 100644 --- a/test/e2e/tests/notebook/notebook-large-r.test.ts +++ b/test/e2e/tests/notebook/notebook-large-r.test.ts @@ -18,18 +18,18 @@ test.describe('Large R Notebook', { test('R - Large notebook execution', { tag: [tags.ARK] - }, async function ({ app, r }) { + }, async function ({ app, openDataFile, runCommand, r }) { test.slow(); - const notebooks = app.workbench.notebooks; + const { notebooks, layouts } = app.workbench; - await app.workbench.quickaccess.openDataFile(join(app.workspacePathOrFolder, 'workspaces', 'large_r_notebook', 'spotify.ipynb')); + // open the large R notebook and run all cells + await openDataFile(join(app.workspacePathOrFolder, 'workspaces', 'large_r_notebook', 'spotify.ipynb')); await notebooks.selectInterpreter('R'); - await notebooks.runAllCells(120000); - await app.workbench.layouts.enterLayout('notebook'); - - await app.workbench.quickaccess.runCommand('notebook.focusTop'); + // scroll through the notebook and count unique plot outputs + await layouts.enterLayout('notebook'); + await runCommand('notebook.focusTop'); await app.code.driver.page.locator('span').filter({ hasText: 'library(dplyr)' }).locator('span').first().click(); const allFigures: any[] = []; From 15cb57a1cff0bfe56f680ce2c9cd13245bd31711 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Wed, 8 Oct 2025 06:53:53 -0500 Subject: [PATCH 04/22] fix workspace path & misc --- test/e2e/infra/workbench.ts | 4 +-- test/e2e/pages/clipboard.ts | 26 ++++++++++++++++--- test/e2e/pages/notebooksPositron.ts | 24 ++++++++++++++--- .../notebook/cell-deletion-action-bar.test.ts | 22 +++++----------- .../notebook-focus-and-selection.test.ts | 21 +++------------ .../notebook/notebook-large-python.test.ts | 2 +- .../tests/notebook/notebook-large-r.test.ts | 2 +- .../positron-notebook-copy-paste.test.ts | 13 +--------- .../notebook/positron-notebook-editor.test.ts | 5 +++- 9 files changed, 61 insertions(+), 58 deletions(-) diff --git a/test/e2e/infra/workbench.ts b/test/e2e/infra/workbench.ts index 7053c9e205e7..1d5aced6fe16 100644 --- a/test/e2e/infra/workbench.ts +++ b/test/e2e/infra/workbench.ts @@ -115,12 +115,12 @@ export class Workbench { this.output = new Output(code, this.quickaccess, this.quickInput); this.console = new Console(code, this.quickInput, this.quickaccess, this.hotKeys, this.contextMenu); this.modals = new Modals(code, this.toasts, this.console); + this.clipboard = new Clipboard(code, this.hotKeys); this.sessions = new Sessions(code, this.quickaccess, this.quickInput, this.console); this.notebooks = new Notebooks(code, this.quickInput, this.quickaccess, this.hotKeys); this.notebooksVscode = new VsCodeNotebooks(code, this.quickInput, this.quickaccess, this.hotKeys); - this.notebooksPositron = new PositronNotebooks(code, this.quickInput, this.quickaccess, this.hotKeys); + this.notebooksPositron = new PositronNotebooks(code, this.quickInput, this.quickaccess, this.hotKeys, this.clipboard); this.welcome = new Welcome(code); - this.clipboard = new Clipboard(code, this.hotKeys); this.terminal = new Terminal(code, this.quickaccess, this.clipboard); this.viewer = new Viewer(code); this.editor = new Editor(code); diff --git a/test/e2e/pages/clipboard.ts b/test/e2e/pages/clipboard.ts index 5282a85911b8..de10ca4c93a7 100644 --- a/test/e2e/pages/clipboard.ts +++ b/test/e2e/pages/clipboard.ts @@ -16,12 +16,12 @@ export class Clipboard { // Seed the clipboard await this.setClipboardText(seed); - // Invoke the copy hotkey - await this.hotKeys.copy(); - // Wait until clipboard value differs from the seed await expect - .poll(async () => (await this.getClipboardText()) ?? '', { + .poll(async () => { + await this.hotKeys.copy(); + return (await this.getClipboardText()) ?? ''; + }, { message: 'clipboard should change after copy', timeout: timeoutMs, intervals: [100, 150, 200, 300, 500, 800], @@ -29,6 +29,24 @@ export class Clipboard { .not.toBe(seed); } + async cut(timeoutMs = 5000): Promise { + const seed = '__SEED__'; + // Seed the clipboard + await this.setClipboardText(seed); + + // Wait until clipboard value differs from the seed + await expect + .poll(async () => { + await this.hotKeys.cut(); + return (await this.getClipboardText()) ?? ''; + }, { + message: 'clipboard should change after cut', + timeout: timeoutMs, + intervals: [100, 150, 200, 300, 500, 800], + }) + .not.toBe(seed); + } + async paste(): Promise { await this.hotKeys.paste(); } diff --git a/test/e2e/pages/notebooksPositron.ts b/test/e2e/pages/notebooksPositron.ts index 26714813e5fa..341c04ae3db9 100644 --- a/test/e2e/pages/notebooksPositron.ts +++ b/test/e2e/pages/notebooksPositron.ts @@ -7,6 +7,7 @@ import { Notebooks } from './notebooks'; import { Code } from '../infra/code'; import { QuickInput } from './quickInput'; import { QuickAccess } from './quickaccess'; +import { Clipboard } from './clipboard.js'; import test, { expect, Locator } from '@playwright/test'; import { HotKeys } from './hotKeys.js'; @@ -49,7 +50,7 @@ export class PositronNotebooks extends Notebooks { cellInfoToolTipOrder = this.cellInfoToolTip.getByLabel('Execution order'); cellInfoToolTipCompleted = this.cellInfoToolTip.getByLabel('Execution completed'); - constructor(code: Code, quickinput: QuickInput, quickaccess: QuickAccess, hotKeys: HotKeys) { + constructor(code: Code, quickinput: QuickInput, quickaccess: QuickAccess, hotKeys: HotKeys, private clipboard: Clipboard) { super(code, quickinput, quickaccess, hotKeys); } @@ -124,6 +125,20 @@ export class PositronNotebooks extends Notebooks { await this.expectToBeVisible(); } + /** + * Create a new Positron notebook. + * @param numCellsToAdd - Number of cells to add after creating the notebook (default: 0). + */ + async newNotebook(numCellsToAdd = 0): Promise { + await this.createNewNotebook(); + await this.expectToBeVisible(); + if (numCellsToAdd > 0) { + for (let i = 0; i < numCellsToAdd; i++) { + await this.addCodeToCell(i, `# Cell ${i}`); + } + } + } + /** * @override * Action: Select a cell at the specified index. @@ -180,6 +195,7 @@ export class PositronNotebooks extends Notebooks { * Action: Move the mouse away from the notebook area to close any open tooltips/popups. */ async moveMouseAway(): Promise { + await this.code.driver.page.waitForTimeout(500); await this.code.driver.page.mouse.move(0, 0); }; @@ -252,13 +268,13 @@ export class PositronNotebooks extends Notebooks { switch (action) { case 'copy': - await this.hotKeys.copy(); + await this.clipboard.copy(); break; case 'cut': - await this.hotKeys.cut(); + await this.clipboard.cut(); break; case 'paste': - await this.hotKeys.paste(); + await this.clipboard.paste(); break; case 'undo': await this.hotKeys.undo(); diff --git a/test/e2e/tests/notebook/cell-deletion-action-bar.test.ts b/test/e2e/tests/notebook/cell-deletion-action-bar.test.ts index 49f31f66e549..3aaea3869776 100644 --- a/test/e2e/tests/notebook/cell-deletion-action-bar.test.ts +++ b/test/e2e/tests/notebook/cell-deletion-action-bar.test.ts @@ -26,23 +26,13 @@ test.describe('Positorn Notebooks: Cell Deletion Action Bar Behavior', { }); test('Cell deletion using action bar', async function ({ app, settings }) { - const { notebooks, notebooksPositron } = app.workbench; + const { notebooksPositron } = app.workbench; // ======================================== // Setup: Create 6 cells with distinct content // ======================================== await test.step(' Test Setup: Create notebook', async () => { - await notebooks.createNewNotebook(); - await notebooksPositron.expectToBeVisible(); - - await notebooksPositron.addCodeToCell(0, '# Cell 0'); - await notebooksPositron.addCodeToCell(1, '# Cell 1'); - await notebooksPositron.addCodeToCell(2, '# Cell 2'); - await notebooksPositron.addCodeToCell(3, '# Cell 3'); - await notebooksPositron.addCodeToCell(4, '# Cell 4'); - await notebooksPositron.addCodeToCell(5, '# Cell 5'); - - // Verify we have 6 cells + await notebooksPositron.newNotebook(6); await notebooksPositron.expectCellCountToBe(6); }); @@ -88,7 +78,7 @@ test.describe('Positorn Notebooks: Cell Deletion Action Bar Behavior', { await notebooksPositron.expectCellContentAtIndexToBe(1, '# Cell 1'); await notebooksPositron.expectCellContentAtIndexToBe(2, '# Cell 3'); await notebooksPositron.expectCellContentAtIndexToBe(3, '# Cell 5'); - }) + }); // ======================================== // Test 3: Delete last cell (cell 3, contains '# Cell 5') @@ -107,7 +97,7 @@ test.describe('Positorn Notebooks: Cell Deletion Action Bar Behavior', { await notebooksPositron.expectCellContentAtIndexToBe(0, '# Cell 0'); await notebooksPositron.expectCellContentAtIndexToBe(1, '# Cell 1'); await notebooksPositron.expectCellContentAtIndexToBe(2, '# Cell 3'); - }) + }); // ======================================== // Test 4: Delete first cell (cell 0) @@ -125,7 +115,7 @@ test.describe('Positorn Notebooks: Cell Deletion Action Bar Behavior', { // Verify what was cell 1 is now at index 0 expect(await notebooksPositron.getCellContent(0)).toBe('# Cell 1'); expect(await notebooksPositron.getCellContent(1)).toBe('# Cell 3'); - }) + }); // ======================================== // Test 5: Delete remaining cells @@ -143,6 +133,6 @@ test.describe('Positorn Notebooks: Cell Deletion Action Bar Behavior', { // Verify we have exactly one cell remaining with the correct content await notebooksPositron.expectCellCountToBe(1); await notebooksPositron.expectCellContentAtIndexToBe(0, '# Cell 3'); - }) + }); }); }); diff --git a/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts b/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts index ab85b1658d32..4e8b1c24195e 100644 --- a/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts +++ b/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts @@ -23,17 +23,7 @@ test.describe('Notebook Focus and Selection', { test.beforeEach(async function ({ app }) { const { notebooksPositron } = app.workbench; - - // Create a fresh notebook with 5 cells for each test - await notebooksPositron.createNewNotebook(); - await notebooksPositron.expectToBeVisible(); - - // Add 5 cells with distinct content - await notebooksPositron.addCodeToCell(0, 'print("Cell 0")'); - await notebooksPositron.addCodeToCell(1, 'print("Cell 1")'); - await notebooksPositron.addCodeToCell(2, 'print("Cell 2")'); - await notebooksPositron.addCodeToCell(3, 'print("Cell 3")'); - await notebooksPositron.addCodeToCell(4, 'print("Cell 4")'); + await notebooksPositron.newNotebook(5); await notebooksPositron.expectCellCountToBe(5); }); @@ -41,7 +31,7 @@ test.describe('Notebook Focus and Selection', { await hotKeys.closeAllEditors(); }); - test('Keyboard behavior with notebook cells', async function ({ app }) { + test('Notebook keyboard behavior with cells', async function ({ app }) { const { notebooksPositron } = app.workbench; await test.step('Test 1: Arrow Down navigation moves focus to next cell', async () => { @@ -92,7 +82,7 @@ test.describe('Notebook Focus and Selection', { await app.code.driver.page.keyboard.press('ArrowUp'); await notebooksPositron.expectCellIndexToBeSelected(2, { inEditMode: false }); }); - }) + }); test('Editor mode behavior with notebook cells', async function ({ app }) { const { notebooksPositron } = app.workbench; @@ -104,7 +94,7 @@ test.describe('Notebook Focus and Selection', { await notebooksPositron.expectCellIndexToBeSelected(1, { isSelected: true, inEditMode: true }); await notebooksPositron.expectCellIndexToBeSelected(2, { isSelected: false, inEditMode: false }); await notebooksPositron.expectCellIndexToBeSelected(3, { isSelected: false, inEditMode: false }); - await notebooksPositron.expectCellIndexToBeSelected(4, { isSelected: false, inEditMode: false }) + await notebooksPositron.expectCellIndexToBeSelected(4, { isSelected: false, inEditMode: false }); // Verify we can type into the editor after clicking await app.code.driver.page.keyboard.type('# editor good'); @@ -148,7 +138,4 @@ test.describe('Notebook Focus and Selection', { await notebooksPositron.expectCellContentAtIndexToContain(5, 'new cell content'); }); }); - - - }); diff --git a/test/e2e/tests/notebook/notebook-large-python.test.ts b/test/e2e/tests/notebook/notebook-large-python.test.ts index f1c1623f59fc..9c8148523649 100644 --- a/test/e2e/tests/notebook/notebook-large-python.test.ts +++ b/test/e2e/tests/notebook/notebook-large-python.test.ts @@ -24,7 +24,7 @@ test.describe('Large Python Notebook', { const { notebooks, layouts } = app.workbench; // open the large Python notebook and run all cells - await openDataFile(join(app.workspacePathOrFolder, 'workspaces', 'large_py_notebook', 'spotify.ipynb')); + await openDataFile(join('workspaces', 'large_py_notebook', 'spotify.ipynb')); await notebooks.selectInterpreter('Python'); await notebooks.runAllCells(120000); diff --git a/test/e2e/tests/notebook/notebook-large-r.test.ts b/test/e2e/tests/notebook/notebook-large-r.test.ts index 1fe2ca604569..875e541bd690 100644 --- a/test/e2e/tests/notebook/notebook-large-r.test.ts +++ b/test/e2e/tests/notebook/notebook-large-r.test.ts @@ -23,7 +23,7 @@ test.describe('Large R Notebook', { const { notebooks, layouts } = app.workbench; // open the large R notebook and run all cells - await openDataFile(join(app.workspacePathOrFolder, 'workspaces', 'large_r_notebook', 'spotify.ipynb')); + await openDataFile(join('workspaces', 'large_r_notebook', 'spotify.ipynb')); await notebooks.selectInterpreter('R'); await notebooks.runAllCells(120000); diff --git a/test/e2e/tests/notebook/positron-notebook-copy-paste.test.ts b/test/e2e/tests/notebook/positron-notebook-copy-paste.test.ts index 879c9fa13618..8b5ba7d63105 100644 --- a/test/e2e/tests/notebook/positron-notebook-copy-paste.test.ts +++ b/test/e2e/tests/notebook/positron-notebook-copy-paste.test.ts @@ -33,18 +33,7 @@ test.describe('Positron Notebooks: Cell Copy-Paste Behavior', { // Setup: Create notebook with 5 cells and distinct content // ======================================== await test.step('Test Setup: Create notebook and add cells', async () => { - // Setup: Create notebook and select kernel once - await notebooksPositron.createNewNotebook(); - await notebooksPositron.expectToBeVisible(); - - // Setup: Create 5 cells with distinct content - await notebooksPositron.addCodeToCell(0, '# Cell 0'); - await notebooksPositron.addCodeToCell(1, '# Cell 1'); - await notebooksPositron.addCodeToCell(2, '# Cell 2'); - await notebooksPositron.addCodeToCell(3, '# Cell 3'); - await notebooksPositron.addCodeToCell(4, '# Cell 4'); - - // Verify we have 5 cells + await notebooksPositron.newNotebook(5); await notebooksPositron.expectCellCountToBe(5); }); diff --git a/test/e2e/tests/notebook/positron-notebook-editor.test.ts b/test/e2e/tests/notebook/positron-notebook-editor.test.ts index a06aa65884b1..33aa5777b7fc 100644 --- a/test/e2e/tests/notebook/positron-notebook-editor.test.ts +++ b/test/e2e/tests/notebook/positron-notebook-editor.test.ts @@ -29,7 +29,10 @@ test.describe('Positron Notebooks: Open & Save', { }); }); - test.afterEach(async function ({ hotKeys }) { + test.afterEach(async function ({ app, settings, hotKeys }) { + await app.workbench.notebooksPositron.enableFeature(settings, { + editor: 'default', + }); await hotKeys.closeAllEditors(); }); From 7d08ed7b9f9bf646246fc70759056197ccd6dc92 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Wed, 8 Oct 2025 07:20:40 -0500 Subject: [PATCH 05/22] getCellCount() --- test/e2e/pages/notebooksPositron.ts | 14 ++++++++++---- .../notebook/cell-deletion-action-bar.test.ts | 4 ++-- .../notebook/notebook-focus-and-selection.test.ts | 4 ++-- .../notebook/positron-notebook-copy-paste.test.ts | 4 ++-- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/test/e2e/pages/notebooksPositron.ts b/test/e2e/pages/notebooksPositron.ts index 341c04ae3db9..b965d42ca28e 100644 --- a/test/e2e/pages/notebooksPositron.ts +++ b/test/e2e/pages/notebooksPositron.ts @@ -57,7 +57,14 @@ export class PositronNotebooks extends Notebooks { // #region GETTERS /** - * Get cell content for identification + * Get cell count. + */ + async getCellCount(): Promise { + return this.cell.count(); + } + + /** + * Get cell content at specified index. */ async getCellContent(cellIndex: number): Promise { const cell = this.code.driver.page.locator('[data-testid="notebook-cell"]').nth(cellIndex); @@ -69,7 +76,6 @@ export class PositronNotebooks extends Notebooks { /** * Get the index of the currently focused cell. - * @returns The index of the focused cell, or null if no cell is focused. */ async getFocusedCellIndex(): Promise { const cells = this.cell; @@ -217,7 +223,7 @@ export class PositronNotebooks extends Notebooks { ): Promise { const { delay = 0, run = false, waitForSpinner = false, waitForPopup = false } = options ?? {}; return await test.step(`Add code and run cell ${cellIndex}`, async () => { - const currentCellCount = await this.cell.count(); + const currentCellCount = await this.getCellCount(); if (cellIndex >= currentCellCount) { if (cellIndex > currentCellCount) { @@ -299,7 +305,7 @@ export class PositronNotebooks extends Notebooks { async deleteCellWithActionBar(cellIndex = 0): Promise { await test.step(`Delete cell ${cellIndex} using action bar`, async () => { // Get the current cell count before deletion - const initialCount = await this.cell.count(); + const initialCount = await this.getCellCount(); // Click on the cell to make the action bar visible await this.cell.nth(cellIndex).click(); diff --git a/test/e2e/tests/notebook/cell-deletion-action-bar.test.ts b/test/e2e/tests/notebook/cell-deletion-action-bar.test.ts index 3aaea3869776..ec8f1b2199f4 100644 --- a/test/e2e/tests/notebook/cell-deletion-action-bar.test.ts +++ b/test/e2e/tests/notebook/cell-deletion-action-bar.test.ts @@ -122,8 +122,8 @@ test.describe('Positorn Notebooks: Cell Deletion Action Bar Behavior', { // ======================================== await test.step('Test 5: Delete remaining cells', async () => { // Delete until only one cell remains - while (await notebooksPositron.cell.count() > 1) { - const currentCount = await notebooksPositron.cell.count(); + while (await notebooksPositron.getCellCount() > 1) { + const currentCount = await notebooksPositron.getCellCount(); await notebooksPositron.deleteCellWithActionBar(0); // Verify count decreased diff --git a/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts b/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts index 4e8b1c24195e..4285c43c624d 100644 --- a/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts +++ b/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts @@ -120,14 +120,14 @@ test.describe('Notebook Focus and Selection', { await notebooksPositron.selectCellAtIndex(4); // Get initial cell count - const initialCount = await notebooksPositron.cell.count(); + const initialCount = await notebooksPositron.getCellCount(); expect(initialCount).toBe(5); // Press Shift+Enter to add a new cell below await app.code.driver.page.keyboard.press('Shift+Enter'); // Verify new cell was added - const newCount = await notebooksPositron.cell.count(); + const newCount = await notebooksPositron.getCellCount(); expect(newCount).toBe(6); // Verify the NEW cell (index 5) is now in edit mode with focus diff --git a/test/e2e/tests/notebook/positron-notebook-copy-paste.test.ts b/test/e2e/tests/notebook/positron-notebook-copy-paste.test.ts index 8b5ba7d63105..e0e075181600 100644 --- a/test/e2e/tests/notebook/positron-notebook-copy-paste.test.ts +++ b/test/e2e/tests/notebook/positron-notebook-copy-paste.test.ts @@ -147,7 +147,7 @@ test.describe('Positron Notebooks: Cell Copy-Paste Behavior', { // ======================================== await test.step('Verify other cells shifted down correctly', async () => { // Delete cells until only one remains - while (await notebooksPositron.cell.count() > 1) { + while (await notebooksPositron.getCellCount() > 1) { await notebooksPositron.selectCellAtIndex(0); await notebooksPositron.performCellAction('cut'); } @@ -159,7 +159,7 @@ test.describe('Positron Notebooks: Cell Copy-Paste Behavior', { await notebooksPositron.performCellAction('cut'); // Check if notebook can be empty (Positron may allow 0 cells) - const finalCount = await notebooksPositron.cell.count(); + const finalCount = await notebooksPositron.getCellCount(); expect(finalCount).toBeLessThanOrEqual(1); }); }); From ad7e3b9a28901391cc168fce5dd4958e9034d192 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Wed, 8 Oct 2025 09:48:51 -0500 Subject: [PATCH 06/22] small fixes --- test/e2e/pages/notebooks.ts | 4 +- test/e2e/pages/notebooksPositron.ts | 51 ++++++++++--------- .../notebook/cell-deletion-action-bar.test.ts | 2 +- .../notebook/cell-execution-info.test.ts | 16 +++--- .../e2e/tests/notebook/notebook-debug.test.ts | 6 +-- .../notebook-focus-and-selection.test.ts | 4 +- .../positron-notebook-copy-paste.test.ts | 2 +- .../notebook/positron-notebook-editor.test.ts | 34 ++++++------- .../positron-notebook-undo-redo.test.ts | 4 +- 9 files changed, 64 insertions(+), 59 deletions(-) diff --git a/test/e2e/pages/notebooks.ts b/test/e2e/pages/notebooks.ts index 5229d8e720b0..d2d21fdffaa6 100644 --- a/test/e2e/pages/notebooks.ts +++ b/test/e2e/pages/notebooks.ts @@ -102,7 +102,9 @@ export class Notebooks { } async createNewNotebook() { - await this.quickaccess.runCommand(NEW_NOTEBOOK_COMMAND); + await test.step('Create new notebook', async () => { + await this.quickaccess.runCommand(NEW_NOTEBOOK_COMMAND); + }); } // Opens a Notebook that lives in the current workspace diff --git a/test/e2e/pages/notebooksPositron.ts b/test/e2e/pages/notebooksPositron.ts index b965d42ca28e..9700b5d47bca 100644 --- a/test/e2e/pages/notebooksPositron.ts +++ b/test/e2e/pages/notebooksPositron.ts @@ -67,33 +67,37 @@ export class PositronNotebooks extends Notebooks { * Get cell content at specified index. */ async getCellContent(cellIndex: number): Promise { - const cell = this.code.driver.page.locator('[data-testid="notebook-cell"]').nth(cellIndex); - const editor = cell.locator('.positron-cell-editor-monaco-widget .view-lines'); - const content = await editor.textContent() ?? ''; - // Replace the weird ascii space with a proper space - return content.replace(/\u00a0/g, ' '); + return await test.step(`Get content of cell at index: ${cellIndex}`, async () => { + const cell = this.code.driver.page.locator('[data-testid="notebook-cell"]').nth(cellIndex); + const editor = cell.locator('.positron-cell-editor-monaco-widget .view-lines'); + const content = await editor.textContent() ?? ''; + // Replace the weird ascii space with a proper space + return content.replace(/\u00a0/g, ' '); + }); } /** * Get the index of the currently focused cell. */ async getFocusedCellIndex(): Promise { - const cells = this.cell; - const cellCount = await cells.count(); - - for (let i = 0; i < cellCount; i++) { - const cell = cells.nth(i); - const isFocused = await cell.evaluate((element) => { - // Check if this cell or any descendant has focus - return element.contains(document.activeElement) || - element === document.activeElement; - }); - - if (isFocused) { - return i; + return await test.step(`Get focused cell index`, async () => { + const cells = this.cell; + const cellCount = await cells.count(); + + for (let i = 0; i < cellCount; i++) { + const cell = cells.nth(i); + const isFocused = await cell.evaluate((element) => { + // Check if this cell or any descendant has focus + return element.contains(document.activeElement) || + element === document.activeElement; + }); + + if (isFocused) { + return i; + } } - } - return null; + return null; + }); } // #endregion @@ -105,7 +109,7 @@ export class PositronNotebooks extends Notebooks { * @param settings - The settings fixture. * @param options - Configuration options for enabling the feature. */ - async enableFeature( + async configure( settings: SettingsFixture, { editor, reload = false, waitMs = 800 }: ConfigureNotebookEditorOptions ): Promise { @@ -222,7 +226,7 @@ export class PositronNotebooks extends Notebooks { options?: { delay?: number; run?: boolean; waitForSpinner?: boolean; waitForPopup?: boolean } ): Promise { const { delay = 0, run = false, waitForSpinner = false, waitForPopup = false } = options ?? {}; - return await test.step(`Add code and run cell ${cellIndex}`, async () => { + return await test.step(`Add code to cell: ${cellIndex}, run: ${run}, waitForSpinner: ${waitForSpinner}, waitForPopup: ${waitForPopup}`, async () => { const currentCellCount = await this.getCellCount(); if (cellIndex >= currentCellCount) { @@ -425,8 +429,9 @@ export class PositronNotebooks extends Notebooks { await test.step( `Expect cell ${cellIndex} content to contain: ${expected instanceof RegExp ? expected.toString() : expected}`, async () => { - const actualContent = await this.getCellContent(cellIndex); await expect(async () => { + const actualContent = await this.getCellContent(cellIndex); + if (expected instanceof RegExp) { expect(actualContent).toMatch(expected); } else { diff --git a/test/e2e/tests/notebook/cell-deletion-action-bar.test.ts b/test/e2e/tests/notebook/cell-deletion-action-bar.test.ts index ec8f1b2199f4..aa3540e1c774 100644 --- a/test/e2e/tests/notebook/cell-deletion-action-bar.test.ts +++ b/test/e2e/tests/notebook/cell-deletion-action-bar.test.ts @@ -15,7 +15,7 @@ test.describe('Positorn Notebooks: Cell Deletion Action Bar Behavior', { }, () => { test.beforeAll(async function ({ app, settings }) { - await app.workbench.notebooksPositron.enableFeature(settings, { + await app.workbench.notebooksPositron.configure(settings, { editor: 'positron', reload: true, }); diff --git a/test/e2e/tests/notebook/cell-execution-info.test.ts b/test/e2e/tests/notebook/cell-execution-info.test.ts index 0839a10ed733..57921f087e35 100644 --- a/test/e2e/tests/notebook/cell-execution-info.test.ts +++ b/test/e2e/tests/notebook/cell-execution-info.test.ts @@ -14,7 +14,7 @@ test.describe('Positron Notebooks: Cell Execution Tooltip', { }, () => { test.beforeAll(async function ({ app, settings }) { - await app.workbench.notebooksPositron.enableFeature(settings, { + await app.workbench.notebooksPositron.configure(settings, { editor: 'positron', reload: true, }); @@ -27,7 +27,7 @@ test.describe('Positron Notebooks: Cell Execution Tooltip', { await notebooksPositron.expectNoActiveSpinners(); await hotKeys.closeAllEditors(); - }) + }); test('Cell Execution Tooltip - Basic Functionality', async function ({ app }) { const { notebooks, notebooksPositron } = app.workbench; @@ -36,7 +36,7 @@ test.describe('Positron Notebooks: Cell Execution Tooltip', { await notebooks.createNewNotebook(); await notebooksPositron.expectCellCountToBe(1); // Important for CI stability await notebooksPositron.selectAndWaitForKernel('Python'); - }) + }); // ======================================== // Cell 0: Basic popup display with successful execution @@ -88,7 +88,7 @@ test.describe('Positron Notebooks: Cell Execution Tooltip', { // Verify auto-close behavior await notebooksPositron.moveMouseAway(); - await notebooksPositron.expectSpinnerAtIndex(2, false) + await notebooksPositron.expectSpinnerAtIndex(2, false); }); // ======================================== @@ -98,10 +98,10 @@ test.describe('Positron Notebooks: Cell Execution Tooltip', { await notebooksPositron.addCodeToCell(3, 'print("relative time test")', { run: true }); await notebooksPositron.expectToolTipToContain({ completed: /Just now|seconds ago/, - }) + }); await notebooksPositron.moveMouseAway(); - }) + }); // ======================================== // Cell 4: Hover timing and interaction @@ -111,11 +111,11 @@ test.describe('Positron Notebooks: Cell Execution Tooltip', { // Test popup closes when mouse moves away await notebooksPositron.moveMouseAway(); - await notebooksPositron.expectToolTipVisible(false) + await notebooksPositron.expectToolTipVisible(false); // Test that hovering again after closing still works await notebooksPositron.runCellButtonAtIndex(4).hover(); await notebooksPositron.expectToolTipVisible(true); - }) + }); }); }); diff --git a/test/e2e/tests/notebook/notebook-debug.test.ts b/test/e2e/tests/notebook/notebook-debug.test.ts index 0018897e4993..83776f193dfe 100644 --- a/test/e2e/tests/notebook/notebook-debug.test.ts +++ b/test/e2e/tests/notebook/notebook-debug.test.ts @@ -41,7 +41,7 @@ test.describe('Notebook Debugging', { }); // Single, simpler test that covers it all basics, instead of many separate and redundant tests. - test('Python - Core debugging workflow: breakpoints, variable inspection, step controls, and output verification', async ({ app }) => { + test('Python - Core debugging workflow: breakpoints, variable inspection, step controls, and output verification', async ({ app, logger }) => { const code = [ '# Initialize variables', 'x = 10', @@ -68,7 +68,7 @@ test.describe('Notebook Debugging', { // BP1 await app.workbench.debug.expectCurrentLineIndicatorVisible(); const vars1 = await app.workbench.debug.getVariables(); - console.log('Variables at first breakpoint:', vars1); + logger.log('Variables at first breakpoint:', vars1); // Step over to execute the intermediate calculation (BP1) await app.workbench.debug.stepOver(); @@ -80,7 +80,7 @@ test.describe('Notebook Debugging', { // BP2 const vars2 = await app.workbench.debug.getVariables(); - console.log('Variables at second breakpoint:', vars2); + logger.log('Variables at second breakpoint:', vars2); // Continue await app.workbench.debug.continue(); diff --git a/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts b/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts index 4285c43c624d..2cd857da9fb5 100644 --- a/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts +++ b/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts @@ -15,7 +15,7 @@ test.describe('Notebook Focus and Selection', { tag: [tags.CRITICAL, tags.WIN, tags.NOTEBOOKS] }, () => { test.beforeAll(async function ({ app, settings }) { - await app.workbench.notebooksPositron.enableFeature(settings, { + await app.workbench.notebooksPositron.configure(settings, { editor: 'positron', reload: true, }); @@ -112,7 +112,7 @@ test.describe('Notebook Focus and Selection', { // Verify we can type into the editor after pressing Enter await app.code.driver.page.keyboard.type('# test'); - await notebooksPositron.expectCellContentAtIndexToContain(2, /^print\("Cell 2"\)# test/); + await notebooksPositron.expectCellContentAtIndexToContain(2, /^# Cell 2# test/); }); await test.step('Test 3: Shift+Enter on last cell creates new cell and enters edit mode', async () => { diff --git a/test/e2e/tests/notebook/positron-notebook-copy-paste.test.ts b/test/e2e/tests/notebook/positron-notebook-copy-paste.test.ts index e0e075181600..3f4c711e2e31 100644 --- a/test/e2e/tests/notebook/positron-notebook-copy-paste.test.ts +++ b/test/e2e/tests/notebook/positron-notebook-copy-paste.test.ts @@ -16,7 +16,7 @@ test.describe('Positron Notebooks: Cell Copy-Paste Behavior', { }, () => { test.beforeAll(async ({ app, settings }) => { - await app.workbench.notebooksPositron.enableFeature(settings, { + await app.workbench.notebooksPositron.configure(settings, { editor: 'positron', reload: true, }); diff --git a/test/e2e/tests/notebook/positron-notebook-editor.test.ts b/test/e2e/tests/notebook/positron-notebook-editor.test.ts index 33aa5777b7fc..56dd0c9461c1 100644 --- a/test/e2e/tests/notebook/positron-notebook-editor.test.ts +++ b/test/e2e/tests/notebook/positron-notebook-editor.test.ts @@ -5,6 +5,7 @@ import path from 'path'; import { test, tags } from '../_test.setup'; +import { expect } from '@playwright/test'; const NOTEBOOK_PATH = path.join('workspaces', 'bitmap-notebook', 'bitmap-notebook.ipynb'); @@ -16,7 +17,7 @@ test.describe('Positron Notebooks: Open & Save', { tag: [tags.CRITICAL, tags.WIN, tags.NOTEBOOKS] }, () => { test.beforeAll(async function ({ app, settings }) { - await app.workbench.notebooksPositron.enableFeature(settings, { + await app.workbench.notebooksPositron.configure(settings, { editor: 'default', reload: true, }); @@ -24,15 +25,12 @@ test.describe('Positron Notebooks: Open & Save', { test.beforeEach(async function ({ app, settings }) { // Reset editor associations to default state before each test - await app.workbench.notebooksPositron.enableFeature(settings, { + await app.workbench.notebooksPositron.configure(settings, { editor: 'default', }); }); test.afterEach(async function ({ app, settings, hotKeys }) { - await app.workbench.notebooksPositron.enableFeature(settings, { - editor: 'default', - }); await hotKeys.closeAllEditors(); }); @@ -46,7 +44,7 @@ test.describe('Positron Notebooks: Open & Save', { // Configure Positron as the default notebook editor // This sets workbench.editorAssociations to map *.ipynb files to the Positron notebook editor - await app.workbench.notebooksPositron.enableFeature(settings, { + await app.workbench.notebooksPositron.configure(settings, { editor: 'positron', }); @@ -58,7 +56,7 @@ test.describe('Positron Notebooks: Open & Save', { // Reset to default configuration and verify VS Code editor is used again // Close all editors first to ensure a clean state for the next test await hotKeys.closeAllEditors(); - await app.workbench.notebooksPositron.enableFeature(settings, { + await app.workbench.notebooksPositron.configure(settings, { editor: 'default', }); @@ -73,7 +71,7 @@ test.describe('Positron Notebooks: Open & Save', { const { notebooks, notebooksPositron, quickInput, editors } = app.workbench; // Configure Positron as the default notebook editor - await app.workbench.notebooksPositron.enableFeature(settings, { + await app.workbench.notebooksPositron.configure(settings, { editor: 'positron', }); @@ -107,7 +105,7 @@ test.describe('Positron Notebooks: Open & Save', { const { notebooks, notebooksPositron, editors } = app.workbench; // Configure Positron as the default notebook editor - await app.workbench.notebooksPositron.enableFeature(settings, { + await app.workbench.notebooksPositron.configure(settings, { editor: 'positron', }); @@ -119,8 +117,8 @@ test.describe('Positron Notebooks: Open & Save', { await editors.waitForTab(/^Untitled-\d+\.ipynb$/, true); // true = isDirty // Count tabs before reload (checking for multiple tabs with same file is the ghost editor symptom) - const tabsBefore = await app.code.driver.page.locator('.tabs-container div.tab').count(); - test.expect(tabsBefore).toBe(1); + const tabsBefore = app.code.driver.page.locator('.tabs-container div.tab'); + await expect(tabsBefore).toHaveCount(1); // Reload the window to simulate restart await hotKeys.reloadWindow(true); @@ -129,19 +127,19 @@ test.describe('Positron Notebooks: Open & Save', { // The bug would cause both a Positron notebook AND a VS Code notebook to be visible // Check tab count - should still be 1, not 2 - const tabsAfter = await app.code.driver.page.locator('.tabs-container div.tab').count(); - test.expect(tabsAfter).toBe(1); + const tabsAfter = app.code.driver.page.locator('.tabs-container div.tab'); + await expect(tabsAfter).toHaveCount(1); // Verify that the Positron notebook is visible await notebooksPositron.expectToBeVisible(); - // Verify that the VS Code notebook is NOT visible (this is the ghost editor we're trying to prevent) - const vscodeNotebookElements = await app.code.driver.page.locator('.notebook-editor').count(); - const positronNotebookElements = await app.code.driver.page.locator('.positron-notebook').count(); + // Verify that the VS Code notebook is NOT visible (this is the ghost editor we're trying to prevent)\ + const positronNotebookElements = app.code.driver.page.locator('.positron-notebook'); + const vscodeNotebookElements = app.code.driver.page.locator('.notebook-editor'); // Should have only one notebook editor, and it should be the Positron one - test.expect(positronNotebookElements).toBe(1); - test.expect(vscodeNotebookElements).toBe(0); + await expect(positronNotebookElements).toHaveCount(1); + await expect(vscodeNotebookElements).toHaveCount(0); // Additional verification: ensure the active tab is still the restored untitled notebook await editors.waitForActiveTab(/^Untitled-\d+\.ipynb$/, true); // true = isDirty diff --git a/test/e2e/tests/notebook/positron-notebook-undo-redo.test.ts b/test/e2e/tests/notebook/positron-notebook-undo-redo.test.ts index 0e01b8db72a9..9e34bec392ce 100644 --- a/test/e2e/tests/notebook/positron-notebook-undo-redo.test.ts +++ b/test/e2e/tests/notebook/positron-notebook-undo-redo.test.ts @@ -15,7 +15,7 @@ test.describe('Postiron Notebooks: Cell Undo-Redo Behavior', { }, () => { test.beforeAll(async function ({ app, settings }) { - await app.workbench.notebooksPositron.enableFeature(settings, { + await app.workbench.notebooksPositron.configure(settings, { editor: 'positron', reload: true, }); @@ -87,6 +87,6 @@ test.describe('Postiron Notebooks: Cell Undo-Redo Behavior', { await notebooksPositron.performCellAction('redo'); await notebooksPositron.expectCellCountToBe(2); await notebooksPositron.expectCellContentAtIndexToBe(1, '# Cell to Delete'); - }) + }); }); }); From 3bbf0b74f5874f229a7d8893559567b27adf29c6 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Wed, 8 Oct 2025 11:52:08 -0500 Subject: [PATCH 07/22] cleanup --- test/e2e/pages/hotKeys.ts | 5 +- test/e2e/pages/notebooksPositron.ts | 69 ++++++++++--------- .../notebook/cell-deletion-action-bar.test.ts | 11 ++- .../notebook/cell-execution-info.test.ts | 7 +- .../notebook-focus-and-selection.test.ts | 7 +- .../positron-notebook-copy-paste.test.ts | 7 +- .../notebook/positron-notebook-editor.test.ts | 28 +++----- .../positron-notebook-undo-redo.test.ts | 7 +- 8 files changed, 59 insertions(+), 82 deletions(-) diff --git a/test/e2e/pages/hotKeys.ts b/test/e2e/pages/hotKeys.ts index 6e91947666a3..47fc73074d24 100644 --- a/test/e2e/pages/hotKeys.ts +++ b/test/e2e/pages/hotKeys.ts @@ -203,9 +203,12 @@ export class HotKeys { public async reloadWindow(waitForReady = false) { await this.pressHotKeys('Cmd+R R', 'Reload window'); + + // wait for workbench to disappear, reappear and be ready + await this.code.driver.page.waitForTimeout(3000); + await this.code.driver.page.locator('.monaco-workbench').waitFor({ state: 'visible' }); if (waitForReady) { await expect(this.code.driver.page.locator('text=/^Starting up|^Starting|^Preparing|^Discovering( \\w+)? interpreters|starting\\.$/i')).toHaveCount(0, { timeout: 90000 }); - } } diff --git a/test/e2e/pages/notebooksPositron.ts b/test/e2e/pages/notebooksPositron.ts index 9700b5d47bca..ccd2d320e0a6 100644 --- a/test/e2e/pages/notebooksPositron.ts +++ b/test/e2e/pages/notebooksPositron.ts @@ -11,20 +11,6 @@ import { Clipboard } from './clipboard.js'; import test, { expect, Locator } from '@playwright/test'; import { HotKeys } from './hotKeys.js'; -type SettingsFixture = { - set: ( - settings: Record, - options?: { reload?: boolean | 'web'; waitMs?: number; waitForReady?: boolean; keepOpen?: boolean } - ) => Promise; -}; - -type ConfigureNotebookEditorOptions = { - editor: 'positron' | 'default'; - reload?: boolean | 'web'; - waitMs?: number; -}; - - const DEFAULT_TIMEOUT = 10000; /** @@ -68,8 +54,7 @@ export class PositronNotebooks extends Notebooks { */ async getCellContent(cellIndex: number): Promise { return await test.step(`Get content of cell at index: ${cellIndex}`, async () => { - const cell = this.code.driver.page.locator('[data-testid="notebook-cell"]').nth(cellIndex); - const editor = cell.locator('.positron-cell-editor-monaco-widget .view-lines'); + const editor = this.cell.nth(cellIndex).locator('.positron-cell-editor-monaco-widget .view-lines'); const content = await editor.textContent() ?? ''; // Replace the weird ascii space with a proper space return content.replace(/\u00a0/g, ' '); @@ -105,25 +90,40 @@ export class PositronNotebooks extends Notebooks { // #region ACTIONS /** - * Action: Enable the Positron Notebooks feature - * @param settings - The settings fixture. - * @param options - Configuration options for enabling the feature. + * Action: Configure Positron notebook editor in settings. + * @param settings - The settings fixture + * @param editor - 'positron' to use Positron notebook editor, 'default' to clear associations + * @param waitMs - The number of milliseconds to wait for the settings to be applied */ - async configure( - settings: SettingsFixture, - { editor, reload = false, waitMs = 800 }: ConfigureNotebookEditorOptions - ): Promise { - const associations = editor === 'positron' - ? { '*.ipynb': 'workbench.editor.positronNotebook' } - : {}; - - await settings.set( - { - 'positron.notebook.enabled': true, - 'workbench.editorAssociations': associations, - }, - { reload, waitMs, waitForReady: true } - ); + async setNotebookEditor( + settings: { + set: (settings: Record, options?: { reload?: boolean | 'web'; waitMs?: number; waitForReady?: boolean; keepOpen?: boolean }) => Promise; + }, + editor: 'positron' | 'default', + waitMs = 800 + ) { + await settings.set({ + 'positron.notebook.enabled': true, + 'workbench.editorAssociations': editor === 'positron' + ? { '*.ipynb': 'workbench.editor.positronNotebook' } + : {} + }, { waitMs }); + } + + /** + * Action: Enable Positron notebooks in settings and set to 'positron' editor. + * @param settings - The settings fixture + */ + async enablePositronNotebooks( + settings: { + set: (settings: Record, options?: { reload?: boolean | 'web'; waitMs?: number; waitForReady?: boolean; keepOpen?: boolean }) => Promise; + }, + ) { + const config: Record = { + 'positron.notebook.enabled': true, + 'workbench.editorAssociations': { '*.ipynb': 'workbench.editor.positronNotebook' } + }; + await settings.set(config, { reload: true }); } /** @@ -249,6 +249,7 @@ export class PositronNotebooks extends Notebooks { if (run) { await this.runCellButtonAtIndex(cellIndex).click(); + await expect(this.code.driver.page.locator('.notification-toast').getByText(/Starting.*interpreter/)).not.toBeVisible({ timeout: 30000 }); if (waitForSpinner) { const spinner = this.spinnerAtIndex(cellIndex); diff --git a/test/e2e/tests/notebook/cell-deletion-action-bar.test.ts b/test/e2e/tests/notebook/cell-deletion-action-bar.test.ts index aa3540e1c774..2db5c6bd5c10 100644 --- a/test/e2e/tests/notebook/cell-deletion-action-bar.test.ts +++ b/test/e2e/tests/notebook/cell-deletion-action-bar.test.ts @@ -10,22 +10,19 @@ test.use({ suiteId: __filename }); -test.describe('Positorn Notebooks: Cell Deletion Action Bar Behavior', { - tag: [tags.CRITICAL, tags.WIN, tags.NOTEBOOKS] +test.describe('Positron Notebooks: Cell Deletion Action Bar Behavior', { + tag: [tags.CRITICAL, tags.WIN, tags.NOTEBOOKS, tags.POSITRON_NOTEBOOKS] }, () => { test.beforeAll(async function ({ app, settings }) { - await app.workbench.notebooksPositron.configure(settings, { - editor: 'positron', - reload: true, - }); + await app.workbench.notebooksPositron.enablePositronNotebooks(settings); }); test.afterEach(async function ({ hotKeys }) { await hotKeys.closeAllEditors(); }); - test('Cell deletion using action bar', async function ({ app, settings }) { + test('Cell deletion using action bar', async function ({ app }) { const { notebooksPositron } = app.workbench; // ======================================== diff --git a/test/e2e/tests/notebook/cell-execution-info.test.ts b/test/e2e/tests/notebook/cell-execution-info.test.ts index 57921f087e35..bcdee9bd9a3f 100644 --- a/test/e2e/tests/notebook/cell-execution-info.test.ts +++ b/test/e2e/tests/notebook/cell-execution-info.test.ts @@ -10,14 +10,11 @@ test.use({ }); test.describe('Positron Notebooks: Cell Execution Tooltip', { - tag: [tags.CRITICAL, tags.WIN, tags.NOTEBOOKS] + tag: [tags.CRITICAL, tags.WIN, tags.NOTEBOOKS, tags.POSITRON_NOTEBOOKS] }, () => { test.beforeAll(async function ({ app, settings }) { - await app.workbench.notebooksPositron.configure(settings, { - editor: 'positron', - reload: true, - }); + await app.workbench.notebooksPositron.enablePositronNotebooks(settings); }); test.afterEach(async function ({ app, hotKeys }) { diff --git a/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts b/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts index 2cd857da9fb5..a14031bd1be7 100644 --- a/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts +++ b/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts @@ -12,13 +12,10 @@ test.use({ // Not running on web due to Positron notebooks being desktop-only test.describe('Notebook Focus and Selection', { - tag: [tags.CRITICAL, tags.WIN, tags.NOTEBOOKS] + tag: [tags.CRITICAL, tags.WIN, tags.NOTEBOOKS, tags.POSITRON_NOTEBOOKS] }, () => { test.beforeAll(async function ({ app, settings }) { - await app.workbench.notebooksPositron.configure(settings, { - editor: 'positron', - reload: true, - }); + await app.workbench.notebooksPositron.enablePositronNotebooks(settings); }); test.beforeEach(async function ({ app }) { diff --git a/test/e2e/tests/notebook/positron-notebook-copy-paste.test.ts b/test/e2e/tests/notebook/positron-notebook-copy-paste.test.ts index 3f4c711e2e31..737f96a10628 100644 --- a/test/e2e/tests/notebook/positron-notebook-copy-paste.test.ts +++ b/test/e2e/tests/notebook/positron-notebook-copy-paste.test.ts @@ -12,14 +12,11 @@ test.use({ // Not running on web due to https://github.com/posit-dev/positron/issues/9193 test.describe('Positron Notebooks: Cell Copy-Paste Behavior', { - tag: [tags.CRITICAL, tags.WIN, tags.NOTEBOOKS] + tag: [tags.CRITICAL, tags.WIN, tags.NOTEBOOKS, tags.POSITRON_NOTEBOOKS] }, () => { test.beforeAll(async ({ app, settings }) => { - await app.workbench.notebooksPositron.configure(settings, { - editor: 'positron', - reload: true, - }); + await app.workbench.notebooksPositron.enablePositronNotebooks(settings); }); test.afterEach(async function ({ hotKeys }) { diff --git a/test/e2e/tests/notebook/positron-notebook-editor.test.ts b/test/e2e/tests/notebook/positron-notebook-editor.test.ts index 56dd0c9461c1..5dd0f6941010 100644 --- a/test/e2e/tests/notebook/positron-notebook-editor.test.ts +++ b/test/e2e/tests/notebook/positron-notebook-editor.test.ts @@ -14,23 +14,19 @@ test.use({ }); test.describe('Positron Notebooks: Open & Save', { - tag: [tags.CRITICAL, tags.WIN, tags.NOTEBOOKS] + tag: [tags.CRITICAL, tags.WIN, tags.NOTEBOOKS, tags.POSITRON_NOTEBOOKS] }, () => { test.beforeAll(async function ({ app, settings }) { - await app.workbench.notebooksPositron.configure(settings, { - editor: 'default', - reload: true, - }); + await app.workbench.notebooksPositron.enablePositronNotebooks(settings); }); test.beforeEach(async function ({ app, settings }) { // Reset editor associations to default state before each test - await app.workbench.notebooksPositron.configure(settings, { - editor: 'default', - }); + await app.workbench.notebooksPositron.setNotebookEditor(settings, 'default') }); test.afterEach(async function ({ app, settings, hotKeys }) { + await app.workbench.notebooksPositron.setNotebookEditor(settings, 'default'); await hotKeys.closeAllEditors(); }); @@ -44,9 +40,7 @@ test.describe('Positron Notebooks: Open & Save', { // Configure Positron as the default notebook editor // This sets workbench.editorAssociations to map *.ipynb files to the Positron notebook editor - await app.workbench.notebooksPositron.configure(settings, { - editor: 'positron', - }); + await app.workbench.notebooksPositron.setNotebookEditor(settings, 'positron'); // Verify that newly opened notebooks now use the Positron editor // The same notebook file should now open with the Positron interface instead of VS Code @@ -56,9 +50,7 @@ test.describe('Positron Notebooks: Open & Save', { // Reset to default configuration and verify VS Code editor is used again // Close all editors first to ensure a clean state for the next test await hotKeys.closeAllEditors(); - await app.workbench.notebooksPositron.configure(settings, { - editor: 'default', - }); + await app.workbench.notebooksPositron.setNotebookEditor(settings, 'default'); // Confirm that removing the association restores VS Code notebook editor // This ensures the configuration change is properly applied and the fallback works @@ -71,9 +63,7 @@ test.describe('Positron Notebooks: Open & Save', { const { notebooks, notebooksPositron, quickInput, editors } = app.workbench; // Configure Positron as the default notebook editor - await app.workbench.notebooksPositron.configure(settings, { - editor: 'positron', - }); + await app.workbench.notebooksPositron.setNotebookEditor(settings, 'positron'); // Create a new untitled notebook await notebooks.createNewNotebook(); @@ -105,9 +95,7 @@ test.describe('Positron Notebooks: Open & Save', { const { notebooks, notebooksPositron, editors } = app.workbench; // Configure Positron as the default notebook editor - await app.workbench.notebooksPositron.configure(settings, { - editor: 'positron', - }); + await app.workbench.notebooksPositron.setNotebookEditor(settings, 'positron'); // Create a new notebook (which starts dirty) await notebooks.createNewNotebook(); diff --git a/test/e2e/tests/notebook/positron-notebook-undo-redo.test.ts b/test/e2e/tests/notebook/positron-notebook-undo-redo.test.ts index 9e34bec392ce..a8971a4e226f 100644 --- a/test/e2e/tests/notebook/positron-notebook-undo-redo.test.ts +++ b/test/e2e/tests/notebook/positron-notebook-undo-redo.test.ts @@ -11,14 +11,11 @@ test.use({ // Not running on web due to https://github.com/posit-dev/positron/issues/9193 test.describe('Postiron Notebooks: Cell Undo-Redo Behavior', { - tag: [tags.CRITICAL, tags.WIN, tags.NOTEBOOKS] + tag: [tags.CRITICAL, tags.WIN, tags.NOTEBOOKS, tags.POSITRON_NOTEBOOKS] }, () => { test.beforeAll(async function ({ app, settings }) { - await app.workbench.notebooksPositron.configure(settings, { - editor: 'positron', - reload: true, - }); + await app.workbench.notebooksPositron.enablePositronNotebooks(settings); }); test.afterEach(async function ({ hotKeys }) { From 8e6f86eea8f10ba7fe582ea7950d28053c1d6d9b Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Wed, 8 Oct 2025 12:42:12 -0500 Subject: [PATCH 08/22] add back in new tests --- test/e2e/pages/notebooksPositron.ts | 3 +- .../notebook-focus-and-selection.test.ts | 47 ++++++++++++++++++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/test/e2e/pages/notebooksPositron.ts b/test/e2e/pages/notebooksPositron.ts index ccd2d320e0a6..57dbf6775f57 100644 --- a/test/e2e/pages/notebooksPositron.ts +++ b/test/e2e/pages/notebooksPositron.ts @@ -146,6 +146,7 @@ export class PositronNotebooks extends Notebooks { for (let i = 0; i < numCellsToAdd; i++) { await this.addCodeToCell(i, `# Cell ${i}`); } + await this.selectCellAtIndex(0, { exitEditMode: true }); } } @@ -207,7 +208,7 @@ export class PositronNotebooks extends Notebooks { async moveMouseAway(): Promise { await this.code.driver.page.waitForTimeout(500); await this.code.driver.page.mouse.move(0, 0); - }; + } /** * Action: Add code to a cell at the specified index and run it. diff --git a/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts b/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts index a14031bd1be7..e440524017ba 100644 --- a/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts +++ b/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts @@ -3,6 +3,7 @@ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ +import path from 'path'; import { test, tags } from '../_test.setup'; import { expect } from '@playwright/test'; @@ -62,7 +63,7 @@ test.describe('Notebook Focus and Selection', { await notebooksPositron.expectCellIndexToBeSelected(2, { inEditMode: false }); }); - await test.step('Test 6: Focus is maintained across multiple navigation operations', async () => { + await test.step.skip('Test 6: Focus is maintained across multiple navigation operations', async () => { await notebooksPositron.selectCellAtIndex(0, { exitEditMode: true }); // Navigate down multiple times @@ -135,4 +136,48 @@ test.describe('Notebook Focus and Selection', { await notebooksPositron.expectCellContentAtIndexToContain(5, 'new cell content'); }); }); + + test('Navigation between notebooks and default cell selection', async function ({ app }) { + const { notebooks, notebooksPositron } = app.workbench; + const keyboard = app.code.driver.page.keyboard; + const clickTab = (name: string) => app.code.driver.page.getByRole('tab', { name }).click(); + const TAB_1 = 'Untitled-1.ipynb'; + const TAB_2 = 'bitmap-notebook.ipynb'; + + // Start a new notebook (tab 1) + await test.step('Open new notebook: Ensure keyboard navigation', async () => { + await keyboard.press('ArrowDown'); + await notebooksPositron.expectCellIndexToBeSelected(1, { inEditMode: false }); + + await keyboard.press('ArrowDown'); + await notebooksPositron.expectCellIndexToBeSelected(2, { inEditMode: false }); + }); + + // Open an existing notebook (tab 2) which will steal focus away from the first notebook + await test.step('Open existing notebook: Ensure 1st cell is selected', async () => { + const notebookPath = path.join('workspaces', 'bitmap-notebook', TAB_2); + await notebooks.openNotebook(notebookPath, false); + await notebooksPositron.expectToBeVisible(); + await notebooksPositron.expectCellCountToBe(20); + + // Verify first cell is selected (without interaction) + await notebooksPositron.expectCellIndexToBeSelected(0, { inEditMode: false }); + }); + + // ISSUE - this fails! + // Switch back between notebooks to ensure selection is preserved + await test.step.skip('Selection is preserved when switching between editors', async () => { + // Switch back to tab 1 and verify selection is still at cell 2 + await clickTab(TAB_1); + await notebooksPositron.expectCellIndexToBeSelected(2, { inEditMode: false }); + await keyboard.press('ArrowDown'); + await notebooksPositron.expectCellIndexToBeSelected(3, { inEditMode: false }); + + // Switch back to tab 2 and verify selection is still at cell 0 + await clickTab(TAB_2); + await notebooksPositron.expectCellIndexToBeSelected(0, { inEditMode: false }); + await keyboard.press('ArrowDown'); + await notebooksPositron.expectCellIndexToBeSelected(1, { inEditMode: false }); + }); + }); }); From 81726225ad7ffb774b7b789c620100f51854b27e Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Wed, 8 Oct 2025 12:47:09 -0500 Subject: [PATCH 09/22] nit --- test/e2e/pages/notebooksPositron.ts | 30 ++++++++++++----------------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/test/e2e/pages/notebooksPositron.ts b/test/e2e/pages/notebooksPositron.ts index 57dbf6775f57..05aad9891ed9 100644 --- a/test/e2e/pages/notebooksPositron.ts +++ b/test/e2e/pages/notebooksPositron.ts @@ -17,24 +17,19 @@ const DEFAULT_TIMEOUT = 10000; * Notebooks functionality exclusive to Positron notebooks. */ export class PositronNotebooks extends Notebooks { - positronNotebook = this.code.driver.page.locator('.positron-notebook').first(); - cell = this.code.driver.page.locator('[data-testid="notebook-cell"]'); - newCellButton = this.code.driver.page.getByLabel(/new code cell/i); - editorAtIndex = (index: number) => this.cell.nth(index).locator('.positron-cell-editor-monaco-widget textarea'); + private positronNotebook = this.code.driver.page.locator('.positron-notebook').first(); + private cell = this.code.driver.page.locator('[data-testid="notebook-cell"]'); + private newCellButton = this.code.driver.page.getByLabel(/new code cell/i); + private editorAtIndex = (index: number) => this.cell.nth(index).locator('.positron-cell-editor-monaco-widget textarea'); runCellButtonAtIndex = (index: number) => this.cell.nth(index).getByLabel(/execute cell/i); - spinner = this.code.driver.page.getByLabel(/cell is executing/i); - spinnerAtIndex = (index: number) => this.cell.nth(index).getByLabel(/cell is executing/i); - cellExecutionInfoAtIndex = (index: number) => this.cell.nth(index).getByLabel(/cell execution info/i); - executionStatusAtIndex = (index: number) => this.cell.nth(index).locator('[data-execution-status]'); - detectingKernelsText = this.code.driver.page.getByText(/detecting kernels/i); - cellStatusSyncIcon = this.code.driver.page.locator('.cell-status-item-has-runnable .codicon-sync'); - kernelStatusBadge = this.code.driver.page.getByTestId('notebook-kernel-status'); - deleteCellButton = this.cell.getByRole('button', { name: /delete the selected cell/i }); - cellInfoToolTip = this.code.driver.page.getByRole('tooltip', { name: /cell execution details/i }); - cellInfoToolTipStatus = this.cellInfoToolTip.getByLabel('Execution status'); - cellInfoToolTipDuration = this.cellInfoToolTip.getByLabel('Execution duration'); - cellInfoToolTipOrder = this.cellInfoToolTip.getByLabel('Execution order'); - cellInfoToolTipCompleted = this.cellInfoToolTip.getByLabel('Execution completed'); + private spinner = this.code.driver.page.getByLabel(/cell is executing/i); + private spinnerAtIndex = (index: number) => this.cell.nth(index).getByLabel(/cell is executing/i); + private executionStatusAtIndex = (index: number) => this.cell.nth(index).locator('[data-execution-status]'); + private detectingKernelsText = this.code.driver.page.getByText(/detecting kernels/i); + private cellStatusSyncIcon = this.code.driver.page.locator('.cell-status-item-has-runnable .codicon-sync'); + private kernelStatusBadge = this.code.driver.page.getByTestId('notebook-kernel-status'); + private deleteCellButton = this.cell.getByRole('button', { name: /delete the selected cell/i }); + private cellInfoToolTip = this.code.driver.page.getByRole('tooltip', { name: /cell execution details/i }); constructor(code: Code, quickinput: QuickInput, quickaccess: QuickAccess, hotKeys: HotKeys, private clipboard: Clipboard) { super(code, quickinput, quickaccess, hotKeys); @@ -151,7 +146,6 @@ export class PositronNotebooks extends Notebooks { } /** - * @override * Action: Select a cell at the specified index. * @param cellIndex - The index of the cell to select. */ From 4f165811a53e26532470ac449de3eeab10c784e6 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Wed, 8 Oct 2025 14:42:57 -0500 Subject: [PATCH 10/22] skip failing test --- .../notebook-focus-and-selection.test.ts | 61 ++++++++++--------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts b/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts index e440524017ba..f402630af8b5 100644 --- a/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts +++ b/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts @@ -31,59 +31,59 @@ test.describe('Notebook Focus and Selection', { test('Notebook keyboard behavior with cells', async function ({ app }) { const { notebooksPositron } = app.workbench; + const keyboard = app.code.driver.page.keyboard; await test.step('Test 1: Arrow Down navigation moves focus to next cell', async () => { await notebooksPositron.selectCellAtIndex(1, { exitEditMode: true }); - await app.code.driver.page.keyboard.press('ArrowDown'); + await keyboard.press('ArrowDown'); await notebooksPositron.expectCellIndexToBeSelected(2, { inEditMode: false }); }); await test.step('Test 2: Arrow Up navigation moves focus to previous cell', async () => { await notebooksPositron.selectCellAtIndex(3, { exitEditMode: true }); - await app.code.driver.page.keyboard.press('ArrowUp'); + await keyboard.press('ArrowUp'); await notebooksPositron.expectCellIndexToBeSelected(2, { inEditMode: false }); }); await test.step('Test 3: Arrow Down at last cell does not change selection', async () => { await notebooksPositron.selectCellAtIndex(4, { exitEditMode: true }); - await app.code.driver.page.keyboard.press('ArrowDown'); + await keyboard.press('ArrowDown'); await notebooksPositron.expectCellIndexToBeSelected(4, { inEditMode: false }); }); await test.step('Test 4: Arrow Up at first cell does not change selection', async () => { await notebooksPositron.selectCellAtIndex(0, { exitEditMode: true }); - await app.code.driver.page.keyboard.press('ArrowUp'); + await keyboard.press('ArrowUp'); await notebooksPositron.expectCellIndexToBeSelected(0, { inEditMode: false }); }); - await test.step('Test 5: Shift+Arrow Down adds next cell to selection', async () => { - await notebooksPositron.selectCellAtIndex(1, { exitEditMode: true }); - await app.code.driver.page.keyboard.press('Shift+ArrowDown'); - await notebooksPositron.expectCellIndexToBeSelected(1, { inEditMode: false }); - await notebooksPositron.expectCellIndexToBeSelected(2, { inEditMode: false }); - }); - - await test.step.skip('Test 6: Focus is maintained across multiple navigation operations', async () => { - await notebooksPositron.selectCellAtIndex(0, { exitEditMode: true }); - + await test.step('Test 5: Focus is maintained across multiple navigation operations', async () => { // Navigate down multiple times - await app.code.driver.page.keyboard.press('ArrowDown'); + await keyboard.press('ArrowDown'); await notebooksPositron.expectCellIndexToBeSelected(1, { inEditMode: false }); - await app.code.driver.page.keyboard.press('ArrowDown'); + await keyboard.press('ArrowDown'); await notebooksPositron.expectCellIndexToBeSelected(2, { inEditMode: false }); - await app.code.driver.page.keyboard.press('ArrowDown'); + await keyboard.press('ArrowDown'); await notebooksPositron.expectCellIndexToBeSelected(3, { inEditMode: false }); // Navigate up - await app.code.driver.page.keyboard.press('ArrowUp'); + await keyboard.press('ArrowUp'); + await notebooksPositron.expectCellIndexToBeSelected(2, { inEditMode: false }); + }); + + await test.step('Test 6: Shift+Arrow Down adds next cell to selection', async () => { + await notebooksPositron.selectCellAtIndex(1, { exitEditMode: true }); + await keyboard.press('Shift+ArrowDown'); + await notebooksPositron.expectCellIndexToBeSelected(1, { inEditMode: false }); await notebooksPositron.expectCellIndexToBeSelected(2, { inEditMode: false }); }); }); test('Editor mode behavior with notebook cells', async function ({ app }) { const { notebooksPositron } = app.workbench; + const keyboard = app.code.driver.page.keyboard; await test.step('Test 1: Clicking into cell focuses editor and enters edit mode', async () => { // Clicking on cell should focus and enter edit mode @@ -95,21 +95,21 @@ test.describe('Notebook Focus and Selection', { await notebooksPositron.expectCellIndexToBeSelected(4, { isSelected: false, inEditMode: false }); // Verify we can type into the editor after clicking - await app.code.driver.page.keyboard.type('# editor good'); + await keyboard.type('# editor good'); await notebooksPositron.expectCellContentAtIndexToContain(1, '# editor good'); }); await test.step('Test 2: Enter key on selected cell enters edit mode and doesn\'t add new lines', async () => { // Verify pressing Enter enters edit mode await notebooksPositron.selectCellAtIndex(2, { exitEditMode: true }); - await app.code.driver.page.keyboard.press('Enter'); + await keyboard.press('Enter'); await notebooksPositron.expectCellIndexToBeSelected(2, { isSelected: true, inEditMode: true }); // Verify we can type into the editor after pressing Enter - await app.code.driver.page.keyboard.type('# test'); + await keyboard.type('# test'); await notebooksPositron.expectCellContentAtIndexToContain(2, /^# Cell 2# test/); }); @@ -118,21 +118,19 @@ test.describe('Notebook Focus and Selection', { await notebooksPositron.selectCellAtIndex(4); // Get initial cell count - const initialCount = await notebooksPositron.getCellCount(); - expect(initialCount).toBe(5); + await notebooksPositron.expectCellCountToBe(5); // Press Shift+Enter to add a new cell below - await app.code.driver.page.keyboard.press('Shift+Enter'); + await keyboard.press('Shift+Enter'); // Verify new cell was added - const newCount = await notebooksPositron.getCellCount(); - expect(newCount).toBe(6); + await notebooksPositron.expectCellCountToBe(6); // Verify the NEW cell (index 5) is now in edit mode with focus await notebooksPositron.expectCellIndexToBeSelected(5, { inEditMode: true }); // Verify we can type immediately in the new cell - await app.code.driver.page.keyboard.type('new cell content'); + await keyboard.type('new cell content'); await notebooksPositron.expectCellContentAtIndexToContain(5, 'new cell content'); }); }); @@ -140,6 +138,7 @@ test.describe('Notebook Focus and Selection', { test('Navigation between notebooks and default cell selection', async function ({ app }) { const { notebooks, notebooksPositron } = app.workbench; const keyboard = app.code.driver.page.keyboard; + const clickTab = (name: string) => app.code.driver.page.getByRole('tab', { name }).click(); const TAB_1 = 'Untitled-1.ipynb'; const TAB_2 = 'bitmap-notebook.ipynb'; @@ -164,8 +163,8 @@ test.describe('Notebook Focus and Selection', { await notebooksPositron.expectCellIndexToBeSelected(0, { inEditMode: false }); }); - // ISSUE - this fails! - // Switch back between notebooks to ensure selection is preserved + // BUG: https://github.com/posit-dev/positron/issues/9849 + // Switch between notebooks to ensure selection is preserved await test.step.skip('Selection is preserved when switching between editors', async () => { // Switch back to tab 1 and verify selection is still at cell 2 await clickTab(TAB_1); @@ -178,6 +177,10 @@ test.describe('Notebook Focus and Selection', { await notebooksPositron.expectCellIndexToBeSelected(0, { inEditMode: false }); await keyboard.press('ArrowDown'); await notebooksPositron.expectCellIndexToBeSelected(1, { inEditMode: false }); + + // Switch back to tab 1 and verify selection is still at cell 3 + await clickTab(TAB_1); + await notebooksPositron.expectCellIndexToBeSelected(3, { inEditMode: false }); }); }); }); From 7ad1934c4cb251378f6f6dfc5f40cf090c6bada5 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Wed, 8 Oct 2025 15:55:26 -0500 Subject: [PATCH 11/22] add back in num lines test --- test/e2e/pages/notebooksPositron.ts | 12 +++ .../notebook-focus-and-selection.test.ts | 83 +++++++------------ 2 files changed, 42 insertions(+), 53 deletions(-) diff --git a/test/e2e/pages/notebooksPositron.ts b/test/e2e/pages/notebooksPositron.ts index 07bdd1349d7f..58808135a605 100644 --- a/test/e2e/pages/notebooksPositron.ts +++ b/test/e2e/pages/notebooksPositron.ts @@ -579,6 +579,18 @@ export class PositronNotebooks extends Notebooks { } ); } + + /** + * Verify: the cell at the specified index has the expected number of lines. + * @param cellIndex - The index of the cell to check. + * @param numLines - The expected number of lines in the cell. + */ + async expectCellToHaveLineCount({ cellIndex, numLines }): Promise { + await test.step(`Expect cell at index ${cellIndex} to have ${numLines} lines`, async () => { + const viewLines = this.cell.nth(cellIndex).locator('.view-line'); + await expect(viewLines).toHaveCount(numLines, { timeout: DEFAULT_TIMEOUT }); + }); + } // #endregion } diff --git a/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts b/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts index 056cc17a2524..7837014771db 100644 --- a/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts +++ b/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts @@ -115,16 +115,10 @@ test.describe('Notebook Focus and Selection', { }); await test.step('Test 3: Shift+Enter on last cell creates new cell and enters edit mode', async () => { - // Select last cell (index 4) + // Verify pressing Shift+Enter adds a new cell below await notebooksPositron.selectCellAtIndex(4); - - // Get initial cell count await notebooksPositron.expectCellCountToBe(5); - - // Press Shift+Enter to add a new cell below await keyboard.press('Shift+Enter'); - - // Verify new cell was added await notebooksPositron.expectCellCountToBe(6); // Verify the NEW cell (index 5) is now in edit mode with focus @@ -135,54 +129,37 @@ test.describe('Notebook Focus and Selection', { await notebooksPositron.expectCellContentAtIndexToContain(5, 'new cell content'); }); - // await test.step('Enter key in edit mode adds newline within cell', async () => { - // const lineText = '# Cell 3'; - - - // // Select cell 1 and enter edit mode - // await notebooksPositron.selectCellAtIndex(3); - // // await keyboard.press('Enter'); - // await notebooksPositron.expectCellIndexToBeSelected(3, { inEditMode: true }); - // await notebooksPositron.expectCellContentAtIndexToBe(3, lineText); - - // // // // Get cell and editor locators for line counting - // // // const cell = app.code.driver.page.locator('[data-testid="notebook-cell"]').nth(1); - // // // const editor = cell.locator('.positron-cell-editor-monaco-widget'); - // const viewLines2 = notebooksPositron.cell.nth(3).locator('positron-cell-editor-monaco-widget'); - // const viewLines = notebooksPositron.editorAtIndex(3).locator('.view-line'); - - // // // Position cursor in the middle of the cells contents to avoid any trailing newline trimming issues - // await keyboard.press('Home'); - // const middleIndex = Math.floor(lineText.length / 2); - // for (let i = 0; i < middleIndex; i++) { // move to middle of line - // await notebooksPositron.editorAtIndex(3).press('ArrowRight'); - // } - - // // // Sanity check: Get initial line count before pressing Enter - // const initialLineCount = await viewLines.count(); - - // // // Press Enter to split the line in the middle - // await app.code.driver.page.keyboard.press('Enter'); - // // await app.workbench.notebooksPositron.waitForFocusSettle(200); - - // // // Verify the line count increased by counting Monaco's .view-line elements - // // // Note: getCellContent uses .textContent() which strips newlines, so we count line elements directly - // const lineCount = await viewLines.count(); - // const lineCount2 = await viewLines2.count(); - - // // // Line count should have increased by 1 after pressing Enter - // expect(lineCount).toBe(initialLineCount + 1); - - // // // Verify we're still in the same cell (cell count unchanged) - // await notebooksPositron.expectCellCountToBe(5); - - // // // Verify we're still in edit mode in cell 1 - // await notebooksPositron.expectCellIndexToBeSelected(1, { inEditMode: true }); - // // }); - // }); + await test.step('Enter key in edit mode adds newline within cell', async () => { + const lineText = '# Cell 3'; + const numCells = 6; + + // Start with 6 cells + await notebooksPositron.expectCellCountToBe(numCells); + + // Go into edit mode in cell 3 + await notebooksPositron.selectCellAtIndex(3); + await notebooksPositron.expectCellIndexToBeSelected(3, { inEditMode: true }); + await notebooksPositron.expectCellContentAtIndexToBe(3, lineText); + + // Position cursor in the middle of the cells contents to avoid any trailing newline trimming issues + await keyboard.press('Home'); + const middleIndex = Math.floor(lineText.length / 2); + for (let i = 0; i < middleIndex; i++) { // move to middle of line + await notebooksPositron.editorAtIndex(3).press('ArrowRight'); + } + + // Verify the content was splits into two lines + await notebooksPositron.expectCellToHaveLineCount({ cellIndex: 3, numLines: 1 }); + await app.code.driver.page.keyboard.press('Enter'); + await notebooksPositron.expectCellToHaveLineCount({ cellIndex: 3, numLines: 2 }); + await notebooksPositron.expectCellIndexToBeSelected(3, { inEditMode: true }); + + // Verify we still have the same number of cells we started with + await notebooksPositron.expectCellCountToBe(numCells); + }); }); - test('Navigation between notebooks and default cell selection', async function ({ app }) { + test('Notebook navigation and default cell selection', async function ({ app }) { const { notebooks, notebooksPositron } = app.workbench; const keyboard = app.code.driver.page.keyboard; From 72b7eb68dba3355a883fdc50a403dc1d31751f82 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Wed, 8 Oct 2025 16:09:11 -0500 Subject: [PATCH 12/22] nit --- test/e2e/fixtures/test-setup/settings.fixtures.ts | 2 ++ test/e2e/tests/notebook/positron-notebook-editor.test.ts | 5 ++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/test/e2e/fixtures/test-setup/settings.fixtures.ts b/test/e2e/fixtures/test-setup/settings.fixtures.ts index 5a7868412cbb..efa572d80b80 100644 --- a/test/e2e/fixtures/test-setup/settings.fixtures.ts +++ b/test/e2e/fixtures/test-setup/settings.fixtures.ts @@ -29,6 +29,8 @@ export function SettingsFixture(app: Application) { } if (waitForReady) { + await app.code.driver.page.waitForTimeout(3000); + await app.code.driver.page.locator('.monaco-workbench').waitFor({ state: 'visible' }); await app.workbench.sessions.expectNoStartUpMessaging(); } }, diff --git a/test/e2e/tests/notebook/positron-notebook-editor.test.ts b/test/e2e/tests/notebook/positron-notebook-editor.test.ts index 5dd0f6941010..658c22e0c96a 100644 --- a/test/e2e/tests/notebook/positron-notebook-editor.test.ts +++ b/test/e2e/tests/notebook/positron-notebook-editor.test.ts @@ -22,11 +22,10 @@ test.describe('Positron Notebooks: Open & Save', { test.beforeEach(async function ({ app, settings }) { // Reset editor associations to default state before each test - await app.workbench.notebooksPositron.setNotebookEditor(settings, 'default') + await app.workbench.notebooksPositron.setNotebookEditor(settings, 'default'); }); - test.afterEach(async function ({ app, settings, hotKeys }) { - await app.workbench.notebooksPositron.setNotebookEditor(settings, 'default'); + test.afterEach(async function ({ hotKeys }) { await hotKeys.closeAllEditors(); }); From cd11398a1955db2e4c6e9e4e5a148bf913ae8524 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Wed, 8 Oct 2025 16:28:29 -0500 Subject: [PATCH 13/22] grr flake --- test/e2e/pages/notebooksPositron.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/e2e/pages/notebooksPositron.ts b/test/e2e/pages/notebooksPositron.ts index 58808135a605..c7e6440d1c3c 100644 --- a/test/e2e/pages/notebooksPositron.ts +++ b/test/e2e/pages/notebooksPositron.ts @@ -220,7 +220,7 @@ export class PositronNotebooks extends Notebooks { code: string, options?: { delay?: number; run?: boolean; waitForSpinner?: boolean; waitForPopup?: boolean } ): Promise { - const { delay = 0, run = false, waitForSpinner = false, waitForPopup = false } = options ?? {}; + const { delay = 0, run = false, waitForSpinner = false, waitForPopup = true } = options ?? {}; return await test.step(`Add code to cell: ${cellIndex}, run: ${run}, waitForSpinner: ${waitForSpinner}, waitForPopup: ${waitForPopup}`, async () => { const currentCellCount = await this.getCellCount(); @@ -244,7 +244,6 @@ export class PositronNotebooks extends Notebooks { if (run) { await this.runCellButtonAtIndex(cellIndex).click(); - await expect(this.code.driver.page.locator('.notification-toast').getByText(/Starting.*interpreter/)).not.toBeVisible({ timeout: 30000 }); if (waitForSpinner) { const spinner = this.spinnerAtIndex(cellIndex); @@ -255,7 +254,6 @@ export class PositronNotebooks extends Notebooks { } if (waitForPopup) { - // const infoPopup = this.cell.nth(cellIndex).getByRole('tooltip', { name: /cell execution details/i }); await expect(this.cellInfoToolTip).toBeVisible(); } } From 4257d2f18e7d856f5ad3c96ac0489d8b2d803cb7 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Wed, 8 Oct 2025 17:51:56 -0500 Subject: [PATCH 14/22] remove waitForPopup --- test/e2e/pages/notebooks.ts | 4 +--- test/e2e/pages/notebooksPositron.ts | 10 +++------- .../notebook/notebook-focus-and-selection.test.ts | 2 -- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/test/e2e/pages/notebooks.ts b/test/e2e/pages/notebooks.ts index d2d21fdffaa6..5229d8e720b0 100644 --- a/test/e2e/pages/notebooks.ts +++ b/test/e2e/pages/notebooks.ts @@ -102,9 +102,7 @@ export class Notebooks { } async createNewNotebook() { - await test.step('Create new notebook', async () => { - await this.quickaccess.runCommand(NEW_NOTEBOOK_COMMAND); - }); + await this.quickaccess.runCommand(NEW_NOTEBOOK_COMMAND); } // Opens a Notebook that lives in the current workspace diff --git a/test/e2e/pages/notebooksPositron.ts b/test/e2e/pages/notebooksPositron.ts index c7e6440d1c3c..b8105713119f 100644 --- a/test/e2e/pages/notebooksPositron.ts +++ b/test/e2e/pages/notebooksPositron.ts @@ -218,10 +218,10 @@ export class PositronNotebooks extends Notebooks { async addCodeToCell( cellIndex: number, code: string, - options?: { delay?: number; run?: boolean; waitForSpinner?: boolean; waitForPopup?: boolean } + options?: { delay?: number; run?: boolean; waitForSpinner?: boolean; } ): Promise { - const { delay = 0, run = false, waitForSpinner = false, waitForPopup = true } = options ?? {}; - return await test.step(`Add code to cell: ${cellIndex}, run: ${run}, waitForSpinner: ${waitForSpinner}, waitForPopup: ${waitForPopup}`, async () => { + const { delay = 0, run = false, waitForSpinner = false } = options ?? {}; + return await test.step(`Add code to cell: ${cellIndex}, run: ${run}, waitForSpinner: ${waitForSpinner}`, async () => { const currentCellCount = await this.getCellCount(); if (cellIndex >= currentCellCount) { @@ -252,10 +252,6 @@ export class PositronNotebooks extends Notebooks { }); await expect(spinner).toHaveCount(0, { timeout: DEFAULT_TIMEOUT }); } - - if (waitForPopup) { - await expect(this.cellInfoToolTip).toBeVisible(); - } } return this.cell.nth(cellIndex); diff --git a/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts b/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts index 7837014771db..e5c5cb670d81 100644 --- a/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts +++ b/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts @@ -5,8 +5,6 @@ import path from 'path'; import { test, tags } from '../_test.setup'; -import { expect } from '@playwright/test'; -import { PositronNotebooks } from '../../pages/notebooksPositron.js'; test.use({ suiteId: __filename From 34726505464e81c036b8cbb0e38712bbf0a5592c Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Wed, 8 Oct 2025 18:07:11 -0500 Subject: [PATCH 15/22] nit --- test/e2e/tests/notebook/positron-notebook-copy-paste.test.ts | 3 +-- test/e2e/tests/notebook/positron-notebook-editor.test.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/test/e2e/tests/notebook/positron-notebook-copy-paste.test.ts b/test/e2e/tests/notebook/positron-notebook-copy-paste.test.ts index d1597a00b664..1887cc63ff31 100644 --- a/test/e2e/tests/notebook/positron-notebook-copy-paste.test.ts +++ b/test/e2e/tests/notebook/positron-notebook-copy-paste.test.ts @@ -5,7 +5,6 @@ import { test, tags } from '../_test.setup'; import { expect } from '@playwright/test'; -import { PositronNotebooks } from '../../pages/notebooksPositron.js'; test.use({ suiteId: __filename @@ -28,7 +27,7 @@ test.describe('Positron Notebooks: Cell Copy-Paste Behavior', { const { notebooksPositron } = app.workbench; // ======================================== - // Setup: Create notebook with 5 cells and distinct content + // Setup: Create 5 cells with distinct content // ======================================== await test.step('Test Setup: Create notebook and add cells', async () => { await notebooksPositron.newNotebook(5); diff --git a/test/e2e/tests/notebook/positron-notebook-editor.test.ts b/test/e2e/tests/notebook/positron-notebook-editor.test.ts index 658c22e0c96a..d43afe28bbe7 100644 --- a/test/e2e/tests/notebook/positron-notebook-editor.test.ts +++ b/test/e2e/tests/notebook/positron-notebook-editor.test.ts @@ -120,7 +120,7 @@ test.describe('Positron Notebooks: Open & Save', { // Verify that the Positron notebook is visible await notebooksPositron.expectToBeVisible(); - // Verify that the VS Code notebook is NOT visible (this is the ghost editor we're trying to prevent)\ + // Verify that the VS Code notebook is NOT visible (this is the ghost editor we're trying const positronNotebookElements = app.code.driver.page.locator('.positron-notebook'); const vscodeNotebookElements = app.code.driver.page.locator('.notebook-editor'); From e2a5f705336f9edcb54e67b44a8f6cf076620501 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Wed, 8 Oct 2025 19:23:05 -0500 Subject: [PATCH 16/22] extra ck --- test/e2e/pages/notebooksPositron.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/e2e/pages/notebooksPositron.ts b/test/e2e/pages/notebooksPositron.ts index b8105713119f..da622b4b2c75 100644 --- a/test/e2e/pages/notebooksPositron.ts +++ b/test/e2e/pages/notebooksPositron.ts @@ -156,9 +156,10 @@ export class PositronNotebooks extends Notebooks { await this.expectCellIndexToBeSelected(cellIndex, { isSelected: true, inEditMode: true }); if (exitEditMode) { + await this.code.driver.page.waitForTimeout(500); await this.code.driver.page.keyboard.press('Escape'); - await this.expectCellIndexToBeSelected(cellIndex, { isSelected: true, inEditMode: false }); } + await this.expectCellIndexToBeSelected(cellIndex, { isSelected: true, inEditMode: !exitEditMode }); }); } From bf3bfd1064c39ce0514ecf8356eea20f909a744e Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Wed, 8 Oct 2025 20:22:46 -0500 Subject: [PATCH 17/22] remove selecting 1st cell --- test/e2e/pages/notebooksPositron.ts | 3 +-- test/e2e/tests/notebook/notebook-focus-and-selection.test.ts | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/e2e/pages/notebooksPositron.ts b/test/e2e/pages/notebooksPositron.ts index da622b4b2c75..e495b07c2df0 100644 --- a/test/e2e/pages/notebooksPositron.ts +++ b/test/e2e/pages/notebooksPositron.ts @@ -141,7 +141,6 @@ export class PositronNotebooks extends Notebooks { for (let i = 0; i < numCellsToAdd; i++) { await this.addCodeToCell(i, `# Cell ${i}`); } - await this.selectCellAtIndex(0, { exitEditMode: true }); } } @@ -219,7 +218,7 @@ export class PositronNotebooks extends Notebooks { async addCodeToCell( cellIndex: number, code: string, - options?: { delay?: number; run?: boolean; waitForSpinner?: boolean; } + options?: { delay?: number; run?: boolean; waitForSpinner?: boolean } ): Promise { const { delay = 0, run = false, waitForSpinner = false } = options ?? {}; return await test.step(`Add code to cell: ${cellIndex}, run: ${run}, waitForSpinner: ${waitForSpinner}`, async () => { diff --git a/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts b/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts index e5c5cb670d81..e7a6a32a2867 100644 --- a/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts +++ b/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts @@ -167,6 +167,7 @@ test.describe('Notebook Focus and Selection', { // Start a new notebook (tab 1) await test.step('Open new notebook: Ensure keyboard navigation', async () => { + await notebooksPositron.selectCellAtIndex(0, { exitEditMode: true }); await keyboard.press('ArrowDown'); await notebooksPositron.expectCellIndexToBeSelected(1, { inEditMode: false }); From 4701bb0d3cf4c1756a3e0c14b102a3db50781c8f Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Thu, 9 Oct 2025 06:50:56 -0500 Subject: [PATCH 18/22] add clipboard debug --- test/e2e/pages/notebooksPositron.ts | 12 ++++++---- .../notebook/cell-execution-info.test.ts | 2 +- .../positron-notebook-copy-paste.test.ts | 22 ++++++++++++++++++- .../positron-notebook-undo-redo.test.ts | 2 +- 4 files changed, 31 insertions(+), 7 deletions(-) diff --git a/test/e2e/pages/notebooksPositron.ts b/test/e2e/pages/notebooksPositron.ts index e495b07c2df0..964d8316f619 100644 --- a/test/e2e/pages/notebooksPositron.ts +++ b/test/e2e/pages/notebooksPositron.ts @@ -436,10 +436,14 @@ export class PositronNotebooks extends Notebooks { * Verify: Cell info tooltip contains expected content. * @param expectedContent - Object with expected content to verify. * Use RegExp for fields where exact match is not feasible (e.g., duration, completed time). + * @param timeout - Optional timeout for the expectation. */ - async expectToolTipToContain(expectedContent: { order?: number; duration?: RegExp; status?: 'Success' | 'Failed' | 'Currently running...'; completed?: RegExp }): Promise { + async expectToolTipToContain( + expectedContent: { order?: number; duration?: RegExp; status?: 'Success' | 'Failed' | 'Currently running...'; completed?: RegExp }, + timeout = DEFAULT_TIMEOUT + ): Promise { await test.step(`Expect cell info tooltip to contain: ${JSON.stringify(expectedContent)}`, async () => { - await expect(this.cellInfoToolTip).toBeVisible({ timeout: DEFAULT_TIMEOUT }); + await expect(this.cellInfoToolTip).toBeVisible({ timeout }); const labelMap: Record = { order: 'Execution Order', @@ -460,11 +464,11 @@ export class PositronNotebooks extends Notebooks { if (key === 'status' && expectedValue === 'Currently running...') { // Special case when cell is actively running: check for label, not value const labelLocator = this.code.driver.page.locator('.popup-label', { hasText: 'Currently running...' }); - await expect(labelLocator).toBeVisible({ timeout: DEFAULT_TIMEOUT }); + await expect(labelLocator).toBeVisible({ timeout }); } else { const valueLocator = getValueLocator(labelMap[key]); const expectedText = expectedValue instanceof RegExp ? expectedValue : expectedValue.toString(); - await expect(valueLocator).toContainText(expectedText, { timeout: DEFAULT_TIMEOUT }); + await expect(valueLocator).toContainText(expectedText, { timeout }); } } } diff --git a/test/e2e/tests/notebook/cell-execution-info.test.ts b/test/e2e/tests/notebook/cell-execution-info.test.ts index bcdee9bd9a3f..54faa2df7496 100644 --- a/test/e2e/tests/notebook/cell-execution-info.test.ts +++ b/test/e2e/tests/notebook/cell-execution-info.test.ts @@ -45,7 +45,7 @@ test.describe('Positron Notebooks: Cell Execution Tooltip', { order: 1, duration: /\d+ms/, status: 'Success' - }); + }, 30000); // Verify auto-close behavior await notebooksPositron.moveMouseAway(); diff --git a/test/e2e/tests/notebook/positron-notebook-copy-paste.test.ts b/test/e2e/tests/notebook/positron-notebook-copy-paste.test.ts index 1887cc63ff31..1289d6b1ebf2 100644 --- a/test/e2e/tests/notebook/positron-notebook-copy-paste.test.ts +++ b/test/e2e/tests/notebook/positron-notebook-copy-paste.test.ts @@ -24,7 +24,7 @@ test.describe('Positron Notebooks: Cell Copy-Paste Behavior', { }); test('Should correctly copy and paste cell content in various scenarios', async function ({ app }) { - const { notebooksPositron } = app.workbench; + const { notebooksPositron, clipboard } = app.workbench; // ======================================== // Setup: Create 5 cells with distinct content @@ -48,6 +48,26 @@ test.describe('Positron Notebooks: Cell Copy-Paste Behavior', { // Move to last cell and paste after it await notebooksPositron.selectCellAtIndex(4); + // DEBUG: clipboard contents. Remove after issue resolved. + await clipboard.expectClipboardTextToBe(JSON.stringify({ + cells: [ + { + cell_type: 'code', + source: [ + '# Cell 2' + ], + metadata: {}, + outputs: [], + execution_count: null + } + ], + metadata: { + kernelspec: {}, + language_info: {} + }, + nbformat: 4, + nbformat_minor: 2 + }, null, 2)); await notebooksPositron.performCellAction('paste'); // Verify cell count increased diff --git a/test/e2e/tests/notebook/positron-notebook-undo-redo.test.ts b/test/e2e/tests/notebook/positron-notebook-undo-redo.test.ts index a8971a4e226f..f682f356c33d 100644 --- a/test/e2e/tests/notebook/positron-notebook-undo-redo.test.ts +++ b/test/e2e/tests/notebook/positron-notebook-undo-redo.test.ts @@ -40,7 +40,7 @@ test.describe('Postiron Notebooks: Cell Undo-Redo Behavior', { await notebooksPositron.expectCellContentAtIndexToBe(0, '# Initial Cell'); // Add a second cell - await notebooksPositron.selectCellAtIndex(0); + await notebooksPositron.selectCellAtIndex(0, { exitEditMode: true }); await notebooksPositron.performCellAction('addCellBelow'); await notebooksPositron.addCodeToCell(1, '# Second Cell'); await notebooksPositron.expectCellCountToBe(2); From bdfcc96f7d48ed0bb0b6c035748f196e77f722d3 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Thu, 9 Oct 2025 07:39:18 -0500 Subject: [PATCH 19/22] better retry on escaping edit mode --- test/e2e/pages/notebooksPositron.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/e2e/pages/notebooksPositron.ts b/test/e2e/pages/notebooksPositron.ts index 964d8316f619..f0fcb557cca2 100644 --- a/test/e2e/pages/notebooksPositron.ts +++ b/test/e2e/pages/notebooksPositron.ts @@ -156,9 +156,12 @@ export class PositronNotebooks extends Notebooks { if (exitEditMode) { await this.code.driver.page.waitForTimeout(500); - await this.code.driver.page.keyboard.press('Escape'); + await expect(async () => { + await this.code.driver.page.keyboard.press('Escape'); + await this.expectCellIndexToBeSelected(cellIndex, { isSelected: true, inEditMode: false }); + }, 'should NOT be in edit mode').toPass({ timeout: DEFAULT_TIMEOUT }); } - await this.expectCellIndexToBeSelected(cellIndex, { isSelected: true, inEditMode: !exitEditMode }); + // await this.expectCellIndexToBeSelected(cellIndex, { isSelected: true, inEditMode: !exitEditMode }); }); } From 10224ba2eb5ab14b99135d9632dde35835aba481 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Thu, 9 Oct 2025 09:02:56 -0500 Subject: [PATCH 20/22] copy-paste flakes --- test/e2e/pages/clipboard.ts | 3 +- test/e2e/pages/notebooksPositron.ts | 58 ++++++------ .../positron-notebook-copy-paste.test.ts | 93 +++++-------------- 3 files changed, 55 insertions(+), 99 deletions(-) diff --git a/test/e2e/pages/clipboard.ts b/test/e2e/pages/clipboard.ts index de10ca4c93a7..e748174347e2 100644 --- a/test/e2e/pages/clipboard.ts +++ b/test/e2e/pages/clipboard.ts @@ -35,9 +35,10 @@ export class Clipboard { await this.setClipboardText(seed); // Wait until clipboard value differs from the seed + await this.hotKeys.cut(); + await expect .poll(async () => { - await this.hotKeys.cut(); return (await this.getClipboardText()) ?? ''; }, { message: 'clipboard should change after cut', diff --git a/test/e2e/pages/notebooksPositron.ts b/test/e2e/pages/notebooksPositron.ts index f0fcb557cca2..842306ae55fd 100644 --- a/test/e2e/pages/notebooksPositron.ts +++ b/test/e2e/pages/notebooksPositron.ts @@ -266,34 +266,36 @@ export class PositronNotebooks extends Notebooks { * @param action - The action to perform: 'copy', 'cut', 'paste', 'undo', 'redo', 'delete', 'addCellBelow'. */ async performCellAction(action: 'copy' | 'cut' | 'paste' | 'undo' | 'redo' | 'delete' | 'addCellBelow'): Promise { - // Press escape to ensure focus is out of the cell editor - await this.code.driver.page.keyboard.press('Escape'); - - switch (action) { - case 'copy': - await this.clipboard.copy(); - break; - case 'cut': - await this.clipboard.cut(); - break; - case 'paste': - await this.clipboard.paste(); - break; - case 'undo': - await this.hotKeys.undo(); - break; - case 'redo': - await this.hotKeys.redo(); - break; - case 'delete': - await this.code.driver.page.keyboard.press('Backspace'); - break; - case 'addCellBelow': - await this.code.driver.page.keyboard.press('KeyB'); - break; - default: - throw new Error(`Unknown cell action: ${action}`); - } + await test.step(`Perform cell action: ${action}`, async () => { + // Press escape to ensure focus is out of the cell editor + await this.code.driver.page.keyboard.press('Escape'); + + switch (action) { + case 'copy': + await this.clipboard.copy(); + break; + case 'cut': + await this.clipboard.cut(); + break; + case 'paste': + await this.clipboard.paste(); + break; + case 'undo': + await this.hotKeys.undo(); + break; + case 'redo': + await this.hotKeys.redo(); + break; + case 'delete': + await this.code.driver.page.keyboard.press('Backspace'); + break; + case 'addCellBelow': + await this.code.driver.page.keyboard.press('KeyB'); + break; + default: + throw new Error(`Unknown cell action: ${action}`); + } + }); } /** diff --git a/test/e2e/tests/notebook/positron-notebook-copy-paste.test.ts b/test/e2e/tests/notebook/positron-notebook-copy-paste.test.ts index 1289d6b1ebf2..d81c4e56678d 100644 --- a/test/e2e/tests/notebook/positron-notebook-copy-paste.test.ts +++ b/test/e2e/tests/notebook/positron-notebook-copy-paste.test.ts @@ -24,7 +24,7 @@ test.describe('Positron Notebooks: Cell Copy-Paste Behavior', { }); test('Should correctly copy and paste cell content in various scenarios', async function ({ app }) { - const { notebooksPositron, clipboard } = app.workbench; + const { notebooksPositron } = app.workbench; // ======================================== // Setup: Create 5 cells with distinct content @@ -38,42 +38,17 @@ test.describe('Positron Notebooks: Cell Copy-Paste Behavior', { // Test 1: Copy single cell and paste at end // ======================================== await test.step('Test 1: Copy single cell and paste at end', async () => { - await notebooksPositron.selectCellAtIndex(2); - - // Verify cell 2 has correct content + // Perform copy on cell 2 + await notebooksPositron.selectCellAtIndex(2, { exitEditMode: true }); await notebooksPositron.expectCellContentAtIndexToBe(2, '# Cell 2'); - - // Copy the cell await notebooksPositron.performCellAction('copy'); - // Move to last cell and paste after it - await notebooksPositron.selectCellAtIndex(4); - // DEBUG: clipboard contents. Remove after issue resolved. - await clipboard.expectClipboardTextToBe(JSON.stringify({ - cells: [ - { - cell_type: 'code', - source: [ - '# Cell 2' - ], - metadata: {}, - outputs: [], - execution_count: null - } - ], - metadata: { - kernelspec: {}, - language_info: {} - }, - nbformat: 4, - nbformat_minor: 2 - }, null, 2)); + // Move to last cell and perform paste + await notebooksPositron.selectCellAtIndex(4, { exitEditMode: true }); await notebooksPositron.performCellAction('paste'); - - // Verify cell count increased await notebooksPositron.expectCellCountToBe(6); - // Verify the pasted cell has the correct content (should be at index 5) + // Verify pasted contents are correct at new index 5 expect(await notebooksPositron.getCellContent(5)).toBe('# Cell 2'); }); @@ -81,28 +56,21 @@ test.describe('Positron Notebooks: Cell Copy-Paste Behavior', { // Test 2: Cut single cell and paste at different position // ======================================== await test.step('Test 2: Cut single cell and paste at different position', async () => { - await notebooksPositron.selectCellAtIndex(1); - - // Verify we're at cell 1 with correct content + // Perform cut on cell 1 + await notebooksPositron.selectCellAtIndex(1, { exitEditMode: true }); await notebooksPositron.expectCellContentAtIndexToBe(1, '# Cell 1'); - - // Cut the cell await notebooksPositron.performCellAction('cut'); - // Verify cell count decreased + // Verify cell count decreased and cell 1 is removed await notebooksPositron.expectCellCountToBe(5); - - // Verify what was cell 2 is now at index 1 await notebooksPositron.expectCellContentAtIndexToBe(1, '# Cell 2'); // Move to index 3 and paste - await notebooksPositron.selectCellAtIndex(3); + await notebooksPositron.selectCellAtIndex(3, { exitEditMode: true }); await notebooksPositron.performCellAction('paste'); - // Verify cell count is back to 6 - await notebooksPositron.expectCellCountToBe(6); - - // Verify the pasted cell has correct content at index 4 + // Verify cell count restored and cell content is correct + await notebooksPositron.expectCellCountToBe(6) await notebooksPositron.expectCellContentAtIndexToBe(4, '# Cell 1'); }); @@ -110,14 +78,15 @@ test.describe('Positron Notebooks: Cell Copy-Paste Behavior', { // Test 3: Copy cell and paste multiple times (clipboard persistence) // ======================================== await test.step('Test 3: Copy cell and paste multiple times (clipboard persistence)', async () => { - await notebooksPositron.selectCellAtIndex(0); + await notebooksPositron.expectCellCountToBe(6); // Copy cell 0 + await notebooksPositron.selectCellAtIndex(0, { exitEditMode: true }); await notebooksPositron.expectCellContentAtIndexToBe(0, '# Cell 0'); await notebooksPositron.performCellAction('copy'); // Paste at position 2 - await notebooksPositron.selectCellAtIndex(2); + await notebooksPositron.selectCellAtIndex(2, { exitEditMode: true }); await notebooksPositron.performCellAction('paste'); // Verify first paste @@ -125,7 +94,7 @@ test.describe('Positron Notebooks: Cell Copy-Paste Behavior', { await notebooksPositron.expectCellContentAtIndexToBe(3, '# Cell 0'); // Paste again at position 5 - await notebooksPositron.selectCellAtIndex(5); + await notebooksPositron.selectCellAtIndex(5, { exitEditMode: true }); await notebooksPositron.performCellAction('paste'); // Verify second paste @@ -137,25 +106,18 @@ test.describe('Positron Notebooks: Cell Copy-Paste Behavior', { // Test 4: Cut and paste at beginning of notebook // ======================================== await test.step('Test 4: Cut and paste at beginning of notebook', async () => { - // Select a middle cell to cut - await notebooksPositron.selectCellAtIndex(4); + // Cut cell 4 (from the middle of the notebook) + await notebooksPositron.selectCellAtIndex(4, { exitEditMode: true }); const cellToMoveContent = await notebooksPositron.getCellContent(4); - - // Cut the cell await notebooksPositron.performCellAction('cut'); - - // Verify cell removed await notebooksPositron.expectCellCountToBe(7); // Move to first cell and paste - // Note: Paste typically inserts after the current cell - await notebooksPositron.selectCellAtIndex(0); + await notebooksPositron.selectCellAtIndex(0, { exitEditMode: true }); await notebooksPositron.performCellAction('paste'); - // Verify cell count restored + // Verify cell count restored and content is correct await notebooksPositron.expectCellCountToBe(8); - - // Verify pasted cell is at index 1 (pasted after cell 0) await notebooksPositron.expectCellContentAtIndexToBe(1, cellToMoveContent); }); @@ -163,21 +125,12 @@ test.describe('Positron Notebooks: Cell Copy-Paste Behavior', { // Test 5: Cut all cells and verify notebook can be empty // ======================================== await test.step('Verify other cells shifted down correctly', async () => { - // Delete cells until only one remains - while (await notebooksPositron.getCellCount() > 1) { - await notebooksPositron.selectCellAtIndex(0); + while (await notebooksPositron.getCellCount() > 0) { + await notebooksPositron.selectCellAtIndex(0, { exitEditMode: true }); await notebooksPositron.performCellAction('cut'); } - // Verify we have exactly one cell - await notebooksPositron.expectCellCountToBe(1); - - // Cut the last cell - in Positron notebooks, this may be allowed - await notebooksPositron.performCellAction('cut'); - - // Check if notebook can be empty (Positron may allow 0 cells) - const finalCount = await notebooksPositron.getCellCount(); - expect(finalCount).toBeLessThanOrEqual(1); + await notebooksPositron.expectCellCountToBe(0); }); }); }); From 440ec04fe471896887fbf7b34ca2ebebafb33014 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Thu, 9 Oct 2025 09:51:53 -0500 Subject: [PATCH 21/22] rename exitMode --- test/e2e/pages/notebooksPositron.ts | 5 ++--- .../notebook-focus-and-selection.test.ts | 14 ++++++------- .../positron-notebook-copy-paste.test.ts | 20 +++++++++---------- .../positron-notebook-undo-redo.test.ts | 2 +- 4 files changed, 20 insertions(+), 21 deletions(-) diff --git a/test/e2e/pages/notebooksPositron.ts b/test/e2e/pages/notebooksPositron.ts index 842306ae55fd..e9331be0e800 100644 --- a/test/e2e/pages/notebooksPositron.ts +++ b/test/e2e/pages/notebooksPositron.ts @@ -148,20 +148,19 @@ export class PositronNotebooks extends Notebooks { * Action: Select a cell at the specified index. * @param cellIndex - The index of the cell to select. */ - async selectCellAtIndex(cellIndex: number, { exitEditMode = false }: { exitEditMode?: boolean } = {}): Promise { + async selectCellAtIndex(cellIndex: number, { editMode = false }: { editMode?: boolean } = {}): Promise { await test.step(`Select cell at index: ${cellIndex}`, async () => { await this.cell.nth(cellIndex).click(); await this.expectCellIndexToBeSelected(cellIndex, { isSelected: true, inEditMode: true }); - if (exitEditMode) { + if (editMode === false) { await this.code.driver.page.waitForTimeout(500); await expect(async () => { await this.code.driver.page.keyboard.press('Escape'); await this.expectCellIndexToBeSelected(cellIndex, { isSelected: true, inEditMode: false }); }, 'should NOT be in edit mode').toPass({ timeout: DEFAULT_TIMEOUT }); } - // await this.expectCellIndexToBeSelected(cellIndex, { isSelected: true, inEditMode: !exitEditMode }); }); } diff --git a/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts b/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts index e7a6a32a2867..5813a7514926 100644 --- a/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts +++ b/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts @@ -33,25 +33,25 @@ test.describe('Notebook Focus and Selection', { const keyboard = app.code.driver.page.keyboard; await test.step('Test 1: Arrow Down navigation moves focus to next cell', async () => { - await notebooksPositron.selectCellAtIndex(1, { exitEditMode: true }); + await notebooksPositron.selectCellAtIndex(1, { editMode: false }); await keyboard.press('ArrowDown'); await notebooksPositron.expectCellIndexToBeSelected(2, { inEditMode: false }); }); await test.step('Test 2: Arrow Up navigation moves focus to previous cell', async () => { - await notebooksPositron.selectCellAtIndex(3, { exitEditMode: true }); + await notebooksPositron.selectCellAtIndex(3, { editMode: false }); await keyboard.press('ArrowUp'); await notebooksPositron.expectCellIndexToBeSelected(2, { inEditMode: false }); }); await test.step('Test 3: Arrow Down at last cell does not change selection', async () => { - await notebooksPositron.selectCellAtIndex(4, { exitEditMode: true }); + await notebooksPositron.selectCellAtIndex(4, { editMode: false }); await keyboard.press('ArrowDown'); await notebooksPositron.expectCellIndexToBeSelected(4, { inEditMode: false }); }); await test.step('Test 4: Arrow Up at first cell does not change selection', async () => { - await notebooksPositron.selectCellAtIndex(0, { exitEditMode: true }); + await notebooksPositron.selectCellAtIndex(0, { editMode: false }); await keyboard.press('ArrowUp'); await notebooksPositron.expectCellIndexToBeSelected(0, { inEditMode: false }); }); @@ -73,7 +73,7 @@ test.describe('Notebook Focus and Selection', { }); await test.step('Test 6: Shift+Arrow Down adds next cell to selection', async () => { - await notebooksPositron.selectCellAtIndex(1, { exitEditMode: true }); + await notebooksPositron.selectCellAtIndex(1, { editMode: false }); await keyboard.press('Shift+ArrowDown'); await notebooksPositron.expectCellIndexToBeSelected(1, { inEditMode: false }); await notebooksPositron.expectCellIndexToBeSelected(2, { inEditMode: false }); @@ -100,7 +100,7 @@ test.describe('Notebook Focus and Selection', { await test.step('Test 2: Enter key on selected cell enters edit mode and doesn\'t add new lines', async () => { // Verify pressing Enter enters edit mode - await notebooksPositron.selectCellAtIndex(2, { exitEditMode: true }); + await notebooksPositron.selectCellAtIndex(2, { editMode: false }); await keyboard.press('Enter'); await notebooksPositron.expectCellIndexToBeSelected(2, { isSelected: true, @@ -167,7 +167,7 @@ test.describe('Notebook Focus and Selection', { // Start a new notebook (tab 1) await test.step('Open new notebook: Ensure keyboard navigation', async () => { - await notebooksPositron.selectCellAtIndex(0, { exitEditMode: true }); + await notebooksPositron.selectCellAtIndex(0, { editMode: false }); await keyboard.press('ArrowDown'); await notebooksPositron.expectCellIndexToBeSelected(1, { inEditMode: false }); diff --git a/test/e2e/tests/notebook/positron-notebook-copy-paste.test.ts b/test/e2e/tests/notebook/positron-notebook-copy-paste.test.ts index d81c4e56678d..42ba61bb01b8 100644 --- a/test/e2e/tests/notebook/positron-notebook-copy-paste.test.ts +++ b/test/e2e/tests/notebook/positron-notebook-copy-paste.test.ts @@ -39,12 +39,12 @@ test.describe('Positron Notebooks: Cell Copy-Paste Behavior', { // ======================================== await test.step('Test 1: Copy single cell and paste at end', async () => { // Perform copy on cell 2 - await notebooksPositron.selectCellAtIndex(2, { exitEditMode: true }); + await notebooksPositron.selectCellAtIndex(2, { editMode: false }); await notebooksPositron.expectCellContentAtIndexToBe(2, '# Cell 2'); await notebooksPositron.performCellAction('copy'); // Move to last cell and perform paste - await notebooksPositron.selectCellAtIndex(4, { exitEditMode: true }); + await notebooksPositron.selectCellAtIndex(4, { editMode: false }); await notebooksPositron.performCellAction('paste'); await notebooksPositron.expectCellCountToBe(6); @@ -57,7 +57,7 @@ test.describe('Positron Notebooks: Cell Copy-Paste Behavior', { // ======================================== await test.step('Test 2: Cut single cell and paste at different position', async () => { // Perform cut on cell 1 - await notebooksPositron.selectCellAtIndex(1, { exitEditMode: true }); + await notebooksPositron.selectCellAtIndex(1, { editMode: false }); await notebooksPositron.expectCellContentAtIndexToBe(1, '# Cell 1'); await notebooksPositron.performCellAction('cut'); @@ -66,7 +66,7 @@ test.describe('Positron Notebooks: Cell Copy-Paste Behavior', { await notebooksPositron.expectCellContentAtIndexToBe(1, '# Cell 2'); // Move to index 3 and paste - await notebooksPositron.selectCellAtIndex(3, { exitEditMode: true }); + await notebooksPositron.selectCellAtIndex(3, { editMode: false }); await notebooksPositron.performCellAction('paste'); // Verify cell count restored and cell content is correct @@ -81,12 +81,12 @@ test.describe('Positron Notebooks: Cell Copy-Paste Behavior', { await notebooksPositron.expectCellCountToBe(6); // Copy cell 0 - await notebooksPositron.selectCellAtIndex(0, { exitEditMode: true }); + await notebooksPositron.selectCellAtIndex(0, { editMode: false }); await notebooksPositron.expectCellContentAtIndexToBe(0, '# Cell 0'); await notebooksPositron.performCellAction('copy'); // Paste at position 2 - await notebooksPositron.selectCellAtIndex(2, { exitEditMode: true }); + await notebooksPositron.selectCellAtIndex(2, { editMode: false }); await notebooksPositron.performCellAction('paste'); // Verify first paste @@ -94,7 +94,7 @@ test.describe('Positron Notebooks: Cell Copy-Paste Behavior', { await notebooksPositron.expectCellContentAtIndexToBe(3, '# Cell 0'); // Paste again at position 5 - await notebooksPositron.selectCellAtIndex(5, { exitEditMode: true }); + await notebooksPositron.selectCellAtIndex(5, { editMode: false }); await notebooksPositron.performCellAction('paste'); // Verify second paste @@ -107,13 +107,13 @@ test.describe('Positron Notebooks: Cell Copy-Paste Behavior', { // ======================================== await test.step('Test 4: Cut and paste at beginning of notebook', async () => { // Cut cell 4 (from the middle of the notebook) - await notebooksPositron.selectCellAtIndex(4, { exitEditMode: true }); + await notebooksPositron.selectCellAtIndex(4, { editMode: false }); const cellToMoveContent = await notebooksPositron.getCellContent(4); await notebooksPositron.performCellAction('cut'); await notebooksPositron.expectCellCountToBe(7); // Move to first cell and paste - await notebooksPositron.selectCellAtIndex(0, { exitEditMode: true }); + await notebooksPositron.selectCellAtIndex(0, { editMode: false }); await notebooksPositron.performCellAction('paste'); // Verify cell count restored and content is correct @@ -126,7 +126,7 @@ test.describe('Positron Notebooks: Cell Copy-Paste Behavior', { // ======================================== await test.step('Verify other cells shifted down correctly', async () => { while (await notebooksPositron.getCellCount() > 0) { - await notebooksPositron.selectCellAtIndex(0, { exitEditMode: true }); + await notebooksPositron.selectCellAtIndex(0, { editMode: false }); await notebooksPositron.performCellAction('cut'); } diff --git a/test/e2e/tests/notebook/positron-notebook-undo-redo.test.ts b/test/e2e/tests/notebook/positron-notebook-undo-redo.test.ts index f682f356c33d..bd5e152496e1 100644 --- a/test/e2e/tests/notebook/positron-notebook-undo-redo.test.ts +++ b/test/e2e/tests/notebook/positron-notebook-undo-redo.test.ts @@ -40,7 +40,7 @@ test.describe('Postiron Notebooks: Cell Undo-Redo Behavior', { await notebooksPositron.expectCellContentAtIndexToBe(0, '# Initial Cell'); // Add a second cell - await notebooksPositron.selectCellAtIndex(0, { exitEditMode: true }); + await notebooksPositron.selectCellAtIndex(0, { editMode: false }); await notebooksPositron.performCellAction('addCellBelow'); await notebooksPositron.addCodeToCell(1, '# Second Cell'); await notebooksPositron.expectCellCountToBe(2); From ba06b7a17f0a20cf50f9ff99408a9996349fa136 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Thu, 9 Oct 2025 11:23:58 -0500 Subject: [PATCH 22/22] oops wrong default --- test/e2e/pages/notebooksPositron.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/e2e/pages/notebooksPositron.ts b/test/e2e/pages/notebooksPositron.ts index e9331be0e800..948e5c610ede 100644 --- a/test/e2e/pages/notebooksPositron.ts +++ b/test/e2e/pages/notebooksPositron.ts @@ -148,18 +148,20 @@ export class PositronNotebooks extends Notebooks { * Action: Select a cell at the specified index. * @param cellIndex - The index of the cell to select. */ - async selectCellAtIndex(cellIndex: number, { editMode = false }: { editMode?: boolean } = {}): Promise { + async selectCellAtIndex(cellIndex: number, { editMode = true }: { editMode?: boolean } = {}): Promise { await test.step(`Select cell at index: ${cellIndex}`, async () => { await this.cell.nth(cellIndex).click(); await this.expectCellIndexToBeSelected(cellIndex, { isSelected: true, inEditMode: true }); - if (editMode === false) { + if (!editMode) { await this.code.driver.page.waitForTimeout(500); await expect(async () => { await this.code.driver.page.keyboard.press('Escape'); await this.expectCellIndexToBeSelected(cellIndex, { isSelected: true, inEditMode: false }); }, 'should NOT be in edit mode').toPass({ timeout: DEFAULT_TIMEOUT }); + } else { + await this.expectCellIndexToBeSelected(cellIndex, { isSelected: true, inEditMode: true }); } }); } @@ -573,6 +575,7 @@ export class PositronNotebooks extends Notebooks { if (inEditMode !== undefined) { const ta = this.editorAtIndex(expectedIndex); const isEditing = await ta.evaluate(el => el === document.activeElement); + inEditMode ? expect(isEditing).toBe(true) : expect(isEditing).toBe(false);