-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathadmin.layout.unit.tests.js
More file actions
195 lines (170 loc) · 7.66 KB
/
admin.layout.unit.tests.js
File metadata and controls
195 lines (170 loc) · 7.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { mount } from '@vue/test-utils';
import { createPinia, setActivePinia } from 'pinia';
import { createVuetify } from 'vuetify';
const adminStoreState = { error: null, currentBreadcrumb: null };
const authStoreState = { serverConfig: null };
vi.mock('../stores/admin.store', () => ({
useAdminStore: () => adminStoreState,
}));
vi.mock('../../auth/stores/auth.store', () => ({
useAuthStore: () => authStoreState,
}));
// IMPORTANT: also mock the ability helper so adminCan() works in tests.
// Match the pattern used by user.view tests.
vi.mock('../../../lib/helpers/ability', () => ({
ability: { rules: [{ action: 'manage', subject: 'all' }], can: () => true },
}));
import AdminLayout from '../views/admin.layout.vue';
const baseConfig = {
vuetify: { theme: { flat: true, rounded: 'rounded-lg' } },
};
/**
* Mount AdminLayout with shared stubs/mocks and optional config overrides.
* @param {object} [configOverrides={}] - Config keys merged (shallow) into the base config mock.
* @param {string} [routePath='/admin/users'] - Mocked current route path for `$route.path`.
* @returns {import('@vue/test-utils').VueWrapper} Fully mounted AdminLayout wrapper.
*/
const mountLayout = (configOverrides = {}, routePath = '/admin/users') =>
mount(AdminLayout, {
global: {
plugins: [createVuetify()],
mocks: {
config: { ...baseConfig, ...configOverrides },
$route: { path: routePath },
$router: { push: vi.fn() },
},
stubs: {
RouterLink: true,
RouterView: { template: '<div class="router-view-stub" />' },
// Single stub for the new bundled header-with-tabs primitive — exposes
// all the props admin.layout passes through, plus the breadcrumb slot.
CorePageHeaderTabs: {
name: 'CorePageHeaderTabs',
props: ['title', 'icon', 'tabs', 'can', 'basePath', 'hideTabs'],
template: `
<div class="page-header-tabs-stub" :data-title="title" :data-icon="icon" :data-hide-tabs="hideTabs">
<slot name="avatar" />
<slot name="breadcrumb" />
<slot name="title" />
<slot name="subtitle" />
<slot name="actions" />
</div>
`,
},
},
},
});
describe('admin.layout', () => {
beforeEach(() => {
setActivePinia(createPinia());
adminStoreState.error = null;
adminStoreState.currentBreadcrumb = null;
authStoreState.serverConfig = null;
});
it('renders the page header', () => {
const wrapper = mountLayout();
expect(wrapper.find('.page-header-tabs-stub').exists()).toBe(true);
});
it('renders a <router-view> for nested admin content', () => {
const wrapper = mountLayout();
expect(wrapper.find('.router-view-stub').exists()).toBe(true);
});
it('passes the four built-in tabs to CorePageHeaderTabs when no extras are configured', () => {
const wrapper = mountLayout();
const headerTabs = wrapper.findComponent({ name: 'CorePageHeaderTabs' });
expect(headerTabs.exists()).toBe(true);
const tabs = headerTabs.props('tabs');
expect(tabs).toHaveLength(4);
expect(tabs.map((t) => t.value)).toEqual(['users', 'organizations', 'readiness', 'activity']);
});
it('passes built-in + extra tabs from config.admin.tabs to CorePageHeaderTabs', () => {
const wrapper = mountLayout({
admin: {
tabs: [
{ value: 'knowledge', label: 'Knowledge', icon: 'fa-solid fa-book', route: 'knowledge' },
{ value: 'costs', label: 'Costs', route: 'costs' },
],
},
});
const headerTabs = wrapper.findComponent({ name: 'CorePageHeaderTabs' });
const tabs = headerTabs.props('tabs');
expect(tabs).toHaveLength(6);
expect(tabs[4].value).toBe('knowledge');
expect(tabs[5].value).toBe('costs');
});
it('passes basePath="/admin" to CorePageHeaderTabs', () => {
const wrapper = mountLayout();
const headerTabs = wrapper.findComponent({ name: 'CorePageHeaderTabs' });
expect(headerTabs.props('basePath')).toBe('/admin');
});
it('passes a function `can` predicate to CorePageHeaderTabs', () => {
const wrapper = mountLayout();
const headerTabs = wrapper.findComponent({ name: 'CorePageHeaderTabs' });
expect(typeof headerTabs.props('can')).toBe('function');
});
it('gracefully handles non-array admin.tabs (CorePageHeaderTabs receives only the built-in 4)', () => {
const wrapper = mountLayout({ admin: { tabs: 'invalid' } });
const headerTabs = wrapper.findComponent({ name: 'CorePageHeaderTabs' });
expect(headerTabs.props('tabs')).toHaveLength(4);
});
it('renders the error banner at the TOP of the layout, before the header', async () => {
adminStoreState.error = 'Boom';
const wrapper = mountLayout();
await wrapper.vm.$nextTick();
const html = wrapper.html();
expect(html.indexOf('Boom')).toBeLessThan(html.indexOf('page-header-tabs-stub'));
});
it('renders the mailer warning at the TOP when serverConfig.mail.configured is false', async () => {
authStoreState.serverConfig = { mail: { configured: false } };
const wrapper = mountLayout();
await wrapper.vm.$nextTick();
const html = wrapper.html();
expect(html.indexOf('No mailer configured')).toBeLessThan(html.indexOf('page-header-tabs-stub'));
});
it('does NOT render the mailer warning when mail is configured', () => {
authStoreState.serverConfig = { mail: { configured: true } };
const wrapper = mountLayout();
expect(wrapper.html()).not.toContain('No mailer configured');
});
it('renders <router-view> OUTSIDE the layout <v-container>', () => {
const wrapper = mountLayout();
const layoutContainer = wrapper.find('.v-container');
const routerViewEl = wrapper.find('.router-view-stub');
expect(layoutContainer.exists()).toBe(true);
expect(routerViewEl.exists()).toBe(true);
expect(layoutContainer.element.contains(routerViewEl.element)).toBe(false);
});
it('CorePageHeaderTabs receives title="Admin" + icon="fa-solid fa-user-tie" in list mode', () => {
const wrapper = mountLayout();
const headerTabs = wrapper.findComponent({ name: 'CorePageHeaderTabs' });
expect(headerTabs.props('title')).toBe('Admin');
expect(headerTabs.props('icon')).toBe('fa-solid fa-user-tie');
});
it('CorePageHeaderTabs receives title="" + icon stays in breadcrumb mode', async () => {
adminStoreState.currentBreadcrumb = { title: 'Jane Doe' };
const wrapper = mountLayout({}, '/admin/users/u1');
await wrapper.vm.$nextTick();
const headerTabs = wrapper.findComponent({ name: 'CorePageHeaderTabs' });
expect(headerTabs.props('title')).toBe('');
expect(headerTabs.props('icon')).toBe('fa-solid fa-user-tie');
});
it('passes hideTabs=true when currentBreadcrumb is set (detail mode)', async () => {
adminStoreState.currentBreadcrumb = { title: 'Jane Doe' };
const wrapper = mountLayout({}, '/admin/users/u1');
await wrapper.vm.$nextTick();
const headerTabs = wrapper.findComponent({ name: 'CorePageHeaderTabs' });
expect(headerTabs.props('hideTabs')).toBe(true);
});
it('passes hideTabs=false when currentBreadcrumb is null (list mode)', () => {
const wrapper = mountLayout();
const headerTabs = wrapper.findComponent({ name: 'CorePageHeaderTabs' });
expect(headerTabs.props('hideTabs')).toBe(false);
});
it('renders the breadcrumb when useAdminStore().currentBreadcrumb is set', async () => {
adminStoreState.currentBreadcrumb = { title: 'Jane Doe', titleClass: 'text-capitalize' };
const wrapper = mountLayout({}, '/admin/users/u1');
await wrapper.vm.$nextTick();
expect(wrapper.text()).toContain('Jane Doe');
});
});