diff --git a/docs/arch_develop.md b/docs/arch_develop.md index 4880d5493c5..a3ad998d3e1 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 + +`CompositeWizard` extends `Wizard` to create and manage a collection of nested/child wizards. + +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 Wizard {...} + + +// 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()