From f4b44f94ca49a94bc6976873e45a08015de3a2d5 Mon Sep 17 00:00:00 2001 From: sharon Date: Thu, 18 Jul 2024 21:44:53 +0200 Subject: [PATCH] Project Wizard Smoke Tests: timing enhancements and documentation updates (#4066) ## Description - Addresses remaining pieces of https://github.com/posit-dev/positron/issues/3879 - Adds Conda installation steps documentation to smoke test README - Fixes and reorders checks in `pythonEnvironmentStep.tsx` to resolve timing issue when selecting Conda as the env provider while interpreter info is still loading - Improves timing handling for clicking project wizard navigation buttons (back, next, create, cancel) - Adds wait for python project wizard dropdown items to load before interacting with them ### QA Notes - The Conda dropdown timing issue should be resolved --- .../steps/pythonEnvironmentStep.tsx | 109 ++++++++-------- .../src/positron/positronNewProjectWizard.ts | 117 ++++++++++++------ test/smoke/README.md | 4 + .../new-project-wizard/new-project.test.ts | 95 +++++++------- 4 files changed, 184 insertions(+), 141 deletions(-) diff --git a/src/vs/workbench/browser/positronNewProjectWizard/components/steps/pythonEnvironmentStep.tsx b/src/vs/workbench/browser/positronNewProjectWizard/components/steps/pythonEnvironmentStep.tsx index 1768834ea91..974d88972a2 100644 --- a/src/vs/workbench/browser/positronNewProjectWizard/components/steps/pythonEnvironmentStep.tsx +++ b/src/vs/workbench/browser/positronNewProjectWizard/components/steps/pythonEnvironmentStep.tsx @@ -55,6 +55,7 @@ export const PythonEnvironmentStep = (props: PropsWithChildren { // Create the disposable store for cleanup. @@ -72,6 +73,7 @@ export const PythonEnvironmentStep = (props: PropsWithChildren { if (context.usesCondaEnv) { - return Boolean( - context.isCondaInstalled && - condaPythonVersionInfo && - condaPythonVersionInfo.versions.length - ); + return !!isCondaInstalled && + !!condaPythonVersionInfo && + !!condaPythonVersionInfo.versions.length; } - return Boolean(interpreters && interpreters.length); + return !!interpreters && !!interpreters.length; }; + // If any of the values are undefined, the interpreters are still loading. const interpretersLoading = () => { if (context.usesCondaEnv) { - return Boolean(context.isCondaInstalled && !condaPythonVersionInfo); + return isCondaInstalled === undefined || condaPythonVersionInfo === undefined; } - return !interpreters; + return interpreters === undefined; }; const envProvidersAvailable = () => Boolean(envProviders && envProviders.length); const envProvidersLoading = () => !envProviders; @@ -234,69 +236,66 @@ export const PythonEnvironmentStep = (props: PropsWithChildren { - // For existing environments, if an interpreter is selected and ipykernel will be installed, - // show a message to notify the user that ipykernel will be installed. - if (envSetupType === EnvironmentSetupType.ExistingEnvironment && - selectedInterpreter && - willInstallIpykernel) { - return ( - - ipykernel - {(() => - localize( - 'pythonInterpreterSubStep.feedback', - " will be installed for Python language support." - ))()} - - ); - } + if (!interpretersLoading() && !interpretersAvailable()) { + // For new environments, if no environment providers were found, show a message to notify + // the user that interpreters can't be shown since no environment providers were found. + if (envSetupType === EnvironmentSetupType.NewEnvironment) { + return ( + + {(() => + localize( + 'pythonInterpreterSubStep.feedback.noInterpretersAvailable', + "No interpreters available since no environment providers were found." + ))()} + + ); + } - // For new environments, if no environment providers were found, show a message to notify - // the user that interpreters can't be shown since no environment providers were found. - if (envSetupType === EnvironmentSetupType.NewEnvironment && - !envProvidersLoading() && - !envProvidersAvailable() - ) { - return ( - - {(() => - localize( - 'pythonInterpreterSubStep.feedback.noInterpretersAvailable', - "No interpreters available since no environment providers were found." - ))()} - - ); - } + if (context.usesCondaEnv) { + return ( + + {(() => + localize( + 'pythonInterpreterSubStep.feedback.condaNotInstalled', + "Conda is not installed. Please install Conda to create a Conda environment." + ))()} + + ); + } - if (context.usesCondaEnv && !context.isCondaInstalled) { + // If the interpreters list is empty, show a message that no interpreters were found. return ( {(() => localize( - 'pythonInterpreterSubStep.feedback.condaNotInstalled', - "Conda is not installed. Please install Conda to create a Conda environment." + 'pythonInterpreterSubStep.feedback.noSuitableInterpreters', + "No suitable interpreters found. Please install a Python interpreter with version {0} or later.", + minimumPythonVersion ))()} ); } - // If the interpreters list is empty, show a message that no interpreters were found. - if (!interpretersLoading() && !interpretersAvailable()) { + // For existing environments, if an interpreter is selected and ipykernel will be installed, + // show a message to notify the user that ipykernel will be installed. + if ( + envSetupType === EnvironmentSetupType.ExistingEnvironment && + selectedInterpreter && + willInstallIpykernel + ) { return ( - + + ipykernel {(() => localize( - 'pythonInterpreterSubStep.feedback.noSuitableInterpreters', - "No suitable interpreters found. Please install a Python interpreter with version {0} or later.", - minimumPythonVersion + 'pythonInterpreterSubStep.feedback', + " will be installed for Python language support." ))()} ); diff --git a/test/automation/src/positron/positronNewProjectWizard.ts b/test/automation/src/positron/positronNewProjectWizard.ts index 797df819e95..77470575ef4 100644 --- a/test/automation/src/positron/positronNewProjectWizard.ts +++ b/test/automation/src/positron/positronNewProjectWizard.ts @@ -3,7 +3,7 @@ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ -import { Locator } from '@playwright/test'; +import { expect, Locator } from '@playwright/test'; import { Code } from '../code'; import { QuickAccess } from '../quickaccess'; import { PositronBaseElement, PositronTextElement } from './positronBaseElement'; @@ -12,6 +12,19 @@ import { PositronBaseElement, PositronTextElement } from './positronBaseElement' 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, +} + /* * Reuseable Positron new project wizard functionality for tests to leverage. */ @@ -22,41 +35,17 @@ export class PositronNewProjectWizard { pythonConfigurationStep: ProjectWizardPythonConfigurationStep; currentOrNewWindowSelectionModal: CurrentOrNewWindowSelectionModal; - cancelButton: PositronBaseElement; - nextButton: PositronBaseElement; - backButton: PositronBaseElement; - disabledCreateButton: PositronBaseElement; + private backButton = this.code.driver.getLocator('div.left-actions > button.positron-button.button.action-bar-button[tabindex="0"][role="button"]'); + private cancelButton = this.code.driver.getLocator('div.right-actions > button.positron-button.button.action-bar-button[tabindex="0"][role="button"]'); + private nextButton = this.code.driver.getLocator(PROJECT_WIZARD_DEFAULT_BUTTON).getByText('Next'); + private createButton = this.code.driver.getLocator(PROJECT_WIZARD_DEFAULT_BUTTON).getByText('Create'); 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); - - this.cancelButton = new PositronBaseElement( - 'div.right-actions > button.positron-button.button.action-bar-button[tabindex="0"][role="button"]', - this.code - ); - this.nextButton = new PositronBaseElement( - 'button.positron-button.button.action-bar-button.default[tabindex="0"][role="button"]', - this.code - ); - this.backButton = new PositronBaseElement( - 'div.left-actions > button.positron-button.button.action-bar-button[tabindex="0"][role="button"]', - this.code - ); - this.disabledCreateButton = new PositronBaseElement( - 'button.positron-button.button.action-bar-button.default.disabled[tabindex="0"][disabled][role="button"][aria-disabled="true"]', - 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); } async startNewProject() { @@ -65,6 +54,37 @@ export class PositronNewProjectWizard { { keepOpen: false } ); } + + /** + * 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}` + ); + } + } } class ProjectWizardProjectTypeStep { @@ -151,19 +171,35 @@ class ProjectWizardPythonConfigurationStep { ); } + 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 } + ); + } + + // The interpreter dropdown is always visible + await expect(this.interpreterDropdown).not.toContainText( + 'Loading interpreters...', + { timeout: 5000 } + ); + } + /** * Selects the specified environment provider in the project wizard environment provider dropdown. * @param provider The environment provider to select. */ async selectEnvProvider(provider: string) { + await this.waitForDataLoading(); + // Open the dropdown await this.envProviderDropdown.click(); // Try to find the env provider in the dropdown try { - await this.code.waitForElement( - PROJECT_WIZARD_DROPDOWN_POPUP_ITEMS - ); + await this.code.waitForElement(PROJECT_WIZARD_DROPDOWN_POPUP_ITEMS); await this.code.driver .getLocator( `${PROJECT_WIZARD_DROPDOWN_POPUP_ITEMS} div.dropdown-entry-title:text-is("${provider}")` @@ -184,18 +220,19 @@ class ProjectWizardPythonConfigurationStep { * @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(); + // Open the dropdown await this.interpreterDropdown.click(); // Try to find the interpreterPath in the dropdown and click the entry if found try { - await this.code.waitForElement( - PROJECT_WIZARD_DROPDOWN_POPUP_ITEMS - ); + await this.code.waitForElement(PROJECT_WIZARD_DROPDOWN_POPUP_ITEMS); await this.code.driver .getLocator( - `${PROJECT_WIZARD_DROPDOWN_POPUP_ITEMS} div.dropdown-entry-subtitle:text-is("${interpreterPath}")` + `${PROJECT_WIZARD_DROPDOWN_POPUP_ITEMS} div.dropdown-entry-subtitle` ) + .getByText(interpreterPath) .click(); return Promise.resolve(); } catch (error) { diff --git a/test/smoke/README.md b/test/smoke/README.md index 583d6e2d8bb..cb109bb0ef8 100644 --- a/test/smoke/README.md +++ b/test/smoke/README.md @@ -148,6 +148,10 @@ Graphviz is external software that has a Python package to render graphs. Instal * **Windows** - `choco install graphviz` * **Mac** - `brew install graphviz` +**Conda** environments are leveraged by some smoke tests. You can install a lightweight version of Conda (instead of installing Anaconda) by installing one of the following: +- [miniforge](https://github.com/conda-forge/miniforge?tab=readme-ov-file#install) (On Mac, you can `brew install miniforge`. The equivalent installer may also be available via package managers on Linux and Windows.) +- [miniconda](https://docs.anaconda.com/miniconda/#quick-command-line-install) (On Mac, you can `brew install miniconda`. The equivalent installer may also be available via package managers on Linux and Windows.) + ## Environment Setup - Resemblejs dependency Make sure that you have followed the [Machine Setup](https://connect.posit.it/positron-wiki/machine-setup.html) instructions so that you can be sure you are set up to build resemblejs (which depends on node-canvas). diff --git a/test/smoke/src/areas/positron/new-project-wizard/new-project.test.ts b/test/smoke/src/areas/positron/new-project-wizard/new-project.test.ts index e0c2e5e2338..7221a617076 100644 --- a/test/smoke/src/areas/positron/new-project-wizard/new-project.test.ts +++ b/test/smoke/src/areas/positron/new-project-wizard/new-project.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { expect } from '@playwright/test'; -import { Application, Logger, PositronPythonFixtures } from '../../../../../automation'; +import { Application, Logger, PositronPythonFixtures, ProjectWizardNavigateAction } from '../../../../../automation'; import { installAllHandlers } from '../../../utils'; /* @@ -27,27 +27,26 @@ export function setup(logger: Logger) { const pw = app.workbench.positronNewProjectWizard; await pw.startNewProject(); await pw.projectTypeStep.pythonProjectButton.click(); - await pw.nextButton.click(); - await pw.nextButton.click(); - await pw.disabledCreateButton.isNotVisible(500); - await pw.nextButton.click(); + await pw.navigate(ProjectWizardNavigateAction.NEXT); + await pw.navigate(ProjectWizardNavigateAction.NEXT); + await pw.navigate(ProjectWizardNavigateAction.CREATE); await pw.currentOrNewWindowSelectionModal.currentWindowButton.click(); await app.workbench.positronExplorer.explorerProjectTitle.waitForText('myPythonProject'); await app.workbench.positronConsole.waitForReady('>>>', 10000); }); - it('Create a new Conda environment [.......]', async function () { + it('Create a new Conda environment [C628628]', async function () { // This test relies on Conda already being installed on the machine const projSuffix = '_condaInstalled'; const app = this.app as Application; const pw = app.workbench.positronNewProjectWizard; await pw.startNewProject(); await pw.projectTypeStep.pythonProjectButton.click(); - await pw.nextButton.click(); + await pw.navigate(ProjectWizardNavigateAction.NEXT); await pw.projectNameLocationStep.appendToProjectName(projSuffix); - await pw.nextButton.click(); + await pw.navigate(ProjectWizardNavigateAction.NEXT); // Select 'Conda' as the environment provider await pw.pythonConfigurationStep.selectEnvProvider('Conda'); - await pw.nextButton.click(); + await pw.navigate(ProjectWizardNavigateAction.CREATE); await pw.currentOrNewWindowSelectionModal.currentWindowButton.click(); await app.workbench.positronExplorer.explorerProjectTitle.waitForText( `myPythonProject${projSuffix}` @@ -77,15 +76,20 @@ export function setup(logger: Logger) { // Create a new Python project and use the selected python interpreter await pw.startNewProject(); await pw.projectTypeStep.pythonProjectButton.click(); - await pw.nextButton.click(); + await pw.navigate(ProjectWizardNavigateAction.NEXT); await pw.projectNameLocationStep.appendToProjectName(projSuffix); - await pw.nextButton.click(); + await pw.navigate(ProjectWizardNavigateAction.NEXT); await pw.pythonConfigurationStep.existingEnvRadioButton.click(); - // Select the interpreter that was started above - await pw.pythonConfigurationStep.selectInterpreterByPath(interpreterInfo!.path); + // 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. + expect( + async () => + await pw.pythonConfigurationStep.selectInterpreterByPath( + interpreterInfo!.path + ) + ).toPass({ timeout: 10000 }); await pw.pythonConfigurationStep.interpreterFeedback.isNotVisible(); - await pw.disabledCreateButton.isNotVisible(500); - await pw.nextButton.click(); + await pw.navigate(ProjectWizardNavigateAction.CREATE); await pw.currentOrNewWindowSelectionModal.currentWindowButton.click(); await app.workbench.positronExplorer.explorerProjectTitle.waitForText( `myPythonProject${projSuffix}` @@ -112,18 +116,23 @@ export function setup(logger: Logger) { // Create a new Python project and use the selected python interpreter await pw.startNewProject(); await pw.projectTypeStep.pythonProjectButton.click(); - await pw.nextButton.click(); + await pw.navigate(ProjectWizardNavigateAction.NEXT); await pw.projectNameLocationStep.appendToProjectName(projSuffix); - await pw.nextButton.click(); + 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 - await pw.pythonConfigurationStep.selectInterpreterByPath(interpreterInfo!.path); + // 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. + expect( + async () => + await pw.pythonConfigurationStep.selectInterpreterByPath( + interpreterInfo!.path + ) + ).toPass({ timeout: 10000 }); await pw.pythonConfigurationStep.interpreterFeedback.waitForText( 'ipykernel will be installed for Python language support.' ); - await pw.disabledCreateButton.isNotVisible(500); - await pw.nextButton.click(); + await pw.navigate(ProjectWizardNavigateAction.CREATE); await pw.currentOrNewWindowSelectionModal.currentWindowButton.click(); await app.workbench.positronExplorer.explorerProjectTitle.waitForText( `myPythonProject${projSuffix}` @@ -134,21 +143,20 @@ export function setup(logger: Logger) { }); }); - it('Default Python Project with git init [......]', async function () { + it('Default Python Project with git init [C674522]', async function () { const projSuffix = '_gitInit'; const app = this.app as Application; const pw = app.workbench.positronNewProjectWizard; await pw.startNewProject(); await pw.projectTypeStep.pythonProjectButton.click(); - await pw.nextButton.click(); + 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.nextButton.click(); - await pw.disabledCreateButton.isNotVisible(500); - await pw.nextButton.click(); + 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(); @@ -186,10 +194,9 @@ export function setup(logger: Logger) { const pw = app.workbench.positronNewProjectWizard; await pw.startNewProject(); await pw.projectTypeStep.rProjectButton.click(); - await pw.nextButton.click(); - await pw.nextButton.click(); - await pw.disabledCreateButton.isNotVisible(500); - await pw.nextButton.click(); + await pw.navigate(ProjectWizardNavigateAction.NEXT); + await pw.navigate(ProjectWizardNavigateAction.NEXT); + await pw.navigate(ProjectWizardNavigateAction.CREATE); await pw.currentOrNewWindowSelectionModal.currentWindowButton.click(); await app.workbench.positronExplorer.explorerProjectTitle.waitForText('myRProject'); // NOTE: For completeness, we probably want to await app.workbench.positronConsole.waitForReady('>', 10000); @@ -204,13 +211,12 @@ export function setup(logger: Logger) { // Create a new R project - select Renv and install await pw.startNewProject(); await pw.projectTypeStep.rProjectButton.click(); - await pw.nextButton.click(); + await pw.navigate(ProjectWizardNavigateAction.NEXT); await pw.projectNameLocationStep.appendToProjectName(projSuffix); - await pw.nextButton.click(); - await pw.disabledCreateButton.isNotVisible(500); + await pw.navigate(ProjectWizardNavigateAction.NEXT); // Select the renv checkbox await pw.rConfigurationStep.renvCheckbox.click(); - await pw.nextButton.click(); + await pw.navigate(ProjectWizardNavigateAction.CREATE); await pw.currentOrNewWindowSelectionModal.currentWindowButton.click(); await app.workbench.positronExplorer.explorerProjectTitle.waitForText( `myRProject${projSuffix}` @@ -250,13 +256,12 @@ export function setup(logger: Logger) { const pw = app.workbench.positronNewProjectWizard; await pw.startNewProject(); await pw.projectTypeStep.rProjectButton.click(); - await pw.nextButton.click(); + await pw.navigate(ProjectWizardNavigateAction.NEXT); await pw.projectNameLocationStep.appendToProjectName(projSuffix); - await pw.nextButton.click(); - await pw.disabledCreateButton.isNotVisible(500); + await pw.navigate(ProjectWizardNavigateAction.NEXT); // Select the renv checkbox await pw.rConfigurationStep.renvCheckbox.click(); - await pw.nextButton.click(); + await pw.navigate(ProjectWizardNavigateAction.CREATE); await pw.currentOrNewWindowSelectionModal.currentWindowButton.click(); await app.workbench.positronExplorer.explorerProjectTitle.waitForText( `myRProject${projSuffix}` @@ -286,13 +291,12 @@ export function setup(logger: Logger) { // Create a new R project - select Renv but opt out of installing await pw.startNewProject(); await pw.projectTypeStep.rProjectButton.click(); - await pw.nextButton.click(); + await pw.navigate(ProjectWizardNavigateAction.NEXT); await pw.projectNameLocationStep.appendToProjectName(projSuffix); - await pw.nextButton.click(); - await pw.disabledCreateButton.isNotVisible(500); + await pw.navigate(ProjectWizardNavigateAction.NEXT); // Select the renv checkbox await pw.rConfigurationStep.renvCheckbox.click(); - await pw.nextButton.click(); + await pw.navigate(ProjectWizardNavigateAction.CREATE); await pw.currentOrNewWindowSelectionModal.currentWindowButton.click(); await app.workbench.positronExplorer.explorerProjectTitle.waitForText( `myRProject${projSuffix}` @@ -323,10 +327,9 @@ export function setup(logger: Logger) { const pw = app.workbench.positronNewProjectWizard; await pw.startNewProject(); await pw.projectTypeStep.jupyterNotebookButton.click(); - await pw.nextButton.click(); - await pw.nextButton.click(); - await pw.disabledCreateButton.isNotVisible(500); - await pw.nextButton.click(); + await pw.navigate(ProjectWizardNavigateAction.NEXT); + await pw.navigate(ProjectWizardNavigateAction.NEXT); + await pw.navigate(ProjectWizardNavigateAction.CREATE); await pw.currentOrNewWindowSelectionModal.currentWindowButton.click(); await app.workbench.positronExplorer.explorerProjectTitle.waitForText('myJupyterNotebook'); // NOTE: For completeness, we probably want to await app.workbench.positronConsole.waitForReady('>>>', 10000);