diff --git a/packages/core/src/dev/activation.ts b/packages/core/src/dev/activation.ts index 7f37b0552eb..b4100b191ce 100644 --- a/packages/core/src/dev/activation.ts +++ b/packages/core/src/dev/activation.ts @@ -411,7 +411,7 @@ async function openStorageFromInput() { title: 'Enter a key', }) } else if (target === 'globalsView') { - return new SkipPrompter('') + return new SkipPrompter() } else if (target === 'globals') { // List all globalState keys in the quickpick menu. const items = globalState @@ -483,7 +483,7 @@ async function resetState() { this.form.key.bindPrompter(({ target }) => { if (target && resettableFeatures.some((f) => f.name === target)) { - return new SkipPrompter('') + return new SkipPrompter() } throw new Error('invalid feature target') }) diff --git a/packages/core/src/shared/ui/common/skipPrompter.ts b/packages/core/src/shared/ui/common/skipPrompter.ts index ab638ee7e18..b99b952de0f 100644 --- a/packages/core/src/shared/ui/common/skipPrompter.ts +++ b/packages/core/src/shared/ui/common/skipPrompter.ts @@ -3,24 +3,17 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { StepEstimator } from '../../wizards/wizard' +import { StepEstimator, WIZARD_SKIP } from '../../wizards/wizard' import { Prompter, PromptResult } from '../prompter' -/** Pseudo-prompter that immediately returns a value (and thus "skips" a step). */ +/** Prompter that returns SKIP control signal to parent wizard */ export class SkipPrompter extends Prompter { - /** - * @param val Value immediately returned by the prompter. - */ - constructor(public readonly val: T) { + constructor() { super() } protected async promptUser(): Promise> { - const promptPromise = new Promise>((resolve) => { - resolve(this.val) - }) - - return await promptPromise + return WIZARD_SKIP } public get recentItem(): any { diff --git a/packages/core/src/shared/ui/nestedWizardPrompter.ts b/packages/core/src/shared/ui/nestedWizardPrompter.ts new file mode 100644 index 00000000000..cb3e91c8747 --- /dev/null +++ b/packages/core/src/shared/ui/nestedWizardPrompter.ts @@ -0,0 +1,63 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Wizard, WizardOptions } from '../wizards/wizard' +import { Prompter } from './prompter' +import { WizardPrompter } from './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 { + /** + * Map to store memoized wizard instances using SHA-256 hashed keys + */ + private wizardInstances: Map = new Map() + + public constructor(options?: WizardOptions) { + super(options) + } + + /** + * Creates a prompter for a wizard instance with memoization. + * + * @template TWizard - The type of wizard, must extend Wizard + * @template TState - The type of state managed by the wizard + * + * @param wizardClass - The wizard class constructor + * @param args - Constructor arguments for the wizard instance + * + * @returns A wizard prompter to be used as prompter + * + * @example + * // Create a prompter for SyncWizard + * const prompter = this.createWizardPrompter( + * SyncWizard, + * template.uri, + * syncUrl + * ) + * + * @remarks + * - Instances are memoized using a SHA-256 hash of the wizard class name and arguments + * - The same wizard instance is reused for identical constructor parameters for restoring wizard prompter + * states during back button click event + */ + protected createWizardPrompter, TState>( + wizardClass: new (...args: any[]) => TWizard, + ...args: ConstructorParameters TWizard> + ): Prompter { + const memoizeKey = createHash('sha256') + .update(wizardClass.name + JSON.stringify(args)) + .digest('hex') + + if (!this.wizardInstances.get(memoizeKey)) { + this.wizardInstances.set(memoizeKey, new wizardClass(...args)) + } + + return new WizardPrompter(this.wizardInstances.get(memoizeKey)) as Prompter + } +} diff --git a/packages/core/src/shared/ui/wizardPrompter.ts b/packages/core/src/shared/ui/wizardPrompter.ts index 76256403375..d4a15c881bf 100644 --- a/packages/core/src/shared/ui/wizardPrompter.ts +++ b/packages/core/src/shared/ui/wizardPrompter.ts @@ -3,19 +3,26 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { StepEstimator, Wizard } from '../wizards/wizard' +import _ from 'lodash' +import { StepEstimator, Wizard, WIZARD_BACK, WIZARD_SKIP } from '../wizards/wizard' import { Prompter, PromptResult } from './prompter' /** - * Wraps {@link Wizard} object into its own {@link Prompter}, allowing wizards to use other - * wizards in their flows. + * 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. + * + * @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. + * - See examples: + * - {@link SingleNestedWizard} + * - {@link DoubleNestedWizard} */ export class WizardPrompter extends Prompter { public get recentItem(): any { return undefined } public set recentItem(response: any) {} - private stepOffset: number = 0 private response: T | undefined @@ -51,6 +58,15 @@ export class WizardPrompter extends Prompter { protected async promptUser(): Promise> { this.response = await this.wizard.run() - return this.response + + if (this.response === undefined) { + return WIZARD_BACK as PromptResult + } else if (_.isEmpty(this.response)) { + return WIZARD_SKIP as PromptResult + } + + return { + ...this.response, + } as PromptResult } } diff --git a/packages/core/src/shared/wizards/stateController.ts b/packages/core/src/shared/wizards/stateController.ts index fc84a4ce13f..bac52f7478c 100644 --- a/packages/core/src/shared/wizards/stateController.ts +++ b/packages/core/src/shared/wizards/stateController.ts @@ -9,7 +9,16 @@ export enum ControlSignal { Retry, Exit, Back, - Continue, + Skip, +} + +/** + * Value for indicating current direction of the wizard + * Created mainly to support skipping prompters + */ +export enum Direction { + Forward, + Backward, } export interface StepResult { @@ -39,9 +48,11 @@ export class StateMachineController { private extraSteps = new Map>() private steps: Branch = [] private internalStep: number = 0 + private direction: Direction public constructor(private state: TState = {} as TState) { this.previousStates = [_.cloneDeep(state)] + this.direction = Direction.Forward } public addStep(step: StepFunction): void { @@ -71,12 +82,14 @@ export class StateMachineController { this.state = this.previousStates.pop()! this.internalStep -= 1 + this.direction = Direction.Backward } protected advanceState(nextState: TState): void { this.previousStates.push(this.state) this.state = nextState this.internalStep += 1 + this.direction = Direction.Forward } protected detectCycle(step: StepFunction): TState | undefined { @@ -105,6 +118,12 @@ export class StateMachineController { } if (isMachineResult(result)) { + if (result.controlSignal === ControlSignal.Skip) { + /** + * Depending on current wizard direction, skip signal get converted to forward or backward control signal + */ + result.controlSignal = this.direction === Direction.Forward ? undefined : ControlSignal.Back + } return result } else { return { nextState: result } diff --git a/packages/core/src/shared/wizards/wizard.ts b/packages/core/src/shared/wizards/wizard.ts index d50399535bb..05d45584802 100644 --- a/packages/core/src/shared/wizards/wizard.ts +++ b/packages/core/src/shared/wizards/wizard.ts @@ -46,10 +46,12 @@ export const WIZARD_RETRY = { // eslint-disable-next-line @typescript-eslint/naming-convention export const WIZARD_BACK = { id: WIZARD_CONTROL, type: ControlSignal.Back, toString: () => makeControlString('Back') } // eslint-disable-next-line @typescript-eslint/naming-convention +export const WIZARD_SKIP = { id: WIZARD_CONTROL, type: ControlSignal.Skip, toString: () => makeControlString('Skip') } +// eslint-disable-next-line @typescript-eslint/naming-convention export const WIZARD_EXIT = { id: WIZARD_CONTROL, type: ControlSignal.Exit, toString: () => makeControlString('Exit') } /** Control signals allow for alterations of the normal wizard flow */ -export type WizardControl = typeof WIZARD_RETRY | typeof WIZARD_BACK | typeof WIZARD_EXIT +export type WizardControl = typeof WIZARD_RETRY | typeof WIZARD_BACK | typeof WIZARD_EXIT | typeof WIZARD_SKIP export function isWizardControl(obj: any): obj is WizardControl { return obj !== undefined && obj.id === WIZARD_CONTROL @@ -269,9 +271,7 @@ export class Wizard>> { if (isValidResponse(answer)) { state.stepCache.picked = prompter.recentItem - } - - if (!isValidResponse(answer)) { + } else { delete state.stepCache.stepOffset } diff --git a/packages/core/src/shared/wizards/wizardForm.ts b/packages/core/src/shared/wizards/wizardForm.ts index 23ca89b2273..7d3a55a2635 100644 --- a/packages/core/src/shared/wizards/wizardForm.ts +++ b/packages/core/src/shared/wizards/wizardForm.ts @@ -25,7 +25,7 @@ interface ContextOptions { * in a single resolution step then they will be added in the order in which they were * bound. */ - showWhen?: (state: WizardState) => boolean + showWhen?: (state: WizardState) => boolean | Promise /** * Sets a default value to the target property. This default is applied to the current state * as long as the property has not been set. @@ -135,7 +135,11 @@ export class WizardForm>> { this.formData.set(key, { ...this.formData.get(key), ...element }) } - public canShowProperty(prop: string, state: TState, defaultState: TState = this.applyDefaults(state)): boolean { + public canShowProperty( + prop: string, + state: TState, + defaultState: TState = this.applyDefaults(state) + ): boolean | Promise { const current = _.get(state, prop) const options = this.formData.get(prop) ?? {} @@ -143,8 +147,18 @@ export class WizardForm>> { return false } - if (options.showWhen !== undefined && !options.showWhen(defaultState as WizardState)) { - return false + if (options.showWhen !== undefined) { + const showStatus = options.showWhen(defaultState as WizardState) + if (showStatus instanceof Promise) { + return showStatus + .then((result) => { + return result + }) + .catch(() => { + return false // Default to not showing if there's an error + }) + } + return showStatus } return options.provider !== undefined diff --git a/packages/core/src/test/shared/wizards/nestedWizard.test.ts b/packages/core/src/test/shared/wizards/nestedWizard.test.ts new file mode 100644 index 00000000000..00f79fc06f2 --- /dev/null +++ b/packages/core/src/test/shared/wizards/nestedWizard.test.ts @@ -0,0 +1,446 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import * as vscode from 'vscode' +import { createCommonButtons } from '../../../shared/ui/buttons' +import { NestedWizard } from '../../../shared/ui/nestedWizardPrompter' +import { createQuickPick, DataQuickPickItem } from '../../../shared/ui/pickerPrompter' +import * as assert from 'assert' +import { PrompterTester } from './prompterTester' +import { TestQuickPick } from '../vscode/quickInput' + +interface ChildWizardForm { + childWizardProp1: string + childWizardProp2: string + childWizardProp3: string +} + +interface SingleNestedWizardForm { + singleNestedWizardProp1: string + singleNestedWizardProp2: string + singleNestedWizardNestedProp: any + singleNestedWizardProp3: string +} + +interface DoubleNestedWizardForm { + doubleNestedWizardProp1: string + doubleNestedWizardProp2: string + doubleNestedWizardNestedProp: any + singleNestedWizardConditionalSkipProps: any + doubleNestedWizardProp3: string +} + +export function createTestPrompter(title: string, itemsString: string[]) { + const items: DataQuickPickItem[] = itemsString.map((s) => ({ + label: s, + data: s, + })) + + return createQuickPick(items, { title: title, buttons: createCommonButtons() }) +} + +class ChildWizard extends NestedWizard { + constructor() { + super() + this.form.childWizardProp1.bindPrompter(() => + createTestPrompter('ChildWizard Prompter 1', ['c1p1', '**', '***']) + ) + this.form.childWizardProp2.bindPrompter(() => + createTestPrompter('ChildWizard Prompter 2', ['c2p1', '**', '***']) + ) + this.form.childWizardProp3.bindPrompter(() => + createTestPrompter('ChildWizard Prompter 3', ['c3p1', '**', '***']) + ) + } +} + +class SingleNestedWizard extends NestedWizard { + constructor() { + super() + + this.form.singleNestedWizardProp1.bindPrompter(() => + createTestPrompter('SingleNestedWizard Prompter 1', ['s1p1', '**', '***']) + ) + this.form.singleNestedWizardProp2.bindPrompter(() => + createTestPrompter('SingleNestedWizard Prompter 2', ['s2p1', '**', '***']) + ) + this.form.singleNestedWizardNestedProp.bindPrompter(() => + this.createWizardPrompter(ChildWizard) + ) + this.form.singleNestedWizardProp3.bindPrompter(() => + createTestPrompter('SingleNestedWizard Prompter 3', ['s3p1', '**', '***']) + ) + } +} + +class DoubleNestedWizard extends NestedWizard { + constructor() { + super() + + this.form.doubleNestedWizardProp1.bindPrompter(() => + createTestPrompter('DoubleNestedWizard Prompter 1', ['d1p1', '**', '***']) + ) + this.form.doubleNestedWizardProp2.bindPrompter(() => + createTestPrompter('DoubleNestedWizard Prompter 2', ['d2p1', '**', '***']) + ) + this.form.doubleNestedWizardNestedProp.bindPrompter(() => + this.createWizardPrompter(SingleNestedWizard) + ) + this.form.doubleNestedWizardProp3.bindPrompter(() => + createTestPrompter('DoubleNestedWizard Prompter 3', ['d3p1', '**', '***']) + ) + } +} + +describe('NestedWizard', () => { + it('return the correct output from nested child wizard', async () => { + /** + * SingleNestedWizard + | + +-- Prompter 1 + | + +-- Prompter 2 + | + +-- ChildWizard + | | + | +-- Prompter 1 + | | + | +-- Prompter 2 + | | + | +-- Prompter 3 + | + +-- Prompter 3 + */ + const expectedCallOrders = [ + 'SingleNestedWizard Prompter 1', + 'SingleNestedWizard Prompter 2', + 'ChildWizard Prompter 1', + 'ChildWizard Prompter 2', + 'ChildWizard Prompter 3', + 'SingleNestedWizard Prompter 3', + ] + const expectedOutput = { + singleNestedWizardProp1: 's1p1', + singleNestedWizardProp2: 's2p1', + singleNestedWizardNestedProp: { + childWizardProp1: 'c1p1', + childWizardProp2: 'c2p1', + childWizardProp3: 'c3p1', + }, + singleNestedWizardProp3: 's3p1', + } + + const prompterTester = setupPrompterTester(expectedCallOrders) + + const parentWizard = new SingleNestedWizard() + const result = await parentWizard.run() + assertWizardOutput(prompterTester, expectedCallOrders, result, expectedOutput) + }) + + it('return the correct output from double nested child wizard', async () => { + /** + * DoubleNestedWizard + | + +-- Prompter 1 + | + +-- Prompter 2 + | + +-- SingleNestedWizard + | | + | +-- Prompter 1 + | | + | +-- Prompter 2 + | | + | +-- ChildWizard + | | | + | | +-- Prompter 1 + | | | + | | +-- Prompter 2 + | | | + | | +-- Prompter 3 + | | + | +-- Prompter 3 + | + +-- Prompter 3 + */ + const expectedCallOrders = [ + 'DoubleNestedWizard Prompter 1', + 'DoubleNestedWizard Prompter 2', + 'SingleNestedWizard Prompter 1', + 'SingleNestedWizard Prompter 2', + 'ChildWizard Prompter 1', + 'ChildWizard Prompter 2', + 'ChildWizard Prompter 3', + 'SingleNestedWizard Prompter 3', + 'DoubleNestedWizard Prompter 3', + ] + const expectedOutput = { + doubleNestedWizardProp1: 'd1p1', + doubleNestedWizardProp2: 'd2p1', + doubleNestedWizardNestedProp: { + singleNestedWizardProp1: 's1p1', + singleNestedWizardProp2: 's2p1', + singleNestedWizardNestedProp: { + childWizardProp1: 'c1p1', + childWizardProp2: 'c2p1', + childWizardProp3: 'c3p1', + }, + singleNestedWizardProp3: 's3p1', + }, + doubleNestedWizardProp3: 'd3p1', + } + + const prompterTester = setupPrompterTester(expectedCallOrders) + + const parentWizard = new DoubleNestedWizard() + const result = await parentWizard.run() + + assertWizardOutput(prompterTester, expectedCallOrders, result, expectedOutput) + }) + + it('regenerates child wizard prompters in correct reverse order when going backward (back button)', async () => { + /** + * DoubleNestedWizard + | + +--> Prompter 1 (1) + | + +--> Prompter 2 (2) + | + | SingleNestedWizard + | | + | +--> Prompter 1 (3) + | | + | +--> Prompter 2 (4) + | | + | | ChildWizard + | | | + | | +--> Prompter 1 (5) + | | | + | | +--> Prompter 2 (6) + | | | + | | +--> Prompter 3 (7) + | | | + | | +--> Prompter 2 (8) <-- Back + | | | + | | +--> Prompter 1 (9) <-- Back + | | | + | | +--> Prompter 2 (10) + | | | + | | +--> Prompter 3 (11) + | | + | +--> Prompter 3 (12) + | + +--> Prompter 3 (13) <-- Back + | + | SingleNestedWizard + | | + | +--> Prompter 3 (14) <-- Back + | | + | | ChildWizard + | | | + | | +--> Prompter 3 (15) + | | + | +--> Prompter 3 (16) + | + +--> Prompter 3 (17) + + */ + const expectedCallOrders = [ + 'DoubleNestedWizard Prompter 1', // 1 + 'DoubleNestedWizard Prompter 2', // 2 + 'SingleNestedWizard Prompter 1', // 3 + 'SingleNestedWizard Prompter 2', // 4 + 'ChildWizard Prompter 1', // 5 + 'ChildWizard Prompter 2', // 6 + 'ChildWizard Prompter 3', // 7 (Back button) + 'ChildWizard Prompter 2', // 8 (Back button) + 'ChildWizard Prompter 1', // 9 + 'ChildWizard Prompter 2', // 10 + 'ChildWizard Prompter 3', // 11 + 'SingleNestedWizard Prompter 3', // 12 + 'DoubleNestedWizard Prompter 3', // 13 (Back button) + 'SingleNestedWizard Prompter 3', // 14 (Back button) + 'ChildWizard Prompter 3', // 15 + 'SingleNestedWizard Prompter 3', // 16 + 'DoubleNestedWizard Prompter 3', // 17 + ] + const prompterTester = PrompterTester.init() + .handleQuickPick('DoubleNestedWizard Prompter 1', (quickPick) => { + // 1st + quickPick.acceptItem(quickPick.items[0]) + }) + .handleQuickPick('DoubleNestedWizard Prompter 2', (quickPick) => { + // 2nd + quickPick.acceptItem(quickPick.items[0]) + }) + .handleQuickPick('SingleNestedWizard Prompter 1', (quickPick) => { + // 3rd + quickPick.acceptItem(quickPick.items[0]) + }) + .handleQuickPick('SingleNestedWizard Prompter 2', (quickPick) => { + // 4th + quickPick.acceptItem(quickPick.items[0]) + }) + .handleQuickPick('ChildWizard Prompter 1', (quickPick) => { + // 5th + // 9th + quickPick.acceptItem(quickPick.items[0]) + }) + .handleQuickPick( + 'ChildWizard Prompter 2', + (() => { + const generator = (function* () { + // 6th + // First call, choose '**' + yield async (picker: TestQuickPick) => { + await picker.untilReady() + assert.strictEqual(picker.items[1].label, '**') + picker.acceptItem(picker.items[1]) + } + // 8th + yield async (picker: TestQuickPick) => { + await picker.untilReady() + picker.pressButton(vscode.QuickInputButtons.Back) + } + // 10th + // Second call and subsequent call, should restore previously selected option (**) + 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) + } + })() + ) + .handleQuickPick( + 'ChildWizard Prompter 3', + (() => { + const generator = (function* () { + // 7th + // First call, check Back Button + yield async (picker: TestQuickPick) => { + await picker.untilReady() + picker.pressButton(vscode.QuickInputButtons.Back) + } + // 11th + // 15th + while (true) { + yield async (picker: TestQuickPick) => { + await picker.untilReady() + picker.acceptItem(picker.items[0]) + } + } + })() + + return (picker: TestQuickPick) => { + const next = generator.next().value + return next(picker) + } + })() + ) + .handleQuickPick( + 'SingleNestedWizard Prompter 3', + (() => { + const generator = (function* () { + // 12th + // First call, choose '***' + yield async (picker: TestQuickPick) => { + await picker.untilReady() + assert.strictEqual(picker.items[2].label, '***') + picker.acceptItem(picker.items[2]) + } + // 14th + yield async (picker: TestQuickPick) => { + await picker.untilReady() + picker.pressButton(vscode.QuickInputButtons.Back) + } + // 16th + // Second call and after should restore previously selected option (**) + while (true) { + yield async (picker: TestQuickPick) => { + await picker.untilReady() + picker.acceptItem(picker.items[2]) + } + } + })() + + return (picker: TestQuickPick) => { + const next = generator.next().value + return next(picker) + } + })() + ) + .handleQuickPick( + 'DoubleNestedWizard Prompter 3', + (() => { + const generator = (function* () { + // 13th + // First call, check Back Button + yield async (picker: TestQuickPick) => { + await picker.untilReady() + picker.pressButton(vscode.QuickInputButtons.Back) + } + // 17th + // Default behavior for any subsequent calls + while (true) { + yield async (picker: TestQuickPick) => { + await picker.untilReady() + picker.acceptItem(picker.items[0]) + } + } + })() + + return (picker: TestQuickPick) => { + const next = generator.next().value + return next(picker) + } + })() + ) + .build() + + const parentWizard = new DoubleNestedWizard() + + const result = await parentWizard.run() + + assertWizardOutput(prompterTester, expectedCallOrders, result, { + doubleNestedWizardProp1: 'd1p1', + doubleNestedWizardProp2: 'd2p1', + doubleNestedWizardNestedProp: { + singleNestedWizardProp1: 's1p1', + singleNestedWizardProp2: 's2p1', + singleNestedWizardNestedProp: { + childWizardProp1: 'c1p1', + childWizardProp2: '**', + childWizardProp3: 'c3p1', + }, + singleNestedWizardProp3: '***', + }, + doubleNestedWizardProp3: 'd3p1', + }) + }) +}) + +function setupPrompterTester(titles: string[]) { + const prompterTester = PrompterTester.init() + titles.forEach((title) => { + prompterTester.handleQuickPick(title, (quickPick) => { + quickPick.acceptItem(quickPick.items[0]) + }) + }) + prompterTester.build() + return prompterTester +} + +function assertWizardOutput(prompterTester: PrompterTester, orderedTitle: string[], result: any, output: any) { + assert.deepStrictEqual(result, output) + orderedTitle.forEach((title, index) => { + prompterTester.assertCallOrder(title, index + 1) + }) +} diff --git a/packages/core/src/test/shared/wizards/wizard.test.ts b/packages/core/src/test/shared/wizards/wizard.test.ts index 0aee4f66502..3644c6050c8 100644 --- a/packages/core/src/test/shared/wizards/wizard.test.ts +++ b/packages/core/src/test/shared/wizards/wizard.test.ts @@ -211,11 +211,11 @@ describe('Wizard', function () { it('binds prompter to (sync AND async) property', async function () { wizard.form.prop1.bindPrompter(() => helloPrompter) - wizard.form.prop3.bindPrompter(async () => new SkipPrompter('helloooo (async)')) + wizard.form.prop3.bindPrompter(async () => new SkipPrompter()) const result = await wizard.run() assert.strictEqual(result?.prop1, 'hello') - assert.strictEqual(result?.prop3, 'helloooo (async)') + assert(!result?.prop3) }) it('initializes state to empty object if not provided', async function () { diff --git a/packages/core/src/test/shared/wizards/wizardTestUtils.ts b/packages/core/src/test/shared/wizards/wizardTestUtils.ts index 5b9f7003b08..76c50f6ae94 100644 --- a/packages/core/src/test/shared/wizards/wizardTestUtils.ts +++ b/packages/core/src/test/shared/wizards/wizardTestUtils.ts @@ -156,8 +156,11 @@ export async function createWizardTester>(wizard: Wizard `No properties of "${propPath}" would be shown` ) case NOT_ASSERT_SHOW: - return () => - failIf(form.canShowProperty(propPath, state), `Property "${propPath}" would be shown`) + return async () => + failIf( + await form.canShowProperty(propPath, state), + `Property "${propPath}" would be shown` + ) case NOT_ASSERT_SHOW_ANY: return assertShowNone(propPath) case ASSERT_VALUE: