From 614f060dd56aaa40283803d09e203091dba74406 Mon Sep 17 00:00:00 2001 From: Ajit Mehrotra Date: Sun, 15 Mar 2026 16:17:15 -0400 Subject: [PATCH 1/5] fix(onboarding): clear draft only on explicit exit - Purpose: decouple onboarding draft cleanup from summary apply success so draft lifecycle follows explicit user exit paths. - Before: the summary step cleared onboardingDraft only after completeOnboarding succeeded, while overview skip and modal exit used different cleanup timing. - Problem: local draft persistence depended on API completion behavior instead of whether the user actually stayed in or left onboarding. - Change: remove draft cleanup from the summary apply path and clear immediately when overview skip triggers an explicit exit. - How it works: summary now preserves onboardingDraft while the flow continues to the result dialog and next step, and overview skip clears storage before attempting completion so failures do not keep stale draft state around. - Verification: added focused tests covering successful summary apply, failed summary completion, and explicit skip cleanup behavior. --- .../Onboarding/OnboardingOverviewStep.test.ts | 25 ++++++++++++++++++- .../Onboarding/OnboardingSummaryStep.test.ts | 16 ++++++++++++ .../steps/OnboardingOverviewStep.vue | 3 ++- .../steps/OnboardingSummaryStep.vue | 2 -- 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/web/__test__/components/Onboarding/OnboardingOverviewStep.test.ts b/web/__test__/components/Onboarding/OnboardingOverviewStep.test.ts index f7a6bd5ddd..facb901888 100644 --- a/web/__test__/components/Onboarding/OnboardingOverviewStep.test.ts +++ b/web/__test__/components/Onboarding/OnboardingOverviewStep.test.ts @@ -8,6 +8,7 @@ import { createTestI18n } from '../../utils/i18n'; const { completeOnboardingMock, + cleanupOnboardingStorageMock, refetchOnboardingMock, partnerInfoRef, isFreshInstallRef, @@ -17,6 +18,7 @@ const { themeRef, } = vi.hoisted(() => ({ completeOnboardingMock: vi.fn().mockResolvedValue({}), + cleanupOnboardingStorageMock: vi.fn(), refetchOnboardingMock: vi.fn().mockResolvedValue({}), partnerInfoRef: { value: { @@ -90,7 +92,7 @@ vi.mock('@/store/theme', () => ({ })); vi.mock('@/components/Onboarding/store/onboardingStorageCleanup', () => ({ - cleanupOnboardingStorage: vi.fn(), + cleanupOnboardingStorage: cleanupOnboardingStorageMock, })); describe('OnboardingOverviewStep', () => { @@ -140,4 +142,25 @@ describe('OnboardingOverviewStep', () => { expect(updatedImg.attributes('src')).toBe(limitlessImage); expect(updatedImg.attributes('alt')).toBe('Limitless Possibilities'); }); + + it('clears onboarding draft immediately when skipping setup', async () => { + const wrapper = mountComponent(); + + await wrapper.findAll('button')[1]?.trigger('click'); + + expect(cleanupOnboardingStorageMock).toHaveBeenCalledWith({ + clearTemporaryBypassSessionState: true, + }); + }); + + it('still clears onboarding draft when skip completion fails', async () => { + completeOnboardingMock.mockRejectedValueOnce(new Error('offline')); + const wrapper = mountComponent(); + + await wrapper.findAll('button')[1]?.trigger('click'); + + expect(cleanupOnboardingStorageMock).toHaveBeenCalledWith({ + clearTemporaryBypassSessionState: true, + }); + }); }); diff --git a/web/__test__/components/Onboarding/OnboardingSummaryStep.test.ts b/web/__test__/components/Onboarding/OnboardingSummaryStep.test.ts index 1b2d14d6e9..201c12a854 100644 --- a/web/__test__/components/Onboarding/OnboardingSummaryStep.test.ts +++ b/web/__test__/components/Onboarding/OnboardingSummaryStep.test.ts @@ -41,6 +41,7 @@ const { installLanguageMock, installPluginMock, submitInternalBootCreationMock, + cleanupOnboardingStorageMock, useMutationMock, useQueryMock, } = vi.hoisted(() => ({ @@ -98,6 +99,7 @@ const { installLanguageMock: vi.fn(), installPluginMock: vi.fn(), submitInternalBootCreationMock: vi.fn(), + cleanupOnboardingStorageMock: vi.fn(), useMutationMock: vi.fn(), useQueryMock: vi.fn(), })); @@ -170,6 +172,10 @@ vi.mock('@/components/Onboarding/store/onboardingStatus', () => ({ }), })); +vi.mock('@/components/Onboarding/store/onboardingStorageCleanup', () => ({ + cleanupOnboardingStorage: cleanupOnboardingStorageMock, +})); + vi.mock('@/components/Onboarding/composables/usePluginInstaller', () => ({ INSTALL_OPERATION_TIMEOUT_CODE: 'INSTALL_OPERATION_TIMEOUT', default: () => ({ @@ -887,6 +893,7 @@ describe('OnboardingSummaryStep', () => { assertExpected: (wrapper: ReturnType['wrapper']) => { expect(completeOnboardingMock).toHaveBeenCalledTimes(1); expect(refetchOnboardingMock).not.toHaveBeenCalled(); + expect(cleanupOnboardingStorageMock).not.toHaveBeenCalled(); expect(wrapper.text()).toContain( 'Could not mark onboarding complete right now (API may be offline): offline' ); @@ -902,6 +909,15 @@ describe('OnboardingSummaryStep', () => { scenario.assertExpected(wrapper); }); + it('does not clear onboarding draft after a successful apply while still in the flow', async () => { + const { wrapper } = mountComponent(); + + await clickApply(wrapper); + + expect(completeOnboardingMock).toHaveBeenCalledTimes(1); + expect(cleanupOnboardingStorageMock).not.toHaveBeenCalled(); + }); + it('retries completeOnboarding after transient network errors when SSH changed', async () => { draftStore.useSsh = true; updateSshSettingsMock.mockResolvedValue({ diff --git a/web/src/components/Onboarding/steps/OnboardingOverviewStep.vue b/web/src/components/Onboarding/steps/OnboardingOverviewStep.vue index b15f23f9de..3b2ac3d548 100644 --- a/web/src/components/Onboarding/steps/OnboardingOverviewStep.vue +++ b/web/src/components/Onboarding/steps/OnboardingOverviewStep.vue @@ -195,11 +195,12 @@ const handleSkipOnboarding = async () => { } isSkipping.value = true; + cleanupOnboardingStorage({ clearTemporaryBypassSessionState: true }); + try { await completeOnboarding(); await new Promise((r) => setTimeout(r, 500)); await refetchOnboarding(); - cleanupOnboardingStorage({ clearTemporaryBypassSessionState: true }); window.location.reload(); } catch (e) { console.error(e); diff --git a/web/src/components/Onboarding/steps/OnboardingSummaryStep.vue b/web/src/components/Onboarding/steps/OnboardingSummaryStep.vue index 800d729b2e..e4915cdeeb 100644 --- a/web/src/components/Onboarding/steps/OnboardingSummaryStep.vue +++ b/web/src/components/Onboarding/steps/OnboardingSummaryStep.vue @@ -44,7 +44,6 @@ import { INSTALLED_UNRAID_PLUGINS_QUERY } from '@/components/Onboarding/graphql/ import { UPDATE_SYSTEM_TIME_MUTATION } from '@/components/Onboarding/graphql/updateSystemTime.mutation'; import { useOnboardingModalStore } from '@/components/Onboarding/store/onboardingModalVisibility'; import { useOnboardingStore } from '@/components/Onboarding/store/onboardingStatus'; -import { cleanupOnboardingStorage } from '@/components/Onboarding/store/onboardingStorageCleanup'; import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue'; import type { LogEntry } from '@/components/Onboarding/components/OnboardingConsole.vue'; @@ -1106,7 +1105,6 @@ const handleComplete = async () => { try { await runWithTransientNetworkRetry(() => completeOnboarding(), shouldRetryNetworkMutations); completionMarked = true; - cleanupOnboardingStorage({ clearTemporaryBypassSessionState: true }); } catch (caughtError: unknown) { hadWarnings = true; addErrorLog(summaryT('logs.completeOnboardingFailed'), caughtError, { From acfed7ce33a300b28dc1fca409e74aa81b054bb6 Mon Sep 17 00:00:00 2001 From: Ajit Mehrotra Date: Sun, 15 Mar 2026 18:34:43 -0400 Subject: [PATCH 2/5] fix(onboarding): route reboot through exit cleanup - Purpose: make the internal-boot reboot action follow the same explicit exit path as other onboarding exits. - Before: the Next Steps reboot confirmation submitted a reboot form directly from the step component and bypassed the modal's shared exit cleanup flow. - Problem: onboardingDraft could survive a reboot-triggered exit even though the user intentionally left onboarding. - Change: add a reboot-aware exit callback in the modal and have Next Steps request reboot through that shared exit path. - How it works: the modal now supports an after-close action, cleans onboarding storage through closeModal, and then triggers the reboot submission. - Verification: added Next Steps coverage and reran the focused onboarding Vitest suite, including modal, summary, overview, cleanup, visibility, and Next Steps tests. --- .../OnboardingNextStepsStep.test.ts | 35 +++++++++++++++++-- .../components/Onboarding/OnboardingModal.vue | 18 +++++++++- .../steps/OnboardingNextStepsStep.vue | 8 ++++- 3 files changed, 56 insertions(+), 5 deletions(-) diff --git a/web/__test__/components/Onboarding/OnboardingNextStepsStep.test.ts b/web/__test__/components/Onboarding/OnboardingNextStepsStep.test.ts index 0b51cc787d..f4de03e9ac 100644 --- a/web/__test__/components/Onboarding/OnboardingNextStepsStep.test.ts +++ b/web/__test__/components/Onboarding/OnboardingNextStepsStep.test.ts @@ -61,9 +61,11 @@ describe('OnboardingNextStepsStep', () => { const mountComponent = () => { const onComplete = vi.fn(); + const onReboot = vi.fn(); const wrapper = mount(OnboardingNextStepsStep, { props: { onComplete, + onReboot, showBack: true, }, global: { @@ -71,7 +73,7 @@ describe('OnboardingNextStepsStep', () => { }, }); - return { wrapper, onComplete }; + return { wrapper, onComplete, onReboot }; }; it('continues to dashboard when reboot is not required', async () => { @@ -87,7 +89,7 @@ describe('OnboardingNextStepsStep', () => { it('shows reboot warning dialog and waits for confirmation', async () => { draftStore.internalBootApplySucceeded = true; - const { wrapper, onComplete } = mountComponent(); + const { wrapper, onComplete, onReboot } = mountComponent(); const button = wrapper.find('[data-testid="brand-button"]'); await button.trigger('click'); @@ -105,7 +107,34 @@ describe('OnboardingNextStepsStep', () => { await confirmButton!.trigger('click'); await flushPromises(); - expect(submitInternalBootRebootMock).toHaveBeenCalledTimes(1); + expect(onReboot).toHaveBeenCalledTimes(1); + expect(submitInternalBootRebootMock).not.toHaveBeenCalled(); expect(onComplete).not.toHaveBeenCalled(); }); + + it('falls back to direct reboot when no exit handler is provided', async () => { + draftStore.internalBootApplySucceeded = true; + const onComplete = vi.fn(); + const wrapper = mount(OnboardingNextStepsStep, { + props: { + onComplete, + showBack: true, + }, + global: { + plugins: [createTestI18n()], + }, + }); + + await wrapper.find('[data-testid="brand-button"]').trigger('click'); + await flushPromises(); + + const confirmButton = wrapper + .findAll('button') + .find((candidate) => candidate.text().trim() === 'I Understand'); + expect(confirmButton).toBeTruthy(); + await confirmButton!.trigger('click'); + await flushPromises(); + + expect(submitInternalBootRebootMock).toHaveBeenCalledTimes(1); + }); }); diff --git a/web/src/components/Onboarding/OnboardingModal.vue b/web/src/components/Onboarding/OnboardingModal.vue index e10ad79446..1256918359 100644 --- a/web/src/components/Onboarding/OnboardingModal.vue +++ b/web/src/components/Onboarding/OnboardingModal.vue @@ -6,6 +6,7 @@ import { useMutation, useQuery } from '@vue/apollo-composable'; import { ArrowTopRightOnSquareIcon, XMarkIcon } from '@heroicons/vue/24/solid'; import { Dialog } from '@unraid/ui'; +import { submitInternalBootReboot } from '@/components/Onboarding/composables/internalBoot'; import { COMPLETE_ONBOARDING_MUTATION } from '@/components/Onboarding/graphql/completeUpgradeStep.mutation'; import { GET_INTERNAL_BOOT_STEP_VISIBILITY_QUERY } from '@/components/Onboarding/graphql/getInternalBootStepVisibility.query'; @@ -190,7 +191,7 @@ const completePendingOnboarding = async () => { } }; -const closeModal = async (options?: { reload?: boolean }) => { +const closeModal = async (options?: { reload?: boolean; onAfterClose?: () => void | Promise }) => { const wasForceOpened = isForceOpened.value; if (shouldShowOnboarding.value && !wasForceOpened) { @@ -205,6 +206,11 @@ const closeModal = async (options?: { reload?: boolean }) => { onboardingModalStore.clearForceOpened(); onboardingModalStore.setIsHidden(true); + if (options?.onAfterClose) { + await options.onAfterClose(); + return; + } + if (options?.reload) { window.location.reload(); } @@ -363,9 +369,19 @@ const currentStepProps = computed>(() => { }; case 'SUMMARY': + return { + ...baseProps, + }; + case 'NEXT_STEPS': return { ...baseProps, + onReboot: () => + closeModal({ + onAfterClose: () => { + submitInternalBootReboot(); + }, + }), }; default: diff --git a/web/src/components/Onboarding/steps/OnboardingNextStepsStep.vue b/web/src/components/Onboarding/steps/OnboardingNextStepsStep.vue index ab567c9734..60a742032f 100644 --- a/web/src/components/Onboarding/steps/OnboardingNextStepsStep.vue +++ b/web/src/components/Onboarding/steps/OnboardingNextStepsStep.vue @@ -22,6 +22,7 @@ import { useOnboardingDraftStore } from '@/components/Onboarding/store/onboardin export interface Props { onComplete: () => void; + onReboot?: () => void | Promise; onBack?: () => void; showBack?: boolean; } @@ -92,8 +93,13 @@ const handlePrimaryAction = () => { props.onComplete(); }; -const handleConfirmReboot = () => { +const handleConfirmReboot = async () => { showRebootWarningDialog.value = false; + if (props.onReboot) { + await props.onReboot(); + return; + } + submitInternalBootReboot(); }; From 9cb7142526a07c1c86f21b8823cd0887c7237717 Mon Sep 17 00:00:00 2001 From: Ajit Mehrotra Date: Sun, 15 Mar 2026 18:41:39 -0400 Subject: [PATCH 3/5] refactor(onboarding): simplify reboot draft cleanup - Purpose: replace the reboot-specific modal exit callback with direct draft cleanup in the Next Steps reboot path. - Before: reboot was routed through extra modal callback plumbing so it shared the closeModal exit machinery. - Problem: that approach worked, but it added indirection for a simple requirement and made the flow harder to follow. - Change: remove the reboot-specific modal abstraction and clear onboarding storage directly when the reboot confirmation is accepted. - How it works: the Next Steps reboot handler now calls onboarding storage cleanup and then submits the reboot form, while the modal returns to its simpler generic close behavior. - Verification: reran the focused onboarding Vitest suite covering Next Steps, modal, summary, overview, storage cleanup, and modal visibility. --- .../OnboardingNextStepsStep.test.ts | 48 +++++++------------ .../components/Onboarding/OnboardingModal.vue | 14 +----- .../steps/OnboardingNextStepsStep.vue | 8 +--- 3 files changed, 19 insertions(+), 51 deletions(-) diff --git a/web/__test__/components/Onboarding/OnboardingNextStepsStep.test.ts b/web/__test__/components/Onboarding/OnboardingNextStepsStep.test.ts index f4de03e9ac..a6e378f837 100644 --- a/web/__test__/components/Onboarding/OnboardingNextStepsStep.test.ts +++ b/web/__test__/components/Onboarding/OnboardingNextStepsStep.test.ts @@ -6,7 +6,12 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import OnboardingNextStepsStep from '~/components/Onboarding/steps/OnboardingNextStepsStep.vue'; import { createTestI18n } from '../../utils/i18n'; -const { draftStore, activationCodeDataStore, submitInternalBootRebootMock } = vi.hoisted(() => ({ +const { + draftStore, + activationCodeDataStore, + submitInternalBootRebootMock, + cleanupOnboardingStorageMock, +} = vi.hoisted(() => ({ draftStore: { internalBootApplySucceeded: false, }, @@ -26,6 +31,7 @@ const { draftStore, activationCodeDataStore, submitInternalBootRebootMock } = vi }, }, submitInternalBootRebootMock: vi.fn(), + cleanupOnboardingStorageMock: vi.fn(), })); vi.mock('@unraid/ui', () => ({ @@ -53,6 +59,10 @@ vi.mock('~/components/Onboarding/composables/internalBoot', () => ({ submitInternalBootReboot: submitInternalBootRebootMock, })); +vi.mock('~/components/Onboarding/store/onboardingStorageCleanup', () => ({ + cleanupOnboardingStorage: cleanupOnboardingStorageMock, +})); + describe('OnboardingNextStepsStep', () => { beforeEach(() => { vi.clearAllMocks(); @@ -61,11 +71,9 @@ describe('OnboardingNextStepsStep', () => { const mountComponent = () => { const onComplete = vi.fn(); - const onReboot = vi.fn(); const wrapper = mount(OnboardingNextStepsStep, { props: { onComplete, - onReboot, showBack: true, }, global: { @@ -73,7 +81,7 @@ describe('OnboardingNextStepsStep', () => { }, }); - return { wrapper, onComplete, onReboot }; + return { wrapper, onComplete }; }; it('continues to dashboard when reboot is not required', async () => { @@ -89,7 +97,7 @@ describe('OnboardingNextStepsStep', () => { it('shows reboot warning dialog and waits for confirmation', async () => { draftStore.internalBootApplySucceeded = true; - const { wrapper, onComplete, onReboot } = mountComponent(); + const { wrapper, onComplete } = mountComponent(); const button = wrapper.find('[data-testid="brand-button"]'); await button.trigger('click'); @@ -107,34 +115,10 @@ describe('OnboardingNextStepsStep', () => { await confirmButton!.trigger('click'); await flushPromises(); - expect(onReboot).toHaveBeenCalledTimes(1); - expect(submitInternalBootRebootMock).not.toHaveBeenCalled(); - expect(onComplete).not.toHaveBeenCalled(); - }); - - it('falls back to direct reboot when no exit handler is provided', async () => { - draftStore.internalBootApplySucceeded = true; - const onComplete = vi.fn(); - const wrapper = mount(OnboardingNextStepsStep, { - props: { - onComplete, - showBack: true, - }, - global: { - plugins: [createTestI18n()], - }, + expect(cleanupOnboardingStorageMock).toHaveBeenCalledWith({ + clearTemporaryBypassSessionState: true, }); - - await wrapper.find('[data-testid="brand-button"]').trigger('click'); - await flushPromises(); - - const confirmButton = wrapper - .findAll('button') - .find((candidate) => candidate.text().trim() === 'I Understand'); - expect(confirmButton).toBeTruthy(); - await confirmButton!.trigger('click'); - await flushPromises(); - expect(submitInternalBootRebootMock).toHaveBeenCalledTimes(1); + expect(onComplete).not.toHaveBeenCalled(); }); }); diff --git a/web/src/components/Onboarding/OnboardingModal.vue b/web/src/components/Onboarding/OnboardingModal.vue index 1256918359..bd2d439f84 100644 --- a/web/src/components/Onboarding/OnboardingModal.vue +++ b/web/src/components/Onboarding/OnboardingModal.vue @@ -6,7 +6,6 @@ import { useMutation, useQuery } from '@vue/apollo-composable'; import { ArrowTopRightOnSquareIcon, XMarkIcon } from '@heroicons/vue/24/solid'; import { Dialog } from '@unraid/ui'; -import { submitInternalBootReboot } from '@/components/Onboarding/composables/internalBoot'; import { COMPLETE_ONBOARDING_MUTATION } from '@/components/Onboarding/graphql/completeUpgradeStep.mutation'; import { GET_INTERNAL_BOOT_STEP_VISIBILITY_QUERY } from '@/components/Onboarding/graphql/getInternalBootStepVisibility.query'; @@ -191,7 +190,7 @@ const completePendingOnboarding = async () => { } }; -const closeModal = async (options?: { reload?: boolean; onAfterClose?: () => void | Promise }) => { +const closeModal = async (options?: { reload?: boolean }) => { const wasForceOpened = isForceOpened.value; if (shouldShowOnboarding.value && !wasForceOpened) { @@ -206,11 +205,6 @@ const closeModal = async (options?: { reload?: boolean; onAfterClose?: () => voi onboardingModalStore.clearForceOpened(); onboardingModalStore.setIsHidden(true); - if (options?.onAfterClose) { - await options.onAfterClose(); - return; - } - if (options?.reload) { window.location.reload(); } @@ -376,12 +370,6 @@ const currentStepProps = computed>(() => { case 'NEXT_STEPS': return { ...baseProps, - onReboot: () => - closeModal({ - onAfterClose: () => { - submitInternalBootReboot(); - }, - }), }; default: diff --git a/web/src/components/Onboarding/steps/OnboardingNextStepsStep.vue b/web/src/components/Onboarding/steps/OnboardingNextStepsStep.vue index 60a742032f..259e0af33c 100644 --- a/web/src/components/Onboarding/steps/OnboardingNextStepsStep.vue +++ b/web/src/components/Onboarding/steps/OnboardingNextStepsStep.vue @@ -19,10 +19,10 @@ import UnraidIconSvg from '@/assets/partners/simple-icons-unraid.svg?raw'; import { submitInternalBootReboot } from '@/components/Onboarding/composables/internalBoot'; import { useActivationCodeDataStore } from '@/components/Onboarding/store/activationCodeData'; import { useOnboardingDraftStore } from '@/components/Onboarding/store/onboardingDraft'; +import { cleanupOnboardingStorage } from '@/components/Onboarding/store/onboardingStorageCleanup'; export interface Props { onComplete: () => void; - onReboot?: () => void | Promise; onBack?: () => void; showBack?: boolean; } @@ -95,11 +95,7 @@ const handlePrimaryAction = () => { const handleConfirmReboot = async () => { showRebootWarningDialog.value = false; - if (props.onReboot) { - await props.onReboot(); - return; - } - + cleanupOnboardingStorage({ clearTemporaryBypassSessionState: true }); submitInternalBootReboot(); }; From bdadd9f84b1b66e017fea3b21da6797d6aaab065 Mon Sep 17 00:00:00 2001 From: Ajit Mehrotra Date: Sun, 15 Mar 2026 18:55:20 -0400 Subject: [PATCH 4/5] refactor(onboarding): remove redundant step prop cases - Purpose: simplify currentStepProps in OnboardingModal now that Summary and Next Steps use the default base props. - Before: the switch kept separate SUMMARY and NEXT_STEPS branches that each returned the same baseProps object. - Problem: this duplication added noise without changing behavior. - Change: remove the redundant branches and let both steps fall through to the default return. - How it works: the modal still passes the same common props to both steps, just without explicit duplicate cases. - Verification: reran the targeted modal, next steps, and summary Vitest suite. --- web/src/components/Onboarding/OnboardingModal.vue | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/web/src/components/Onboarding/OnboardingModal.vue b/web/src/components/Onboarding/OnboardingModal.vue index bd2d439f84..cd9d184fc1 100644 --- a/web/src/components/Onboarding/OnboardingModal.vue +++ b/web/src/components/Onboarding/OnboardingModal.vue @@ -362,16 +362,6 @@ const currentStepProps = computed>(() => { showActivationCodeHint: hasActivationCode.value, }; - case 'SUMMARY': - return { - ...baseProps, - }; - - case 'NEXT_STEPS': - return { - ...baseProps, - }; - default: return baseProps; } From 95d12b2d10ee97730bf326bfc68ee9f8eb8a200b Mon Sep 17 00:00:00 2001 From: Ajit Mehrotra Date: Sun, 15 Mar 2026 19:17:04 -0400 Subject: [PATCH 5/5] refactor(onboarding): address coderabbit nitpicks - Purpose: resolve the remaining CodeRabbit nitpicks on the onboarding draft cleanup PR. - Before: overview tests used an index-based button selector and the reboot confirm handler was marked async without awaiting anything. - Problem: the test selector was fragile to markup order changes, and the async modifier implied behavior that the handler did not have. - Change: add a stable test id for the overview skip button, update the tests to use it, and remove the unnecessary async keyword from the reboot confirm handler. - How it works: the overview tests now target the intended button directly and the reboot handler remains a plain synchronous function with unchanged behavior. - Verification: reran targeted Vitest coverage for the overview and next-steps onboarding files while working each nit one at a time. --- .../components/Onboarding/OnboardingOverviewStep.test.ts | 4 ++-- .../components/Onboarding/steps/OnboardingNextStepsStep.vue | 2 +- .../components/Onboarding/steps/OnboardingOverviewStep.vue | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/web/__test__/components/Onboarding/OnboardingOverviewStep.test.ts b/web/__test__/components/Onboarding/OnboardingOverviewStep.test.ts index facb901888..9b68d4affb 100644 --- a/web/__test__/components/Onboarding/OnboardingOverviewStep.test.ts +++ b/web/__test__/components/Onboarding/OnboardingOverviewStep.test.ts @@ -146,7 +146,7 @@ describe('OnboardingOverviewStep', () => { it('clears onboarding draft immediately when skipping setup', async () => { const wrapper = mountComponent(); - await wrapper.findAll('button')[1]?.trigger('click'); + await wrapper.find('[data-testid="skip-setup-button"]').trigger('click'); expect(cleanupOnboardingStorageMock).toHaveBeenCalledWith({ clearTemporaryBypassSessionState: true, @@ -157,7 +157,7 @@ describe('OnboardingOverviewStep', () => { completeOnboardingMock.mockRejectedValueOnce(new Error('offline')); const wrapper = mountComponent(); - await wrapper.findAll('button')[1]?.trigger('click'); + await wrapper.find('[data-testid="skip-setup-button"]').trigger('click'); expect(cleanupOnboardingStorageMock).toHaveBeenCalledWith({ clearTemporaryBypassSessionState: true, diff --git a/web/src/components/Onboarding/steps/OnboardingNextStepsStep.vue b/web/src/components/Onboarding/steps/OnboardingNextStepsStep.vue index 259e0af33c..972c4c4a64 100644 --- a/web/src/components/Onboarding/steps/OnboardingNextStepsStep.vue +++ b/web/src/components/Onboarding/steps/OnboardingNextStepsStep.vue @@ -93,7 +93,7 @@ const handlePrimaryAction = () => { props.onComplete(); }; -const handleConfirmReboot = async () => { +const handleConfirmReboot = () => { showRebootWarningDialog.value = false; cleanupOnboardingStorage({ clearTemporaryBypassSessionState: true }); submitInternalBootReboot(); diff --git a/web/src/components/Onboarding/steps/OnboardingOverviewStep.vue b/web/src/components/Onboarding/steps/OnboardingOverviewStep.vue index 3b2ac3d548..6dc9c84c77 100644 --- a/web/src/components/Onboarding/steps/OnboardingOverviewStep.vue +++ b/web/src/components/Onboarding/steps/OnboardingOverviewStep.vue @@ -315,6 +315,7 @@ const openDocs = () => {