From 7ed563ab2d0d163198c833b9b15509716aecdcf8 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Mon, 24 Feb 2025 15:41:10 -0600 Subject: [PATCH] e2e-test: organize session code together (#6457) ### Summary Re-organizing the session code. There should be no functional change. It was just getting gnarly with having console & sessions together in same file. ### QA Notes @:sessions --- test/e2e/infra/fixtures/interpreter-new.ts | 162 ----- test/e2e/infra/index.ts | 2 +- test/e2e/infra/workbench.ts | 8 +- test/e2e/pages/console.ts | 395 +----------- test/e2e/pages/sessions.ts | 568 ++++++++++++++++++ .../tests/console/console-sessions.test.ts | 120 ++-- .../top-action-bar/session-button.test.ts | 12 +- 7 files changed, 642 insertions(+), 625 deletions(-) delete mode 100644 test/e2e/infra/fixtures/interpreter-new.ts create mode 100644 test/e2e/pages/sessions.ts diff --git a/test/e2e/infra/fixtures/interpreter-new.ts b/test/e2e/infra/fixtures/interpreter-new.ts deleted file mode 100644 index 8f1946d0987..00000000000 --- a/test/e2e/infra/fixtures/interpreter-new.ts +++ /dev/null @@ -1,162 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (C) 2024 Posit Software, PBC. All rights reserved. - * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. - *--------------------------------------------------------------------------------------------*/ - -import test, { expect } from '@playwright/test'; -import { Code } from '../code'; - -const DESIRED_PYTHON = process.env.POSITRON_PY_VER_SEL; -const DESIRED_R = process.env.POSITRON_R_VER_SEL; - -export interface InterpreterInfo { - language: 'Python' | 'R'; - version: string; // e.g. Python 3.12.4 64-bit or Python 3.9.19 64-bit ('3.9.19') or R 4.4.0 - path: string; // e.g. /usr/local/bin/python3 - source?: string; // e.g. Pyenv, Global, System, etc -} - -export class InterpreterNew { - private interpreterButton = this.code.driver.page.getByRole('button', { name: 'Open Active Session Picker' }) - private interpreterQuickMenu = this.code.driver.page.getByText(/(Select a Session)|(Start a New Session)/); - private newSessionQuickOption = this.code.driver.page.getByText(/New Session.../); - - constructor(private code: Code) { } - - // -- Actions -- - - /** - * Action: Open the interpreter dropdown in the top action bar. - */ - async openSessionQuickPickMenu(viewAllRuntimes = true) { - if (!await this.interpreterQuickMenu.isVisible()) { - await this.interpreterButton.click(); - } - - if (viewAllRuntimes) { - await this.newSessionQuickOption.click(); - await expect(this.code.driver.page.getByText(/Start a New Session/)).toBeVisible(); - } else { - await expect(this.code.driver.page.getByText(/Select a Session/)).toBeVisible(); - } - } - - /** - * Action: Close the interpreter dropdown in the top action bar. - */ - async closeSessionQuickPickMenu() { - if (await this.interpreterQuickMenu.isVisible()) { - await this.code.driver.page.keyboard.press('Escape'); - await expect(this.interpreterQuickMenu).not.toBeVisible(); - } - } - - // --- Utils --- - - /** - * Util: Get active sessions from the session picker. - * @returns The list of active sessions. - */ - async getActiveSessions(): Promise { - await this.openSessionQuickPickMenu(false); - const allSessions = await this.code.driver.page.locator('.quick-input-list-rows').all(); - - // Get the text of all sessions - const activeSessions = await Promise.all( - allSessions.map(async element => { - const runtime = (await element.locator('.quick-input-list-row').nth(0).textContent())?.replace('Currently Selected', ''); - const path = await element.locator('.quick-input-list-row').nth(1).textContent(); - return { name: runtime?.trim() || '', path: path?.trim() || '' }; - }) - ); - - // Filter out the one with "New Session..." - const filteredSessions = activeSessions - .filter(session => !session.name.includes("New Session...")) - - await this.closeSessionQuickPickMenu(); - return filteredSessions; - } - - /** - * Util: Get the interpreter info for the currently selected interpreter in the dropdown. - * @returns The interpreter info for the selected interpreter if found, otherwise undefined. - */ - async getSelectedSessionInfo(): Promise { - await this.openSessionQuickPickMenu(false); - const selectedInterpreter = this.code.driver.page.locator('.quick-input-list-entry').filter({ hasText: 'Currently Selected' }) - - // Extract the runtime name - const runtime = await selectedInterpreter.locator('.monaco-icon-label-container .label-name .monaco-highlighted-label').nth(0).textContent(); - - // Extract the language, version, and source from runtime name - const { language, version, source } = await this.parseRuntimeName(runtime); - - // Extract the path - const path = await selectedInterpreter.locator('.quick-input-list-label-meta .monaco-icon-label-container .label-name .monaco-highlighted-label').nth(0).textContent(); - - await this.closeSessionQuickPickMenu(); - - return { - language: language as 'Python' | 'R', - version, - source, - path: path || '', - } - } - - - // --- Helpers --- - - /** - * Helper: Parse the full runtime name into language, version, and source. - * @param runtimeName the full runtime name to parse. E.g., "Python 3.10.15 (Pyenv)" - * @returns The parsed runtime name. E.g., { language: "Python", version: "3.10.15", source: "Pyenv" } - */ - async parseRuntimeName(runtimeName: string | null) { - if (!runtimeName) { - throw new Error('No interpreter string provided'); - } - - // Note: Some interpreters may not have a source, so the source is optional - const match = runtimeName.match(/^(\w+)\s([\d.]+)(?:\s\(([^)]+)\))?$/); - if (!match) { - throw new Error(`Invalid interpreter format: ${runtimeName}`); - } - - return { - language: match[1], // e.g., "Python", "R" - version: match[2], // e.g., "3.10.15", "4.4.1" - source: match[3] || undefined // e.g., "Pyenv", "System" - }; - } - - // --- Verifications --- - - /** - * Verify: the selected interpreter is the expected interpreter. - * @param version The descriptive string of the interpreter to verify. - */ - async verifySessionIsSelected( - options: { language?: 'Python' | 'R'; version?: string } = {} - ) { - if (!DESIRED_PYTHON || !DESIRED_R) { - throw new Error('Please set env vars: POSITRON_PY_VER_SEL, POSITRON_R_VER_SEL'); - } - - const { - language = 'Python', - version = language === 'Python' ? DESIRED_PYTHON : DESIRED_R, - } = options; - await test.step(`Verify interpreter is selected: ${language} ${version}`, async () => { - const interpreterInfo = await this.getSelectedSessionInfo(); - expect(interpreterInfo.language).toContain(language); - expect(interpreterInfo.version).toContain(version); - }); - } -} - -export type QuickPickSessionInfo = { - name: string; - path: string; -}; diff --git a/test/e2e/infra/index.ts b/test/e2e/infra/index.ts index 4294e5f64a8..bb4fbfadea0 100644 --- a/test/e2e/infra/index.ts +++ b/test/e2e/infra/index.ts @@ -39,10 +39,10 @@ export * from '../pages/debug'; export * from '../pages/problems'; export * from '../pages/references'; export * from '../pages/scm'; +export * from '../pages/sessions'; // fixtures export * from './fixtures/userSettings'; -export * from './fixtures/interpreter-new'; // test-runner export * from './test-runner'; diff --git a/test/e2e/infra/workbench.ts b/test/e2e/infra/workbench.ts index c39e604e7bc..04b6d370f32 100644 --- a/test/e2e/infra/workbench.ts +++ b/test/e2e/infra/workbench.ts @@ -36,7 +36,7 @@ import { EditorActionBar } from '../pages/editorActionBar'; import { Problems } from '../pages/problems'; import { References } from '../pages/references'; import { SCM } from '../pages/scm'; -import { InterpreterNew } from './fixtures/interpreter-new'; +import { Sessions } from '../pages/sessions'; export interface Commands { runCommand(command: string, options?: { exactLabelMatch?: boolean }): Promise; @@ -45,7 +45,6 @@ export interface Commands { export class Workbench { readonly interpreter: Interpreter; - readonly interpreterNew: InterpreterNew; readonly popups: Popups; readonly console: Console; readonly variables: Variables; @@ -77,6 +76,7 @@ export class Workbench { readonly problems: Problems; readonly references: References; readonly scm: SCM; + readonly sessions: Sessions; constructor(code: Code) { @@ -95,8 +95,7 @@ export class Workbench { this.connections = new Connections(code, this.quickaccess); this.newProjectWizard = new NewProjectWizard(code, this.quickaccess); this.output = new Output(code, this.quickaccess, this.quickInput); - this.interpreterNew = new InterpreterNew(code); - this.console = new Console(code, this.quickaccess, this.quickInput, this.interpreterNew); + this.console = new Console(code, this.quickaccess, this.quickInput); this.interpreter = new Interpreter(code, this.console); this.notebooks = new Notebooks(code, this.quickInput, this.quickaccess); this.welcome = new Welcome(code); @@ -113,5 +112,6 @@ export class Workbench { this.problems = new Problems(code, this.quickaccess); this.references = new References(code); this.scm = new SCM(code, this.layouts); + this.sessions = new Sessions(code, this.console, this.quickaccess, this.quickInput); } } diff --git a/test/e2e/pages/console.ts b/test/e2e/pages/console.ts index 3dd3b814c11..c27adf7b382 100644 --- a/test/e2e/pages/console.ts +++ b/test/e2e/pages/console.ts @@ -3,11 +3,10 @@ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ -import test, { expect, Locator, Page } from '@playwright/test'; +import test, { expect, Locator } from '@playwright/test'; import { Code } from '../infra/code'; import { QuickAccess } from './quickaccess'; import { QuickInput } from './quickInput'; -import { InterpreterNew, QuickPickSessionInfo } from '../infra'; import { InterpreterType } from '../infra/fixtures/interpreter'; const CONSOLE_INPUT = '.console-input'; @@ -19,8 +18,6 @@ const EMPTY_CONSOLE = '.positron-console .empty-console'; const INTERRUPT_RUNTIME = 'div.action-bar-button-face .codicon-positron-interrupt-runtime'; const SUGGESTION_LIST = '.suggest-widget .monaco-list-row'; const CONSOLE_LINES = `${ACTIVE_CONSOLE_INSTANCE} div span`; -const DESIRED_PYTHON = process.env.POSITRON_PY_VER_SEL; -const DESIRED_R = process.env.POSITRON_R_VER_SEL; /* * Reuseable Positron console functionality for tests to leverage. Includes the ability to select an interpreter and execute code which @@ -33,20 +30,18 @@ export class Console { consoleRestartButton: Locator; activeConsole: Locator; suggestionList: Locator; - session: Session; get emptyConsole() { return this.code.driver.page.locator(EMPTY_CONSOLE).getByText('There is no interpreter running'); } - constructor(private code: Code, private quickaccess: QuickAccess, private quickinput: QuickInput, private interpreter: InterpreterNew) { + constructor(private code: Code, private quickaccess: QuickAccess, private quickinput: QuickInput) { this.barPowerButton = this.code.driver.page.getByLabel('Shutdown console'); this.barRestartButton = this.code.driver.page.getByLabel('Restart console'); this.barClearButton = this.code.driver.page.getByLabel('Clear console'); this.consoleRestartButton = this.code.driver.page.locator(CONSOLE_RESTART_BUTTON); this.activeConsole = this.code.driver.page.locator(ACTIVE_CONSOLE_INSTANCE); this.suggestionList = this.code.driver.page.locator(SUGGESTION_LIST); - this.session = new Session(this.code.driver.page, this); } async selectInterpreter(desiredInterpreterType: InterpreterType, desiredInterpreterString: string, waitForReady: boolean = true): Promise { @@ -84,62 +79,6 @@ export class Console { return; } - /** - * Action: Start a session via the top action bar button or quickaccess. - * @param options - Configuration options for selecting the interpreter. - * @param options.language the programming language interpreter to select. - * @param options.version the specific version of the interpreter to select (e.g., "3.10.15"). - * @param options.triggerMode the method used to trigger the selection: top-action-bar or quickaccess. - * @param options.waitForReady whether to wait for the console to be ready after selecting the interpreter. - */ - async startSession(options: { - language: 'Python' | 'R'; - version?: string; - triggerMode?: 'top-action-bar' | 'quickaccess'; - waitForReady?: boolean; - }): Promise { - - if (!DESIRED_PYTHON || !DESIRED_R) { - throw new Error('Please set env vars: POSITRON_PY_VER_SEL, POSITRON_R_VER_SEL'); - } - - const { - language, - version = language === 'Python' ? DESIRED_PYTHON : DESIRED_R, - waitForReady = true, - triggerMode = 'quickaccess', - } = options; - - await test.step(`Start session via ${triggerMode}: ${language} ${version}`, async () => { - // Don't try to start a new interpreter if one is currently starting up - await this.waitForReadyOrNoInterpreterNew(); - - // Start the interpreter via the dropdown or quickaccess - const command = language === 'Python' ? 'python.setInterpreter' : 'r.selectInterpreter'; - triggerMode === 'quickaccess' - ? await this.quickaccess.runCommand(command, { keepOpen: true }) - : await this.interpreter.openSessionQuickPickMenu(); - - await this.quickinput.waitForQuickInputOpened(); - await this.quickinput.type(`${language} ${version}`); - - // Wait until the desired interpreter string appears in the list and select it. - // We need to click instead of using 'enter' because the Python select interpreter command - // may include additional items above the desired interpreter string. - await this.quickinput.selectQuickInputElementContaining(`${language} ${version}`); - await this.quickinput.waitForQuickInputClosed(); - - // Move mouse to prevent tooltip hover - await this.code.driver.page.mouse.move(0, 0); - - if (waitForReady) { - language === 'Python' - ? await this.waitForReadyAndStarted('>>>', 40000) - : await this.waitForReadyAndStarted('>', 40000); - } - }); - } - async executeCode(languageName: 'Python' | 'R', code: string): Promise { await test.step(`Execute ${languageName} code in console: ${code}`, async () => { @@ -252,41 +191,6 @@ export class Console { throw new Error('Console is not ready after waiting for R or Python to start'); } - /** - * Check if the console is ready with Python or R, or if Select Runtime. - * @throws An error if the console is not ready after the retry count. - */ - async waitForReadyOrNoInterpreterNew() { - await test.step('Wait for console to be ready or no session', async () => { - const page = this.code.driver.page; - - await this.waitForInterpretersToFinishLoading(); - - // ensure we are on Console tab - await page.getByRole('tab', { name: 'Console', exact: true }).locator('a').click(); - - // Move mouse to prevent tooltip hover - await this.code.driver.page.mouse.move(0, 0); - - // wait for the dropdown to contain R, Python, or Select Runtime. - const currentSession = await page.getByRole('button', { name: 'Open Active Session Picker' }).textContent() || ''; - - if (currentSession.includes('Python')) { - await expect(page.getByRole('code').getByText('>>>')).toBeVisible({ timeout: 30000 }); - return; - } else if (currentSession.includes('R') && !currentSession.includes('Choose Session')) { - await expect(page.getByRole('code').getByText('>')).toBeVisible({ timeout: 30000 }); - return; - } else if (currentSession.includes('Choose Session')) { - await expect(page.getByText('Choose Session')).toBeVisible(); - return; - } - - // If we reach here, the console is not ready. - throw new Error('Console is not ready after waiting for session to start'); - }); - } - async waitForInterpreterShutdown() { await this.waitForConsoleContents('shut down successfully'); } @@ -383,298 +287,3 @@ export class Console { } } -/** - * Helper class to manage sessions in the console - */ -class Session { - activeStatus: (session: Locator) => Locator; - idleStatus: (session: Locator) => Locator; - disconnectedStatus: (session: Locator) => Locator; - sessions: Locator; - metadataButton: Locator; - metadataDialog: Locator; - - constructor(private page: Page, private console: Console) { - this.activeStatus = (session: Locator) => session.locator('.codicon-positron-status-active'); - this.idleStatus = (session: Locator) => session.locator('.codicon-positron-status-idle'); - this.disconnectedStatus = (session: Locator) => session.locator('.codicon-positron-status-disconnected'); - this.sessions = this.page.getByTestId(/console-tab/); - this.metadataButton = this.page.getByRole('button', { name: 'Console information' }); - this.metadataDialog = this.page.getByRole('dialog'); - } - - /** - * Helper: Get the locator for the session tab. - * @param session Either a session object (language and version) or a string representing the session name. - * @returns The locator for the session tab. - */ - getSessionLocator(session: SessionName | string): Locator { - const sessionName = typeof session === 'string' - ? session - : `${session.language} ${session.version}`; - - return this.page.getByRole('tab', { name: new RegExp(sessionName) }); - } - - /** - * Helper: Get the status of the session tab - * @param session Either a session object (language and version) or a string representing the session name. - * @returns 'active', 'idle', 'disconnected', or 'unknown' - */ - private async getStatus(session: SessionName | string): Promise<'active' | 'idle' | 'disconnected' | 'unknown'> { - const expectedSession = this.getSessionLocator(session); - - if (await this.activeStatus(expectedSession).isVisible()) { return 'active'; } - if (await this.idleStatus(expectedSession).isVisible()) { return 'idle'; } - if (await this.disconnectedStatus(expectedSession).isVisible()) { return 'disconnected'; } - return 'unknown'; - } - - /** - * Verify: Check the status of the session tab - * @param session Either a session object (language and version) or a string representing the session name. - * @param expectedStatus status to check for ('active', 'idle', 'disconnected') - */ - async checkStatus(session: SessionName | string, expectedStatus: 'active' | 'idle' | 'disconnected') { - const stepTitle = session instanceof Object - ? `Verify ${session.language} ${session.version} session status: ${expectedStatus}` - : `Verify ${session} session status: ${expectedStatus}`; - - await test.step(stepTitle, async () => { - const sessionLocator = this.getSessionLocator(session); - const statusClass = `.codicon-positron-status-${expectedStatus}`; - - await expect(sessionLocator).toBeVisible(); - await expect(sessionLocator.locator(statusClass)).toBeVisible({ timeout: 30000 }); - }); - } - - /** - * Action: Open the metadata dialog and select the desired menu item - * @param menuItem the menu item to click on the metadata dialog - */ - async clickMetadataMenuItem(menuItem: 'Show Kernel Output Channel' | 'Show Console Output Channel' | 'Show LSP Output Channel') { - await this.console.clickConsoleTab(); - await this.metadataButton.click(); - await this.metadataDialog.getByText(menuItem).click(); - - await expect(this.page.getByRole('tab', { name: 'Output' })).toHaveClass('action-item checked'); - // Todo: https://github.com/posit-dev/positron/issues/6389 - // Todo: remove when menu closes on click as expected - await this.page.keyboard.press('Escape'); - } - - /** - * Verify: Check the metadata of the session dialog - * @param data the expected metadata to verify - */ - async checkMetadata(data: SessionName & { state: 'active' | 'idle' | 'disconnected' | 'exited' }) { - await test.step(`Verify ${data.language} ${data.version} metadata`, async () => { - - // Click metadata button for desired session - const sessionLocator = this.getSessionLocator({ language: data.language, version: data.version }); - await sessionLocator.click(); - await this.metadataButton.click(); - - // Verify metadata - await expect(this.metadataDialog.getByText(`${data.language} ${data.version}`)).toBeVisible(); - await expect(this.metadataDialog.getByText(new RegExp(`Session ID: ${data.language.toLowerCase()}-[a-zA-Z0-9]+`))).toBeVisible(); - await expect(this.metadataDialog.getByText(`State: ${data.state}`)).toBeVisible(); - await expect(this.metadataDialog.getByText(/^Path: [\/~a-zA-Z0-9.]+/)).toBeVisible(); - await expect(this.metadataDialog.getByText(/^Source: (Pyenv|System)$/)).toBeVisible(); - await this.page.keyboard.press('Escape'); - - // Verify Language Console - await this.clickMetadataMenuItem('Show Console Output Channel'); - await expect(this.page.getByRole('combobox')).toHaveValue(new RegExp(`^${data.language} ${data.version}.*: Console$`)); - - // Verify Output Channel - await this.clickMetadataMenuItem('Show Kernel Output Channel'); - await expect(this.page.getByRole('combobox')).toHaveValue(new RegExp(`^${data.language} ${data.version}.*: Kernel$`)); - - // Verify LSP Output Channel - await this.clickMetadataMenuItem('Show LSP Output Channel'); - await expect(this.page.getByRole('combobox')).toHaveValue(/Language Server \(Console\)$/); - - // Go back to console when done - await this.console.clickConsoleTab(); - }); - } - - /** - * Util: Get the session ID from the session name - * @param sessionName the session name to get the ID for - * @returns the session ID - */ - async getSessionId(sessionName: string): Promise { - const testId = await this.getTestIdFromName(sessionName); - return testId.match(/-(\S+)$/)?.[1] || ''; - } - - /** - * Util: Get the test ID from the session name - * @param sessionName the session name to get the ID for - * @returns the session ID - */ - async getTestIdFromName(sessionName: string): Promise { - const session = this.getSessionLocator(sessionName); - const testId = await session.getAttribute('data-testid'); - const match = testId?.match(/console-tab-(\S+)/); - return match ? match[1] : ''; - } - - /** - * Util: Get the metadata of the session - * @param sessionId the session ID to get metadata for - * @returns the metadata of the session - */ - async getMetadata(sessionId: string): Promise { - // select the session tab and open the metadata dialog - await this.page.getByTestId(`console-tab-${sessionId}`).click(); - await this.metadataButton.click(); - - // get metadata - const name = (await this.metadataDialog.getByTestId('session-name').textContent() || '').trim(); - const id = (await this.metadataDialog.getByTestId('session-id').textContent() || '').replace('Session ID: ', ''); - const state = (await this.metadataDialog.getByTestId('session-state').textContent() || '').replace('State: ', ''); - const path = (await this.metadataDialog.getByTestId('session-path').textContent() || '').replace('Path: ', ''); - const source = (await this.metadataDialog.getByTestId('session-source').textContent() || '').replace('Source: ', ''); - - // temporary: close metadata dialog - await this.metadataButton.click({ force: true }); - - return { name, id, state, path, source }; - } - - /** - * Action: Select session in tab list and start the session - * @param session details of the session (language and version) - * @param waitForIdle wait for the session to display as "idle" (ready) - */ - async start(session: SessionName, waitForIdle = true): Promise { - await test.step(`Start session: ${session.language} ${session.version}`, async () => { - const sessionLocator = this.getSessionLocator(session); - await sessionLocator.click(); - await this.page.getByLabel('Start console', { exact: true }).click(); - - if (waitForIdle) { - await this.checkStatus(session, 'idle'); - } - }); - } - - /** - * Action: Restart the session - * @param session details of the session (language and version) - * @param waitForIdle wait for the session to display as "idle" (ready) - */ - async restart(session: SessionName, waitForIdle = true): Promise { - await test.step(`Restart session: ${session.language} ${session.version}`, async () => { - const sessionLocator = this.getSessionLocator(session); - await sessionLocator.click(); - await this.page.getByLabel('Restart console', { exact: true }).click(); - - if (waitForIdle) { - await this.checkStatus(session, 'idle'); - } - }); - } - - /** - * Action: Shutdown the session - * @param session details of the session (language and version) - * @param waitForDisconnected wait for the session to display as disconnected - */ - async shutdown(session: SessionName, waitForDisconnected = true): Promise { - await test.step(`Shutdown session: ${session.language} ${session.version}`, async () => { - const sessionLocator = this.getSessionLocator(session); - await sessionLocator.click(); - await this.page.getByLabel('Shutdown console', { exact: true }).click(); - - if (waitForDisconnected) { - await this.checkStatus(session, 'disconnected'); - } - }); - } - - /** - * Action: Select the session - * @param session details of the session (language and version) - */ - async select(session: SessionName): Promise { - await test.step(`Select session: ${session.language} ${session.version}`, async () => { - const sessionLocator = this.getSessionLocator(session); - await sessionLocator.click(); - }); - } - - /** - * Helper: Ensure the session is started and idle (ready) - * @param session details of the session (language and version) - */ - async ensureStartedAndIdle(session: SessionName): Promise { - await test.step(`Ensure ${session.language} ${session.version} session is started and idle`, async () => { - // Start Session if it does not exist - const sessionExists = await this.getSessionLocator(session).isVisible({ timeout: 30000 }); - if (!sessionExists) { - await this.console.startSession(session); - } - - // Ensure session is idle (ready) - const status = await this.console.session.getStatus(session); - if (status !== 'idle') { - await this.console.session.start(session); - } - }); - } - - /** - * Util: Get Active Sessions in the Console Session Tab List - * Note: Sessions that are disconnected are filtered out - */ - async getActiveSessions(): Promise { - const allSessions = await this.sessions.all(); - - const activeSessions = ( - await Promise.all( - allSessions.map(async session => { - const isDisconnected = await session.locator('.codicon-positron-status-disconnected').isVisible(); - if (isDisconnected) { return null; } - - // Extract session ID from data-testid attribute - const testId = await session.getAttribute('data-testid'); - const match = testId?.match(/console-tab-(\S+)/); - const sessionId = match ? match[1] : null; - - return sessionId ? { sessionId, session } : null; - }) - ) - ).filter(session => session !== null) as { sessionId: string; session: Locator }[]; - - - const activeSessionInfo: QuickPickSessionInfo[] = []; - - for (const session of activeSessions) { - const { name, path } = await this.getMetadata(session.sessionId); - - activeSessionInfo.push({ - name, - path, - }); - } - return activeSessionInfo; - } -} - -export type SessionName = { - language: 'Python' | 'R'; - version: string; // e.g. '3.10.15' -}; - -export type SessionMetaData = { - name: string; - id: string; - state: string; - source: string; - path: string; -}; diff --git a/test/e2e/pages/sessions.ts b/test/e2e/pages/sessions.ts new file mode 100644 index 00000000000..ef3822620f9 --- /dev/null +++ b/test/e2e/pages/sessions.ts @@ -0,0 +1,568 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2025 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import test, { expect, Locator, Page } from '@playwright/test'; +import { Code, Console, QuickAccess } from '../infra'; +import { QuickInput } from './quickInput'; + +const DESIRED_PYTHON = process.env.POSITRON_PY_VER_SEL; +const DESIRED_R = process.env.POSITRON_R_VER_SEL; + + +/** + * Class to manage console sessions + */ +export class Sessions { + page: Page; + activeStatus: (session: Locator) => Locator; + idleStatus: (session: Locator) => Locator; + disconnectedStatus: (session: Locator) => Locator; + sessions: Locator; + metadataButton: Locator; + metadataDialog: Locator; + chooseSessionButton: Locator; + quickPick: SessionQuickPick; + + constructor(private code: Code, private console: Console, private quickaccess: QuickAccess, private quickinput: QuickInput) { + this.page = this.code.driver.page; + this.activeStatus = (session: Locator) => session.locator('.codicon-positron-status-active'); + this.idleStatus = (session: Locator) => session.locator('.codicon-positron-status-idle'); + this.disconnectedStatus = (session: Locator) => session.locator('.codicon-positron-status-disconnected'); + this.sessions = this.page.getByTestId(/console-tab/); + this.metadataButton = this.page.getByRole('button', { name: 'Console information' }); + this.metadataDialog = this.page.getByRole('dialog'); + this.quickPick = new SessionQuickPick(this.code, this); + this.chooseSessionButton = this.page.getByRole('button', { name: 'Open Active Session Picker' }); + } + + // -- Actions -- + + /** + * Action: Start a session via the top action bar button or quickaccess. + * @param options - Configuration options for selecting the interpreter. + * @param options.language the programming language interpreter to select. + * @param options.version the specific version of the interpreter to select (e.g., "3.10.15"). + * @param options.triggerMode the method used to trigger the selection: top-action-bar or quickaccess. + * @param options.waitForReady whether to wait for the console to be ready after selecting the interpreter. + */ + async launch(options: { + language: 'Python' | 'R'; + version?: string; + triggerMode?: 'top-action-bar' | 'quickaccess'; + waitForReady?: boolean; + }): Promise { + + if (!DESIRED_PYTHON || !DESIRED_R) { + throw new Error('Please set env vars: POSITRON_PY_VER_SEL, POSITRON_R_VER_SEL'); + } + + const { + language, + version = language === 'Python' ? DESIRED_PYTHON : DESIRED_R, + waitForReady = true, + triggerMode = 'quickaccess', + } = options; + + await test.step(`Start session via ${triggerMode}: ${language} ${version}`, async () => { + // Don't try to start a new runtime if one is currently starting up + await this.waitForReadyOrNoSessions(); + + // Start the runtime via the dropdown or quickaccess + const command = language === 'Python' ? 'python.setInterpreter' : 'r.selectInterpreter'; + triggerMode === 'quickaccess' + ? await this.quickaccess.runCommand(command, { keepOpen: true }) + : await this.quickPick.openSessionQuickPickMenu(); + + await this.quickinput.type(`${language} ${version}`); + + // Wait until the desired runtime appears in the list and select it. + // We need to click instead of using 'enter' because the Python select interpreter command + // may include additional items above the desired interpreter string. + await this.quickinput.selectQuickInputElementContaining(`${language} ${version}`); + await this.quickinput.waitForQuickInputClosed(); + + // Move mouse to prevent tooltip hover + await this.code.driver.page.mouse.move(0, 0); + + if (waitForReady) { + language === 'Python' + ? await this.console.waitForReadyAndStarted('>>>', 40000) + : await this.console.waitForReadyAndStarted('>', 40000); + } + }); + } + + /** + * Action: Select the session + * @param session details of the session (language and version) + */ + async select(session: SessionName): Promise { + await test.step(`Select session: ${session.language} ${session.version}`, async () => { + const sessionLocator = this.getSessionLocator(session); + await sessionLocator.click(); + }); + } + + /** + * Action: Start the session + * @param session details of the session (language and version) + * @param waitForIdle wait for the session to display as "idle" (ready) + */ + async start(session: SessionName, waitForIdle = true): Promise { + await test.step(`Start session: ${session.language} ${session.version}`, async () => { + const sessionLocator = this.getSessionLocator(session); + await sessionLocator.click(); + await this.page.getByLabel('Start console', { exact: true }).click(); + + if (waitForIdle) { + await this.checkStatus(session, 'idle'); + } + }); + } + + /** + * Action: Restart the session + * @param session details of the session (language and version) + * @param waitForIdle wait for the session to display as "idle" (ready) + */ + async restart(session: SessionName, waitForIdle = true): Promise { + await test.step(`Restart session: ${session.language} ${session.version}`, async () => { + const sessionLocator = this.getSessionLocator(session); + await sessionLocator.click(); + await this.page.getByLabel('Restart console', { exact: true }).click(); + + if (waitForIdle) { + await this.checkStatus(session, 'idle'); + } + }); + } + + /** + * Action: Shutdown the session + * @param session details of the session (language and version) + * @param waitForDisconnected wait for the session to display as disconnected + */ + async shutdown(session: SessionName, waitForDisconnected = true): Promise { + await test.step(`Shutdown session: ${session.language} ${session.version}`, async () => { + const sessionLocator = this.getSessionLocator(session); + await sessionLocator.click(); + await this.page.getByLabel('Shutdown console', { exact: true }).click(); + + if (waitForDisconnected) { + await this.checkStatus(session, 'disconnected'); + } + }); + } + + /** + * Action: Open the metadata dialog and select the desired menu item + * @param menuItem the menu item to click on the metadata dialog + */ + async clickMetadataMenuItem(menuItem: 'Show Kernel Output Channel' | 'Show Console Output Channel' | 'Show LSP Output Channel') { + await this.console.clickConsoleTab(); + await this.metadataButton.click(); + await this.metadataDialog.getByText(menuItem).click(); + + await expect(this.page.getByRole('tab', { name: 'Output' })).toHaveClass('action-item checked'); + // Todo: https://github.com/posit-dev/positron/issues/6389 + // Todo: remove when menu closes on click as expected + await this.page.keyboard.press('Escape'); + } + + // -- Helpers -- + + /** + * Helper: Ensure the session is started and idle (ready) + * @param session details of the session (language and version) + */ + async ensureStartedAndIdle(session: SessionName): Promise { + await test.step(`Ensure ${session.language} ${session.version} session is started and idle`, async () => { + + // Start Session if it does not exist + const sessionExists = await this.getSessionLocator(session).isVisible({ timeout: 30000 }); + if (!sessionExists) { + await this.launch(session); + } + + // Ensure session is idle (ready) + const status = await this.getStatus(session); + if (status !== 'idle') { + await this.start(session); + } + }); + } + + /** + * Helper: Wait for runtimes to finish loading + */ + async waitForRuntimesToLoad() { + await expect(this.page.locator('text=/^Starting up|^Starting|^Preparing|^Discovering( \\w+)? interpreters|starting\\.$/i')).toHaveCount(0, { timeout: 80000 }); + } + + /** + * Helper: Wait for the console to be ready or no sessions have been started + */ + async waitForReadyOrNoSessions() { + await test.step('Wait for console to be ready or no session', async () => { + + await this.waitForRuntimesToLoad(); + + // ensure we are on Console tab + await this.page.getByRole('tab', { name: 'Console', exact: true }).locator('a').click(); + + // Move mouse to prevent tooltip hover + await this.code.driver.page.mouse.move(0, 0); + + // wait for the dropdown to contain R, Python, or Choose Session. + const currentSession = await this.chooseSessionButton.textContent() || ''; + + if (currentSession.includes('Python')) { + await expect(this.page.getByRole('code').getByText('>>>')).toBeVisible({ timeout: 30000 }); + return; + } else if (currentSession.includes('R') && !currentSession.includes('Choose Session')) { + await expect(this.page.getByRole('code').getByText('>')).toBeVisible({ timeout: 30000 }); + return; + } else if (currentSession.includes('Choose Session')) { + await expect(this.page.getByText('Choose Session')).toBeVisible(); + return; + } + + // If we reach here, the console is not ready. + throw new Error('Console is not ready after waiting for session to start'); + }); + } + + /** + * Helper: Get the session ID from the session name + * @param sessionName the session name to get the ID for + * @returns the session ID + */ + async getSessionId(sessionName: string): Promise { + const testId = await this.getTestIdFromSessionName(sessionName); + return testId.match(/-(\S+)$/)?.[1] || ''; + } + + /** + * Helper: Get the test ID from the session name + * @param sessionName the session name to get the ID for + * @returns the session ID + */ + async getTestIdFromSessionName(sessionName: string): Promise { + const session = this.getSessionLocator(sessionName); + const testId = await session.getAttribute('data-testid'); + const match = testId?.match(/console-tab-(\S+)/); + return match ? match[1] : ''; + } + + /** + * Helper: Get the metadata of the session + * @param sessionId the session ID to get metadata for + * @returns the metadata of the session + */ + async getMetadata(sessionId: string): Promise { + // select the session tab and open the metadata dialog + await this.page.getByTestId(`console-tab-${sessionId}`).click(); + await this.metadataButton.click(); + + // get metadata + const name = (await this.metadataDialog.getByTestId('session-name').textContent() || '').trim(); + const id = (await this.metadataDialog.getByTestId('session-id').textContent() || '').replace('Session ID: ', ''); + const state = (await this.metadataDialog.getByTestId('session-state').textContent() || '').replace('State: ', ''); + const path = (await this.metadataDialog.getByTestId('session-path').textContent() || '').replace('Path: ', ''); + const source = (await this.metadataDialog.getByTestId('session-source').textContent() || '').replace('Source: ', ''); + + // temporary: close metadata dialog + await this.metadataButton.click({ force: true }); + + return { name, id, state, path, source }; + } + + /** + * Helper: Get Active Sessions in the Console Session Tab List + * Note: Sessions that are disconnected are filtered out + */ + async getActiveSessions(): Promise { + const allSessions = await this.sessions.all(); + + const activeSessions = ( + await Promise.all( + allSessions.map(async session => { + const isDisconnected = await session.locator('.codicon-positron-status-disconnected').isVisible(); + if (isDisconnected) { return null; } + + // Extract session ID from data-testid attribute + const testId = await session.getAttribute('data-testid'); + const match = testId?.match(/console-tab-(\S+)/); + const sessionId = match ? match[1] : null; + + return sessionId ? { sessionId, session } : null; + }) + ) + ).filter(session => session !== null) as { sessionId: string; session: Locator }[]; + + + const activeSessionInfo: QuickPickSessionInfo[] = []; + + for (const session of activeSessions) { + const { name, path } = await this.getMetadata(session.sessionId); + + activeSessionInfo.push({ + name, + path, + }); + } + return activeSessionInfo; + } + + /** + * Helper: Get the locator for the session tab. + * @param session Either a session object (language and version) or a string representing the session name. + * @returns The locator for the session tab. + */ + private getSessionLocator(session: SessionName | string): Locator { + const sessionName = typeof session === 'string' + ? session + : `${session.language} ${session.version}`; + + return this.page.getByRole('tab', { name: new RegExp(sessionName) }); + } + + /** + * Helper: Get the status of the session tab + * @param session Either a session object (language and version) or a string representing the session name. + * @returns 'active', 'idle', 'disconnected', or 'unknown' + */ + async getStatus(session: SessionName | string): Promise<'active' | 'idle' | 'disconnected' | 'unknown'> { + const expectedSession = this.getSessionLocator(session); + + if (await this.activeStatus(expectedSession).isVisible()) { return 'active'; } + if (await this.idleStatus(expectedSession).isVisible()) { return 'idle'; } + if (await this.disconnectedStatus(expectedSession).isVisible()) { return 'disconnected'; } + return 'unknown'; + } + + // -- Verifications -- + + /** + * Verify: Check the status of the session tab + * @param session Either a session object (language and version) or a string representing the session name. + * @param expectedStatus status to check for ('active', 'idle', 'disconnected') + */ + async checkStatus(session: SessionName | string, expectedStatus: 'active' | 'idle' | 'disconnected') { + const stepTitle = session instanceof Object + ? `Verify ${session.language} ${session.version} session status: ${expectedStatus}` + : `Verify ${session} session status: ${expectedStatus}`; + + await test.step(stepTitle, async () => { + const sessionLocator = this.getSessionLocator(session); + const statusClass = `.codicon-positron-status-${expectedStatus}`; + + await expect(sessionLocator).toBeVisible(); + await expect(sessionLocator.locator(statusClass)).toBeVisible({ timeout: 30000 }); + }); + } + + + /** + * Verify: Check the metadata of the session dialog + * @param data the expected metadata to verify + */ + async checkMetadata(data: SessionName & { state: 'active' | 'idle' | 'disconnected' | 'exited' }) { + await test.step(`Verify ${data.language} ${data.version} metadata`, async () => { + + // Click metadata button for desired session + const sessionLocator = this.getSessionLocator({ language: data.language, version: data.version }); + await sessionLocator.click(); + await this.metadataButton.click(); + + // Verify metadata + await expect(this.metadataDialog.getByText(`${data.language} ${data.version}`)).toBeVisible(); + await expect(this.metadataDialog.getByText(new RegExp(`Session ID: ${data.language.toLowerCase()}-[a-zA-Z0-9]+`))).toBeVisible(); + await expect(this.metadataDialog.getByText(`State: ${data.state}`)).toBeVisible(); + await expect(this.metadataDialog.getByText(/^Path: [\/~a-zA-Z0-9.]+/)).toBeVisible(); + await expect(this.metadataDialog.getByText(/^Source: (Pyenv|System)$/)).toBeVisible(); + await this.page.keyboard.press('Escape'); + + // Verify Language Console + await this.clickMetadataMenuItem('Show Console Output Channel'); + await expect(this.page.getByRole('combobox')).toHaveValue(new RegExp(`^${data.language} ${data.version}.*: Console$`)); + + // Verify Output Channel + await this.clickMetadataMenuItem('Show Kernel Output Channel'); + await expect(this.page.getByRole('combobox')).toHaveValue(new RegExp(`^${data.language} ${data.version}.*: Kernel$`)); + + // Verify LSP Output Channel + await this.clickMetadataMenuItem('Show LSP Output Channel'); + await expect(this.page.getByRole('combobox')).toHaveValue(/Language Server \(Console\)$/); + + // Go back to console when done + await this.console.clickConsoleTab(); + }); + } + + /** + * Verify: the selected runtime is the expected runtime in the Session Picker button + * @param version The descriptive string of the runtime to verify. + */ + async verifySessionPickerValue( + options: { language?: 'Python' | 'R'; version?: string } = {} + ) { + if (!DESIRED_PYTHON || !DESIRED_R) { + throw new Error('Please set env vars: POSITRON_PY_VER_SEL, POSITRON_R_VER_SEL'); + } + + const { + language = 'Python', + version = language === 'Python' ? DESIRED_PYTHON : DESIRED_R, + } = options; + await test.step(`Verify runtime is selected: ${language} ${version}`, async () => { + const runtimeInfo = await this.quickPick.getSelectedSessionInfo(); + expect(runtimeInfo.language).toContain(language); + expect(runtimeInfo.version).toContain(version); + }); + } +} + +/** + * Helper class to manage the session quick pick + */ +export class SessionQuickPick { + private sessionQuickMenu = this.code.driver.page.getByText(/(Select a Session)|(Start a New Session)/); + private newSessionQuickOption = this.code.driver.page.getByText(/New Session.../); + + constructor(private code: Code, private sessions: Sessions) { } + + // -- Actions -- + + /** + * Action: Open the session quickpick menu via the "Choose Session" button in top action bar. + */ + async openSessionQuickPickMenu(viewAllRuntimes = true) { + if (!await this.sessionQuickMenu.isVisible()) { + await this.sessions.chooseSessionButton.click(); + } + + if (viewAllRuntimes) { + await this.newSessionQuickOption.click(); + await expect(this.code.driver.page.getByText(/Start a New Session/)).toBeVisible(); + } else { + await expect(this.code.driver.page.getByText(/Select a Session/)).toBeVisible(); + } + } + + /** + * Action: Close the session quickpick menu if it is open. + */ + async closeSessionQuickPickMenu() { + if (await this.sessionQuickMenu.isVisible()) { + await this.code.driver.page.keyboard.press('Escape'); + await expect(this.sessionQuickMenu).not.toBeVisible(); + } + } + + // --- Helpers --- + + /** + * Helper: Get active sessions from the session picker. + * @returns The list of active sessions. + */ + async getActiveSessions(): Promise { + await this.openSessionQuickPickMenu(false); + const allSessions = await this.code.driver.page.locator('.quick-input-list-rows').all(); + + // Get the text of all sessions + const activeSessions = await Promise.all( + allSessions.map(async element => { + const runtime = (await element.locator('.quick-input-list-row').nth(0).textContent())?.replace('Currently Selected', ''); + const path = await element.locator('.quick-input-list-row').nth(1).textContent(); + return { name: runtime?.trim() || '', path: path?.trim() || '' }; + }) + ); + + // Filter out the one with "New Session..." + const filteredSessions = activeSessions + .filter(session => !session.name.includes('New Session...')); + + await this.closeSessionQuickPickMenu(); + return filteredSessions; + } + + /** + * Helper: Get the interpreter info for the currently selected runtime via the quickpick menu. + * @returns The interpreter info for the selected interpreter if found, otherwise undefined. + */ + async getSelectedSessionInfo(): Promise { + await this.openSessionQuickPickMenu(false); + const selectedInterpreter = this.code.driver.page.locator('.quick-input-list-entry').filter({ hasText: 'Currently Selected' }); + + // Extract the runtime name + const runtime = await selectedInterpreter.locator('.monaco-icon-label-container .label-name .monaco-highlighted-label').nth(0).textContent(); + + // Extract the language, version, and source from runtime name + const { language, version, source } = await this.parseRuntimeName(runtime); + + // Extract the path + const path = await selectedInterpreter.locator('.quick-input-list-label-meta .monaco-icon-label-container .label-name .monaco-highlighted-label').nth(0).textContent(); + + await this.closeSessionQuickPickMenu(); + + return { + language: language as 'Python' | 'R', + version, + source, + path: path || '', + }; + } + + // -- Utils -- + + /** + * Utils: Parse the full runtime name into language, version, and source. + * @param runtimeName the full runtime name to parse. E.g., "Python 3.10.15 (Pyenv)" + * @returns The parsed runtime name. E.g., { language: "Python", version: "3.10.15", source: "Pyenv" } + */ + async parseRuntimeName(runtimeName: string | null) { + if (!runtimeName) { + throw new Error('No interpreter string provided'); + } + + // Note: Some interpreters may not have a source, so the source is optional + const match = runtimeName.match(/^(\w+)\s([\d.]+)(?:\s\(([^)]+)\))?$/); + if (!match) { + throw new Error(`Invalid interpreter format: ${runtimeName}`); + } + + return { + language: match[1], // e.g., "Python", "R" + version: match[2], // e.g., "3.10.15", "4.4.1" + source: match[3] || undefined // e.g., "Pyenv", "System" + }; + } +} + +export type QuickPickSessionInfo = { + name: string; + path: string; +}; + + +export type SessionName = { + language: 'Python' | 'R'; + version: string; // e.g. '3.10.15' +}; + + +export interface SessionInfo extends SessionName { + path: string; // e.g. /usr/local/bin/python3 + source?: string; // e.g. Pyenv, Global, System, etc +} + +export type SessionMetaData = { + name: string; + id: string; + state: string; + source: string; + path: string; +}; diff --git a/test/e2e/tests/console/console-sessions.test.ts b/test/e2e/tests/console/console-sessions.test.ts index de8e3571942..20cecc535ec 100644 --- a/test/e2e/tests/console/console-sessions.test.ts +++ b/test/e2e/tests/console/console-sessions.test.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { expect } from '@playwright/test'; -import { SessionName } from '../../infra'; import { test, tags } from '../_test.setup'; +import { SessionName } from '../../infra'; const pythonSession: SessionName = { language: 'Python', @@ -32,102 +32,104 @@ test.describe('Console: Sessions', { await app.workbench.variables.togglePane('hide'); }); - test('Validate state between sessions (active, idle, disconnect) ', async function ({ app, interpreter }) { - const console = app.workbench.console; + test('Validate state between sessions (active, idle, disconnect) ', async function ({ app }) { + const sessions = app.workbench.sessions; // Start Python session - await app.workbench.console.startSession({ ...pythonSession, waitForReady: false }); + await app.workbench.sessions.launch({ ...pythonSession, waitForReady: false }); // Verify Python session is visible and transitions from active --> idle - await console.session.checkStatus(pythonSession, 'active'); - await console.session.checkStatus(pythonSession, 'idle'); + await sessions.checkStatus(pythonSession, 'active'); + await sessions.checkStatus(pythonSession, 'idle'); // Restart Python session and confirm state returns to active --> idle - await console.session.restart(pythonSession, false); - await console.session.checkStatus(pythonSession, 'active'); - await console.session.checkStatus(pythonSession, 'idle'); + await sessions.restart(pythonSession, false); + await sessions.checkStatus(pythonSession, 'active'); + await sessions.checkStatus(pythonSession, 'idle'); // Start R session - await app.workbench.console.startSession({ ...rSession, waitForReady: false }); + await app.workbench.sessions.launch({ ...rSession, waitForReady: false }); // Verify R session transitions from active --> idle while Python session remains idle - await console.session.checkStatus(rSession, 'active'); - await console.session.checkStatus(rSession, 'idle'); - await console.session.checkStatus(pythonSession, 'idle'); + await sessions.checkStatus(rSession, 'active'); + await sessions.checkStatus(rSession, 'idle'); + await sessions.checkStatus(pythonSession, 'idle'); // Shutdown Python session, verify Python transitions to disconnected while R remains idle - await console.session.shutdown(pythonSession, false); - await console.session.checkStatus(pythonSession, 'disconnected'); - await console.session.checkStatus(rSession, 'idle'); + await sessions.shutdown(pythonSession, false); + await sessions.checkStatus(pythonSession, 'disconnected'); + await sessions.checkStatus(rSession, 'idle'); // Restart R session, verify R to returns to active --> idle and Python remains disconnected - await console.session.restart(rSession, false); - await console.session.checkStatus(rSession, 'active'); - await console.session.checkStatus(rSession, 'idle'); - await console.session.checkStatus(pythonSession, 'disconnected'); + await sessions.restart(rSession, false); + await sessions.checkStatus(rSession, 'active'); + await sessions.checkStatus(rSession, 'idle'); + await sessions.checkStatus(pythonSession, 'disconnected'); // Shutdown R, verify both Python and R in disconnected state - await console.session.shutdown(rSession, false); - await console.session.checkStatus(rSession, 'disconnected'); - await console.session.checkStatus(pythonSession, 'disconnected'); + await sessions.shutdown(rSession, false); + await sessions.checkStatus(rSession, 'disconnected'); + await sessions.checkStatus(pythonSession, 'disconnected'); }); test('Validate session state displays as active when executing code', async function ({ app }) { + const sessions = app.workbench.sessions; const console = app.workbench.console; // Ensure sessions exist and are idle - await console.session.ensureStartedAndIdle(pythonSession); - await console.session.ensureStartedAndIdle(rSession); + await sessions.ensureStartedAndIdle(pythonSession); + await sessions.ensureStartedAndIdle(rSession); // Verify Python session transitions to active when executing code - await console.session.select(pythonSession); + await sessions.select(pythonSession); await console.typeToConsole('import time', true); await console.typeToConsole('time.sleep(1)', true); - await console.session.checkStatus(pythonSession, 'active'); - await console.session.checkStatus(pythonSession, 'idle'); + await sessions.checkStatus(pythonSession, 'active'); + await sessions.checkStatus(pythonSession, 'idle'); // Verify R session transitions to active when executing code - await console.session.select(rSession); + await sessions.select(rSession); await console.typeToConsole('Sys.sleep(1)', true); - await console.session.checkStatus(rSession, 'active'); - await console.session.checkStatus(rSession, 'idle'); + await sessions.checkStatus(rSession, 'active'); + await sessions.checkStatus(rSession, 'idle'); }); test('Validate metadata between sessions', { annotation: [ { type: 'issue', description: 'https://github.com/posit-dev/positron/issues/6389' }] }, async function ({ app }) { - const console = app.workbench.console; + const sessions = app.workbench.sessions; // Ensure sessions exist and are idle - await console.session.ensureStartedAndIdle(pythonSession); - await console.session.ensureStartedAndIdle(rSession); + await sessions.ensureStartedAndIdle(pythonSession); + await sessions.ensureStartedAndIdle(rSession); // Verify Python session metadata - await console.session.checkMetadata({ ...pythonSession, state: 'idle' }); - await console.session.checkMetadata({ ...rSession, state: 'idle' }); + await sessions.checkMetadata({ ...pythonSession, state: 'idle' }); + await sessions.checkMetadata({ ...rSession, state: 'idle' }); // Shutdown Python session and verify metadata - await console.session.shutdown(pythonSession); - await console.session.checkMetadata({ ...pythonSession, state: 'exited' }); + await sessions.shutdown(pythonSession); + await sessions.checkMetadata({ ...pythonSession, state: 'exited' }); // Shutdown R session and verify metadata - await console.session.shutdown(rSession); - await console.session.checkMetadata({ ...rSession, state: 'exited' }); + await sessions.shutdown(rSession); + await sessions.checkMetadata({ ...rSession, state: 'exited' }); }); test('Validate variables between sessions', { tag: [tags.VARIABLES] }, async function ({ app }) { + const sessions = app.workbench.sessions; const console = app.workbench.console; const variables = app.workbench.variables; // Ensure sessions exist and are idle - await console.session.ensureStartedAndIdle(pythonSession); - await console.session.ensureStartedAndIdle(rSession); + await sessions.ensureStartedAndIdle(pythonSession); + await sessions.ensureStartedAndIdle(rSession); // Set and verify variables in Python - await console.session.select(pythonSession); + await sessions.select(pythonSession); await console.typeToConsole('x = 1', true); await console.typeToConsole('y = 2', true); await variables.checkRuntime(pythonSession); @@ -135,7 +137,7 @@ test.describe('Console: Sessions', { await variables.checkVariableValue('y', '2'); // Set and verify variables in R - await console.session.select(rSession); + await sessions.select(rSession); await console.typeToConsole('x <- 3', true); await console.typeToConsole('z <- 4', true); await variables.checkRuntime(rSession); @@ -143,14 +145,14 @@ test.describe('Console: Sessions', { await variables.checkVariableValue('z', '4'); // Switch back to Python, update variables, and verify - await console.session.select(pythonSession); + await sessions.select(pythonSession); await console.typeToConsole('x = 0', true); await variables.checkRuntime(pythonSession); await variables.checkVariableValue('x', '0'); await variables.checkVariableValue('y', '2'); // Switch back to R, verify variables remain unchanged - await console.session.select(rSession); + await sessions.select(rSession); await variables.checkRuntime(rSession); await variables.checkVariableValue('x', '3'); await variables.checkVariableValue('z', '4'); @@ -161,39 +163,39 @@ test.describe('Console: Sessions', { { type: 'issue', description: 'sessions are not correctly sorted atm. see line 174.' } ] }, async function ({ app }) { - const console = app.workbench.console; - const interpreter = app.workbench.interpreterNew; + const sessions = app.workbench.sessions; + const interpreter = app.workbench.sessions.quickPick; // Ensure sessions exist and are idle - await console.session.ensureStartedAndIdle(pythonSession); - await console.session.ensureStartedAndIdle(rSession); + await sessions.ensureStartedAndIdle(pythonSession); + await sessions.ensureStartedAndIdle(rSession); // Get active sessions and verify they match the session picker: order matters! - let activeSessionsFromConsole = await console.session.getActiveSessions(); + let activeSessionsFromConsole = await sessions.getActiveSessions(); let activeSessionsFromPicker = await interpreter.getActiveSessions(); expect(activeSessionsFromConsole).toStrictEqual(activeSessionsFromPicker); // Shutdown Python session and verify active sessions - await console.session.shutdown(pythonSession); - activeSessionsFromConsole = await console.session.getActiveSessions(); + await sessions.shutdown(pythonSession); + activeSessionsFromConsole = await sessions.getActiveSessions(); activeSessionsFromPicker = await interpreter.getActiveSessions(); expect(activeSessionsFromConsole).toStrictEqual(activeSessionsFromPicker); // Shutdown R session and verify active sessions - await console.session.shutdown(rSession); - activeSessionsFromConsole = await console.session.getActiveSessions(); + await sessions.shutdown(rSession); + activeSessionsFromConsole = await sessions.getActiveSessions(); activeSessionsFromPicker = await interpreter.getActiveSessions(); expect(activeSessionsFromConsole).toStrictEqual(activeSessionsFromPicker); // Start Python session (again) and verify active sessions - await console.session.start(pythonSession); - activeSessionsFromConsole = await console.session.getActiveSessions(); + await sessions.start(pythonSession); + activeSessionsFromConsole = await sessions.getActiveSessions(); activeSessionsFromPicker = await interpreter.getActiveSessions(); expect(activeSessionsFromConsole).toStrictEqual(activeSessionsFromPicker); // Restart Python session and verify active sessions - await console.session.restart(pythonSession); - activeSessionsFromConsole = await console.session.getActiveSessions(); + await sessions.restart(pythonSession); + activeSessionsFromConsole = await sessions.getActiveSessions(); activeSessionsFromPicker = await interpreter.getActiveSessions(); expect(activeSessionsFromConsole).toStrictEqual(activeSessionsFromPicker); }); diff --git a/test/e2e/tests/top-action-bar/session-button.test.ts b/test/e2e/tests/top-action-bar/session-button.test.ts index dd746a824dd..bee5603bcbf 100644 --- a/test/e2e/tests/top-action-bar/session-button.test.ts +++ b/test/e2e/tests/top-action-bar/session-button.test.ts @@ -28,14 +28,14 @@ test.describe('Top Action Bar: Session Button', { }); test('Python - Verify session starts and displays as running', async function ({ app }) { - await app.workbench.console.startSession({ ...pythonSession, triggerMode: 'top-action-bar' }); - await app.workbench.interpreterNew.verifySessionIsSelected(pythonSession); - await app.workbench.console.session.checkStatus(pythonSession, 'idle'); + await app.workbench.sessions.launch({ ...pythonSession, triggerMode: 'top-action-bar' }); + await app.workbench.sessions.verifySessionPickerValue(pythonSession); + await app.workbench.sessions.checkStatus(pythonSession, 'idle'); }); test('R - Verify session starts and displays as running', async function ({ app }) { - await app.workbench.console.startSession({ ...rSession, triggerMode: 'top-action-bar' }); - await app.workbench.interpreterNew.verifySessionIsSelected(rSession); - await app.workbench.console.session.checkStatus(rSession, 'idle'); + await app.workbench.sessions.launch({ ...rSession, triggerMode: 'top-action-bar' }); + await app.workbench.sessions.verifySessionPickerValue(rSession); + await app.workbench.sessions.checkStatus(rSession, 'idle'); }); });