From 0ab3475d54a09fc500aec21cfe64f6caf2ec1d42 Mon Sep 17 00:00:00 2001 From: Vichym Date: Thu, 12 Dec 2024 17:23:00 -0800 Subject: [PATCH 1/2] refactor NestedWizard class and update arch doc --- docs/arch_develop.md | 103 +++++++++++++++++- packages/core/src/shared/ui/wizardPrompter.ts | 4 +- .../compositeWizard.ts} | 8 +- ...dWizard.test.ts => compositWizard.test.ts} | 8 +- 4 files changed, 112 insertions(+), 11 deletions(-) rename packages/core/src/shared/{ui/nestedWizardPrompter.ts => wizards/compositeWizard.ts} (90%) rename packages/core/src/test/shared/wizards/{nestedWizard.test.ts => compositWizard.test.ts} (98%) diff --git a/docs/arch_develop.md b/docs/arch_develop.md index 4880d5493c5..8ad4832df44 100644 --- a/docs/arch_develop.md +++ b/docs/arch_develop.md @@ -444,7 +444,7 @@ await tester.result(items[0].data) // Execute the actions, asserting the final r Abstractly, a 'wizard' is a collection of discrete, linear steps (subroutines), where each step can potentially be dependent on prior steps, that results in some final state. Wizards are extremely common in top-level flows such as creating a new resource, deployments, or confirmation messages. For these kinds of flows, we have a shared `Wizard` class that handles the bulk of control flow and state management logic for us. -### Creating a Wizard (Quick Picks) +### 1. `Wizard` Class Create a new wizard by extending the base `Wizard` class, using the template type to specify the shape of the wizard state. All wizards have an internal `form` property that is used to assign @@ -482,6 +482,41 @@ class ExampleWizard extends Wizard { } ``` +### 2. `CompositeWizard` Class + +This abstract class extends the `Wizard` class with an addition method to create and manage state of the nested wizards. + +Extends this class to create a wizard that contains other wizards as part of the prompter flow. +Use `this.createWizardPrompter()` to use another wizard class as a prompter in the main wizard. + +Example: + +```ts + +// Child wizard +class ChildWizard extends Wizards {...} + + +// Composite wizard +interface SingleNestedWizardForm { + ... + singleNestedWizardNestedProp: string + ... +} + +class SingleNestedWizard extends CompositeWizard { + constructor() { + super() + ... + this.form.singleNestedWizardNestedProp.bindPrompter(() => + this.createWizardPrompter(ChildWizard) + ) + ... + } +} + +``` + ### Executing Wizards can be ran by calling the async `run` method: @@ -495,6 +530,8 @@ Note that all wizards can potentially return `undefined` if the workflow was can ### Testing +#### Using `WizardTester` + Use `createWizardTester` on an instance of a wizard. Tests can then be constructed by asserting both the user-defined and internal state. Using the above `ExampleWizard`: ```ts @@ -505,6 +542,70 @@ tester.foo.applyInput('Hello, world!') // Manipulate 'user' state tester.bar.assertShow() // True since 'foo' has a defined value ``` +#### Using `PrompterTester` + +Use `PrompterTester` to simulate user behavior (click, input and selection) on prompters to test end-to-end flow of a wizard. + +Example: + +```ts +// 1. Register PrompterTester handlers +const prompterTester = PrompterTester.init() + .handleInputBox('Input Prompter title 1', (inputBox) => { + // Register Input Prompter handler + inputBox.acceptValue('my-source-bucket-name') + }) + .handleQuickPick('Quick Pick Prompter title 2', (quickPick) => { + // Register Quick Pick Prompter handler + + // Optional assertion can be added as part of the handler function + assert.strictEqual(quickPick.items.length, 2) + assert.strictEqual(quickPick.items[0].label, 'Specify required parameters and save as defaults') + assert.strictEqual(quickPick.items[1].label, 'Specify required parameters') + // Choose item + quickPick.acceptItem(quickPick.items[0]) + }) + .handleQuickPick( + 'Quick Pick Prompter with various handler behavior title 3', + (() => { + // Register handler with dynamic behavior + const generator = (function* () { + // First call, choose '**' + yield async (picker: TestQuickPick) => { + await picker.untilReady() + assert.strictEqual(picker.items[1].label, '**') + picker.acceptItem(picker.items[1]) + } + // Second call, choose BACK button + yield async (picker: TestQuickPick) => { + await picker.untilReady() + picker.pressButton(vscode.QuickInputButtons.Back) + } + // Third and subsequent call + while (true) { + yield async (picker: TestQuickPick) => { + await picker.untilReady() + picker.acceptItem(picker.items[1]) + } + } + })() + + return (picker: TestQuickPick) => { + const next = generator.next().value + return next(picker) + } + })() + ) + .build() + +// 2. Run your wizard class +const result = await wizard.run() + +// 3. Assert your tests +prompterTester.assertCallAll() +prompterTester.assertCallOrder('Input Prompter title 1', 1) +``` + ## Module path debugging Node has an environment variable `NODE_DEBUG=module` that helps to debug module imports. This can be helpful on windows, which can load node modules into uppercase or lower case drive letters, depending on the drive letter of the parent module. diff --git a/packages/core/src/shared/ui/wizardPrompter.ts b/packages/core/src/shared/ui/wizardPrompter.ts index d4a15c881bf..64668b7340e 100644 --- a/packages/core/src/shared/ui/wizardPrompter.ts +++ b/packages/core/src/shared/ui/wizardPrompter.ts @@ -9,11 +9,11 @@ import { Prompter, PromptResult } from './prompter' /** * Wraps {@link Wizard} object into its own {@link Prompter}, allowing wizards to use other wizards in their flows. - * This is meant to be used exclusively in createWizardPrompter() method of {@link NestedWizard} class. + * This is meant to be used exclusively in createWizardPrompter() method of {@link CompositeWizard} class. * * @remarks * - The WizardPrompter class should never be instantiated with directly. - * - Use createWizardPrompter() method of {@link NestedWizard} when creating a nested wizard prompter for proper state management. + * - Use createWizardPrompter() method of {@link CompositeWizard} when creating a nested wizard prompter for proper state management. * - See examples: * - {@link SingleNestedWizard} * - {@link DoubleNestedWizard} diff --git a/packages/core/src/shared/ui/nestedWizardPrompter.ts b/packages/core/src/shared/wizards/compositeWizard.ts similarity index 90% rename from packages/core/src/shared/ui/nestedWizardPrompter.ts rename to packages/core/src/shared/wizards/compositeWizard.ts index cb3e91c8747..57bf2b90f6d 100644 --- a/packages/core/src/shared/ui/nestedWizardPrompter.ts +++ b/packages/core/src/shared/wizards/compositeWizard.ts @@ -3,16 +3,16 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Wizard, WizardOptions } from '../wizards/wizard' -import { Prompter } from './prompter' -import { WizardPrompter } from './wizardPrompter' +import { Wizard, WizardOptions } from './wizard' +import { Prompter } from '../ui/prompter' +import { WizardPrompter } from '../ui/wizardPrompter' import { createHash } from 'crypto' /** * An abstract class that extends the base Wizard class plus the ability to * use other wizard classes as prompters */ -export abstract class NestedWizard extends Wizard { +export abstract class CompositeWizard extends Wizard { /** * Map to store memoized wizard instances using SHA-256 hashed keys */ diff --git a/packages/core/src/test/shared/wizards/nestedWizard.test.ts b/packages/core/src/test/shared/wizards/compositWizard.test.ts similarity index 98% rename from packages/core/src/test/shared/wizards/nestedWizard.test.ts rename to packages/core/src/test/shared/wizards/compositWizard.test.ts index 00f79fc06f2..44e074fc96c 100644 --- a/packages/core/src/test/shared/wizards/nestedWizard.test.ts +++ b/packages/core/src/test/shared/wizards/compositWizard.test.ts @@ -4,7 +4,7 @@ */ import * as vscode from 'vscode' import { createCommonButtons } from '../../../shared/ui/buttons' -import { NestedWizard } from '../../../shared/ui/nestedWizardPrompter' +import { CompositeWizard } from '../../../shared/wizards/compositeWizard' import { createQuickPick, DataQuickPickItem } from '../../../shared/ui/pickerPrompter' import * as assert from 'assert' import { PrompterTester } from './prompterTester' @@ -40,7 +40,7 @@ export function createTestPrompter(title: string, itemsString: string[]) { return createQuickPick(items, { title: title, buttons: createCommonButtons() }) } -class ChildWizard extends NestedWizard { +class ChildWizard extends CompositeWizard { constructor() { super() this.form.childWizardProp1.bindPrompter(() => @@ -55,7 +55,7 @@ class ChildWizard extends NestedWizard { } } -class SingleNestedWizard extends NestedWizard { +class SingleNestedWizard extends CompositeWizard { constructor() { super() @@ -74,7 +74,7 @@ class SingleNestedWizard extends NestedWizard { } } -class DoubleNestedWizard extends NestedWizard { +class DoubleNestedWizard extends CompositeWizard { constructor() { super() From d46857dae35b65af4fabe0248642c8d085062d5b Mon Sep 17 00:00:00 2001 From: vicheey Date: Fri, 13 Dec 2024 11:04:49 -0800 Subject: [PATCH 2/2] Apply suggestions from code review Co-authored-by: Justin M. Keyes --- docs/arch_develop.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/arch_develop.md b/docs/arch_develop.md index 8ad4832df44..a3ad998d3e1 100644 --- a/docs/arch_develop.md +++ b/docs/arch_develop.md @@ -484,17 +484,17 @@ class ExampleWizard extends Wizard { ### 2. `CompositeWizard` Class -This abstract class extends the `Wizard` class with an addition method to create and manage state of the nested wizards. +`CompositeWizard` extends `Wizard` to create and manage a collection of nested/child wizards. -Extends this class to create a wizard that contains other wizards as part of the prompter flow. -Use `this.createWizardPrompter()` to use another wizard class as a prompter in the main wizard. +Extend this class to create a wizard that contains other wizards as part of a prompter flow. +Use `this.createWizardPrompter()` to use a wizard as a prompter in the `CompositeWizard`. Example: ```ts // Child wizard -class ChildWizard extends Wizards {...} +class ChildWizard extends Wizard {...} // Composite wizard