Skip to content

Commit

Permalink
refactor(wizard): rename NestedWizard, update docs #6229
Browse files Browse the repository at this point in the history
  • Loading branch information
vicheey authored Dec 13, 2024
1 parent 96ce5ae commit 0609e5b
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 11 deletions.
103 changes: 102 additions & 1 deletion docs/arch_develop.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -482,6 +482,41 @@ class ExampleWizard extends Wizard<ExampleState> {
}
```

### 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<ChildWizardForm> {...}


// Composite wizard
interface SingleNestedWizardForm {
...
singleNestedWizardNestedProp: string
...
}

class SingleNestedWizard extends CompositeWizard<SingleNestedWizardForm> {
constructor() {
super()
...
this.form.singleNestedWizardNestedProp.bindPrompter(() =>
this.createWizardPrompter<ChildWizard, ChildWizardForm>(ChildWizard)
)
...
}
}

```

### Executing

Wizards can be ran by calling the async `run` method:
Expand All @@ -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
Expand All @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/shared/ui/wizardPrompter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> extends Wizard<T> {
export abstract class CompositeWizard<T> extends Wizard<T> {
/**
* Map to store memoized wizard instances using SHA-256 hashed keys
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -40,7 +40,7 @@ export function createTestPrompter(title: string, itemsString: string[]) {
return createQuickPick(items, { title: title, buttons: createCommonButtons() })
}

class ChildWizard extends NestedWizard<ChildWizardForm> {
class ChildWizard extends CompositeWizard<ChildWizardForm> {
constructor() {
super()
this.form.childWizardProp1.bindPrompter(() =>
Expand All @@ -55,7 +55,7 @@ class ChildWizard extends NestedWizard<ChildWizardForm> {
}
}

class SingleNestedWizard extends NestedWizard<SingleNestedWizardForm> {
class SingleNestedWizard extends CompositeWizard<SingleNestedWizardForm> {
constructor() {
super()

Expand All @@ -74,7 +74,7 @@ class SingleNestedWizard extends NestedWizard<SingleNestedWizardForm> {
}
}

class DoubleNestedWizard extends NestedWizard<DoubleNestedWizardForm> {
class DoubleNestedWizard extends CompositeWizard<DoubleNestedWizardForm> {
constructor() {
super()

Expand Down

0 comments on commit 0609e5b

Please sign in to comment.