Skip to content

Commit b6acf50

Browse files
mdatellemdatelle
andauthored
refactor: update modals and color picker (#1494)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * The Welcome modal now automatically appears when visiting the `/welcome` page. * "Create a password" button in the Welcome modal is now disabled while loading. * **Refactor** * Activation and Welcome modals now use a new Dialog component for improved layout and styling. * Theme and server selection components now use a simplified Select dropdown with options passed as data for a cleaner interface. * **Tests** * Updated modal-related tests to use the new Dialog component and improved mocking for more accurate and maintainable test coverage. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: mdatelle <[email protected]>
1 parent 8279531 commit b6acf50

File tree

7 files changed

+166
-188
lines changed

7 files changed

+166
-188
lines changed

unraid-ui/src/components/form/select/Select.vue

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,11 @@ import {
99
SelectTrigger,
1010
SelectValue,
1111
} from '@/components/ui/select';
12+
import type { AcceptableValue } from 'reka-ui';
1213
import { computed } from 'vue';
1314
1415
type SelectValueType = string | number;
1516
16-
type AcceptableValue = SelectValueType | SelectValueType[] | Record<string, unknown> | bigint | null;
17-
1817
interface SelectItemInterface {
1918
label: string;
2019
value: SelectValueType;

web/__test__/components/Activation/ActivationModal.test.ts

Lines changed: 26 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -11,33 +11,34 @@ import type { ComposerTranslation } from 'vue-i18n';
1111

1212
import ActivationModal from '~/components/Activation/ActivationModal.vue';
1313

14+
vi.mock('@unraid/ui', async (importOriginal) => {
15+
const actual = await importOriginal<typeof import('@unraid/ui')>();
16+
return {
17+
...actual,
18+
Dialog: {
19+
name: 'Dialog',
20+
props: ['modelValue', 'title', 'description', 'showFooter', 'size', 'showCloseButton'],
21+
emits: ['update:modelValue'],
22+
template: `
23+
<div v-if="modelValue" role="dialog" aria-modal="true">
24+
<div v-if="$slots.header" class="dialog-header"><slot name="header" /></div>
25+
<div class="dialog-body"><slot /></div>
26+
<div v-if="$slots.footer" class="dialog-footer"><slot name="footer" /></div>
27+
</div>
28+
`,
29+
},
30+
BrandButton: {
31+
template:
32+
'<button data-testid="brand-button" :type="type" @click="$emit(\'click\')"><slot /></button>',
33+
props: ['text', 'iconRight', 'variant', 'external', 'href', 'size', 'type'],
34+
emits: ['click'],
35+
},
36+
};
37+
});
38+
1439
const mockT = (key: string, args?: unknown[]) => (args ? `${key} ${JSON.stringify(args)}` : key);
1540

1641
const mockComponents = {
17-
Modal: {
18-
template: `
19-
<div data-testid="modal" v-if="open">
20-
<div data-testid="modal-header"><slot name="header" /></div>
21-
<div data-testid="modal-body"><slot /></div>
22-
<div data-testid="modal-footer"><slot name="footer" /></div>
23-
<div data-testid="modal-subfooter"><slot name="subFooter" /></div>
24-
</div>
25-
`,
26-
props: [
27-
't',
28-
'open',
29-
'showCloseX',
30-
'title',
31-
'titleInMain',
32-
'description',
33-
'overlayColor',
34-
'overlayOpacity',
35-
'maxWidth',
36-
'modalVerticalCenter',
37-
'disableShadow',
38-
'disableOverlayClose',
39-
],
40-
},
4142
ActivationPartnerLogo: {
4243
template: '<div data-testid="partner-logo"></div>',
4344
props: ['name'],
@@ -46,12 +47,6 @@ const mockComponents = {
4647
template: '<div data-testid="activation-steps" :active-step="activeStep"></div>',
4748
props: ['activeStep'],
4849
},
49-
BrandButton: {
50-
template:
51-
'<button data-testid="brand-button" :type="type" @click="$emit(\'click\')"><slot /></button>',
52-
props: ['text', 'iconRight', 'variant', 'external', 'href', 'size', 'type'],
53-
emits: ['click'],
54-
},
5550
};
5651

5752
const mockActivationCodeDataStore = {
@@ -246,7 +241,7 @@ describe('Activation/ActivationModal.vue', () => {
246241
mockActivationCodeModalStore.isVisible.value = false;
247242
const wrapper = mountComponent();
248243

249-
expect(wrapper.html()).toBe('<!--v-if-->');
244+
expect(wrapper.find('[role="dialog"]').exists()).toBe(false);
250245
});
251246

252247
it('renders activation steps with correct active step', () => {

web/__test__/components/Activation/WelcomeModal.test.ts

Lines changed: 65 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -11,40 +11,35 @@ import type { ComposerTranslation } from 'vue-i18n';
1111

1212
import WelcomeModal from '~/components/Activation/WelcomeModal.ce.vue';
1313

14+
vi.mock('@unraid/ui', async (importOriginal) => {
15+
const actual = await importOriginal<typeof import('@unraid/ui')>();
16+
return {
17+
...actual,
18+
Dialog: {
19+
name: 'Dialog',
20+
props: ['modelValue', 'title', 'description', 'showFooter', 'size', 'showCloseButton'],
21+
emits: ['update:modelValue'],
22+
template: `
23+
<div v-if="modelValue" role="dialog" aria-modal="true">
24+
<div v-if="$slots.header" class="dialog-header"><slot name="header" /></div>
25+
<div class="dialog-body"><slot /></div>
26+
<div v-if="$slots.footer" class="dialog-footer"><slot name="footer" /></div>
27+
</div>
28+
`,
29+
},
30+
};
31+
});
32+
1433
const mockT = (key: string, args?: unknown[]) => (args ? `${key} ${JSON.stringify(args)}` : key);
1534

1635
const mockComponents = {
17-
Dialog: {
18-
template: `
19-
<div data-testid="modal" v-if="modelValue" role="dialog" aria-modal="true">
20-
<div data-testid="modal-header"><slot name="header" /></div>
21-
<div data-testid="modal-body"><slot /></div>
22-
<div data-testid="modal-footer"><slot name="footer" /></div>
23-
<div data-testid="modal-subfooter"><slot name="subFooter" /></div>
24-
</div>
25-
`,
26-
props: [
27-
'modelValue',
28-
'title',
29-
'description',
30-
'showFooter',
31-
'size',
32-
],
33-
emits: ['update:modelValue'],
34-
},
3536
ActivationPartnerLogo: {
3637
template: '<div data-testid="partner-logo"></div>',
3738
},
3839
ActivationSteps: {
3940
template: '<div data-testid="activation-steps" :active-step="activeStep"></div>',
4041
props: ['activeStep'],
4142
},
42-
BrandButton: {
43-
template:
44-
'<button data-testid="brand-button" :disabled="disabled" @click="$emit(\'click\')"><slot /></button>',
45-
props: ['text', 'disabled'],
46-
emits: ['click'],
47-
},
4843
};
4944

5045
const mockActivationCodeDataStore = {
@@ -73,33 +68,6 @@ vi.mock('~/store/theme', () => ({
7368
useThemeStore: () => mockThemeStore,
7469
}));
7570

76-
vi.mock('@unraid/ui', () => ({
77-
BrandButton: {
78-
template:
79-
'<button data-testid="brand-button" :disabled="disabled" @click="$emit(\'click\')">{{ text }}</button>',
80-
props: ['text', 'disabled'],
81-
emits: ['click'],
82-
},
83-
Dialog: {
84-
template: `
85-
<div data-testid="modal" v-if="modelValue" role="dialog" aria-modal="true">
86-
<div data-testid="modal-header"><slot name="header" /></div>
87-
<div data-testid="modal-body"><slot /></div>
88-
<div data-testid="modal-footer"><slot name="footer" /></div>
89-
<div data-testid="modal-subfooter"><slot name="subFooter" /></div>
90-
</div>
91-
`,
92-
props: [
93-
'modelValue',
94-
'title',
95-
'description',
96-
'showFooter',
97-
'size',
98-
],
99-
emits: ['update:modelValue'],
100-
},
101-
}));
102-
10371
describe('Activation/WelcomeModal.ce.vue', () => {
10472
let mockSetProperty: ReturnType<typeof vi.fn>;
10573
let mockQuerySelector: ReturnType<typeof vi.fn>;
@@ -124,19 +92,29 @@ describe('Activation/WelcomeModal.ce.vue', () => {
12492
value: mockSetProperty,
12593
writable: true,
12694
});
95+
96+
// Mock window.location.pathname to simulate being on /welcome page
97+
Object.defineProperty(window, 'location', {
98+
value: {
99+
pathname: '/welcome',
100+
},
101+
writable: true,
102+
});
127103
});
128104

129105
afterEach(() => {
130106
vi.useRealTimers();
131107
});
132108

133-
const mountComponent = () => {
134-
return mount(WelcomeModal, {
109+
const mountComponent = async () => {
110+
const wrapper = mount(WelcomeModal, {
135111
props: { t: mockT as unknown as ComposerTranslation },
136112
global: {
137113
stubs: mockComponents,
138114
},
139115
});
116+
await wrapper.vm.$nextTick();
117+
return wrapper;
140118
};
141119

142120
it('uses the correct title text when no partner name is provided', () => {
@@ -169,39 +147,46 @@ describe('Activation/WelcomeModal.ce.vue', () => {
169147
);
170148
});
171149

172-
it('displays the partner logo when available', () => {
150+
it('displays the partner logo when available', async () => {
173151
mockActivationCodeDataStore.partnerInfo.value = {
174152
hasPartnerLogo: true,
175153
partnerName: 'Test Partner',
176154
};
177-
const wrapper = mountComponent();
155+
const wrapper = await mountComponent();
178156

179157
expect(wrapper.html()).toContain('data-testid="partner-logo"');
180158
});
181159

182160
it('hides modal when Create a password button is clicked', async () => {
183-
const wrapper = mountComponent();
184-
const button = wrapper.find('[data-testid="brand-button"]');
161+
const wrapper = await mountComponent();
162+
const button = wrapper.find('button');
185163

186164
expect(button.exists()).toBe(true);
187165

166+
// Initially dialog should be visible
167+
let dialog = wrapper.findComponent({ name: 'Dialog' });
168+
expect(dialog.exists()).toBe(true);
169+
expect(dialog.props('modelValue')).toBe(true);
170+
188171
await button.trigger('click');
189172
await wrapper.vm.$nextTick();
190173

191-
expect(wrapper.find('[data-testid="modal"]').exists()).toBe(false);
174+
// After click, dialog modelValue should be false
175+
dialog = wrapper.findComponent({ name: 'Dialog' });
176+
expect(dialog.props('modelValue')).toBe(false);
192177
});
193178

194-
it('does not disable the Create a password button when loading', () => {
179+
it('disables the Create a password button when loading', async () => {
195180
mockActivationCodeDataStore.loading.value = true;
196181

197-
const wrapper = mountComponent();
198-
const button = wrapper.find('[data-testid="brand-button"]');
182+
const wrapper = await mountComponent();
183+
const button = wrapper.find('button');
199184

200-
expect(button.attributes('disabled')).toBe(undefined);
185+
expect(button.attributes('disabled')).toBe('');
201186
});
202187

203-
it('renders activation steps with correct active step', () => {
204-
const wrapper = mountComponent();
188+
it('renders activation steps with correct active step', async () => {
189+
const wrapper = await mountComponent();
205190

206191
expect(wrapper.html()).toContain('data-testid="activation-steps"');
207192
expect(wrapper.html()).toContain('active-step="1"');
@@ -250,13 +235,13 @@ describe('Activation/WelcomeModal.ce.vue', () => {
250235

251236
it('sets font-size to 100% when modal is hidden', async () => {
252237
mockQuerySelector.mockReturnValue({ exists: true });
253-
const wrapper = mountComponent();
238+
const wrapper = await mountComponent();
254239

255240
await vi.runAllTimersAsync();
256241

257242
expect(mockSetProperty).toHaveBeenCalledWith('font-size', '62.5%');
258243

259-
const button = wrapper.find('[data-testid="brand-button"]');
244+
const button = wrapper.find('button');
260245
await button.trigger('click');
261246
await wrapper.vm.$nextTick();
262247

@@ -265,23 +250,25 @@ describe('Activation/WelcomeModal.ce.vue', () => {
265250
});
266251

267252
describe('Modal properties', () => {
268-
it('passes correct props to Dialog component', () => {
269-
const wrapper = mountComponent();
270-
const dialog = wrapper.find('[data-testid="modal"]');
253+
it('passes correct props to Dialog component', async () => {
254+
const wrapper = await mountComponent();
255+
const dialog = wrapper.findComponent({ name: 'Dialog' });
271256

272257
expect(dialog.exists()).toBe(true);
273-
// The Dialog component is rendered correctly
274-
expect(wrapper.html()).toContain('data-testid="modal"');
258+
expect(dialog.props()).toMatchObject({
259+
modelValue: true,
260+
showFooter: false,
261+
showCloseButton: false,
262+
size: 'full',
263+
});
275264
});
276265

277-
it('renders modal with correct accessibility attributes', () => {
278-
const wrapper = mountComponent();
279-
const dialog = wrapper.find('[data-testid="modal"]');
266+
it('renders modal with correct content', async () => {
267+
const wrapper = await mountComponent();
280268

281-
expect(dialog.attributes()).toMatchObject({
282-
role: 'dialog',
283-
'aria-modal': 'true',
284-
});
269+
// Check that the modal is rendered
270+
expect(wrapper.text()).toContain('Welcome to Unraid!');
271+
expect(wrapper.text()).toContain('Create a password');
285272
});
286273
});
287274
});

0 commit comments

Comments
 (0)