Skip to content

Commit f4b44f9

Browse files
authored
Project Wizard Smoke Tests: timing enhancements and documentation updates (#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
1 parent 44241a7 commit f4b44f9

File tree

4 files changed

+184
-141
lines changed

4 files changed

+184
-141
lines changed

src/vs/workbench/browser/positronNewProjectWizard/components/steps/pythonEnvironmentStep.tsx

Lines changed: 54 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export const PythonEnvironmentStep = (props: PropsWithChildren<NewProjectWizardS
5555
const [minimumPythonVersion, setMinimumPythonVersion] = useState(context.minimumPythonVersion);
5656
const [condaPythonVersionInfo, setCondaPythonVersionInfo] = useState(context.condaPythonVersionInfo);
5757
const [selectedCondaPythonVersion, setSelectedCondaPythonVersion] = useState(context.condaPythonVersion);
58+
const [isCondaInstalled, setIsCondaInstalled] = useState(context.isCondaInstalled);
5859

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

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

8183
// Utility functions.
84+
// At least one interpreter is available.
8285
const interpretersAvailable = () => {
8386
if (context.usesCondaEnv) {
84-
return Boolean(
85-
context.isCondaInstalled &&
86-
condaPythonVersionInfo &&
87-
condaPythonVersionInfo.versions.length
88-
);
87+
return !!isCondaInstalled &&
88+
!!condaPythonVersionInfo &&
89+
!!condaPythonVersionInfo.versions.length;
8990
}
90-
return Boolean(interpreters && interpreters.length);
91+
return !!interpreters && !!interpreters.length;
9192
};
93+
// If any of the values are undefined, the interpreters are still loading.
9294
const interpretersLoading = () => {
9395
if (context.usesCondaEnv) {
94-
return Boolean(context.isCondaInstalled && !condaPythonVersionInfo);
96+
return isCondaInstalled === undefined || condaPythonVersionInfo === undefined;
9597
}
96-
return !interpreters;
98+
return interpreters === undefined;
9799
};
98100
const envProvidersAvailable = () => Boolean(envProviders && envProviders.length);
99101
const envProvidersLoading = () => !envProviders;
@@ -234,69 +236,66 @@ export const PythonEnvironmentStep = (props: PropsWithChildren<NewProjectWizardS
234236

235237
// Construct the feedback message for the interpreter step.
236238
const interpreterStepFeedback = () => {
237-
// For existing environments, if an interpreter is selected and ipykernel will be installed,
238-
// show a message to notify the user that ipykernel will be installed.
239-
if (envSetupType === EnvironmentSetupType.ExistingEnvironment &&
240-
selectedInterpreter &&
241-
willInstallIpykernel) {
242-
return (
243-
<WizardFormattedText
244-
type={WizardFormattedTextType.Info}
245-
>
246-
<code>ipykernel</code>
247-
{(() =>
248-
localize(
249-
'pythonInterpreterSubStep.feedback',
250-
" will be installed for Python language support."
251-
))()}
252-
</WizardFormattedText>
253-
);
254-
}
239+
if (!interpretersLoading() && !interpretersAvailable()) {
240+
// For new environments, if no environment providers were found, show a message to notify
241+
// the user that interpreters can't be shown since no environment providers were found.
242+
if (envSetupType === EnvironmentSetupType.NewEnvironment) {
243+
return (
244+
<WizardFormattedText
245+
type={WizardFormattedTextType.Warning}
246+
>
247+
{(() =>
248+
localize(
249+
'pythonInterpreterSubStep.feedback.noInterpretersAvailable',
250+
"No interpreters available since no environment providers were found."
251+
))()}
252+
</WizardFormattedText>
253+
);
254+
}
255255

256-
// For new environments, if no environment providers were found, show a message to notify
257-
// the user that interpreters can't be shown since no environment providers were found.
258-
if (envSetupType === EnvironmentSetupType.NewEnvironment &&
259-
!envProvidersLoading() &&
260-
!envProvidersAvailable()
261-
) {
262-
return (
263-
<WizardFormattedText
264-
type={WizardFormattedTextType.Warning}
265-
>
266-
{(() =>
267-
localize(
268-
'pythonInterpreterSubStep.feedback.noInterpretersAvailable',
269-
"No interpreters available since no environment providers were found."
270-
))()}
271-
</WizardFormattedText>
272-
);
273-
}
256+
if (context.usesCondaEnv) {
257+
return (
258+
<WizardFormattedText
259+
type={WizardFormattedTextType.Warning}
260+
>
261+
{(() =>
262+
localize(
263+
'pythonInterpreterSubStep.feedback.condaNotInstalled',
264+
"Conda is not installed. Please install Conda to create a Conda environment."
265+
))()}
266+
</WizardFormattedText>
267+
);
268+
}
274269

275-
if (context.usesCondaEnv && !context.isCondaInstalled) {
270+
// If the interpreters list is empty, show a message that no interpreters were found.
276271
return (
277272
<WizardFormattedText
278273
type={WizardFormattedTextType.Warning}
279274
>
280275
{(() =>
281276
localize(
282-
'pythonInterpreterSubStep.feedback.condaNotInstalled',
283-
"Conda is not installed. Please install Conda to create a Conda environment."
277+
'pythonInterpreterSubStep.feedback.noSuitableInterpreters',
278+
"No suitable interpreters found. Please install a Python interpreter with version {0} or later.",
279+
minimumPythonVersion
284280
))()}
285281
</WizardFormattedText>
286282
);
287283
}
288284

289-
// If the interpreters list is empty, show a message that no interpreters were found.
290-
if (!interpretersLoading() && !interpretersAvailable()) {
285+
// For existing environments, if an interpreter is selected and ipykernel will be installed,
286+
// show a message to notify the user that ipykernel will be installed.
287+
if (
288+
envSetupType === EnvironmentSetupType.ExistingEnvironment &&
289+
selectedInterpreter &&
290+
willInstallIpykernel
291+
) {
291292
return (
292-
<WizardFormattedText
293-
type={WizardFormattedTextType.Warning}
294-
>
293+
<WizardFormattedText type={WizardFormattedTextType.Info}>
294+
<code>ipykernel</code>
295295
{(() =>
296296
localize(
297-
'pythonInterpreterSubStep.feedback.noSuitableInterpreters',
298-
"No suitable interpreters found. Please install a Python interpreter with version {0} or later.",
299-
minimumPythonVersion
297+
'pythonInterpreterSubStep.feedback',
298+
" will be installed for Python language support."
300299
))()}
301300
</WizardFormattedText>
302301
);

test/automation/src/positron/positronNewProjectWizard.ts

Lines changed: 77 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { Locator } from '@playwright/test';
6+
import { expect, Locator } from '@playwright/test';
77
import { Code } from '../code';
88
import { QuickAccess } from '../quickaccess';
99
import { PositronBaseElement, PositronTextElement } from './positronBaseElement';
@@ -12,6 +12,19 @@ import { PositronBaseElement, PositronTextElement } from './positronBaseElement'
1212
const PROJECT_WIZARD_DROPDOWN_POPUP_ITEMS =
1313
'div.positron-modal-popup-children button.positron-button.item';
1414

15+
// Selector for the default button in the project wizard, which will either be 'Next' or 'Create'
16+
const PROJECT_WIZARD_DEFAULT_BUTTON = 'button.positron-button.button.action-bar-button.default[tabindex="0"][role="button"]';
17+
18+
/**
19+
* Enum representing the possible navigation actions that can be taken in the project wizard.
20+
*/
21+
export enum ProjectWizardNavigateAction {
22+
BACK,
23+
NEXT,
24+
CANCEL,
25+
CREATE,
26+
}
27+
1528
/*
1629
* Reuseable Positron new project wizard functionality for tests to leverage.
1730
*/
@@ -22,41 +35,17 @@ export class PositronNewProjectWizard {
2235
pythonConfigurationStep: ProjectWizardPythonConfigurationStep;
2336
currentOrNewWindowSelectionModal: CurrentOrNewWindowSelectionModal;
2437

25-
cancelButton: PositronBaseElement;
26-
nextButton: PositronBaseElement;
27-
backButton: PositronBaseElement;
28-
disabledCreateButton: PositronBaseElement;
38+
private backButton = this.code.driver.getLocator('div.left-actions > button.positron-button.button.action-bar-button[tabindex="0"][role="button"]');
39+
private cancelButton = this.code.driver.getLocator('div.right-actions > button.positron-button.button.action-bar-button[tabindex="0"][role="button"]');
40+
private nextButton = this.code.driver.getLocator(PROJECT_WIZARD_DEFAULT_BUTTON).getByText('Next');
41+
private createButton = this.code.driver.getLocator(PROJECT_WIZARD_DEFAULT_BUTTON).getByText('Create');
2942

3043
constructor(private code: Code, private quickaccess: QuickAccess) {
3144
this.projectTypeStep = new ProjectWizardProjectTypeStep(this.code);
32-
this.projectNameLocationStep = new ProjectWizardProjectNameLocationStep(
33-
this.code
34-
);
35-
this.rConfigurationStep = new ProjectWizardRConfigurationStep(
36-
this.code
37-
);
38-
this.pythonConfigurationStep = new ProjectWizardPythonConfigurationStep(
39-
this.code
40-
);
41-
this.currentOrNewWindowSelectionModal =
42-
new CurrentOrNewWindowSelectionModal(this.code);
43-
44-
this.cancelButton = new PositronBaseElement(
45-
'div.right-actions > button.positron-button.button.action-bar-button[tabindex="0"][role="button"]',
46-
this.code
47-
);
48-
this.nextButton = new PositronBaseElement(
49-
'button.positron-button.button.action-bar-button.default[tabindex="0"][role="button"]',
50-
this.code
51-
);
52-
this.backButton = new PositronBaseElement(
53-
'div.left-actions > button.positron-button.button.action-bar-button[tabindex="0"][role="button"]',
54-
this.code
55-
);
56-
this.disabledCreateButton = new PositronBaseElement(
57-
'button.positron-button.button.action-bar-button.default.disabled[tabindex="0"][disabled][role="button"][aria-disabled="true"]',
58-
this.code
59-
);
45+
this.projectNameLocationStep = new ProjectWizardProjectNameLocationStep(this.code);
46+
this.rConfigurationStep = new ProjectWizardRConfigurationStep(this.code);
47+
this.pythonConfigurationStep = new ProjectWizardPythonConfigurationStep(this.code);
48+
this.currentOrNewWindowSelectionModal = new CurrentOrNewWindowSelectionModal(this.code);
6049
}
6150

6251
async startNewProject() {
@@ -65,6 +54,37 @@ export class PositronNewProjectWizard {
6554
{ keepOpen: false }
6655
);
6756
}
57+
58+
/**
59+
* Clicks the specified navigation button in the project wizard.
60+
* @param action The navigation action to take in the project wizard.
61+
*/
62+
async navigate(action: ProjectWizardNavigateAction) {
63+
switch (action) {
64+
case ProjectWizardNavigateAction.BACK:
65+
await this.backButton.waitFor();
66+
await this.backButton.click();
67+
break;
68+
case ProjectWizardNavigateAction.NEXT:
69+
await this.nextButton.waitFor();
70+
await this.nextButton.isEnabled({ timeout: 5000 });
71+
await this.nextButton.click();
72+
break;
73+
case ProjectWizardNavigateAction.CANCEL:
74+
await this.cancelButton.waitFor();
75+
await this.cancelButton.click();
76+
break;
77+
case ProjectWizardNavigateAction.CREATE:
78+
await this.createButton.waitFor();
79+
await this.createButton.isEnabled({ timeout: 5000 });
80+
await this.createButton.click();
81+
break;
82+
default:
83+
throw new Error(
84+
`Invalid project wizard navigation action: ${action}`
85+
);
86+
}
87+
}
6888
}
6989

7090
class ProjectWizardProjectTypeStep {
@@ -151,19 +171,35 @@ class ProjectWizardPythonConfigurationStep {
151171
);
152172
}
153173

174+
private async waitForDataLoading() {
175+
// The env provider dropdown is only visible when New Environment is selected
176+
if (await this.envProviderDropdown.isVisible()) {
177+
await expect(this.envProviderDropdown).not.toContainText(
178+
'Loading environment providers...',
179+
{ timeout: 5000 }
180+
);
181+
}
182+
183+
// The interpreter dropdown is always visible
184+
await expect(this.interpreterDropdown).not.toContainText(
185+
'Loading interpreters...',
186+
{ timeout: 5000 }
187+
);
188+
}
189+
154190
/**
155191
* Selects the specified environment provider in the project wizard environment provider dropdown.
156192
* @param provider The environment provider to select.
157193
*/
158194
async selectEnvProvider(provider: string) {
195+
await this.waitForDataLoading();
196+
159197
// Open the dropdown
160198
await this.envProviderDropdown.click();
161199

162200
// Try to find the env provider in the dropdown
163201
try {
164-
await this.code.waitForElement(
165-
PROJECT_WIZARD_DROPDOWN_POPUP_ITEMS
166-
);
202+
await this.code.waitForElement(PROJECT_WIZARD_DROPDOWN_POPUP_ITEMS);
167203
await this.code.driver
168204
.getLocator(
169205
`${PROJECT_WIZARD_DROPDOWN_POPUP_ITEMS} div.dropdown-entry-title:text-is("${provider}")`
@@ -184,18 +220,19 @@ class ProjectWizardPythonConfigurationStep {
184220
* @returns A promise that resolves once the interpreter is selected, or rejects if the interpreter is not found.
185221
*/
186222
async selectInterpreterByPath(interpreterPath: string) {
223+
await this.waitForDataLoading();
224+
187225
// Open the dropdown
188226
await this.interpreterDropdown.click();
189227

190228
// Try to find the interpreterPath in the dropdown and click the entry if found
191229
try {
192-
await this.code.waitForElement(
193-
PROJECT_WIZARD_DROPDOWN_POPUP_ITEMS
194-
);
230+
await this.code.waitForElement(PROJECT_WIZARD_DROPDOWN_POPUP_ITEMS);
195231
await this.code.driver
196232
.getLocator(
197-
`${PROJECT_WIZARD_DROPDOWN_POPUP_ITEMS} div.dropdown-entry-subtitle:text-is("${interpreterPath}")`
233+
`${PROJECT_WIZARD_DROPDOWN_POPUP_ITEMS} div.dropdown-entry-subtitle`
198234
)
235+
.getByText(interpreterPath)
199236
.click();
200237
return Promise.resolve();
201238
} catch (error) {

test/smoke/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,10 @@ Graphviz is external software that has a Python package to render graphs. Instal
148148
* **Windows** - `choco install graphviz`
149149
* **Mac** - `brew install graphviz`
150150

151+
**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:
152+
- [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.)
153+
- [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.)
154+
151155
## Environment Setup - Resemblejs dependency
152156

153157
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).

0 commit comments

Comments
 (0)