diff --git a/ui/app/components/page/namespaces.hbs b/ui/app/components/page/namespaces.hbs index 43ccec6450..8a401469b6 100644 --- a/ui/app/components/page/namespaces.hbs +++ b/ui/app/components/page/namespaces.hbs @@ -4,9 +4,7 @@ }} {{#if (has-feature "Namespaces")}} - {{#if this.showWizard}} - - {{else}} + {{#if this.showPageHeader}} <:breadcrumbs> <:actions> {{#unless @model.namespaces}} + {{#unless this.showWizard}} + + {{/unless}} - {{/unless}} + {{/if}} + {{#if this.showWizard}} + + {{else}} + {{! Show namespace list }} {{#if @model.namespaces}} @@ -120,6 +126,7 @@ {{/if}} {{else}} + {{! Show empty state }} {{#if this.showSetupAlert}} Your current setup is 1 namespace. diff --git a/ui/app/components/page/namespaces.ts b/ui/app/components/page/namespaces.ts index 13a0841175..b5633fddcd 100644 --- a/ui/app/components/page/namespaces.ts +++ b/ui/app/components/page/namespaces.ts @@ -57,6 +57,7 @@ export default class PageNamespacesComponent extends Component { @tracked query; @tracked nsToDelete = null; @tracked showSetupAlert = false; + @tracked isIntroModal = false; wizardId = 'namespace'; @@ -80,6 +81,12 @@ export default class PageNamespacesComponent extends Component { return this.namespace.path; } + // Show header and breadcrumbs when viewing the intro page or during the list view. + // Do not show during Guided Start as that has its own header + get showPageHeader() { + return !this.showWizard || this.wizard.isIntroVisible(this.wizardId); + } + get showWizard() { // Show when there are no existing namespaces and it is not in a dismissed state return !this.wizard.isDismissed(this.wizardId) && !this.args.model.namespaces?.length; @@ -136,9 +143,10 @@ export default class PageNamespacesComponent extends Component { } @action - enterGuidedStart() { + showIntroPage() { // Reset the wizard dismissal state to allow re-entering the wizard this.wizard.reset(this.wizardId); + this.isIntroModal = true; } @action handlePageChange() { diff --git a/ui/app/components/wizard/guided-setup.hbs b/ui/app/components/wizard/guided-start.hbs similarity index 92% rename from ui/app/components/wizard/guided-setup.hbs rename to ui/app/components/wizard/guided-start.hbs index d2cb8c42d2..bf1e10c0aa 100644 --- a/ui/app/components/wizard/guided-setup.hbs +++ b/ui/app/components/wizard/guided-start.hbs @@ -3,9 +3,9 @@ SPDX-License-Identifier: BUSL-1.1 }} - + -
+
{ +export default class GuidedStart extends Component { get isFinalStep() { return this.args.currentStep === this.args.steps.length - 1; } diff --git a/ui/app/components/wizard/index.hbs b/ui/app/components/wizard/index.hbs index 4e30dd872d..6b29ecc539 100644 --- a/ui/app/components/wizard/index.hbs +++ b/ui/app/components/wizard/index.hbs @@ -3,39 +3,27 @@ SPDX-License-Identifier: BUSL-1.1 }} -{{#if (and (has-block "welcome") this.showWelcome)}} -
- - <:welcome> - {{yield to="welcome"}} - - - - - - - -
+{{#if (and (has-block "intro") this.isIntroVisible)}} + {{! pass @onDismiss on for modal closure via clicking outside modal @onClose}} + + <:body> + {{yield to="intro"}} + + <:actions> + {{yield to="introActions"}} + + {{else}} - <:exit> {{#if (has-block "exit")}} @@ -51,5 +39,5 @@ {{/if}} - + {{/if}} \ No newline at end of file diff --git a/ui/app/components/wizard/index.ts b/ui/app/components/wizard/index.ts index 905859a2f0..11cb9f5c2d 100644 --- a/ui/app/components/wizard/index.ts +++ b/ui/app/components/wizard/index.ts @@ -3,10 +3,27 @@ * SPDX-License-Identifier: BUSL-1.1 */ +import { service } from '@ember/service'; import Component from '@glimmer/component'; -import { tracked } from '@glimmer/tracking'; +import type WizardService from 'vault/services/wizard'; interface Args { + /** + * The unique identifier for the wizard used for handling wizard dismissal and intro visibility state + */ + wizardId: string; + /** + * Whether the intro page is in the default view or in modal view depending on how it is triggered + */ + isModal: boolean; + /** + * Title of the wizard + */ + title: string; + /** + * Whether the current step allows proceeding to the next step + */ + canProceed: boolean; /** * The active step. Steps are zero-indexed. */ @@ -20,27 +37,24 @@ interface Args { */ onDismiss: CallableFunction; /** - * Callback to update the current step when navigating backwards or - * forwards through the wizard + * Whether the current step allows proceeding to the next step */ onStepChange: CallableFunction; - /** - * Whether the current step allows proceeding to the next step - */ - canProceed?: boolean; /** * State tracked across steps. */ - wizardState?: unknown; + wizardState: unknown; /** * Callback to update state tracked across steps. */ - updateWizardState?: CallableFunction; + updateWizardState: CallableFunction; } -// each wizard implementation can track whether the user has already dismissed the wizard via local storage -export const DISMISSED_WIZARD_KEY = 'dismissed-wizards'; +export default class WizardComponent extends Component { + @service declare readonly wizard: WizardService; -export default class Wizard extends Component { - @tracked showWelcome = true; + get isIntroVisible(): boolean { + // If wizardId is provided, use the wizard service to check intro visibility + return this.wizard.isIntroVisible(this.args.wizardId); + } } diff --git a/ui/app/components/wizard/intro-content.hbs b/ui/app/components/wizard/intro-content.hbs new file mode 100644 index 0000000000..51453fc696 --- /dev/null +++ b/ui/app/components/wizard/intro-content.hbs @@ -0,0 +1,22 @@ +{{! + Copyright IBM Corp. 2016, 2025 + SPDX-License-Identifier: BUSL-1.1 +}} + + + + + {{#if @isOptional}} + + {{/if}} + + + {{@description}} + + {{yield to="features"}} + + + + {{@imageAlt}} + {{@imageCaption}} + \ No newline at end of file diff --git a/ui/app/components/wizard/intro-content/feature.hbs b/ui/app/components/wizard/intro-content/feature.hbs new file mode 100644 index 0000000000..65cd338f9b --- /dev/null +++ b/ui/app/components/wizard/intro-content/feature.hbs @@ -0,0 +1,11 @@ +{{! + Copyright IBM Corp. 2016, 2025 + SPDX-License-Identifier: BUSL-1.1 +}} + + + + + {{yield}} + + \ No newline at end of file diff --git a/ui/app/components/wizard/intro.hbs b/ui/app/components/wizard/intro.hbs new file mode 100644 index 0000000000..8a37c185c2 --- /dev/null +++ b/ui/app/components/wizard/intro.hbs @@ -0,0 +1,28 @@ +{{! + Copyright IBM Corp. 2016, 2025 + SPDX-License-Identifier: BUSL-1.1 +}} +{{#if @isModal}} + + Welcome to {{@title}} + + + {{yield to="body"}} + + + + {{yield to="actions"}} + + +{{else}} + + + Welcome to + {{@title}} + + + {{yield to="body"}} + + {{yield to="actions"}} + +{{/if}} \ No newline at end of file diff --git a/ui/app/components/wizard/namespaces/intro.hbs b/ui/app/components/wizard/namespaces/intro.hbs new file mode 100644 index 0000000000..fef95f0249 --- /dev/null +++ b/ui/app/components/wizard/namespaces/intro.hbs @@ -0,0 +1,29 @@ +{{! + Copyright IBM Corp. 2016, 2025 + SPDX-License-Identifier: BUSL-1.1 +}} + + <:features> + + Use for multi-tenancy: strict + administrative and configuration isolation + between business units and client environments. + + + Namespaces allow you to segment your cluster into + separate logical partitions, with isolated data, policy, and tokens. + + + Namespaces should not be used like folders. Use them for + large-scale, highly-regulated + environments. + + + \ No newline at end of file diff --git a/ui/app/components/wizard/namespaces/namespace-wizard.hbs b/ui/app/components/wizard/namespaces/namespace-wizard.hbs index 31be720699..64e951a4aa 100644 --- a/ui/app/components/wizard/namespaces/namespace-wizard.hbs +++ b/ui/app/components/wizard/namespaces/namespace-wizard.hbs @@ -4,18 +4,39 @@ }} - <:welcome> - - + <:intro> + + + <:introActions> + + + + + + + {{! The yielded blocks below are used in the Guided Start }} <:exit> {{#if this.shouldShowExitButton}} diff --git a/ui/app/components/wizard/namespaces/namespace-wizard.ts b/ui/app/components/wizard/namespaces/namespace-wizard.ts index c1a41f2fb6..7229552d90 100644 --- a/ui/app/components/wizard/namespaces/namespace-wizard.ts +++ b/ui/app/components/wizard/namespaces/namespace-wizard.ts @@ -24,6 +24,7 @@ const DEFAULT_STEPS = [ ]; interface Args { + isIntroModal: boolean; onRefresh: CallableFunction; } @@ -42,6 +43,7 @@ export default class WizardNamespacesWizardComponent extends Component { @service declare readonly wizard: WizardService; @service declare namespace: NamespaceService; + @tracked currentStep = 0; @tracked steps = DEFAULT_STEPS; @tracked wizardState: WizardState = { securityPolicyChoice: null, @@ -50,7 +52,6 @@ export default class WizardNamespacesWizardComponent extends Component { creationMethod: null, codeSnippet: null, }; - @tracked currentStep = 0; methods = CreationMethod; policy = SecurityPolicy; @@ -132,6 +133,11 @@ export default class WizardNamespacesWizardComponent extends Component { await this.args.onRefresh(); } + @action + onIntroChange(visible: boolean) { + this.wizard.setIntroVisible(this.wizardId, visible); + } + @action async createNamespacesFromWizard() { try { diff --git a/ui/app/components/wizard/namespaces/welcome.hbs b/ui/app/components/wizard/namespaces/welcome.hbs deleted file mode 100644 index 3b2c9ac179..0000000000 --- a/ui/app/components/wizard/namespaces/welcome.hbs +++ /dev/null @@ -1,38 +0,0 @@ -{{! - Copyright IBM Corp. 2016, 2025 - SPDX-License-Identifier: BUSL-1.1 -}} - - - - Welcome to Namespaces - -
- - -
- Namespaces let you create secure, isolated environments where - independent teams can manage their own secrets engines, auth methods, and policies within a single Vault cluster. -
- - Use for multi-tenancy: strict - administrative and configuration isolation - between business units and client environments. -
-
- - Namespaces allow you to segment your cluster into - separate logical partitions, with isolated data, policy, and tokens. -
-
- - Namespaces should not be used like folders. Use them for - large-scale, highly-regulated - environments. -
-
-
- namespace hierarchy example - Namespaces provide necessary isolation based on your company’s organization and - access requirements. -
\ No newline at end of file diff --git a/ui/app/components/wizard/welcome.hbs b/ui/app/components/wizard/welcome.hbs deleted file mode 100644 index 7f923230e3..0000000000 --- a/ui/app/components/wizard/welcome.hbs +++ /dev/null @@ -1,12 +0,0 @@ -{{! - Copyright IBM Corp. 2016, 2025 - SPDX-License-Identifier: BUSL-1.1 -}} - -
- - - {{yield to="welcome"}} - - -
\ No newline at end of file diff --git a/ui/app/services/wizard.ts b/ui/app/services/wizard.ts index 86ae5713fb..7c2098fd93 100644 --- a/ui/app/services/wizard.ts +++ b/ui/app/services/wizard.ts @@ -17,6 +17,7 @@ const DISMISSED_WIZARD_KEY = 'dismissed-wizards'; */ export default class WizardService extends Service { @tracked dismissedWizards: string[] = this.loadDismissedWizards(); + @tracked introVisibleState: Record = {}; /** * Load dismissed wizards from localStorage @@ -53,6 +54,8 @@ export default class WizardService extends Service { reset(wizardId: string): void { this.dismissedWizards = this.dismissedWizards.filter((id: string) => id !== wizardId); localStorage.setItem(DISMISSED_WIZARD_KEY, this.dismissedWizards); + // Reset intro visibility when wizard is reset + this.setIntroVisible(wizardId, true); } /** @@ -61,5 +64,33 @@ export default class WizardService extends Service { resetAll(): void { this.dismissedWizards = []; localStorage.removeItem(DISMISSED_WIZARD_KEY); + this.introVisibleState = {}; + } + + /** + * Check if the intro is visible for a specific wizard + * @param wizardId - The unique identifier for the wizard + * @returns true if the intro is visible, false otherwise (defaults to true if wizard not dismissed, false if dismissed) + */ + isIntroVisible(wizardId: string): boolean { + // If intro visibility has been explicitly set, use that value + if (this.introVisibleState[wizardId] !== undefined) { + return this.introVisibleState[wizardId]; + } + // Otherwise, default to true if wizard is not dismissed (first time showing) + // and false if wizard is dismissed + return !this.isDismissed(wizardId); + } + + /** + * Set the intro visibility state for a specific wizard + * @param wizardId - The unique identifier for the wizard + * @param visible - Whether the intro should be visible + */ + setIntroVisible(wizardId: string, visible: boolean): void { + this.introVisibleState = { + ...this.introVisibleState, + [wizardId]: visible, + }; } } diff --git a/ui/app/styles/components/wizard.scss b/ui/app/styles/components/wizard.scss index c68642ed9d..996340a7ea 100644 --- a/ui/app/styles/components/wizard.scss +++ b/ui/app/styles/components/wizard.scss @@ -7,11 +7,14 @@ */ .wizard { - @extend .is-flex-column; - @extend .is-flex-grow-1; + &.guided-start { + @extend .is-flex-column; + @extend .is-flex-grow-1; + } + + &.intro { + @extend .top-margin-32; - &.welcome { - @extend .has-top-margin-xxl; svg { flex-shrink: 0; } diff --git a/ui/public/images/namespaces-welcome.png b/ui/public/images/namespaces-intro.png similarity index 100% rename from ui/public/images/namespaces-welcome.png rename to ui/public/images/namespaces-intro.png diff --git a/ui/tests/integration/components/page/namespaces-wizard-test.js b/ui/tests/integration/components/page/namespaces-wizard-test.js index d1060e8424..fe89a412c0 100644 --- a/ui/tests/integration/components/page/namespaces-wizard-test.js +++ b/ui/tests/integration/components/page/namespaces-wizard-test.js @@ -13,10 +13,10 @@ import { GENERAL } from 'vault/tests/helpers/general-selectors'; const SELECTORS = { content: '[data-test-content]', - guidedSetup: '[data-test-guided-setup]', + guidedStart: '[data-test-guided-start]', stepTitle: '[data-test-step-title]', tree: '[data-test-tree]', - welcome: '[data-test-welcome]', + intro: '[data-test-intro]', inputRow: (index) => (index ? `[data-test-input-row="${index}"]` : '[data-test-input-row]'), }; @@ -44,12 +44,12 @@ module('Integration | Component | page/namespaces | Namespace Wizard', function test('it shows wizard when no namespaces exist', async function (assert) { await this.renderComponent(); - assert.dom(SELECTORS.welcome).exists('Wizard welcome is rendered'); + assert.dom(SELECTORS.intro).exists('Wizard intro is rendered'); }); test('it progresses through wizard steps with strict policy', async function (assert) { await this.renderComponent(); - await click(GENERAL.button('Guided setup')); + await click(GENERAL.button('Guided start')); // Step 1: Choose security policy assert.dom(GENERAL.button('Next')).isDisabled('Next button disabled with no policy choice'); @@ -70,7 +70,7 @@ module('Integration | Component | page/namespaces | Namespace Wizard', function test('it skips step 2 with flexible policy', async function (assert) { await this.renderComponent(); - await click(GENERAL.button('Guided setup')); + await click(GENERAL.button('Guided start')); // Step 1: Choose flexible policy await click(GENERAL.radioByAttr('flexible')); @@ -84,7 +84,7 @@ module('Integration | Component | page/namespaces | Namespace Wizard', function test('it shows different code snippets per creation method option', async function (assert) { await this.renderComponent(); - await click(GENERAL.button('Guided setup')); + await click(GENERAL.button('Guided start')); await click(GENERAL.radioByAttr('strict')); await click(GENERAL.button('Next')); @@ -116,7 +116,7 @@ module('Integration | Component | page/namespaces | Namespace Wizard', function test('it allows adding and removing blocks, org, and project inputs', async function (assert) { await this.renderComponent(); - await click(GENERAL.button('Guided setup')); + await click(GENERAL.button('Guided start')); await click(GENERAL.radioByAttr('strict')); await click(GENERAL.button('Next')); @@ -147,7 +147,7 @@ module('Integration | Component | page/namespaces | Namespace Wizard', function .doesNotExist('Second org input was removed'); }); - test('it dismisses from the welcome page', async function (assert) { + test('it dismisses from the intro page', async function (assert) { await this.renderComponent(); assert.false(this.wizardService.isDismissed('namespace'), 'Wizard is not dismissed initially'); @@ -158,9 +158,9 @@ module('Integration | Component | page/namespaces | Namespace Wizard', function assert.true(this.refreshSpy.calledOnce, 'onRefresh callback was called'); }); - test('it dismisses from the guided setup', async function (assert) { + test('it dismisses from the Guided start', async function (assert) { await this.renderComponent(); - await click(GENERAL.button('Guided setup')); + await click(GENERAL.button('Guided start')); assert.false(this.wizardService.isDismissed('namespace'), 'Wizard is not dismissed initially'); @@ -172,7 +172,7 @@ module('Integration | Component | page/namespaces | Namespace Wizard', function test('it dismisses after completing flexible policy flow', async function (assert) { await this.renderComponent(); - await click(GENERAL.button('Guided setup')); + await click(GENERAL.button('Guided start')); await click(GENERAL.radioByAttr('flexible')); await click(GENERAL.button('Next')); @@ -186,7 +186,7 @@ module('Integration | Component | page/namespaces | Namespace Wizard', function test('it shows tree chart only when there are multiple globals, orgs, or projects', async function (assert) { await this.renderComponent(); - await click(GENERAL.button('Guided setup')); + await click(GENERAL.button('Guided start')); await click(GENERAL.radioByAttr('strict')); await click(GENERAL.button('Next')); diff --git a/ui/tests/integration/components/wizard-test.js b/ui/tests/integration/components/wizard-test.js index da1e993c0a..298a1af7ac 100644 --- a/ui/tests/integration/components/wizard-test.js +++ b/ui/tests/integration/components/wizard-test.js @@ -5,138 +5,181 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; -import { render, click } from '@ember/test-helpers'; +import { render, click, waitFor } from '@ember/test-helpers'; import sinon from 'sinon'; import hbs from 'htmlbars-inline-precompile'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; const SELECTORS = { - welcome: '[data-test-welcome]', - guidedSetup: '[data-test-guided-setup]', + intro: '[data-test-intro]', + guidedStart: '[data-test-guided-start]', }; module('Integration | Component | Wizard', function (hooks) { setupRenderingTest(hooks); hooks.beforeEach(function () { + this.canProceed = true; + this.currentStep = 0; + this.isModal = false; this.steps = [ { title: 'First step' }, { title: 'Another stage' }, { title: 'Almost done' }, { title: 'Finale' }, ]; - this.currentStep = 0; - this.canProceed = true; - this.welcomeDocLink = 'test'; + this.title = 'Example Wizard'; + this.wizardState = undefined; + this.updateWizardState = sinon.spy(); this.onDismiss = sinon.spy(); this.onStepChange = sinon.spy(); + this.wizardId = 'test-wizard'; + this.wizardService = this.owner.lookup('service:wizard'); + this.wizardService.setIntroVisible(this.wizardId, false); + + this.renderComponent = () => { + return render(hbs` + + <:intro> +
Some intro content
+ + <:introActions> +
Some actions
+ + <:submit> + + + <:exit> + + +
`); + }; }); - test('it shows welcome content initially, then hides it when entering wizard', async function (assert) { - await render(hbs` - <:welcome> -
Some welcome content
- -
`); + test('it shows intro content initially, then hides it when entering wizard', async function (assert) { + this.wizardService.setIntroVisible(this.wizardId, true); + await this.renderComponent(); - // Assert welcome content is rendered and guided setup content is not - assert.dom(SELECTORS.welcome).exists('Welcome content is rendered initially'); - assert.dom(SELECTORS.welcome).hasTextContaining('Some welcome content'); + // Assert intro content is rendered and guided start content is not + assert.dom(SELECTORS.intro).exists('intro content is rendered initially'); + assert.dom(SELECTORS.intro).hasTextContaining('Some intro content'); assert - .dom(SELECTORS.guidedSetup) - .doesNotExist('guidedSetup content is not rendered when welcome is displayed'); + .dom(SELECTORS.guidedStart) + .doesNotExist('guidedStart content is not rendered when intro is displayed'); - await click(GENERAL.button('Guided setup')); + // Use wizard service to hide the intro + this.wizardService.setIntroVisible(this.wizardId, false); + await waitFor(SELECTORS.guidedStart); - // Assert welcome content is no longer rendered and that guided setup content is rendered - assert.dom(SELECTORS.welcome).doesNotExist('Welcome content is hidden after entering wizard'); - assert.dom(SELECTORS.guidedSetup).exists('guidedSetup content is now rendered'); - assert.dom(SELECTORS.guidedSetup).hasTextContaining('First step'); + // Assert intro content is no longer rendered and that guided start content is rendered + assert.dom(SELECTORS.intro).doesNotExist('intro content is hidden after entering wizard'); + assert.dom(SELECTORS.guidedStart).exists('guidedStart content is now rendered'); + assert.dom(SELECTORS.guidedStart).hasTextContaining('First step'); }); test('it shows custom submit block when provided', async function (assert) { - // Go to final step + // Start wizard and go to final step this.currentStep = 3; - this.onCustomSubmit = sinon.spy(); - await render(hbs` - <:submit> - - - `); + await this.renderComponent(); assert.dom('[data-test-custom-submit]').exists('Custom submit button is rendered'); assert.dom(GENERAL.submitButton).doesNotExist('Default submit button is not rendered'); - await click('[data-test-custom-submit]'); - assert.true(this.onCustomSubmit.calledOnce, 'Custom submit handler is called'); }); test('it shows default submit button when custom submit block is not provided', async function (assert) { - // Go to final step + // Start wizard and go to final step this.currentStep = 3; - await render(hbs` - <:quickstart> -
Quickstart content
- -
`); + await render(hbs` + + <:intro> +
Some intro content
+ + <:introActions> +
Some actions
+ +
`); assert .dom(GENERAL.submitButton) .exists('Default submit button is rendered when no custom submit provided'); }); + test('it shows custom exit block when provided', async function (assert) { + await this.renderComponent(); + + assert.dom('[data-test-custom-exit]').exists('Custom exit button is rendered'); + assert.dom(GENERAL.cancelButton).doesNotExist('Default exit button is not rendered'); + }); + + test('it shows default exit button when custom exit block is not provided', async function (assert) { + await render(hbs` + + <:intro> +
Some intro content
+ + <:introActions> +
Some actions
+ +
`); + + assert.dom(GENERAL.cancelButton).exists('Default exit button is rendered when no custom exit provided'); + await click(GENERAL.cancelButton); + assert.true(this.onDismiss.calledOnce, 'onDismiss is called when exit button is clicked'); + }); + test('it renders next button when not on final step', async function (assert) { - await render(hbs` - `); + await this.renderComponent(); assert.dom(GENERAL.button('Next')).exists('Next button is rendered when not on final step'); - assert.dom(GENERAL.submitButton).doesNotExist('Submit button is not rendered when not on final step'); + assert + .dom('[data-test-custom-submit]') + .doesNotExist('Custom submit button is not rendered when not on final step'); await click(GENERAL.button('Next')); assert.true(this.onStepChange.calledOnce, 'onStepChange is called'); // Go to final step this.set('currentStep', 3); - assert.dom(GENERAL.button('next')).doesNotExist('Next button is not rendered when on the final step'); - assert.dom(GENERAL.submitButton).exists('Submit button is rendered on final step'); + assert.dom(GENERAL.button('Next')).doesNotExist('Next button is not rendered when on the final step'); + assert.dom('[data-test-custom-submit]').exists('Custom submit button is rendered on final step'); }); test('it renders back button when not on first step', async function (assert) { - await render(hbs` - `); + await this.renderComponent(); assert.dom(GENERAL.backButton).doesNotExist('Back button is not rendered on the first step'); this.set('currentStep', 2); @@ -144,20 +187,4 @@ module('Integration | Component | Wizard', function (hooks) { await click(GENERAL.backButton); assert.true(this.onStepChange.calledOnce, 'onStepChange is called'); }); - - test('it dismisses wizard when exit button is clicked within guided setup', async function (assert) { - await render(hbs` - `); - - assert.dom(GENERAL.cancelButton).exists('Exit button is shown within guided setup'); - await click(GENERAL.cancelButton); - assert.true(this.onDismiss.calledOnce, 'onDismiss is called when exit button is clicked'); - }); }); diff --git a/ui/tests/unit/services/wizard-test.js b/ui/tests/unit/services/wizard-test.js index 1a45f88bb2..204ef25961 100644 --- a/ui/tests/unit/services/wizard-test.js +++ b/ui/tests/unit/services/wizard-test.js @@ -178,4 +178,58 @@ module('Unit | Service | wizard', function (hooks) { assert.true(this.removeItemStub.calledWith(DISMISSED_WIZARD_KEY), 'removes key from localStorage'); }); }); + + module('#isIntroVisible', function () { + test('returns true by default when wizard not dismissed and intro visibility not set', function (assert) { + this.service.dismissedWizards = []; + + assert.true( + this.service.isIntroVisible('onboarding'), + 'returns true for non-dismissed wizard with unset visibility' + ); + }); + + test('returns false by default when wizard is dismissed', function (assert) { + this.service.dismissedWizards = ['onboarding']; + + assert.false(this.service.isIntroVisible('onboarding'), 'returns false for dismissed wizard'); + }); + + test('returns true when intro visibility is set to true', function (assert) { + this.service.introVisibleState = { onboarding: true }; + assert.true(this.service.isIntroVisible('onboarding'), 'returns true when set'); + }); + + test('returns false when intro visibility is set to false', function (assert) { + this.service.introVisibleState = { onboarding: false }; + + assert.false(this.service.isIntroVisible('onboarding'), 'returns false when set to false'); + }); + }); + + module('#setIntroVisible', function () { + test('sets intro visibility to false', function (assert) { + this.service.setIntroVisible('onboarding', false); + + assert.false(this.service.introVisibleState.onboarding, 'sets intro visibility to false in state'); + assert.false(this.service.isIntroVisible('onboarding'), 'isIntroVisible returns false'); + }); + + test('sets intro visibility to true', function (assert) { + this.service.setIntroVisible('onboarding', true); + + assert.true(this.service.introVisibleState.onboarding, 'sets intro visibility to true in state'); + assert.true(this.service.isIntroVisible('onboarding'), 'isIntroVisible returns true'); + }); + + test('handles multiple wizards independently', function (assert) { + this.service.setIntroVisible('wizard1', false); + this.service.setIntroVisible('wizard2', true); + this.service.setIntroVisible('wizard3', false); + + assert.false(this.service.isIntroVisible('wizard1'), 'wizard1 is not visible'); + assert.true(this.service.isIntroVisible('wizard2'), 'wizard2 is visible'); + assert.false(this.service.isIntroVisible('wizard3'), 'wizard3 is not visible'); + }); + }); });