diff --git a/web/__test__/components/Onboarding/OnboardingPluginsStep.test.ts b/web/__test__/components/Onboarding/OnboardingPluginsStep.test.ts index 34ac963845..6b14ea49b5 100644 --- a/web/__test__/components/Onboarding/OnboardingPluginsStep.test.ts +++ b/web/__test__/components/Onboarding/OnboardingPluginsStep.test.ts @@ -6,12 +6,15 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import OnboardingPluginsStep from '~/components/Onboarding/steps/OnboardingPluginsStep.vue'; import { createTestI18n } from '../../utils/i18n'; -const { draftStore, installedPluginsResult, useQueryMock } = vi.hoisted(() => ({ +const { draftStore, installedPluginsLoading, installedPluginsResult, useQueryMock } = vi.hoisted(() => ({ draftStore: { selectedPlugins: new Set(), pluginSelectionInitialized: false, setPlugins: vi.fn(), }, + installedPluginsLoading: { + value: false, + }, installedPluginsResult: { value: { installedUnraidPlugins: [], @@ -63,6 +66,7 @@ describe('OnboardingPluginsStep', () => { vi.clearAllMocks(); draftStore.selectedPlugins = new Set(); draftStore.pluginSelectionInitialized = false; + installedPluginsLoading.value = false; installedPluginsResult.value = { installedUnraidPlugins: [], }; @@ -71,6 +75,7 @@ describe('OnboardingPluginsStep', () => { if (query === INSTALLED_UNRAID_PLUGINS_QUERY) { return { result: installedPluginsResult, + loading: installedPluginsLoading, }; } return { result: { value: null } }; @@ -103,7 +108,7 @@ describe('OnboardingPluginsStep', () => { await flushPromises(); - const switches = wrapper.findAll('[data-testid="plugin-switch"]'); + const switches = wrapper.findAll('input[type="checkbox"]'); expect(switches.length).toBe(3); expect((switches[0].element as HTMLInputElement).checked).toBe(true); expect((switches[1].element as HTMLInputElement).checked).toBe(false); @@ -113,7 +118,7 @@ describe('OnboardingPluginsStep', () => { } const nextButton = wrapper - .findAll('[data-testid="brand-button"]') + .findAll('button') .find((button) => button.text().toLowerCase().includes('next')); expect(nextButton).toBeTruthy(); @@ -126,6 +131,51 @@ describe('OnboardingPluginsStep', () => { expect(props.onComplete).toHaveBeenCalledTimes(1); }); + it('persists already installed plugins alongside manual selections', async () => { + installedPluginsResult.value = { + installedUnraidPlugins: ['fix.common.problems.plg', 'tailscale.plg'], + }; + + const { wrapper, props } = mountComponent(); + + await flushPromises(); + + const switches = wrapper.findAll('input[type="checkbox"]'); + expect(switches.length).toBe(3); + expect((switches[0].element as HTMLInputElement).checked).toBe(true); + expect((switches[1].element as HTMLInputElement).checked).toBe(true); + expect((switches[2].element as HTMLInputElement).checked).toBe(true); + + const nextButton = wrapper + .findAll('button') + .find((button) => button.text().toLowerCase().includes('next')); + + expect(nextButton).toBeTruthy(); + await nextButton!.trigger('click'); + + expect(draftStore.setPlugins).toHaveBeenCalled(); + const lastCallIndex = draftStore.setPlugins.mock.calls.length - 1; + const selected = draftStore.setPlugins.mock.calls[lastCallIndex][0] as Set; + expect(Array.from(selected).sort()).toEqual(['community-apps', 'fix-common-problems', 'tailscale']); + expect(props.onComplete).toHaveBeenCalledTimes(1); + }); + + it('disables the primary action until installed plugins finish loading', async () => { + installedPluginsLoading.value = true; + installedPluginsResult.value = null; + + const { wrapper } = mountComponent(); + + await flushPromises(); + + const nextButton = wrapper + .findAll('button') + .find((button) => button.text().toLowerCase().includes('next')); + + expect(nextButton).toBeTruthy(); + expect((nextButton!.element as HTMLButtonElement).disabled).toBe(true); + }); + it('skip clears selection and calls onSkip', async () => { draftStore.pluginSelectionInitialized = true; draftStore.selectedPlugins = new Set(['community-apps']); @@ -147,4 +197,29 @@ describe('OnboardingPluginsStep', () => { expect(props.onSkip).toHaveBeenCalledTimes(1); expect(props.onComplete).not.toHaveBeenCalled(); }); + + it('skip preserves detected installed plugins without keeping manual selections', async () => { + draftStore.pluginSelectionInitialized = true; + draftStore.selectedPlugins = new Set(['community-apps', 'tailscale']); + installedPluginsResult.value = { + installedUnraidPlugins: ['fix.common.problems.plg'], + }; + + const { wrapper, props } = mountComponent(); + + await flushPromises(); + + const skipButton = wrapper + .findAll('button') + .find((button) => button.text().toLowerCase().includes('skip')); + + expect(skipButton).toBeTruthy(); + await skipButton!.trigger('click'); + + expect(draftStore.setPlugins).toHaveBeenCalledTimes(1); + const selected = draftStore.setPlugins.mock.calls[0][0] as Set; + expect(Array.from(selected)).toEqual(['fix-common-problems']); + expect(props.onSkip).toHaveBeenCalledTimes(1); + expect(props.onComplete).not.toHaveBeenCalled(); + }); }); diff --git a/web/src/components/Onboarding/steps/OnboardingPluginsStep.vue b/web/src/components/Onboarding/steps/OnboardingPluginsStep.vue index d8a9e91130..f42b729414 100644 --- a/web/src/components/Onboarding/steps/OnboardingPluginsStep.vue +++ b/web/src/components/Onboarding/steps/OnboardingPluginsStep.vue @@ -78,14 +78,25 @@ const initialSelection = draftStore.pluginSelectionInitialized const selectedPlugins = ref>(initialSelection); const installedPluginIds = ref>(new Set()); -const { result: installedPluginsResult } = useQuery(INSTALLED_UNRAID_PLUGINS_QUERY, null, { - fetchPolicy: 'network-only', -}); +const { result: installedPluginsResult, loading: installedPluginsLoading } = useQuery( + INSTALLED_UNRAID_PLUGINS_QUERY, + null, + { + fetchPolicy: 'network-only', + } +); const isPluginInstalled = (pluginId: string) => installedPluginIds.value.has(pluginId); const isPluginEnabled = (pluginId: string) => installedPluginIds.value.has(pluginId) || selectedPlugins.value.has(pluginId); -const isBusy = computed(() => props.isSavingStep ?? false); +const isInstalledPluginsPending = computed( + () => + installedPluginsLoading.value && !Array.isArray(installedPluginsResult.value?.installedUnraidPlugins) +); +const isBusy = computed(() => Boolean(props.isSavingStep) || isInstalledPluginsPending.value); +const persistedSelectedPlugins = computed( + () => new Set([...selectedPlugins.value, ...installedPluginIds.value]) +); const applyInstalledPlugins = (installedPlugins: string[] | null | undefined) => { if (!Array.isArray(installedPlugins)) { @@ -134,9 +145,7 @@ const togglePlugin = (pluginId: string, value: boolean) => { }; const handleSkip = () => { - // Clear selection? Or just move on? - // User clicked "Skip", so we probably shouldn't install anything. - draftStore.setPlugins(new Set()); + draftStore.setPlugins(new Set(installedPluginIds.value)); if (props.onSkip) { props.onSkip(); } else { @@ -149,7 +158,7 @@ const handleBack = () => { }; const handlePrimaryAction = async () => { - draftStore.setPlugins(selectedPlugins.value); + draftStore.setPlugins(persistedSelectedPlugins.value); props.onComplete(); };