Skip to content

Commit

Permalink
Project Wizard Smoke Tests: timing enhancements and documentation upd…
Browse files Browse the repository at this point in the history
…ates (#4066)

## Description

- Addresses remaining pieces of #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
  • Loading branch information
sharon-wang authored Jul 18, 2024
1 parent 44241a7 commit f4b44f9
Show file tree
Hide file tree
Showing 4 changed files with 184 additions and 141 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export const PythonEnvironmentStep = (props: PropsWithChildren<NewProjectWizardS
const [minimumPythonVersion, setMinimumPythonVersion] = useState(context.minimumPythonVersion);
const [condaPythonVersionInfo, setCondaPythonVersionInfo] = useState(context.condaPythonVersionInfo);
const [selectedCondaPythonVersion, setSelectedCondaPythonVersion] = useState(context.condaPythonVersion);
const [isCondaInstalled, setIsCondaInstalled] = useState(context.isCondaInstalled);

useEffect(() => {
// Create the disposable store for cleanup.
Expand All @@ -72,28 +73,29 @@ export const PythonEnvironmentStep = (props: PropsWithChildren<NewProjectWizardS
setMinimumPythonVersion(context.minimumPythonVersion);
setCondaPythonVersionInfo(context.condaPythonVersionInfo);
setSelectedCondaPythonVersion(context.condaPythonVersion);
setIsCondaInstalled(context.isCondaInstalled);
}));

// Return the cleanup function that will dispose of the event handlers.
return () => disposableStore.dispose();
}, [context]);

// Utility functions.
// At least one interpreter is available.
const interpretersAvailable = () => {
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;
Expand Down Expand Up @@ -234,69 +236,66 @@ export const PythonEnvironmentStep = (props: PropsWithChildren<NewProjectWizardS

// Construct the feedback message for the interpreter step.
const interpreterStepFeedback = () => {
// 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 (
<WizardFormattedText
type={WizardFormattedTextType.Info}
>
<code>ipykernel</code>
{(() =>
localize(
'pythonInterpreterSubStep.feedback',
" will be installed for Python language support."
))()}
</WizardFormattedText>
);
}
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 (
<WizardFormattedText
type={WizardFormattedTextType.Warning}
>
{(() =>
localize(
'pythonInterpreterSubStep.feedback.noInterpretersAvailable',
"No interpreters available since no environment providers were found."
))()}
</WizardFormattedText>
);
}

// 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 (
<WizardFormattedText
type={WizardFormattedTextType.Warning}
>
{(() =>
localize(
'pythonInterpreterSubStep.feedback.noInterpretersAvailable',
"No interpreters available since no environment providers were found."
))()}
</WizardFormattedText>
);
}
if (context.usesCondaEnv) {
return (
<WizardFormattedText
type={WizardFormattedTextType.Warning}
>
{(() =>
localize(
'pythonInterpreterSubStep.feedback.condaNotInstalled',
"Conda is not installed. Please install Conda to create a Conda environment."
))()}
</WizardFormattedText>
);
}

if (context.usesCondaEnv && !context.isCondaInstalled) {
// If the interpreters list is empty, show a message that no interpreters were found.
return (
<WizardFormattedText
type={WizardFormattedTextType.Warning}
>
{(() =>
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
))()}
</WizardFormattedText>
);
}

// 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 (
<WizardFormattedText
type={WizardFormattedTextType.Warning}
>
<WizardFormattedText type={WizardFormattedTextType.Info}>
<code>ipykernel</code>
{(() =>
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."
))()}
</WizardFormattedText>
);
Expand Down
117 changes: 77 additions & 40 deletions test/automation/src/positron/positronNewProjectWizard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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.
*/
Expand All @@ -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() {
Expand All @@ -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 {
Expand Down Expand Up @@ -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}")`
Expand All @@ -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) {
Expand Down
4 changes: 4 additions & 0 deletions test/smoke/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
Loading

0 comments on commit f4b44f9

Please sign in to comment.