Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 78 additions & 3 deletions web/__test__/components/Onboarding/OnboardingPluginsStep.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>(),
pluginSelectionInitialized: false,
setPlugins: vi.fn(),
},
installedPluginsLoading: {
value: false,
},
installedPluginsResult: {
value: {
installedUnraidPlugins: [],
Expand Down Expand Up @@ -63,6 +66,7 @@ describe('OnboardingPluginsStep', () => {
vi.clearAllMocks();
draftStore.selectedPlugins = new Set();
draftStore.pluginSelectionInitialized = false;
installedPluginsLoading.value = false;
installedPluginsResult.value = {
installedUnraidPlugins: [],
};
Expand All @@ -71,6 +75,7 @@ describe('OnboardingPluginsStep', () => {
if (query === INSTALLED_UNRAID_PLUGINS_QUERY) {
return {
result: installedPluginsResult,
loading: installedPluginsLoading,
};
}
return { result: { value: null } };
Expand Down Expand Up @@ -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);
Expand All @@ -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();
Expand All @@ -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<string>;
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']);
Expand All @@ -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<string>;
expect(Array.from(selected)).toEqual(['fix-common-problems']);
expect(props.onSkip).toHaveBeenCalledTimes(1);
expect(props.onComplete).not.toHaveBeenCalled();
});
});
25 changes: 17 additions & 8 deletions web/src/components/Onboarding/steps/OnboardingPluginsStep.vue
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,25 @@ const initialSelection = draftStore.pluginSelectionInitialized
const selectedPlugins = ref<Set<string>>(initialSelection);
const installedPluginIds = ref<Set<string>>(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<string>([...selectedPlugins.value, ...installedPluginIds.value])
);

const applyInstalledPlugins = (installedPlugins: string[] | null | undefined) => {
if (!Array.isArray(installedPlugins)) {
Expand Down Expand Up @@ -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 {
Expand All @@ -149,7 +158,7 @@ const handleBack = () => {
};

const handlePrimaryAction = async () => {
draftStore.setPlugins(selectedPlugins.value);
draftStore.setPlugins(persistedSelectedPlugins.value);
props.onComplete();
};

Expand Down
Loading