diff --git a/package.json b/package.json index bd0270d2771..b318703f5e3 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "update-grammars": "node build/npm/update-all-grammars.mjs", "update-localization-extension": "node build/npm/update-localization-extension.js", "e2e": "npm run e2e-electron", + "e2e-watch": "npm run --prefix test/e2e watch", "e2e-electron": "npx playwright test --project e2e-electron", "e2e-browser": "npx playwright test --project e2e-browser", "e2e-pr": "npx playwright test --project e2e-electron --grep @critical", diff --git a/test/e2e/infra/index.ts b/test/e2e/infra/index.ts index 0599909a620..1c843eeb828 100644 --- a/test/e2e/infra/index.ts +++ b/test/e2e/infra/index.ts @@ -42,4 +42,7 @@ export * from './fixtures/python'; export * from './fixtures/r'; export * from './fixtures/userSettings'; +// test-runner +export * from './test-runner'; + export { getDevElectronPath, getBuildElectronPath, getBuildVersion } from './electron'; diff --git a/test/e2e/pages/console.ts b/test/e2e/pages/console.ts index 68fd3070cc2..413034906c4 100644 --- a/test/e2e/pages/console.ts +++ b/test/e2e/pages/console.ts @@ -108,10 +108,14 @@ export class Console { contents.forEach(line => this.code.logger.log(line)); } - async typeToConsole(text: string, delay = 30) { + async typeToConsole(text: string, delay = 30, pressEnter = false) { await this.code.driver.page.waitForTimeout(500); await this.activeConsole.click(); await this.code.driver.page.keyboard.type(text, { delay }); + + if (pressEnter) { + await this.code.driver.page.keyboard.press('Enter'); + } } async sendEnterKey() { @@ -134,7 +138,7 @@ export class Console { const page = this.code.driver.page; // ensure interpreter(s) containing starting/discovering do not exist in DOM - await expect(page.locator('text=/^Starting up|^Starting|^Discovering( \\w+)? interpreters|starting\\.$/i')).toHaveCount(0, { timeout: 30000 }); + await expect(page.locator('text=/^Starting up|^Starting|^Discovering( \\w+)? interpreters|starting\\.$/i')).toHaveCount(0, { timeout: 50000 }); // ensure we are on Console tab await page.getByRole('tab', { name: 'Console', exact: true }).locator('a').click(); diff --git a/test/e2e/pages/explorer.ts b/test/e2e/pages/explorer.ts index b1c12e30397..72882e645c1 100644 --- a/test/e2e/pages/explorer.ts +++ b/test/e2e/pages/explorer.ts @@ -8,7 +8,6 @@ import { expect, Locator } from '@playwright/test'; import { Code } from '../infra/code'; const POSITRON_EXPLORER_PROJECT_TITLE = 'div[id="workbench.view.explorer"] h3.title'; -const POSITRON_EXPLORER_PROJECT_FILES = 'div[id="workbench.view.explorer"] span[class="monaco-highlighted-label"]'; /* @@ -20,23 +19,12 @@ export class Explorer { constructor(protected code: Code) { } - /** - * Constructs a string array of the top-level project files/directories in the explorer. - * @param locator - The locator for the project files/directories in the explorer. - * @returns Promise Array of strings representing the top-level project files/directories in the explorer. - */ - async getExplorerProjectFiles(locator: string = POSITRON_EXPLORER_PROJECT_FILES): Promise { - const explorerProjectFiles = this.code.driver.page.locator(locator); - const filesList = await explorerProjectFiles.all(); - const fileNames = filesList.map(async file => { - const fileText = await file.textContent(); - return fileText || ''; - }); - return await Promise.all(fileNames); - } + async verifyProjectFilesExist(files: string[]) { + const projectFiles = this.code.driver.page.locator('.monaco-list > .monaco-scrollable-element'); - async waitForProjectFileToAppear(filename: string) { - const escapedFilename = filename.replace(/\./g, '\\.').toLowerCase(); - await expect(this.code.driver.page.locator(`.${escapedFilename}-name-file-icon`)).toBeVisible({ timeout: 30000 }); + for (let i = 0; i < files.length; i++) { + const timeout = i === 0 ? 50000 : undefined; // 50s for the first check, default for the rest as sometimes waiting for project to load + await expect(projectFiles.getByLabel(files[i], { exact: true }).locator('a')).toBeVisible({ timeout }); + } } } diff --git a/test/e2e/pages/newProjectWizard.ts b/test/e2e/pages/newProjectWizard.ts index a8da76597c3..5ea4f7b5153 100644 --- a/test/e2e/pages/newProjectWizard.ts +++ b/test/e2e/pages/newProjectWizard.ts @@ -3,303 +3,189 @@ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ -import { fail } from 'assert'; import { expect } from '@playwright/test'; import { Code } from '../infra/code'; import { QuickAccess } from './quickaccess'; -// Selector for the pre-selected dropdown item in the project wizard -const PROJECT_WIZARD_PRESELECTED_DROPDOWN_ITEM = - 'button.drop-down-list-box div.title'; - -// Selector for the currently open dropdown popup items in the project wizard -const PROJECT_WIZARD_DROPDOWN_POPUP_ITEMS = - 'div.positron-modal-popup-children button.positron-button.item'; - -// Selector for the default button in the project wizard, which will either be 'Next' or 'Create' -const PROJECT_WIZARD_DEFAULT_BUTTON = 'button.positron-button.button.action-bar-button.default[tabindex="0"][role="button"]'; - -/** - * Enum representing the possible navigation actions that can be taken in the project wizard. - */ -export enum ProjectWizardNavigateAction { - BACK, - NEXT, - CANCEL, - CREATE, -} - -/** - * Enum representing the possible project types that can be selected in the project wizard. - */ -export enum ProjectType { - PYTHON_PROJECT = 'Python Project', - R_PROJECT = 'R Project', - JUPYTER_NOTEBOOK = 'Jupyter Notebook', -} - -/* - * Reuseable Positron new project wizard functionality for tests to leverage. - */ export class NewProjectWizard { - projectTypeStep: ProjectWizardProjectTypeStep; - projectNameLocationStep: ProjectWizardProjectNameLocationStep; - rConfigurationStep: ProjectWizardRConfigurationStep; - pythonConfigurationStep: ProjectWizardPythonConfigurationStep; - currentOrNewWindowSelectionModal: CurrentOrNewWindowSelectionModal; + private backButton = this.code.driver.page.getByRole('button', { name: 'Back', exact: true }); + private cancelButton = this.code.driver.page.getByRole('button', { name: 'Cancel' }); + private nextButton = this.code.driver.page.getByRole('button', { name: 'Next', exact: true }); + private createButton = this.code.driver.page.getByRole('button', { name: 'Create', exact: true }); + private projectNameInput = this.code.driver.page.getByLabel(/Enter a name for your new/); + private existingEnvRadioButton = this.code.driver.page.getByText(/Use an existing/); + private envProviderDropdown = this.code.driver.page.locator('#wizard-sub-step-python-environment').locator('button'); + private envProviderDropdownTitle = this.envProviderDropdown.locator('.dropdown-entry-title'); + private dropDropdownOptions = this.code.driver.page.locator('.positron-modal-popup-children').getByRole('button'); + private interpreterDropdown = this.code.driver.page.locator('#wizard-sub-step-python-interpreter').locator('button'); + private interpreterDropdownSubtitle = this.interpreterDropdown.locator('.dropdown-entry-subtitle'); + + constructor(private code: Code, private quickaccess: QuickAccess) { } + + /** + * NEW PROJECT WIZARD: + * Step through the new project wizard in order to create a new project. + * @param options The options to configure the new project. + */ + async createNewProject(options: CreateProjectOptions) { + await this.quickaccess.runCommand('positron.workbench.action.newProject', { keepOpen: false }); - private backButton = this.code.driver.page.locator('div.left-actions > button.positron-button.button.action-bar-button[tabindex="0"][role="button"]'); - private cancelButton = this.code.driver.page.locator('div.right-actions > button.positron-button.button.action-bar-button[tabindex="0"][role="button"]'); - private nextButton = this.code.driver.page.locator(PROJECT_WIZARD_DEFAULT_BUTTON).getByText('Next'); - private createButton = this.code.driver.page.locator(PROJECT_WIZARD_DEFAULT_BUTTON).getByText('Create'); + await this.setProjectType(options.type); + await this.setProjectNameLocation(options); + await this.setProjectConfiguration(options); - constructor(private code: Code, private quickaccess: QuickAccess) { - this.projectTypeStep = new ProjectWizardProjectTypeStep(this.code); - this.projectNameLocationStep = new ProjectWizardProjectNameLocationStep(this.code); - this.rConfigurationStep = new ProjectWizardRConfigurationStep(this.code); - this.pythonConfigurationStep = new ProjectWizardPythonConfigurationStep(this.code); - this.currentOrNewWindowSelectionModal = new CurrentOrNewWindowSelectionModal(this.code); + await this.code.driver.page.getByRole('button', { name: 'Current Window' }).click(); + await expect(this.code.driver.page.locator('.simple-title-bar').filter({ hasText: 'Create New Project' })).not.toBeVisible(); } /** - * Starts a new project of the specified type in the project wizard. - * @param projectType The type of project to select. - * @returns A promise that resolves once the project wizard is open and the project type is selected. + * Step 1. Select the project type in the project wizard. + * @param projectType The project type to select. */ - async startNewProject(projectType: ProjectType) { - await this.quickaccess.runCommand( - 'positron.workbench.action.newProject', - { keepOpen: false } - ); - // Select the specified project type in the project wizard - await this.projectTypeStep.selectProjectType(projectType); + async setProjectType(projectType: ProjectType) { + this.code.driver.page.locator('label').filter({ hasText: projectType }).click({ force: true }); + await this.clickWizardButton(WizardButton.NEXT); } /** - * Clicks the specified navigation button in the project wizard. - * @param action The navigation action to take in the project wizard. - */ - async navigate(action: ProjectWizardNavigateAction) { - switch (action) { - case ProjectWizardNavigateAction.BACK: - await this.backButton.waitFor(); - await this.backButton.click(); - break; - case ProjectWizardNavigateAction.NEXT: - await this.nextButton.waitFor(); - await this.nextButton.isEnabled({ timeout: 5000 }); - await this.nextButton.click(); - break; - case ProjectWizardNavigateAction.CANCEL: - await this.cancelButton.waitFor(); - await this.cancelButton.click(); - break; - case ProjectWizardNavigateAction.CREATE: - await this.createButton.waitFor(); - await this.createButton.isEnabled({ timeout: 5000 }); - await this.createButton.click(); - break; - default: - throw new Error( - `Invalid project wizard navigation action: ${action}` - ); + * Step 2. Set the project name and location in the project wizard. + * @param projectTitle The title to set for the project. + * @param initAsGitRepo Whether to initialize the project as a Git repository + **/ + async setProjectNameLocation(options: CreateProjectOptions) { + const { title: projectTitle, initAsGitRepo } = options; + + await this.projectNameInput.fill(projectTitle); + if (initAsGitRepo) { + await this.code.driver.page.getByText('Initialize project as Git').check(); } - } -} - -class ProjectWizardProjectTypeStep { - constructor(private code: Code) { } - cssEscape(value: string): string { - return value.replace(/[\s!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g, '\\$&'); + await this.clickWizardButton(WizardButton.NEXT); } - async selectProjectType(projectType: ProjectType) { - const locator = this.code.driver.page.locator('label').filter({ hasText: projectType }); - await locator.click({ force: true }); - } -} - -class ProjectWizardProjectNameLocationStep { - projectNameInput = this.code.driver.page.locator( - 'div[id="wizard-sub-step-project-name"] .wizard-sub-step-input input.text-input' - ); - projectOptionCheckboxes = this.code.driver.page.locator( - 'div[id="wizard-sub-step-misc-proj-options"] div.checkbox' - ); - gitInitCheckbox = this.projectOptionCheckboxes.getByText( - 'Initialize project as Git repository' - ); + /** + * Step 3. Set the project configuration in the project wizard. + * @param options The options to configure the project. + */ + async setProjectConfiguration(options: CreateProjectOptions) { + const { type, rEnvCheckbox, pythonEnv, ipykernelFeedback, interpreterPath, status } = options; - constructor(private code: Code) { } + // configure R Project + if (type === ProjectType.R_PROJECT && rEnvCheckbox) { + await this.code.driver.page.getByText('Use `renv` to create a').click(); + } - async appendToProjectName(text: string) { - await this.projectNameInput.waitFor(); - await this.projectNameInput.page().keyboard.type(text); - } -} + // configure Python Project + if (type === ProjectType.PYTHON_PROJECT) { + if (status === 'existing') { + await this.existingEnvRadioButton.click(); + } -class ProjectWizardRConfigurationStep { - renvCheckbox = this.code.driver.page.locator( - 'div.renv-configuration > div.checkbox' - ); + if (pythonEnv) { + await this.selectEnvProvider(pythonEnv); + } - constructor(private code: Code) { } -} + if (interpreterPath) { + await this.selectInterpreterByPath(interpreterPath); + } -class ProjectWizardPythonConfigurationStep { - existingEnvRadioButton = this.code.driver.page.locator( - 'div[id="wizard-step-set-up-python-environment"] div[id="wizard-sub-step-pythonenvironment-howtosetupenv"] .radio-button-input[id="existingEnvironment"]' - ); - envProviderDropdown = this.code.driver.page.locator( - 'div[id="wizard-sub-step-python-environment"] .wizard-sub-step-input button.drop-down-list-box' - ); - interpreterFeedback = this.code.driver.page.locator( - 'div[id="wizard-sub-step-python-interpreter"] .wizard-sub-step-feedback .wizard-formatted-text' - ); - interpreterDropdown = this.code.driver.page.locator( - 'div[id="wizard-sub-step-python-interpreter"] .wizard-sub-step-input button.drop-down-list-box' - ); + if (ipykernelFeedback) { + const ipykernelMessage = this.code.driver.page.getByText('ipykernel will be installed'); + ipykernelFeedback === 'show' + ? await expect(ipykernelMessage).toBeVisible() + : await expect(ipykernelMessage).not.toBeVisible(); + } + } - constructor(private code: Code) { } + await this.clickWizardButton(WizardButton.CREATE); + } - private async waitForDataLoading() { - // The env provider dropdown is only visible when New Environment is selected - if (await this.envProviderDropdown.isVisible()) { - await expect(this.envProviderDropdown).not.toContainText( - 'Loading environment providers...', - { timeout: 5000 } - ); + /** + * Helper: Clicks the specified navigation button in the project wizard. + * @param action The navigation action to take in the project wizard. + */ + async clickWizardButton(action: WizardButton) { + const button = { + [WizardButton.BACK]: this.backButton, + [WizardButton.NEXT]: this.nextButton, + [WizardButton.CANCEL]: this.cancelButton, + [WizardButton.CREATE]: this.createButton, + }[action]; + + if (!button) { + throw new Error(`Invalid wizard button action: ${action}`); } - // The interpreter dropdown is always visible - await expect(this.interpreterDropdown).not.toContainText( - 'Loading interpreters...', - { timeout: 5000 } - ); + await button.click(); } /** - * Selects the specified environment provider in the project wizard environment provider dropdown. - * @param provider The environment provider to select. + * Helper: Selects the specified environment provider in the project wizard environment provider dropdown. + * @param providerToSelect The environment provider to select. */ - async selectEnvProvider(provider: string) { - await this.waitForDataLoading(); + async selectEnvProvider(providerToSelect: string) { + // Wait for loading to finish + await expect(this.code.driver.page.getByText(/Loading/)).toHaveCount(0, { timeout: 30000 }); - try { - const preselected = - (await this.code.driver - .page.locator( - `${PROJECT_WIZARD_PRESELECTED_DROPDOWN_ITEM} div.dropdown-entry-title` - ) - .getByText(provider) - .count()) === 1; - if (preselected) { - return; - } - } catch (error) { - // The env provider isn't pre-selected in the dropdown, so let's try to find it by clicking - // the dropdown and then clicking the env provider - this.code.logger.log( - `Environment provider '${provider}' is not pre-selected in the Project Wizard environment provider dropdown.` - ); + // Skip if the desired provider is already selected + if (await this.envProviderDropdownTitle.innerText() === providerToSelect) { + return; } - // Open the dropdown + // Select the desired provider from the dropdown await this.envProviderDropdown.click(); - - // Try to find the env provider in the dropdown - try { - await expect(this.code.driver.page.locator(PROJECT_WIZARD_DROPDOWN_POPUP_ITEMS).first()).toBeVisible(); - await this.code.driver - .page.locator( - `${PROJECT_WIZARD_DROPDOWN_POPUP_ITEMS} div.dropdown-entry-title` - ) - .getByText(provider) - .click(); - return; - } catch (error) { - throw new Error( - `Could not find env provider in project wizard dropdown: ${error}` - ); - } + await this.dropDropdownOptions.filter({ hasText: providerToSelect }).click(); } /** - * Selects the interpreter corresponding to the given path in the project wizard interpreter - * dropdown. + * Helper: Selects the interpreter corresponding to the given path in the project wizard interpreter dropdown. * @param interpreterPath The path of the interpreter to select in the dropdown. - * @returns A promise that resolves once the interpreter is selected, or rejects if the interpreter is not found. */ async selectInterpreterByPath(interpreterPath: string) { - await this.waitForDataLoading(); + // Wait for loading to complete + await expect(this.code.driver.page.getByText(/Loading/)).toHaveCount(0, { timeout: 30000 }); - try { - const preselected = - (await this.code.driver - .page.locator( - `${PROJECT_WIZARD_PRESELECTED_DROPDOWN_ITEM} div.dropdown-entry-subtitle` - ) - .getByText(interpreterPath) - .count()) === 1; - if (preselected) { - return; - } - } catch (error) { - // The interpreter isn't pre-selected in the dropdown, so let's try to find it by clicking - // the dropdown and then clicking the interpreter - this.code.logger.log( - `Interpreter '${interpreterPath}' is not pre-selected in the Project Wizard interpreter dropdown.` - ); + // Skip if the desired interpreter is already selected + if (await this.interpreterDropdownSubtitle.innerText() === interpreterPath) { + return; } - // Open the interpreter dropdown. + // Open the dropdown and select the interpreter by path await this.interpreterDropdown.click(); - - // Try to find the interpreterPath in the dropdown and click the entry if found - try { - await expect(this.code.driver.page.locator(PROJECT_WIZARD_DROPDOWN_POPUP_ITEMS)).toBeVisible(); - } catch (error) { - throw new Error( - `Wait for element ${PROJECT_WIZARD_DROPDOWN_POPUP_ITEMS} failed: ${error}` - ); - } - - // Get all the dropdown entry subtitles and build a comma-separated string of them for - // logging purposes. - const dropdownEntrySubtitleLocators = await this.code.driver - .page.locator( - `${PROJECT_WIZARD_DROPDOWN_POPUP_ITEMS} div.dropdown-entry-subtitle` - ).all(); - const dropdownEntrySubtitles = dropdownEntrySubtitleLocators.map - (async (locator) => await locator.innerText()); - const subtitles = (await Promise.all(dropdownEntrySubtitles)).join(', '); - - // Find the dropdown item with the interpreterPath. - const dropdownItem = this.code.driver - .page.locator(`${PROJECT_WIZARD_DROPDOWN_POPUP_ITEMS} div.dropdown-entry-subtitle`) - .getByText(interpreterPath); - - // There should be one dropdown item with the interpreterPath. - if ((await dropdownItem.count()) !== 1) { - // Close the interpreter dropdown. - await this.code.driver.page.keyboard.press('Escape'); - - // Fail the test. - fail(`Could not find interpreter path ("${interpreterPath}") in ("${subtitles}") project wizard dropdown`); - } - - // Click the interpreter. - await dropdownItem.click(); + await this.dropDropdownOptions + .locator('div.dropdown-entry-subtitle') + .getByText(interpreterPath) + .first() + .click(); } } -class CurrentOrNewWindowSelectionModal { - currentWindowButton = this.code.driver - .page.locator( - 'button.positron-button.button.action-bar-button[tabindex="0"][role="button"]' - ) - .getByText('Current Window'); +export interface CreateProjectOptions { + type: ProjectType; + title: string; + status?: 'new' | 'existing'; + rEnvCheckbox?: boolean; + pythonEnv?: 'conda' | 'venv'; + initAsGitRepo?: boolean; + ipykernelFeedback?: 'show' | 'hide'; + interpreterPath?: string; +} + +/** + * Enum representing the possible navigation actions that can be taken in the project wizard. + */ +export enum WizardButton { + BACK, + NEXT, + CANCEL, + CREATE, +} - constructor(private code: Code) { } +/** + * Enum representing the possible project types that can be selected in the project wizard. + */ +export enum ProjectType { + PYTHON_PROJECT = 'Python Project', + R_PROJECT = 'R Project', + JUPYTER_NOTEBOOK = 'Jupyter Notebook', } diff --git a/test/e2e/pages/popups.ts b/test/e2e/pages/popups.ts index a4764d7217a..b6921486e59 100644 --- a/test/e2e/pages/popups.ts +++ b/test/e2e/pages/popups.ts @@ -54,26 +54,27 @@ export class Popups { /** * Interacts with the Renv install modal dialog box. This dialog box appears when a user opts to * use Renv in the Project Wizard and creates a new project, but Renv is not installed. - * @param install Whether to install Renv or not. Default is true. + * @param action The action to take on the modal dialog box. Either 'install' or 'cancel'. */ - async installRenv(install: boolean = true) { + async installRenvModal(action: 'install' | 'cancel') { try { - this.code.logger.log('Checking for install Renv modal dialog box'); - // fail fast if the renv install modal is not present - await this.waitForModalDialogTitle('Missing R package'); - - if (install) { - this.code.logger.log('Installing Renv'); - await this.code.driver.page.locator(POSITRON_MODAL_DIALOG_BOX_OK).click(); - this.code.logger.log('Installed Renv'); - } else { - this.code.logger.log('Skipping Renv installation'); - await this.code.driver.page.locator(POSITRON_MODAL_DIALOG_BOX_CANCEL).click(); + await expect(this.code.driver.page.locator('.simple-title-bar').filter({ hasText: 'Missing R package' })).toBeVisible({ timeout: 30000 }); + + if (action === 'install') { + this.code.logger.log('Install Renv modal detected: clicking `Install now`'); + await this.code.driver.page.getByRole('button', { name: 'Install now' }).click(); + } else if (action === 'cancel') { + this.code.logger.log('Install Renv modal detected: clicking `Cancel`'); + await this.code.driver.page.getByRole('button', { name: 'Cancel', exact: true }).click(); + } + } catch (error) { + this.code.logger.log('No Renv modal detected'); + if (process.env.CI) { + throw new Error('Renv modal not detected'); } - } catch { - this.code.logger.log('Did not find install Renv modal dialog box'); } } + async waitForToastToDisappear() { this.code.logger.log('Waiting for toast to be detacted'); await this.toastLocator.waitFor({ state: 'detached', timeout: 20000 }); @@ -145,6 +146,6 @@ export class Popups { await expect(async () => { const textContent = await this.code.driver.page.locator(POSITRON_MODAL_DIALOG_BOX_TITLE).textContent(); expect(textContent).toContain(title); - }).toPass({ timeout: 30000 }); + }).toPass({ timeout: 10000 }); } } diff --git a/test/e2e/pages/testExplorer.ts b/test/e2e/pages/testExplorer.ts index a0237ebc438..d5636506a3f 100644 --- a/test/e2e/pages/testExplorer.ts +++ b/test/e2e/pages/testExplorer.ts @@ -3,7 +3,7 @@ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ - +import { expect } from '@playwright/test'; import { Explorer } from './explorer'; const TEST_RESULT_ITEM = '.monaco-list-row[aria-level="2"] .test-peek-item'; @@ -54,12 +54,13 @@ export class TestExplorer extends Explorer { await this.code.driver.page.locator(TEST_EXPLORER_ICON).click(); } - /** - * Gets the top level tests from the test explorer - * @returns Promise Array of test names. - */ - async getTestExplorerFiles(): Promise { - return await this.getExplorerProjectFiles('.test-explorer .monaco-list-row .label'); + async verifyTestFilesExist(files: string[]) { + const projectFiles = this.code.driver.page.locator('.test-explorer'); + + for (let i = 0; i < files.length; i++) { + const timeout = i === 0 ? 50000 : undefined; // 50s for the first check, default for the rest as sometimes waiting for project to load + await expect(projectFiles.getByLabel(files[i])).toBeVisible({ timeout }); + } } /** diff --git a/test/e2e/pages/utils/packageManager.ts b/test/e2e/pages/utils/packageManager.ts new file mode 100644 index 00000000000..9e55aa6770d --- /dev/null +++ b/test/e2e/pages/utils/packageManager.ts @@ -0,0 +1,79 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Application } from '../../infra'; + +type PackageAction = 'install' | 'uninstall'; + +const Packages = [ + { name: 'ipykernel', type: 'Python' }, + { name: 'renv', type: 'R' } +] as const; + +type PackageName = (typeof Packages[number])['name']; // "ipykernel" | "renv", etc + +export class PackageManager { + private app: Application; + + constructor(app: Application) { + this.app = app; + } + + /** + * Manages the installation or uninstallation of a package. + * @param packageName The name of the package (e.g., ipykernel or renv). + * @param action The action to perform ('install' or 'uninstall'). + */ + async manage(packageName: PackageName, action: PackageAction): Promise { + const packageInfo = Packages.find(pkg => pkg.name === packageName); + if (!packageInfo) { + throw new Error(`Package ${packageName} not found`); + } + + await test.step(`${action}: ${packageName}`, async () => { + const command = this.getCommand(packageInfo.type, packageName, action); + const expectedOutput = this.getExpectedOutput(packageName, action); + const prompt = packageInfo.type === 'Python' ? '>>> ' : '> '; + + await this.app.workbench.console.executeCode(packageInfo.type, command, prompt); + await expect(this.app.code.driver.page.getByText(expectedOutput)).toBeVisible(); + }); + } + + /** + * Returns the command for the specified action. + * @param language The language associated with the package ('R' or 'Python'). + * @param packageName The name of the package. + * @param action The action to perform ('install' or 'uninstall'). + */ + private getCommand(language: 'R' | 'Python', packageName: PackageName, action: PackageAction): string { + if (language === 'Python') { + return action === 'install' + ? `pip install ${packageName}` + : `pip uninstall -y ${packageName}`; + } else { + return action === 'install' + ? `install.packages("${packageName}")` + : `remove.packages("${packageName}")`; + } + } + + /** + * Returns the expected console output for the specified action. + * @param packageName The name of the package. + * @param action The action to perform ('install' or 'uninstall'). + */ + private getExpectedOutput(packageName: PackageName, action: PackageAction): RegExp { + switch (packageName) { + case 'ipykernel': + return action === 'install' + ? /Note: you may need to restart the kernel to use updated packages/ + : /Successfully uninstalled ipykernel|Skipping ipykernel as it is not installed/; + default: + return action === 'install' ? /Installing/ : /Removing/; + } + } +} diff --git a/test/e2e/tests/_test.setup.ts b/test/e2e/tests/_test.setup.ts index b8b3019e8f2..435a4796fc3 100644 --- a/test/e2e/tests/_test.setup.ts +++ b/test/e2e/tests/_test.setup.ts @@ -21,8 +21,8 @@ import { randomUUID } from 'crypto'; import archiver from 'archiver'; // Local imports -import { createLogger, createApp, TestTags } from '../infra/test-runner'; -import { Application, Logger, PythonFixtures, RFixtures, UserSetting, UserSettingsFixtures } from '../infra'; +import { Application, Logger, PythonFixtures, RFixtures, UserSetting, UserSettingsFixtures, createLogger, createApp, TestTags } from '../infra'; +import { PackageManager } from '../pages/utils/packageManager'; // Constants const TEMP_DIR = `temp-${randomUUID()}`; @@ -139,6 +139,11 @@ export const test = base.extend({ }, { scope: 'test' }], + packages: [async ({ app }, use) => { + const packageManager = new PackageManager(app); + await use(packageManager); + }, { scope: 'test' }], + devTools: [async ({ app }, use) => { await app.workbench.quickaccess.runCommand('workbench.action.toggleDevTools'); await use(); @@ -334,6 +339,7 @@ interface TestFixtures { interpreter: { set: (interpreterName: 'Python' | 'R') => Promise }; r: void; python: void; + packages: PackageManager; autoTestFixture: any; devTools: void; } diff --git a/test/e2e/tests/new-project-wizard/new-project-python.test.ts b/test/e2e/tests/new-project-wizard/new-project-python.test.ts index 670b32c2aa6..4cd72d9d3c6 100644 --- a/test/e2e/tests/new-project-wizard/new-project-python.test.ts +++ b/test/e2e/tests/new-project-wizard/new-project-python.test.ts @@ -3,187 +3,170 @@ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ -import { PythonFixtures, ProjectType, ProjectWizardNavigateAction } from '../../infra'; +import { Application, CreateProjectOptions, ProjectType } from '../../infra'; import { test, expect, tags } from '../_test.setup'; test.use({ suiteId: __filename }); -test.beforeEach(async function ({ app }) { - await app.workbench.console.waitForReadyOrNoInterpreter(); -}); - -// Not running conda test on windows becuase conda reeks havoc on selecting the correct python interpreter +// Not running conda test on windows because conda reeks havoc on selecting the correct python interpreter test.describe('Python - New Project Wizard', { tag: [tags.NEW_PROJECT_WIZARD] }, () => { - const defaultProjectName = 'my-python-project'; - - test('Create a new Conda environment [C628628]', async function ({ app, page }) { - // This test relies on Conda already being installed on the machine - test.slow(); - const projSuffix = addRandomNumSuffix('_condaInstalled'); - const pw = app.workbench.newProjectWizard; - await pw.startNewProject(ProjectType.PYTHON_PROJECT); - await pw.navigate(ProjectWizardNavigateAction.NEXT); - await pw.projectNameLocationStep.appendToProjectName(projSuffix); - await pw.navigate(ProjectWizardNavigateAction.NEXT); - // Select 'Conda' as the environment provider - await pw.pythonConfigurationStep.selectEnvProvider('Conda'); - await pw.navigate(ProjectWizardNavigateAction.CREATE); - await pw.currentOrNewWindowSelectionModal.currentWindowButton.click(); - await expect(page.getByRole('button', { name: `Explorer Section: ${defaultProjectName + projSuffix}` })).toBeVisible({ timeout: 20000 }); - // Check that the `.conda` folder gets created in the project - await expect(async () => { - const projectFiles = await app.workbench.explorer.getExplorerProjectFiles(); - expect(projectFiles).toContain('.conda'); - }).toPass({ timeout: 50000 }); - // The console should initialize without any prompts to install ipykernel - await expect(app.workbench.console.activeConsole.getByText('>>>')).toBeVisible({ timeout: 45000 }); - await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar'); - await app.workbench.console.barClearButton.click(); - await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar'); + + test('Create a new Conda environment [C628628]', async function ({ app }) { + const projectTitle = addRandomNumSuffix('conda-installed'); + await createNewProject(app, { + type: ProjectType.PYTHON_PROJECT, + title: projectTitle, + status: 'new', + pythonEnv: 'conda', // test relies on conda already installed on machine + }); + + await verifyProjectCreation(app, projectTitle); + await verifyCondaFilesArePresent(app); + await verifyCondaEnvStarts(app); }); - test('Create a new Venv environment [C627912]', { tag: [tags.CRITICAL, tags.WIN] }, async function ({ app, page }) { - // This is the default behavior for a new Python Project in the Project Wizard - const projSuffix = addRandomNumSuffix('_new_venv'); - const pw = app.workbench.newProjectWizard; - await pw.startNewProject(ProjectType.PYTHON_PROJECT); - await pw.navigate(ProjectWizardNavigateAction.NEXT); - await pw.projectNameLocationStep.appendToProjectName(projSuffix); - await pw.navigate(ProjectWizardNavigateAction.NEXT); - await pw.navigate(ProjectWizardNavigateAction.CREATE); - await pw.currentOrNewWindowSelectionModal.currentWindowButton.click(); - await expect(page.getByRole('button', { name: `Explorer Section: ${defaultProjectName + projSuffix}` })).toBeVisible({ timeout: 20000 }); - await expect(app.workbench.console.activeConsole.getByText('>>>')).toBeVisible({ timeout: 100000 }); - await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar'); - await app.workbench.console.barClearButton.click(); - await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar'); + test('Create a new Venv environment [C627912]', { tag: [tags.CRITICAL, tags.WIN] }, async function ({ app }) { + const projectTitle = addRandomNumSuffix('new-venv'); + + await createNewProject(app, { + type: ProjectType.PYTHON_PROJECT, + title: projectTitle, + status: 'new', + pythonEnv: 'venv', + }); + + await verifyProjectCreation(app, projectTitle); + await verifyVenEnvStarts(app); }); - test.skip('With ipykernel already installed [C609619]', { - tag: [tags.WIN], - annotation: [{ type: 'issue', description: 'https://github.com/posit-dev/positron/issues/5730' }], - }, async function ({ app, page, python }) { - const projSuffix = addRandomNumSuffix('_ipykernelInstalled'); - const pw = app.workbench.newProjectWizard; - const pythonFixtures = new PythonFixtures(app); - // Start the Python interpreter and ensure ipykernel is installed - await pythonFixtures.startAndGetPythonInterpreter(true); + test('With ipykernel already installed [C609619]', { tag: [tags.WIN], }, async function ({ app, python, packages }) { + const projectTitle = addRandomNumSuffix('ipykernel-installed'); - const interpreterInfo = - await app.workbench.interpreterDropdown.getSelectedInterpreterInfo(); - expect(interpreterInfo?.path).toBeDefined(); - await app.workbench.interpreterDropdown.closeInterpreterDropdown(); - // Create a new Python project and use the selected python interpreter - await pw.startNewProject(ProjectType.PYTHON_PROJECT); - await pw.navigate(ProjectWizardNavigateAction.NEXT); - await pw.projectNameLocationStep.appendToProjectName(projSuffix); - await pw.navigate(ProjectWizardNavigateAction.NEXT); - await pw.pythonConfigurationStep.existingEnvRadioButton.click(); - // Select the interpreter that was started above. It's possible that this needs - // to be attempted a few times to ensure the interpreters are properly loaded. - await expect( - async () => - await pw.pythonConfigurationStep.selectInterpreterByPath( - interpreterInfo!.path - ) - ).toPass({ - intervals: [1_000, 2_000, 10_000], - timeout: 50_000 + await packages.manage('ipykernel', 'install'); + await createNewProject(app, { + type: ProjectType.PYTHON_PROJECT, + title: projectTitle, + status: 'existing', + ipykernelFeedback: 'hide', + interpreterPath: await getInterpreterPath(app), }); - await expect(pw.pythonConfigurationStep.interpreterFeedback).not.toBeVisible(); - await pw.navigate(ProjectWizardNavigateAction.CREATE); - await pw.currentOrNewWindowSelectionModal.currentWindowButton.click(); - await expect(page.getByRole('button', { name: `Explorer Section: ${defaultProjectName + projSuffix}` })).toBeVisible({ timeout: 20000 }); - await expect(app.workbench.console.activeConsole.getByText('>>>')).toBeVisible({ timeout: 90000 }); + + await verifyProjectCreation(app, projectTitle); }); - test('With ipykernel not already installed [C609617]', { - tag: [tags.WIN], - }, async function ({ app, page }) { - const projSuffix = addRandomNumSuffix('_noIpykernel'); - const pw = app.workbench.newProjectWizard; - const pythonFixtures = new PythonFixtures(app); - // Start the Python interpreter and uninstall ipykernel - await pythonFixtures.startAndGetPythonInterpreter(true); + test('With ipykernel not already installed [C609617]', { tag: [tags.WIN] }, async function ({ app, python, packages }) { + const projectTitle = addRandomNumSuffix('no-ipykernel'); - const interpreterInfo = - await app.workbench.interpreterDropdown.getSelectedInterpreterInfo(); - expect(interpreterInfo?.path).toBeDefined(); - await app.workbench.interpreterDropdown.closeInterpreterDropdown(); - await app.workbench.console.typeToConsole('pip uninstall -y ipykernel'); - await app.workbench.console.sendEnterKey(); - await app.workbench.console.waitForConsoleContents('Successfully uninstalled ipykernel'); - - // Create a new Python project and use the selected python interpreter - await pw.startNewProject(ProjectType.PYTHON_PROJECT); - await pw.navigate(ProjectWizardNavigateAction.NEXT); - await pw.projectNameLocationStep.appendToProjectName(projSuffix); - await pw.navigate(ProjectWizardNavigateAction.NEXT); - // Choose the existing environment which does not have ipykernel - await pw.pythonConfigurationStep.existingEnvRadioButton.click(); - // Select the interpreter that was started above. It's possible that this needs - // to be attempted a few times to ensure the interpreters are properly loaded. - await expect( - async () => - await pw.pythonConfigurationStep.selectInterpreterByPath( - interpreterInfo!.path - ) - ).toPass({ - intervals: [1_000, 2_000, 10_000], - timeout: 50_000 + await packages.manage('ipykernel', 'uninstall'); + await createNewProject(app, { + type: ProjectType.PYTHON_PROJECT, + title: projectTitle, + status: 'existing', + interpreterPath: await getInterpreterPath(app), + ipykernelFeedback: 'show' }); - await expect(pw.pythonConfigurationStep.interpreterFeedback).toHaveText( - 'ipykernel will be installed for Python language support.', - { timeout: 10_000 } - ); - await pw.navigate(ProjectWizardNavigateAction.CREATE); - await pw.currentOrNewWindowSelectionModal.currentWindowButton.click(); - await expect(page.getByRole('button', { name: `Explorer Section: ${defaultProjectName + projSuffix}` })).toBeVisible({ timeout: 20000 }); - - // If ipykernel was successfully installed during the new project initialization, - // the console should be ready without any prompts to install ipykernel - await expect(app.workbench.console.activeConsole.getByText('>>>')).toBeVisible({ timeout: 90000 }); - await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar'); - await app.workbench.console.barClearButton.click(); - await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar'); + + await verifyProjectCreation(app, projectTitle); + await verifyIpykernelInstalled(app); }); - test('Default Python Project with git init [C674522]', { tag: [tags.CRITICAL, tags.WIN] }, async function ({ app, page }) { - const projSuffix = addRandomNumSuffix('_gitInit'); - const pw = app.workbench.newProjectWizard; - await pw.startNewProject(ProjectType.PYTHON_PROJECT); - await pw.navigate(ProjectWizardNavigateAction.NEXT); - await pw.projectNameLocationStep.appendToProjectName(projSuffix); - - // Check the git init checkbox - await pw.projectNameLocationStep.gitInitCheckbox.waitFor(); - await pw.projectNameLocationStep.gitInitCheckbox.setChecked(true); - await pw.navigate(ProjectWizardNavigateAction.NEXT); - await pw.navigate(ProjectWizardNavigateAction.CREATE); - - // Open the new project in the current window and wait for the console to be ready - await pw.currentOrNewWindowSelectionModal.currentWindowButton.click(); - await expect(page.getByRole('button', { name: `Explorer Section: ${defaultProjectName + projSuffix}` })).toBeVisible({ timeout: 20000 }); - await expect(app.workbench.console.activeConsole.getByText('>>>')).toBeVisible({ timeout: 90000 }); - - // Verify git-related files are present - await expect(async () => { - const projectFiles = await app.workbench.explorer.getExplorerProjectFiles(); - expect(projectFiles).toContain('.gitignore'); - expect(projectFiles).toContain('README.md'); - // Ideally, we'd check for the .git folder, but it's not visible in the Explorer - // by default due to the default `files.exclude` setting in the workspace. - }).toPass({ timeout: 50000 }); + test('Default Python Project with git init [C674522]', { tag: [tags.CRITICAL, tags.WIN] }, async function ({ app }) { + const projectTitle = addRandomNumSuffix('git-init'); + + await createNewProject(app, { + type: ProjectType.PYTHON_PROJECT, + title: projectTitle, + initAsGitRepo: true, + status: 'new', + pythonEnv: 'venv', + }); + + await verifyProjectCreation(app, projectTitle); + await verifyGitFilesArePresent(app); + await verifyVenEnvStarts(app); + await verifyGitStatus(app); + }); +}); +// Helper functions +function addRandomNumSuffix(name: string): string { + return `${name}_${Math.floor(Math.random() * 1000000)}`; +} + +async function createNewProject(app: Application, options: CreateProjectOptions) { + await test.step(`Create a new project: ${options.title}`, async () => { + await app.workbench.newProjectWizard.createNewProject(options); + }); +} + +async function verifyProjectCreation(app: Application, projectTitle: string) { + await test.step(`Verify project created`, async () => { + await expect(app.code.driver.page.getByLabel('Folder Commands')).toHaveText(projectTitle, { timeout: 60000 }); // this is really slow on windows CI for some reason + await app.workbench.console.waitForReady('>>>', 60000); + }); +} + +async function verifyCondaFilesArePresent(app: Application) { + await test.step('Verify .conda files are present', async () => { + await app.workbench.explorer.verifyProjectFilesExist(['.conda']); + }); +} + +async function verifyCondaEnvStarts(app: Application) { + await test.step('Verify conda environment starts', async () => { + await app.workbench.console.waitForConsoleContents('(Conda) started'); + }); +} + +async function verifyVenEnvStarts(app: Application) { + await test.step('Verify venv environment starts', async () => { + await app.workbench.console.waitForConsoleContents('(Venv: .venv) started.'); + }); +} + +async function verifyGitFilesArePresent(app: Application) { + await test.step('Verify that the .git files are present', async () => { + const projectFiles = app.code.driver.page.locator('.monaco-list > .monaco-scrollable-element'); + expect(projectFiles.getByText('.git')).toBeVisible({ timeout: 50000 }); + expect(projectFiles.getByText('.gitignore')).toBeVisible(); + // Ideally, we'd check for the .git folder, but it's not visible in the Explorer + // by default due to the default `files.exclude` setting in the workspace. + }); +} + +async function verifyGitStatus(app: Application) { + await test.step('Verify git status', async () => { // Git status should show that we're on the main branch await app.workbench.terminal.createTerminal(); await app.workbench.terminal.runCommandInTerminal('git status'); await app.workbench.terminal.waitForTerminalText('On branch main'); }); -}); +} -function addRandomNumSuffix(name: string): string { - return `${name}_${Math.floor(Math.random() * 1000000)}`; + +async function verifyIpykernelInstalled(app: Application) { + await test.step('Verify ipykernel is installed', async () => { + await app.workbench.console.typeToConsole('pip show ipykernel', 10, true); + await app.workbench.console.waitForConsoleContents('Name: ipykernel'); + }); +} + +async function getInterpreterPath(app: Application): Promise { + let interpreterPath: string | undefined; + + await test.step('Get the interpreter path', async () => { + const interpreterInfo = + await app.workbench.interpreterDropdown.getSelectedInterpreterInfo(); + + expect(interpreterInfo?.path).toBeDefined(); + interpreterPath = interpreterInfo?.path; + }); + + if (!interpreterPath) { + throw new Error('Interpreter path is undefined'); + } + + return interpreterPath; } diff --git a/test/e2e/tests/new-project-wizard/new-project-r-jupyter.test.ts b/test/e2e/tests/new-project-wizard/new-project-r-jupyter.test.ts index 31ea3989037..f27a301fd13 100644 --- a/test/e2e/tests/new-project-wizard/new-project-r-jupyter.test.ts +++ b/test/e2e/tests/new-project-wizard/new-project-r-jupyter.test.ts @@ -3,8 +3,9 @@ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ -import { ProjectType, ProjectWizardNavigateAction } from '../../infra'; -import { test, expect, tags } from '../_test.setup'; +import { expect } from '@playwright/test'; +import { Application, CreateProjectOptions, ProjectType, } from '../../infra'; +import { test, tags } from '../_test.setup'; test.use({ suiteId: __filename @@ -15,141 +16,108 @@ test.beforeEach(async function ({ app }) { await app.workbench.layouts.enterLayout("stacked"); }); -test.describe('R - New Project Wizard', { tag: [tags.NEW_PROJECT_WIZARD] }, () => { +test.describe('R - New Project Wizard', { tag: [tags.NEW_PROJECT_WIZARD, tags.WEB] }, () => { test.describe.configure({ mode: 'serial' }); - const defaultProjectName = 'my-r-project'; - test('R - Project Defaults [C627913]', { tag: [tags.CRITICAL, tags.WIN] }, async function ({ app }) { - const projSuffix = addRandomNumSuffix('_defaults'); - const pw = app.workbench.newProjectWizard; - await pw.startNewProject(ProjectType.R_PROJECT); - await pw.navigate(ProjectWizardNavigateAction.NEXT); - await pw.projectNameLocationStep.appendToProjectName(projSuffix); - await pw.navigate(ProjectWizardNavigateAction.NEXT); - await pw.navigate(ProjectWizardNavigateAction.CREATE); - await pw.currentOrNewWindowSelectionModal.currentWindowButton.click(); - await app.workbench.layouts.enterLayout("fullSizedSidebar"); - await expect(app.code.driver.page.getByRole('button', { name: `Explorer Section: ${defaultProjectName + projSuffix}` })).toBeVisible({ timeout: 15000 }); - // NOTE: For completeness, we probably want to await app.workbench.console.waitForReady('>', 10000); - // here, but it's timing out in CI, so it is not included for now. + const projectTitle = addRandomNumSuffix('r-defaults'); + + await createNewProject(app, { + type: ProjectType.R_PROJECT, + title: projectTitle + }); + + await verifyProjectCreation(app, projectTitle); }); - test('R - Accept Renv install [C633084]', { tag: [tags.WIN] }, async function ({ app, r }) { - const projSuffix = addRandomNumSuffix('_installRenv'); - const pw = app.workbench.newProjectWizard; - // Create a new R project - select Renv and install - await pw.startNewProject(ProjectType.R_PROJECT); - await pw.navigate(ProjectWizardNavigateAction.NEXT); - await pw.projectNameLocationStep.appendToProjectName(projSuffix); - await pw.navigate(ProjectWizardNavigateAction.NEXT); - // Select the renv checkbox - await pw.rConfigurationStep.renvCheckbox.click(); - await pw.navigate(ProjectWizardNavigateAction.CREATE); - await pw.currentOrNewWindowSelectionModal.currentWindowButton.click(); - - // Interact with the modal to install renv - await app.workbench.popups.installRenv(); - - // If this test is running on a machine that is using Renv for the first time, we - // may need to interact with the Console to allow the renv installation to complete - // An example: https://github.com/posit-dev/positron/pull/3881#issuecomment-2211123610. - - // You should either manually interact with the Console to proceed with the Renv - // install or temporarily uncomment the code below to automate the interaction. - // await app.workbench.console.waitForConsoleContents('Do you want to proceed?') - // await app.workbench.console.typeToConsole('y'); - // await app.workbench.console.sendEnterKey(); - - await app.workbench.layouts.enterLayout("fullSizedSidebar"); - await expect(app.code.driver.page.getByRole('button', { name: 'Explorer Section: my-r-' })).toHaveText(defaultProjectName + projSuffix, { timeout: 15000 }); - // Verify renv files are present - await expect(async () => { - const projectFiles = await app.workbench.explorer.getExplorerProjectFiles(); - expect(projectFiles).toContain('renv'); - expect(projectFiles).toContain('.Rprofile'); - expect(projectFiles).toContain('renv.lock'); - }).toPass({ timeout: 50000 }); - // Verify that renv output in the console confirms no issues occurred + test('R - Accept Renv install [C633084]', { tag: [tags.WIN] }, async function ({ app, r, page }) { + const projectTitle = addRandomNumSuffix('r-installRenv'); + + await createNewProject(app, { + type: ProjectType.R_PROJECT, + title: projectTitle, + rEnvCheckbox: true, + }); + + await handleRenvInstallModal(app, 'install'); + await verifyProjectCreation(app, projectTitle); + await verifyRenvFilesArePresent(app); await app.workbench.console.waitForConsoleContents('renv activated'); }); test('R - Renv already installed [C656251]', { tag: [tags.WIN] }, async function ({ app }) { // Renv will already be installed from the previous test - which is why tests are marked as "serial" - const projSuffix = addRandomNumSuffix('_renvAlreadyInstalled'); - const pw = app.workbench.newProjectWizard; - await pw.startNewProject(ProjectType.R_PROJECT); - await pw.navigate(ProjectWizardNavigateAction.NEXT); - await pw.projectNameLocationStep.appendToProjectName(projSuffix); - await pw.navigate(ProjectWizardNavigateAction.NEXT); - // Select the renv checkbox - await pw.rConfigurationStep.renvCheckbox.click(); - await pw.navigate(ProjectWizardNavigateAction.CREATE); - await pw.currentOrNewWindowSelectionModal.currentWindowButton.click(); - await expect(app.code.driver.page.getByRole('button', { name: 'Explorer Section: my-r-' })).toHaveText(defaultProjectName + projSuffix, { timeout: 15000 }); - // Verify renv files are present - await expect(async () => { - const projectFiles = await app.workbench.explorer.getExplorerProjectFiles(); - expect(projectFiles).toContain('renv'); - expect(projectFiles).toContain('.Rprofile'); - expect(projectFiles).toContain('renv.lock'); - }).toPass({ timeout: 100000 }); - // Verify that renv output in the console confirms no issues occurred + const projectTitle = addRandomNumSuffix('r-renvAlreadyInstalled'); + await createNewProject(app, { + type: ProjectType.R_PROJECT, + title: projectTitle, + rEnvCheckbox: true, + }); + + await verifyProjectCreation(app, projectTitle); + await verifyRenvFilesArePresent(app); await app.workbench.console.waitForConsoleContents('renv activated'); }); - test('R - Cancel Renv install [C656252]', { tag: [tags.WIN] }, async function ({ app }) { - const projSuffix = addRandomNumSuffix('_cancelRenvInstall'); - const pw = app.workbench.newProjectWizard; - // Remove renv package so we are prompted to install it again - await app.workbench.console.executeCode('R', 'remove.packages("renv")', '>'); - await app.workbench.console.waitForConsoleContents(`Removing package`); - // Create a new R project - select Renv but opt out of installing - await pw.startNewProject(ProjectType.R_PROJECT); - await pw.navigate(ProjectWizardNavigateAction.NEXT); - await pw.projectNameLocationStep.appendToProjectName(projSuffix); - await pw.navigate(ProjectWizardNavigateAction.NEXT); - // Select the renv checkbox - await pw.rConfigurationStep.renvCheckbox.click(); - await pw.navigate(ProjectWizardNavigateAction.CREATE); - await pw.currentOrNewWindowSelectionModal.currentWindowButton.click(); - await expect(app.code.driver.page.getByRole('button', { name: 'Explorer Section: my-r-' })).toHaveText(defaultProjectName + projSuffix, { timeout: 15000 }); - // Interact with the modal to skip installing renv - await app.workbench.popups.installRenv(false); - // Verify renv files are **not** present - await expect(async () => { - const projectFiles = await app.workbench.explorer.getExplorerProjectFiles(); - expect(projectFiles).not.toContain('renv'); - expect(projectFiles).not.toContain('.Rprofile'); - expect(projectFiles).not.toContain('renv.lock'); - }).toPass({ timeout: 50000 }); + test('R - Cancel Renv install [C656252]', { tag: [tags.WIN] }, async function ({ app, packages }) { + const projectTitle = addRandomNumSuffix('r-cancelRenvInstall'); + + await packages.manage('renv', 'uninstall'); + await createNewProject(app, { + type: ProjectType.R_PROJECT, + title: projectTitle, + rEnvCheckbox: true, + }); + + await handleRenvInstallModal(app, 'cancel'); + await verifyProjectCreation(app, projectTitle); }); }); -test.describe('Jupyter - New Project Wizard', () => { - const defaultProjectName = 'my-jupyter-notebook'; - - test.skip('Jupyter Project Defaults [C629352]', { +test.describe('Jupyter - New Project Wizard', { + tag: [tags.NEW_PROJECT_WIZARD], + annotation: [{ type: 'issue', description: 'https://github.com/posit-dev/positron/issues/5914' }], // uncomment line 103 when fixed +}, () => { + test('Jupyter Project Defaults [C629352]', { tag: [tags.CRITICAL, tags.WIN], - annotation: [{ type: 'issue', description: 'https://github.com/posit-dev/positron/issues/5730' }], }, async function ({ app }) { - const projSuffix = addRandomNumSuffix('_defaults'); - const pw = app.workbench.newProjectWizard; - await pw.startNewProject(ProjectType.JUPYTER_NOTEBOOK); - await pw.navigate(ProjectWizardNavigateAction.NEXT); - await pw.projectNameLocationStep.appendToProjectName(projSuffix); - await pw.navigate(ProjectWizardNavigateAction.NEXT); - await pw.navigate(ProjectWizardNavigateAction.CREATE); - await pw.currentOrNewWindowSelectionModal.currentWindowButton.click(); - await app.code.driver.wait(10000); - await app.workbench.layouts.enterLayout("fullSizedSidebar"); - await expect(app.code.driver.page.getByRole('button', { name: `Explorer Section: ${defaultProjectName + projSuffix}` })).toBeVisible({ timeout: 15000 }); - // NOTE: For completeness, we probably want to await app.workbench.console.waitForReady('>>>', 10000); - // here, but it's timing out in CI, so it is not included for now. + const projectTitle = addRandomNumSuffix('jupyter-defaults'); + await app.workbench.newProjectWizard.createNewProject({ + type: ProjectType.JUPYTER_NOTEBOOK, + title: projectTitle + }); + + await verifyProjectCreation(app, projectTitle); }); }); function addRandomNumSuffix(name: string): string { return `${name}_${Math.floor(Math.random() * 1000000)}`; } + +async function verifyProjectCreation(app: Application, projectTitle: string) { + await test.step(`Verify project created`, async () => { + await expect(app.code.driver.page.getByLabel('Folder Commands')).toHaveText(projectTitle, { timeout: 20000 }); + // await app.workbench.console.waitForReady('>', 30000); // issue 5914 causes this to fail + }); +} + +async function verifyRenvFilesArePresent(app: Application,) { + await test.step(`Verify renv files are present`, async () => { + await app.workbench.explorer.verifyProjectFilesExist(['renv', '.Rprofile', 'renv.lock']); + }); +} + +async function createNewProject(app: Application, options: CreateProjectOptions) { + await test.step(`Create new project: ${options.title}`, async () => { + await app.workbench.newProjectWizard.createNewProject(options); + }); +} + +async function handleRenvInstallModal(app: Application, action: 'install' | 'cancel') { + await test.step(`Handle Renv modal: ${action}`, async () => { + await app.workbench.popups.installRenvModal(action); + }); +} + diff --git a/test/e2e/tests/plots/plots.test.ts b/test/e2e/tests/plots/plots.test.ts index ddee87032ed..d9af0efdc2f 100644 --- a/test/e2e/tests/plots/plots.test.ts +++ b/test/e2e/tests/plots/plots.test.ts @@ -158,7 +158,7 @@ test.describe('Plots', { tag: [tags.PLOTS, tags.EDITOR] }, () => { await app.workbench.plots.savePlot({ name: 'Python-scatter', format: 'JPEG' }); await app.workbench.layouts.enterLayout('stacked'); - await app.workbench.explorer.waitForProjectFileToAppear('Python-scatter.jpeg'); + await app.workbench.explorer.verifyProjectFilesExist(['Python-scatter.jpeg']); }); test('Python - Verifies bqplot Python widget [C720869]', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { @@ -313,10 +313,10 @@ test.describe('Plots', { tag: [tags.PLOTS, tags.EDITOR] }, () => { await app.workbench.plots.waitForCurrentPlot(); await app.workbench.plots.savePlot({ name: 'plot', format: 'PNG' }); - await app.workbench.explorer.waitForProjectFileToAppear('plot.png'); + await app.workbench.explorer.verifyProjectFilesExist(['plot.png']); await app.workbench.plots.savePlot({ name: 'R-cars', format: 'SVG' }); - await app.workbench.explorer.waitForProjectFileToAppear('R-cars.svg'); + await app.workbench.explorer.verifyProjectFilesExist(['R-cars.svg']); }); test('R - Verifies rplot plot [C720873]', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { diff --git a/test/e2e/tests/r-markdown/r-markdown.test.ts b/test/e2e/tests/r-markdown/r-markdown.test.ts index 5ffb113aaab..7479d941bb1 100644 --- a/test/e2e/tests/r-markdown/r-markdown.test.ts +++ b/test/e2e/tests/r-markdown/r-markdown.test.ts @@ -11,32 +11,20 @@ test.use({ }); test.describe('R Markdown', { tag: [tags.WEB, tags.R_MARKDOWN] }, () => { + test.describe.configure({ mode: 'serial' }); // 2nd test depends on 1st test + test('Render R Markdown [C680618]', async function ({ app, r }) { await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'workspaces', 'basic-rmd-file', 'basicRmd.rmd')); - - // Sometimes running render too quickly fails, saying pandoc is not installed. - // Using expect.toPass allows it to retry. - await expect(async () => { - await app.workbench.quickaccess.runCommand('r.rmarkdownRender'); - await app.workbench.terminal.waitForTerminalText('Output created: basicRmd.html'); - }).toPass({ timeout: 80000 }); - - // Wrapped in expect.toPass to allow UI to update/render - await expect(async () => { - const projectFiles = await app.workbench.explorer.getExplorerProjectFiles(); - expect(projectFiles).toContain('basicRmd.html'); - }).toPass({ timeout: 80000 }); - + await app.workbench.quickaccess.runCommand('r.rmarkdownRender'); + await app.workbench.explorer.verifyProjectFilesExist(['basicRmd.html']); }); - // test depends on the previous test test('Preview R Markdown [C709147]', async function ({ app, r }) { - // Preview await app.code.driver.page.keyboard.press(process.platform === 'darwin' ? 'Meta+Shift+K' : 'Control+Shift+K'); // inner most frame has no useful identifying features // not factoring this locator because its not part of positron - const gettingStarted = app.workbench.viewer.getViewerFrame().frameLocator('iframe').locator('h2[data-anchor-id="getting-started"]'); + const gettingStarted = app.workbench.viewer.viewerFrame.frameLocator('iframe').locator('h2[data-anchor-id="getting-started"]'); await expect(gettingStarted).toBeVisible({ timeout: 60000 }); await expect(gettingStarted).toHaveText('Getting started'); diff --git a/test/e2e/tests/test-explorer/test-explorer.test.ts b/test/e2e/tests/test-explorer/test-explorer.test.ts index 764b6609e08..5ef56825836 100644 --- a/test/e2e/tests/test-explorer/test-explorer.test.ts +++ b/test/e2e/tests/test-explorer/test-explorer.test.ts @@ -35,21 +35,12 @@ test.describe('Test Explorer', { tag: [tags.TEST_EXPLORER] }, () => { await app.workbench.quickaccess.runCommand('workbench.action.files.openFolder', { keepOpen: true }); await app.workbench.quickInput.waitForQuickInputOpened(); await app.workbench.quickInput.type(path.join(app.workspacePathOrFolder, 'workspaces', 'r_testing')); - // Had to add a positron class, because Microsoft did not have this: await app.workbench.quickInput.clickOkOnQuickInput(); - - // Wait for the console to be ready await app.workbench.console.waitForReady('>', 10000); }).toPass({ timeout: 50000 }); - await expect(async () => { - await app.workbench.testExplorer.clickTestExplorerIcon(); - - const projectFiles = await app.workbench.testExplorer.getTestExplorerFiles(); - - // test-mathstuff.R is the last section of tests in https://github.com/posit-dev/qa-example-content/tree/main/workspaces/r_testing - expect(projectFiles).toContain('test-mathstuff.R'); - }).toPass({ timeout: 50000 }); + await app.workbench.testExplorer.clickTestExplorerIcon(); + await app.workbench.testExplorer.verifyTestFilesExist(['test-mathstuff.R']); await app.workbench.testExplorer.runAllTests();