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/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..e748174347e2 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,25 @@ 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 this.hotKeys.cut(); + + await expect + .poll(async () => { + 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/hotKeys.ts b/test/e2e/pages/hotKeys.ts index 970e4c35c972..47fc73074d24 100644 --- a/test/e2e/pages/hotKeys.ts +++ b/test/e2e/pages/hotKeys.ts @@ -201,8 +201,15 @@ 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'); + + // 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 }); + } } 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 d72504ac367e..948e5c610ede 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'; @@ -16,248 +17,227 @@ const DEFAULT_TIMEOUT = 10000; * Notebooks functionality exclusive to Positron notebooks. */ export class PositronNotebooks extends Notebooks { - positronNotebook: Locator; - - // Selector constants for Positron notebook elements - public static readonly RUN_CELL_LABEL = /execute cell/i; - public static readonly NOTEBOOK_CELL_SELECTOR = '[data-testid="notebook-cell"]'; - public static readonly NEW_CODE_CELL_LABEL = /new code cell/i; - public static readonly MONACO_EDITOR_SELECTOR = '.positron-cell-editor-monaco-widget textarea'; - public static readonly CELL_EXECUTING_LABEL = /cell is executing/i; - public static readonly CELL_EXECUTION_INFO_LABEL = /cell execution info/i; - public static readonly NOTEBOOK_KERNEL_STATUS_TESTID = 'notebook-kernel-status'; - public static readonly DELETE_CELL_LABEL = /delete the selected cell/i; - public static readonly POSITRON_NOTEBOOK_SELECTOR = '.positron-notebook'; - public static readonly CELL_STATUS_SYNC_SELECTOR = '.cell-status-item-has-runnable .codicon-sync'; - public static readonly DETECTING_KERNELS_TEXT = /detecting kernels/i; - - constructor(code: Code, quickinput: QuickInput, quickaccess: QuickAccess, hotKeys: HotKeys) { + private positronNotebook = this.code.driver.page.locator('.positron-notebook').first(); + cell = this.code.driver.page.locator('[data-testid="notebook-cell"]'); + private newCellButton = this.code.driver.page.getByLabel(/new code cell/i); + 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); + 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); - - this.positronNotebook = this.code.driver.page.locator(PositronNotebooks.POSITRON_NOTEBOOK_SELECTOR).first(); } - // -- Actions -- + // #region GETTERS /** - * 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. + * Get cell count. */ - async openNotebook(path: string): Promise { - await super.openNotebook(path, false); - await this.expectToBeVisible(); + async getCellCount(): Promise { + return this.cell.count(); } /** - * Override selectCellAtIndex to use Positron-specific selectors + * Get cell content at 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(); + async getCellContent(cellIndex: number): Promise { + return await test.step(`Get content of cell at index: ${cellIndex}`, async () => { + 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, ' '); }); } /** - * Wait for at least one cell to exist in the DOM + * Get the index of the currently focused cell. */ - async waitForCellsInDOM(timeoutMs: number = 2000): Promise { - await this.code.driver.page.locator(PositronNotebooks.NOTEBOOK_CELL_SELECTOR).first().waitFor({ - state: 'visible', - timeout: timeoutMs + async getFocusedCellIndex(): Promise { + 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; }); } + // #endregion + + // #region ACTIONS + /** - * Wait for focus to settle on a notebook cell - * Waits until any notebook cell has focus (or until timeout) + * 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 waitForFocusSettle(timeoutMs: number = 2000): Promise { - const page = this.code.driver.page; - - // First, ensure at least one cell exists in the DOM - await this.waitForCellsInDOM(timeoutMs); - - // Now wait for one of them to have focus - 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 }); + 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 }); } /** - * Exit edit mode by pressing Escape and waiting for focus to leave the Monaco editor. - * This ensures we're actually in selection mode before proceeding with keyboard shortcuts. + * Action: Enable Positron notebooks in settings and set to 'positron' editor. + * @param settings - The settings fixture */ - async exitEditMode(): Promise { - await test.step('Exit edit mode', async () => { - await this.code.driver.page.keyboard.press('Escape'); - - // Wait for focus to leave the Monaco editor textarea AND settle on a cell - // This is critical for ensuring keyboard shortcuts work correctly - await this.code.driver.page.waitForFunction(() => { - const activeElement = document.activeElement; - - // Check if focus is on a Monaco editor textarea (edit mode) - const isMonacoTextarea = activeElement?.classList.contains('inputarea') && - activeElement?.closest('.monaco-editor') !== null; - - // Check if focus is on a notebook cell or its focusable container - const isOnCell = activeElement?.closest('[data-testid="notebook-cell"]') !== null; - - // We want to wait until: - // 1. Focus is NOT in Monaco editor textarea - // 2. Focus IS on a cell (somewhere) - return !isMonacoTextarea && isOnCell; - }, { timeout: DEFAULT_TIMEOUT }); - }); + 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 }); } /** - * Get the current number of cells in the notebook + * Action: Open a Positron notebook. + * @param path - The path to the notebook to open. */ - private async getCellCount(): Promise { - return await this.code.driver.page.locator(PositronNotebooks.NOTEBOOK_CELL_SELECTOR).count(); + async openNotebook(path: string): Promise { + await super.openNotebook(path, false); + await this.expectToBeVisible(); } - async expectCellCount(expectedCount: number, timeout = DEFAULT_TIMEOUT): Promise { - await expect(this.code.driver.page.locator(PositronNotebooks.NOTEBOOK_CELL_SELECTOR)).toHaveCount(expectedCount, { timeout }); + /** + * 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}`); + } + } } /** - * Create a new code cell at the specified index + * Action: Select a cell at the specified index. + * @param cellIndex - The index of the cell to select. */ - 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) { - throw new Error('No "New Code Cell" buttons found'); - } + async selectCellAtIndex(cellIndex: number, { editMode = true }: { editMode?: boolean } = {}): Promise { + await test.step(`Select cell at index: ${cellIndex}`, async () => { + await this.cell.nth(cellIndex).click(); - // 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.expectCellIndexToBeSelected(cellIndex, { isSelected: true, inEditMode: true }); - // Wait for the new cell to appear - await expect(this.code.driver.page.locator(PositronNotebooks.NOTEBOOK_CELL_SELECTOR).nth(index)).toBeVisible({ timeout: DEFAULT_TIMEOUT }); + 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 }); + } }); } /** - * Override addCodeToCellAtIndex to use Positron-specific selectors and Monaco editor + * Action: Create a new code cell at the END of the notebook. */ - 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(); - - 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.`); - } + private async addCodeCellToEnd(): Promise { + await test.step(`Create new code cell at end`, async () => { + const newCellButtonCount = await this.newCellButton.count(); - // Create the new cell - await this.createNewCodeCell(cellIndex); + if (newCellButtonCount === 0) { + throw new Error('No "New Code Cell" buttons found'); } - // 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(); - if (delay) { - await editor.pressSequentially(code, { delay }); - } else { - await editor.fill(code); - } + // Click the last "New Code Cell" button to add a cell at the end + await this.newCellButton.last().click(); + await expect(this.cell).toHaveCount(newCellButtonCount + 1, { timeout: DEFAULT_TIMEOUT }); }); } /** - * 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 { - await test.step('Execute code in Positron notebook cell', async () => { - // Select the cell first + async runCodeAtIndex(cellIndex = 0): Promise { + await test.step(`Run code in cell ${cellIndex}`, async () => { await this.selectCellAtIndex(cellIndex); + await this.runCellButtonAtIndex(cellIndex).click(); - // 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(); - - // Wait for execution to complete by checking the execution spinner is gone - const spinner = cell.getByLabel(PositronNotebooks.CELL_EXECUTING_LABEL); - - // Wait for spinner to appear (cell is executing) + // Wait for spinner to appear (cell is executing) and disappear (execution complete) + const spinner = this.spinnerAtIndex(cellIndex); 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 }); }); } /** - * 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 () => { - // 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(); - }); + async moveMouseAway(): Promise { + await this.code.driver.page.waitForTimeout(500); + 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 { - return await test.step(`Add code and run cell ${cellIndex}`, async () => { - // Check if the cell exists + async addCodeToCell( + cellIndex: number, + code: string, + 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 () => { const currentCellCount = await this.getCellCount(); 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(cellIndex); + await this.addCodeCellToEnd(); } - // 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) { @@ -266,53 +246,83 @@ export class PositronNotebooks extends Notebooks { await editor.fill(code); } - // Find and click the run button - const runButton = cell.getByLabel(PositronNotebooks.RUN_CELL_LABEL); - await runButton.click(); - - // // Wait for execution to complete - // const spinner = cell.getByLabel('Cell is executing'); + if (run) { + await this.runCellButtonAtIndex(cellIndex).click(); - // // 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 (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 disappear (execution complete) - // await expect(spinner).toHaveCount(0, { timeout: DEFAULT_TIMEOUT }); - return cell; + return this.cell.nth(cellIndex); }); } /** - * Get execution info icon for a specific cell + * Action: Perform a cell action using keyboard shortcuts. + * @param action - The action to perform: 'copy', 'cut', 'paste', 'undo', 'redo', 'delete', 'addCellBelow'. */ - 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); - } - + async performCellAction(action: 'copy' | 'cut' | 'paste' | 'undo' | 'redo' | 'delete' | 'addCellBelow'): Promise { + 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'); - /** - * 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'); + 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}`); + } + }); } /** - * Wait for execution info icon to be visible after cell execution + * Action: Delete a cell using the action bar button. */ - async waitForExecutionInfoIcon(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 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.getCellCount(); + + // Click on the cell to make the action bar visible + await this.cell.nth(cellIndex).click(); + + // 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); }); } /** - * 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. */ @@ -327,16 +337,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; @@ -349,7 +358,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(); @@ -365,12 +374,14 @@ 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'); }); } - // -- Verifications -- + // #endregion + + // #region VERIFICATIONS /** * Verify: a Positron notebook is visible on the page. @@ -381,81 +392,211 @@ export class PositronNotebooks extends Notebooks { }); } - /** - * 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 count matches expected count. + * @param expectedCount - The expected number of cells. */ - 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 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 enable Positron notebooks with reload - * @param settings - The settings fixture + * 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 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 }); + 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 delete cell using action bar delete button + * 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 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); - - // Click on the cell to make the action bar visible - await cell.click(); + async expectCellContentAtIndexToContain(cellIndex: number, expected: string | RegExp): Promise { + await test.step( + `Expect cell ${cellIndex} content to contain: ${expected instanceof RegExp ? expected.toString() : expected}`, + async () => { + await expect(async () => { + const actualContent = await this.getCellContent(cellIndex); + + if (expected instanceof RegExp) { + expect(actualContent).toMatch(expected); + } else { + expect(actualContent).toContain(expected); + } + }).toPass({ timeout: DEFAULT_TIMEOUT }); + } + ); + } - // Find and click the delete button in the action bar - const deleteButton = cell.getByRole('button', { name: PositronNotebooks.DELETE_CELL_LABEL }); + /** + * 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 }, + timeout = DEFAULT_TIMEOUT + ): Promise { + await test.step(`Expect cell info tooltip to contain: ${JSON.stringify(expectedContent)}`, async () => { + await expect(this.cellInfoToolTip).toBeVisible({ 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 }); + } else { + const valueLocator = getValueLocator(labelMap[key]); + const expectedText = expectedValue instanceof RegExp ? expectedValue : expectedValue.toString(); + await expect(valueLocator).toContainText(expectedText, { timeout }); + } + } + } + }); + } - // Wait for the delete button to be visible - await expect(deleteButton).toBeVisible({ timeout: DEFAULT_TIMEOUT }); + /** + * 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 }); + }); + } - // Click the delete button - await deleteButton.click(); + /** + * 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) { + await expect(this.spinnerAtIndex(cellIndex)).toBeVisible({ timeout }); + } else { + await expect(this.spinnerAtIndex(cellIndex)).toHaveCount(0, { timeout }); + } + }); + } - // 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 }); + /** + * 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 }); + }); + } - // Give a small delay for focus to settle - await this.code.driver.page.waitForTimeout(100); + /** + * Verify: Cell info tooltip visibility. + * @param visible - Whether the tooltip should be visible. + * @param timeout - Timeout for the expectation. + */ + 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 cell content for identification + * 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 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 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 }); + } + ); } + + /** + * 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/cell-deletion-action-bar.test.ts b/test/e2e/tests/notebook/cell-deletion-action-bar.test.ts index c3e2b9b21436..2db5c6bd5c10 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,126 @@ 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('Positron Notebooks: Cell Deletion Action Bar Behavior', { tag: [tags.CRITICAL, tags.WIN, tags.NOTEBOOKS, tags.POSITRON_NOTEBOOKS] }, () => { - test('Cell deletion using action bar', async function ({ app, settings }) { - // Enable Positron notebooks + test.beforeAll(async function ({ app, settings }) { await app.workbench.notebooksPositron.enablePositronNotebooks(settings); - await app.workbench.notebooksPositron.setNotebookEditor(settings, 'positron'); + }); + + 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 }) { + const { 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 notebooksPositron.newNotebook(6); + 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); + await test.step('Test 2: Delete another cell (cell 3, originally cell 4)', async () => { + // Select cell 3 for deletion + await notebooksPositron.selectCellAtIndex(3); - // Verify cell 3 has correct content before deletion - expect(await app.workbench.notebooksPositron.getCellContent(3)).toBe('# Cell 4'); + // Verify cell 3 has correct content before deletion + await notebooksPositron.expectCellContentAtIndexToBe(3, '# Cell 4'); - // Delete the selected cell using action bar - await app.workbench.notebooksPositron.deleteCellWithActionBar(3); + // Delete the selected cell using action bar + await notebooksPositron.deleteCellWithActionBar(3); - // Verify cell count decreased - expect(await getCellCount(app)).toBe(4); + // Verify cell count decreased + await notebooksPositron.expectCellCountToBe(4); - // 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 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.getCellCount() > 1) { + const currentCount = await notebooksPositron.getCellCount(); + 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 07265aba329a..54faa2df7496 100644 --- a/test/e2e/tests/notebook/cell-execution-info.test.ts +++ b/test/e2e/tests/notebook/cell-execution-info.test.ts @@ -3,152 +3,116 @@ * 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, tags.POSITRON_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('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(); - // 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 }); + await hotKeys.closeAllEditors(); + }); - await app.workbench.notebooksPositron.selectAndWaitForKernel('Python'); + test('Cell Execution Tooltip - Basic Functionality', async function ({ app }) { + const { notebooks, notebooksPositron } = app.workbench; + + 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' + }, 30000); + + // 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..f4a07236545b 100644 --- a/test/e2e/tests/notebook/notebook-create.test.ts +++ b/test/e2e/tests/notebook/notebook-create.test.ts @@ -39,30 +39,34 @@ test.describe('Notebooks', { await cleanup.removeTestFiles([newFileName]); }); - test('Python - Verify code cell execution in notebook', async function ({ app }) { - await app.workbench.notebooks.addCodeToCellAtIndex('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('foo = "bar"'); + await notebooks.addCodeToCellAtIndex(0, 'foo = "bar"'); await expect.poll( async () => { @@ -102,7 +106,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(); @@ -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('import torch'); - - await app.workbench.notebooks.insertNotebookCell('code'); - - await app.workbench.notebooks.addCodeToCellAtIndex('torch.rand(10)', 1); + 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('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-debug.test.ts b/test/e2e/tests/notebook/notebook-debug.test.ts index 9ce9a7df298a..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', @@ -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 @@ -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 2ec531b9cd40..5813a7514926 100644 --- a/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts +++ b/test/e2e/tests/notebook/notebook-focus-and-selection.test.ts @@ -4,451 +4,206 @@ *--------------------------------------------------------------------------------------------*/ import path from 'path'; -import { Application } from '../../infra/index.js'; import { test, tags } from '../_test.setup'; -import { expect } from '@playwright/test'; test.use({ suiteId: __filename }); -/** - * 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; -} - -/** - * 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'; -} - -/** - * Get cell count - */ -async function getCellCount(app: Application): Promise { - return await app.code.driver.page.locator('[data-testid="notebook-cell"]').count(); -} - -/** - * 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); - }); -} - -/** - * 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, ' '); -} - -/** - * Create a fresh notebook with 5 pre-populated cells - * Call this in tests that need a notebook with existing cells - */ -async function createNotebookWith5Cells(app: Application): Promise { - await app.workbench.notebooks.createNewNotebook(); - await app.workbench.notebooksPositron.expectToBeVisible(); - - // 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); - - // After bulk adding cells, select the first cell to simulate proper initial state - // (In reality, opening an existing notebook selects first cell automatically via invariant) - await app.workbench.notebooksPositron.selectCellAtIndex(0); - await app.code.driver.page.waitForTimeout(100); -} - // Not running on web due to Positron notebooks being desktop-only test.describe('Notebook Focus and Selection', { tag: [tags.CRITICAL, tags.WIN, tags.NOTEBOOKS, tags.POSITRON_NOTEBOOKS] }, () => { test.beforeAll(async function ({ app, settings }) { await app.workbench.notebooksPositron.enablePositronNotebooks(settings); - await app.workbench.notebooksPositron.setNotebookEditor(settings, 'positron'); - }); - - test.afterEach(async function ({ hotKeys }) { - await hotKeys.closeAllEditors(); - }); - - test('Cell selection via click focuses cell and adds selection styling', async function ({ app }) { - await createNotebookWith5Cells(app); - - // Click on cell 2 - await app.workbench.notebooksPositron.selectCellAtIndex(2); - await app.workbench.notebooksPositron.waitForFocusSettle(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); - }); - - test('Arrow Down navigation moves focus to next cell', async function ({ app }) { - await createNotebookWith5Cells(app); - - // Select cell 1 - await app.workbench.notebooksPositron.selectCellAtIndex(1); - await app.workbench.notebooksPositron.waitForFocusSettle(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 app.workbench.notebooksPositron.waitForFocusSettle(100); - - // Press Arrow Down - await app.code.driver.page.keyboard.press('ArrowDown'); - await app.workbench.notebooksPositron.waitForFocusSettle(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 }) { - await createNotebookWith5Cells(app); - - // Select cell 3 - await app.workbench.notebooksPositron.selectCellAtIndex(3); - await app.workbench.notebooksPositron.waitForFocusSettle(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 app.workbench.notebooksPositron.waitForFocusSettle(100); - - // Press Arrow Up - await app.code.driver.page.keyboard.press('ArrowUp'); - await app.workbench.notebooksPositron.waitForFocusSettle(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 }) { - await createNotebookWith5Cells(app); - - // Select last cell (index 4) - await app.workbench.notebooksPositron.selectCellAtIndex(4); - await app.workbench.notebooksPositron.waitForFocusSettle(200); - expect(await getFocusedCellIndex(app)).toBe(4); - - // Press Escape to ensure we're not in edit mode - await app.code.driver.page.keyboard.press('Escape'); - await app.workbench.notebooksPositron.waitForFocusSettle(100); - - // Press Arrow Down - await app.code.driver.page.keyboard.press('ArrowDown'); - await app.workbench.notebooksPositron.waitForFocusSettle(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 }) { - await createNotebookWith5Cells(app); - - // Select first cell (index 0) - await app.workbench.notebooksPositron.selectCellAtIndex(0); - await app.workbench.notebooksPositron.waitForFocusSettle(200); - expect(await getFocusedCellIndex(app)).toBe(0); - - // Press Escape to ensure we're not in edit mode - await app.code.driver.page.keyboard.press('Escape'); - await app.workbench.notebooksPositron.waitForFocusSettle(100); - - // Press Arrow Up - await app.code.driver.page.keyboard.press('ArrowUp'); - await app.workbench.notebooksPositron.waitForFocusSettle(200); - - // Focus should remain on cell 0 - expect(await getFocusedCellIndex(app)).toBe(0); }); - test('Shift+Arrow Down adds next cell to selection', async function ({ app }) { - await createNotebookWith5Cells(app); - - // Select cell 1 - await app.workbench.notebooksPositron.selectCellAtIndex(1); - await app.workbench.notebooksPositron.waitForFocusSettle(200); - - // Press Escape to ensure we're not in edit mode - await app.code.driver.page.keyboard.press('Escape'); - await app.workbench.notebooksPositron.waitForFocusSettle(100); - - // Shift+Arrow Down - await app.code.driver.page.keyboard.press('Shift+ArrowDown'); - await app.workbench.notebooksPositron.waitForFocusSettle(200); - - // Both cell 1 and cell 2 should be selected - expect(await isCellSelected(app, 1)).toBe(true); - expect(await isCellSelected(app, 2)).toBe(true); - }); - - test('Focus is maintained across multiple navigation operations', async function ({ app }) { - await createNotebookWith5Cells(app); - - // Start at cell 0 - await app.workbench.notebooksPositron.selectCellAtIndex(0); - await app.workbench.notebooksPositron.waitForFocusSettle(200); - await app.code.driver.page.keyboard.press('Escape'); - await app.workbench.notebooksPositron.waitForFocusSettle(100); - - // Navigate down twice - await app.code.driver.page.keyboard.press('ArrowDown'); - await app.workbench.notebooksPositron.waitForFocusSettle(150); - expect(await getFocusedCellIndex(app)).toBe(1); - - await app.code.driver.page.keyboard.press('ArrowDown'); - await app.workbench.notebooksPositron.waitForFocusSettle(150); - expect(await getFocusedCellIndex(app)).toBe(2); - - // Navigate down once more - await app.code.driver.page.keyboard.press('ArrowDown'); - await app.workbench.notebooksPositron.waitForFocusSettle(150); - expect(await getFocusedCellIndex(app)).toBe(3); - - // Navigate up - await app.code.driver.page.keyboard.press('ArrowUp'); - await app.workbench.notebooksPositron.waitForFocusSettle(150); - expect(await getFocusedCellIndex(app)).toBe(2); + test.beforeEach(async function ({ app }) { + const { notebooksPositron } = app.workbench; + await notebooksPositron.newNotebook(5); + await notebooksPositron.expectCellCountToBe(5); }); - test('Enter key on selected cell enters edit mode', async function ({ app }) { - await createNotebookWith5Cells(app); - - // Select cell 2 - await app.workbench.notebooksPositron.selectCellAtIndex(2); - await app.workbench.notebooksPositron.waitForFocusSettle(200); - - // Press Escape to ensure we're not in edit mode - await app.code.driver.page.keyboard.press('Escape'); - await app.workbench.notebooksPositron.waitForFocusSettle(200); - - // Verify cell is selected (not in edit mode) - expect(await isCellSelected(app, 2)).toBe(true); - expect(await isEditorFocused(app, 2)).toBe(false); - - // Press Enter to enter edit mode - await app.code.driver.page.keyboard.press('Enter'); - await app.workbench.notebooksPositron.waitForFocusSettle(300); - - // Verify Monaco editor is now focused - expect(await isEditorFocused(app, 2)).toBe(true); - - // Verify we can type in the editor - await app.code.driver.page.keyboard.type('# test'); - await app.workbench.notebooksPositron.waitForFocusSettle(100); - - // 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 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"\)/); + test.afterEach(async function ({ hotKeys }) { + await hotKeys.closeAllEditors(); }); - test('Shift+Enter on last cell creates new cell and enters edit mode', async function ({ app }) { - await createNotebookWith5Cells(app); + test('Notebook keyboard behavior with cells', async function ({ app }) { + const { notebooksPositron } = app.workbench; + const keyboard = app.code.driver.page.keyboard; - // Select last cell (index 4) - await app.workbench.notebooksPositron.selectCellAtIndex(4); - await app.workbench.notebooksPositron.waitForFocusSettle(200); + await test.step('Test 1: Arrow Down navigation moves focus to next cell', async () => { + await notebooksPositron.selectCellAtIndex(1, { editMode: false }); + await keyboard.press('ArrowDown'); + await notebooksPositron.expectCellIndexToBeSelected(2, { inEditMode: false }); + }); - // Enter edit mode on the last cell - await app.code.driver.page.keyboard.press('Enter'); - await app.workbench.notebooksPositron.waitForFocusSettle(300); - expect(await isEditorFocused(app, 4)).toBe(true); + await test.step('Test 2: Arrow Up navigation moves focus to previous cell', async () => { + await notebooksPositron.selectCellAtIndex(3, { editMode: false }); + await keyboard.press('ArrowUp'); + await notebooksPositron.expectCellIndexToBeSelected(2, { inEditMode: false }); + }); - // Get initial cell count - const initialCount = await getCellCount(app); - expect(initialCount).toBe(5); + await test.step('Test 3: Arrow Down at last cell does not change selection', async () => { + await notebooksPositron.selectCellAtIndex(4, { editMode: false }); + await keyboard.press('ArrowDown'); + await notebooksPositron.expectCellIndexToBeSelected(4, { inEditMode: false }); + }); - // Press Shift+Enter to add a new cell below - await app.code.driver.page.keyboard.press('Shift+Enter'); - await app.workbench.notebooksPositron.waitForFocusSettle(500); + await test.step('Test 4: Arrow Up at first cell does not change selection', async () => { + await notebooksPositron.selectCellAtIndex(0, { editMode: false }); + await keyboard.press('ArrowUp'); + await notebooksPositron.expectCellIndexToBeSelected(0, { inEditMode: false }); + }); - // Verify new cell was added - const newCount = await getCellCount(app); - expect(newCount).toBe(6); + await test.step('Test 5: Focus is maintained across multiple navigation operations', async () => { + // Navigate down multiple times + await keyboard.press('ArrowDown'); + await notebooksPositron.expectCellIndexToBeSelected(1, { inEditMode: false }); - // 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); + await keyboard.press('ArrowDown'); + await notebooksPositron.expectCellIndexToBeSelected(2, { inEditMode: false }); - // Verify we can type immediately in the new cell - await app.code.driver.page.keyboard.type('new cell content'); - await app.workbench.notebooksPositron.waitForFocusSettle(100); + await keyboard.press('ArrowDown'); + await notebooksPositron.expectCellIndexToBeSelected(3, { inEditMode: false }); - const newCellContent = await app.workbench.notebooksPositron.getCellContent(5); - const normalizedContent = normalizeCellContent(newCellContent); - expect(normalizedContent).toContain('new cell content'); - }); + // Navigate up + await keyboard.press('ArrowUp'); + await notebooksPositron.expectCellIndexToBeSelected(2, { inEditMode: false }); + }); - test('Enter key in edit mode adds newline within cell', async function ({ app }) { - await createNotebookWith5Cells(app); - - // Select cell 1 and enter edit mode - await app.workbench.notebooksPositron.selectCellAtIndex(1); - await app.workbench.notebooksPositron.waitForFocusSettle(200); - await app.code.driver.page.keyboard.press('Enter'); - await app.workbench.notebooksPositron.waitForFocusSettle(300); - expect(await isEditorFocused(app, 1)).toBe(true); - - // Get initial content - const initialContent = await app.workbench.notebooksPositron.getCellContent(1); - const normalizedInitial = normalizeCellContent(initialContent); - const lineText = 'print("Cell 1")'; - expect(normalizedInitial).toBe(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 viewLines = editor.locator('.view-line'); - - // Position cursor in the middle of the cells contents to avoid any trailing newline trimming issues - await app.code.driver.page.keyboard.press('Home'); - const middleIndex = Math.floor(lineText.length / 2); - for (let i = 0; i < middleIndex; i++) { // move to middle of line - await app.code.driver.page.keyboard.press('ArrowRight'); - } - await app.workbench.notebooksPositron.waitForFocusSettle(100); - - // 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(); - - // 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) - expect(await getCellCount(app)).toBe(5); - - // Verify we're still in edit mode in cell 1 - expect(await isEditorFocused(app, 1)).toBe(true); + await test.step('Test 6: Shift+Arrow Down adds next cell to selection', async () => { + await notebooksPositron.selectCellAtIndex(1, { editMode: false }); + await keyboard.press('Shift+ArrowDown'); + await notebooksPositron.expectCellIndexToBeSelected(1, { inEditMode: false }); + await notebooksPositron.expectCellIndexToBeSelected(2, { inEditMode: false }); + }); }); - test('First cell is automatically selected when notebook loads', async function ({ app }) { - // Open a real notebook file to test initial load behavior - const notebookPath = path.join('workspaces', 'bitmap-notebook', 'bitmap-notebook.ipynb'); - await app.workbench.notebooks.openNotebook(notebookPath, false); - await app.workbench.notebooksPositron.expectToBeVisible(); - - // Wait for cells to be in DOM and for initial focus to settle - await app.workbench.notebooksPositron.waitForCellsInDOM(5000); - await app.workbench.notebooksPositron.waitForFocusSettle(); - - // EXPECTED: First cell should be automatically selected without any user interaction - const focusedIndex = await getFocusedCellIndex(app); - expect(focusedIndex).toBe(0); - expect(await isCellSelected(app, 0)).toBe(true); - }); + test('Editor mode behavior with notebook cells', async function ({ app }) { + const { notebooksPositron } = app.workbench; + const keyboard = app.code.driver.page.keyboard; - test('Keyboard navigation works immediately without clicking any cell', async function ({ app }) { - await createNotebookWith5Cells(app); + 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 }); - // Wait for initial selection to settle - await app.workbench.notebooksPositron.waitForFocusSettle(); + // Verify we can type into the editor after clicking + await keyboard.type('# editor good'); + await notebooksPositron.expectCellContentAtIndexToContain(1, '# editor good'); + }); - // Press Escape first to ensure we're not in edit mode - await app.code.driver.page.keyboard.press('Escape'); - await app.workbench.notebooksPositron.waitForFocusSettle(500); + 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, { editMode: false }); + await keyboard.press('Enter'); + await notebooksPositron.expectCellIndexToBeSelected(2, { + isSelected: true, + inEditMode: true + }); + + // Verify we can type into the editor after pressing Enter + await keyboard.type('# test'); + await notebooksPositron.expectCellContentAtIndexToContain(2, /^# Cell 2# test/); + }); - // Press Arrow Down - EXPECTED: should move from cell 0 to cell 1 - await app.code.driver.page.keyboard.press('ArrowDown'); - await app.workbench.notebooksPositron.waitForFocusSettle(500); + await test.step('Test 3: Shift+Enter on last cell creates new cell and enters edit mode', async () => { + // Verify pressing Shift+Enter adds a new cell below + await notebooksPositron.selectCellAtIndex(4); + await notebooksPositron.expectCellCountToBe(5); + await keyboard.press('Shift+Enter'); + await notebooksPositron.expectCellCountToBe(6); - expect(await getFocusedCellIndex(app)).toBe(1); - expect(await isCellSelected(app, 1)).toBe(true); + // Verify the NEW cell (index 5) is now in edit mode with focus + await notebooksPositron.expectCellIndexToBeSelected(5, { inEditMode: true }); - // Arrow Down again should move to cell 2 - await app.code.driver.page.keyboard.press('ArrowDown'); - await app.workbench.notebooksPositron.waitForFocusSettle(500); + // Verify we can type immediately in the new cell + await keyboard.type('new cell content'); + await notebooksPositron.expectCellContentAtIndexToContain(5, 'new cell content'); + }); - expect(await getFocusedCellIndex(app)).toBe(2); - expect(await isCellSelected(app, 2)).toBe(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('Selection is preserved when switching between editors', async function ({ app }) { - await createNotebookWith5Cells(app); + test('Notebook navigation and default cell selection', async function ({ app }) { + const { notebooks, notebooksPositron } = app.workbench; + const keyboard = app.code.driver.page.keyboard; - // Select cell 2 explicitly - await app.workbench.notebooksPositron.selectCellAtIndex(2); - await app.workbench.notebooksPositron.waitForFocusSettle(200); - expect(await getFocusedCellIndex(app)).toBe(2); + const clickTab = (name: string) => app.code.driver.page.getByRole('tab', { name }).click(); + const TAB_1 = 'Untitled-1.ipynb'; + const TAB_2 = 'bitmap-notebook.ipynb'; - // Press Escape to exit edit mode - await app.code.driver.page.keyboard.press('Escape'); - await app.workbench.notebooksPositron.waitForFocusSettle(100); + // Start a new notebook (tab 1) + await test.step('Open new notebook: Ensure keyboard navigation', async () => { + await notebooksPositron.selectCellAtIndex(0, { editMode: false }); + await keyboard.press('ArrowDown'); + await notebooksPositron.expectCellIndexToBeSelected(1, { inEditMode: false }); - // Create a new untitled file (switches editor focus away) - await app.workbench.quickaccess.runCommand('workbench.action.files.newUntitledFile'); + await keyboard.press('ArrowDown'); + await notebooksPositron.expectCellIndexToBeSelected(2, { inEditMode: false }); + }); - // Switch back to notebook using Ctrl/Cmd+Tab (or keyboard navigation) - // Use Cmd+Shift+P to open command palette, then navigate back - await app.workbench.quickaccess.runCommand('workbench.action.previousEditor'); + // 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); - // EXPECTED: Cell 2 should still be selected and focused - await app.workbench.notebooksPositron.waitForFocusSettle(1000); - expect(await getFocusedCellIndex(app)).toBe(2); - expect(await isCellSelected(app, 2)).toBe(true); + // Verify first cell is selected (without interaction) + await notebooksPositron.expectCellIndexToBeSelected(0, { inEditMode: false }); + }); - // Keyboard navigation should still work - await app.code.driver.page.keyboard.press('ArrowDown'); - await app.workbench.notebooksPositron.waitForFocusSettle(200); - expect(await getFocusedCellIndex(app)).toBe(3); + // 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); + 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 }); + + // Switch back to tab 1 and verify selection is still at cell 3 + await clickTab(TAB_1); + await notebooksPositron.expectCellIndexToBeSelected(3, { inEditMode: false }); + }); }); - }); 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-large-python.test.ts b/test/e2e/tests/notebook/notebook-large-python.test.ts index 4786bfbde5d0..9c8148523649 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('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..875e541bd690 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('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[] = []; 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 f1586be7ebef..42ba61bb01b8 100644 --- a/test/e2e/tests/notebook/positron-notebook-copy-paste.test.ts +++ b/test/e2e/tests/notebook/positron-notebook-copy-paste.test.ts @@ -3,207 +3,134 @@ * 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'; -import { PositronNotebooks } from '../../pages/notebooksPositron.js'; test.use({ suiteId: __filename }); -/** - * Clipboard operations (copy/cut) are asynchronous OS-level operations that may not complete - * immediately after the keyboard shortcut is pressed. On slower CI environments (especially Ubuntu), - * the clipboard may not be populated by the time the next operation (paste) executes, causing - * race conditions. This delay ensures the clipboard operation has time to propagate. - */ -const CLIPBOARD_OPERATION_DELAY_MS = 100; - -/** - * Helper function to copy cells using keyboard shortcut - */ -async function copyCellsWithKeyboard(app: Application): Promise { - // Exit edit mode and wait for focus to leave Monaco editor - await app.workbench.notebooksPositron.exitEditMode(); - await app.code.driver.page.keyboard.press('ControlOrMeta+C'); - // Wait for clipboard operation to complete - await app.code.driver.page.waitForTimeout(CLIPBOARD_OPERATION_DELAY_MS); -} - -/** - * Helper function to cut cells using keyboard shortcut - */ -async function cutCellsWithKeyboard(app: Application): Promise { - // Exit edit mode and wait for focus to leave Monaco editor - await app.workbench.notebooksPositron.exitEditMode(); - await app.code.driver.page.keyboard.press('ControlOrMeta+X'); - // Wait for clipboard operation to complete - await app.code.driver.page.waitForTimeout(CLIPBOARD_OPERATION_DELAY_MS); -} - -/** - * Helper function to paste cells using keyboard shortcut - */ -async function pasteCellsWithKeyboard(app: Application): Promise { - // Exit edit mode and wait for focus to leave Monaco editor - await app.workbench.notebooksPositron.exitEditMode(); - await app.code.driver.page.keyboard.press('ControlOrMeta+V'); - // Wait for paste operation to complete before asserting results - await app.code.driver.page.waitForTimeout(CLIPBOARD_OPERATION_DELAY_MS); -} - // 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, tags.POSITRON_NOTEBOOKS] }, () => { - // Skip these tests on CI due to flakiness - will address in followup PR - test.skip(process.env.CI === 'true', 'Skipping copy-paste tests on CI due to flakiness'); - test.beforeAll(async function ({ app, settings }) { + test.beforeAll(async ({ app, settings }) => { await app.workbench.notebooksPositron.enablePositronNotebooks(settings); - // Configure Positron as the notebook editor - await app.workbench.notebooksPositron.setNotebookEditor(settings, 'positron'); }); - 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(); + test.afterEach(async function ({ hotKeys }) { + await hotKeys.closeAllEditors(); + }); + + test('Should correctly copy and paste cell content in various scenarios', async function ({ app }) { + const { notebooksPositron } = app.workbench; // ======================================== // Setup: Create 5 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); - - // Verify we have 5 cells - await app.workbench.notebooksPositron.expectCellCount(5); + await test.step('Test Setup: Create notebook and add cells', async () => { + await notebooksPositron.newNotebook(5); + await notebooksPositron.expectCellCountToBe(5); + }); // ======================================== // Test 1: Copy single cell and paste at end // ======================================== - await app.workbench.notebooksPositron.selectCellAtIndex(2); - - // Verify cell 2 has correct content - expect(await app.workbench.notebooksPositron.getCellContent(2)).toBe('# Cell 2'); - - // Copy the cell - await copyCellsWithKeyboard(app); + await test.step('Test 1: Copy single cell and paste at end', async () => { + // Perform copy on cell 2 + await notebooksPositron.selectCellAtIndex(2, { editMode: false }); + await notebooksPositron.expectCellContentAtIndexToBe(2, '# Cell 2'); + await notebooksPositron.performCellAction('copy'); - // Move to last cell and paste after it - await app.workbench.notebooksPositron.selectCellAtIndex(4); - await pasteCellsWithKeyboard(app); + // Move to last cell and perform paste + await notebooksPositron.selectCellAtIndex(4, { editMode: false }); + await notebooksPositron.performCellAction('paste'); + await notebooksPositron.expectCellCountToBe(6); - // Verify cell count increased - await app.workbench.notebooksPositron.expectCellCount(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 pasted contents are correct at new 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); - - // Verify we're at cell 1 with correct content - expect(await app.workbench.notebooksPositron.getCellContent(1)).toBe('# Cell 1'); - - // Cut the cell - await cutCellsWithKeyboard(app); + await test.step('Test 2: Cut single cell and paste at different position', async () => { + // Perform cut on cell 1 + await notebooksPositron.selectCellAtIndex(1, { editMode: false }); + await notebooksPositron.expectCellContentAtIndexToBe(1, '# Cell 1'); + await notebooksPositron.performCellAction('cut'); - // Verify cell count decreased - await app.workbench.notebooksPositron.expectCellCount(5); + // Verify cell count decreased and cell 1 is removed + await notebooksPositron.expectCellCountToBe(5); + await notebooksPositron.expectCellContentAtIndexToBe(1, '# Cell 2'); - // Verify what was cell 2 is now at index 1 - expect(await app.workbench.notebooksPositron.getCellContent(1)).toBe('# Cell 2'); + // Move to index 3 and paste + await notebooksPositron.selectCellAtIndex(3, { editMode: false }); + await notebooksPositron.performCellAction('paste'); - // Move to index 3 and paste - await app.workbench.notebooksPositron.selectCellAtIndex(3); - await pasteCellsWithKeyboard(app); - - // Verify cell count is back to 6 - await app.workbench.notebooksPositron.expectCellCount(6); - - // Verify the pasted cell has correct content at index 4 - expect(await app.workbench.notebooksPositron.getCellContent(4)).toBe('# Cell 1'); + // Verify cell count restored and cell content is correct + await notebooksPositron.expectCellCountToBe(6) + 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.expectCellCountToBe(6); - // Copy cell 0 - expect(await app.workbench.notebooksPositron.getCellContent(0)).toBe('# Cell 0'); - await copyCellsWithKeyboard(app); + // Copy cell 0 + await notebooksPositron.selectCellAtIndex(0, { editMode: false }); + await notebooksPositron.expectCellContentAtIndexToBe(0, '# Cell 0'); + await notebooksPositron.performCellAction('copy'); - // Paste at position 2 - await app.workbench.notebooksPositron.selectCellAtIndex(2); - await pasteCellsWithKeyboard(app); + // Paste at position 2 + await notebooksPositron.selectCellAtIndex(2, { editMode: false }); + await notebooksPositron.performCellAction('paste'); - // Verify first paste - await app.workbench.notebooksPositron.expectCellCount(7); - expect(await app.workbench.notebooksPositron.getCellContent(3)).toBe('# Cell 0'); + // Verify first paste + await notebooksPositron.expectCellCountToBe(7); + await notebooksPositron.expectCellContentAtIndexToBe(3, '# Cell 0'); - // Paste again at position 5 - await app.workbench.notebooksPositron.selectCellAtIndex(5); - await pasteCellsWithKeyboard(app); + // Paste again at position 5 + await notebooksPositron.selectCellAtIndex(5, { editMode: false }); + await notebooksPositron.performCellAction('paste'); - // Verify second paste - await app.workbench.notebooksPositron.expectCellCount(8); - expect(await app.workbench.notebooksPositron.getCellContent(6)).toBe('# Cell 0'); + // Verify second paste + await notebooksPositron.expectCellCountToBe(8); + await notebooksPositron.expectCellContentAtIndexToBe(6, '# 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); - - // Cut the cell - await cutCellsWithKeyboard(app); - - // Verify cell removed - await app.workbench.notebooksPositron.expectCellCount(7); - - // Move to first cell and paste - // Note: Paste typically inserts after the current cell - await app.workbench.notebooksPositron.selectCellAtIndex(0); - await pasteCellsWithKeyboard(app); - - // Verify cell count restored - await app.workbench.notebooksPositron.expectCellCount(8); - - // Verify pasted cell is at index 1 (pasted after cell 0) - expect(await app.workbench.notebooksPositron.getCellContent(1)).toBe(cellToMoveContent); + 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, { 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, { editMode: false }); + await notebooksPositron.performCellAction('paste'); + + // Verify cell count restored and content is correct + await notebooksPositron.expectCellCountToBe(8); + await notebooksPositron.expectCellContentAtIndexToBe(1, cellToMoveContent); + }); // ======================================== // Test 5: Cut all cells and verify notebook can be empty // ======================================== - // Delete cells until only one remains - while ((await app.code.driver.page.locator(PositronNotebooks.NOTEBOOK_CELL_SELECTOR).count()) > 1) { - await app.workbench.notebooksPositron.selectCellAtIndex(0); - await cutCellsWithKeyboard(app); - } - - // Verify we have exactly one cell - await app.workbench.notebooksPositron.expectCellCount(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 app.code.driver.page.locator(PositronNotebooks.NOTEBOOK_CELL_SELECTOR).count(); - 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 () => { + while (await notebooksPositron.getCellCount() > 0) { + await notebooksPositron.selectCellAtIndex(0, { editMode: false }); + await notebooksPositron.performCellAction('cut'); + } + + await notebooksPositron.expectCellCountToBe(0); + }); }); - }); diff --git a/test/e2e/tests/notebook/positron-notebook-editor.test.ts b/test/e2e/tests/notebook/positron-notebook-editor.test.ts index 03dfca004f27..d43afe28bbe7 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'); @@ -12,8 +13,7 @@ test.use({ suiteId: __filename }); - -test.describe('Positron notebook opening and saving', { +test.describe('Positron Notebooks: Open & Save', { tag: [tags.CRITICAL, tags.WIN, tags.NOTEBOOKS, tags.POSITRON_NOTEBOOKS] }, () => { test.beforeAll(async function ({ app, settings }) { @@ -25,8 +25,7 @@ test.describe('Positron notebook opening and saving', { await app.workbench.notebooksPositron.setNotebookEditor(settings, 'default'); }); - test.afterEach(async function ({ app, hotKeys, settings }) { - await app.workbench.notebooksPositron.setNotebookEditor(settings, 'default'); + test.afterEach(async function ({ hotKeys }) { await hotKeys.closeAllEditors(); }); @@ -95,7 +94,7 @@ 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.setNotebookEditor(settings, 'positron'); // Create a new notebook (which starts dirty) await notebooks.createNewNotebook(); @@ -105,32 +104,29 @@ test.describe('Positron notebook opening and saving', { 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(); - // 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 // 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 + 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 944d5e9242ca..bd5e152496e1 100644 --- a/test/e2e/tests/notebook/positron-notebook-undo-redo.test.ts +++ b/test/e2e/tests/notebook/positron-notebook-undo-redo.test.ts @@ -3,133 +3,87 @@ * 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 { - // Exit edit mode and wait for focus to leave Monaco editor - await app.workbench.notebooksPositron.exitEditMode(); - 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 { - // Exit edit mode and wait for focus to leave Monaco editor - await app.workbench.notebooksPositron.exitEditMode(); - 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 { - // Exit edit mode and wait for focus to leave Monaco editor - await app.workbench.notebooksPositron.exitEditMode(); - 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 { - // Exit edit mode and wait for focus to leave Monaco editor - await app.workbench.notebooksPositron.exitEditMode(); - 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, tags.POSITRON_NOTEBOOKS] }, () => { - // Skip these tests on CI due to flakiness - will address in followup PR - test.skip(process.env.CI === 'true', 'Skipping undo-redo tests on CI due to flakiness'); 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('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, { editMode: false }); + 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