@classic-homes/theme-svelte 0.1.56 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Navigation Access Filtering Tests
3
+ *
4
+ * Covers which navigation items a user sees given the roles and permissions
5
+ * they hold — including the nested case, where a restricted child sits under
6
+ * an unrestricted parent.
7
+ */
8
+ import { describe, it, expect } from 'vitest';
9
+ import { filterNavItems, isNavItemVisible, matchesAccess } from '../lib/utils/nav-access.js';
10
+ function item(partial) {
11
+ return { name: partial.id, ...partial };
12
+ }
13
+ const ids = (items) => items.map((i) => i.id);
14
+ describe('matchesAccess', () => {
15
+ it('matches when nothing is required', () => {
16
+ expect(matchesAccess(undefined, [])).toBe(true);
17
+ expect(matchesAccess([], [])).toBe(true);
18
+ });
19
+ it('does not match a requirement when the user holds nothing', () => {
20
+ expect(matchesAccess(['admin'], undefined)).toBe(false);
21
+ expect(matchesAccess(['admin'], [])).toBe(false);
22
+ });
23
+ it('matches any by default', () => {
24
+ expect(matchesAccess(['admin', 'manager'], ['manager'])).toBe(true);
25
+ expect(matchesAccess(['admin', 'manager'], ['agent'])).toBe(false);
26
+ });
27
+ it('can require all', () => {
28
+ expect(matchesAccess(['admin', 'manager'], ['admin'], true)).toBe(false);
29
+ expect(matchesAccess(['admin', 'manager'], ['admin', 'manager'], true)).toBe(true);
30
+ });
31
+ });
32
+ describe('isNavItemVisible', () => {
33
+ it('shows unrestricted items to anyone', () => {
34
+ expect(isNavItemVisible(item({ id: 'a' }), {})).toBe(true);
35
+ });
36
+ it('matches against every role held, not just one', () => {
37
+ const nav = item({ id: 'a', roles: ['agent'] });
38
+ expect(isNavItemVisible(nav, { userRoles: ['manager', 'agent'] })).toBe(true);
39
+ });
40
+ it('does not imply roles from one another', () => {
41
+ // Mirrors the auth package: holding admin does not imply agent, so the UI
42
+ // cannot offer something the server would reject.
43
+ const nav = item({ id: 'a', roles: ['agent'] });
44
+ expect(isNavItemVisible(nav, { userRoles: ['admin'] })).toBe(false);
45
+ });
46
+ it('combines roles and permissions as AND', () => {
47
+ const nav = item({ id: 'a', roles: ['manager'], permissions: ['users:write'] });
48
+ expect(isNavItemVisible(nav, { userRoles: ['manager'], userPermissions: ['users:write'] })).toBe(true);
49
+ expect(isNavItemVisible(nav, { userRoles: ['manager'], userPermissions: ['users:read'] })).toBe(false);
50
+ expect(isNavItemVisible(nav, { userRoles: ['agent'], userPermissions: ['users:write'] })).toBe(false);
51
+ });
52
+ it('gates on permissions alone', () => {
53
+ const nav = item({ id: 'a', permissions: ['settings:read'] });
54
+ expect(isNavItemVisible(nav, { userPermissions: ['settings:read'] })).toBe(true);
55
+ expect(isNavItemVisible(nav, { userRoles: ['admin'] })).toBe(false);
56
+ });
57
+ });
58
+ describe('filterNavItems', () => {
59
+ it('shows the union across a multi-role user', () => {
60
+ const items = [
61
+ item({ id: 'dashboard' }),
62
+ item({ id: 'users', roles: ['manager'] }),
63
+ item({ id: 'homes', roles: ['agent'] }),
64
+ item({ id: 'system', roles: ['admin'] }),
65
+ ];
66
+ expect(ids(filterNavItems(items, { userRoles: ['manager', 'agent'] }))).toEqual([
67
+ 'dashboard',
68
+ 'users',
69
+ 'homes',
70
+ ]);
71
+ });
72
+ it('filters a restricted child under an unrestricted parent', () => {
73
+ // The parent passes on its own merits; without recursion nothing would ever
74
+ // look inside it and the admin-only child would show to everyone.
75
+ const items = [
76
+ item({
77
+ id: 'main',
78
+ children: [item({ id: 'open' }), item({ id: 'audit', roles: ['admin'] })],
79
+ }),
80
+ ];
81
+ const filtered = filterNavItems(items, { userRoles: ['agent'] });
82
+ expect(ids(filtered[0].children)).toEqual(['open']);
83
+ });
84
+ it('filters arbitrarily deep', () => {
85
+ const items = [
86
+ item({
87
+ id: 'l1',
88
+ children: [
89
+ item({
90
+ id: 'l2',
91
+ children: [item({ id: 'l3', roles: ['admin'] }), item({ id: 'l3-open' })],
92
+ }),
93
+ ],
94
+ }),
95
+ ];
96
+ const filtered = filterNavItems(items, { userRoles: ['agent'] });
97
+ expect(ids(filtered[0].children[0].children)).toEqual(['l3-open']);
98
+ });
99
+ it('drops a container whose children are all filtered away', () => {
100
+ const items = [
101
+ item({ id: 'admin-area', children: [item({ id: 'system', roles: ['admin'] })] }),
102
+ ];
103
+ // Keeping it would render an expandable with nothing behind it.
104
+ expect(ids(filterNavItems(items, { userRoles: ['agent'] }))).toEqual([]);
105
+ });
106
+ it('keeps an emptied parent that is a destination in its own right', () => {
107
+ const items = [
108
+ item({
109
+ id: 'reports',
110
+ href: '/reports',
111
+ children: [item({ id: 'revenue', roles: ['admin'] })],
112
+ }),
113
+ ];
114
+ const filtered = filterNavItems(items, { userRoles: ['agent'] });
115
+ expect(ids(filtered)).toEqual(['reports']);
116
+ expect(filtered[0].children).toEqual([]);
117
+ });
118
+ it('hides everything restricted from a user with no roles', () => {
119
+ const items = [item({ id: 'dashboard' }), item({ id: 'users', roles: ['manager'] })];
120
+ expect(ids(filterNavItems(items, {}))).toEqual(['dashboard']);
121
+ });
122
+ it('honors requireAllRoles', () => {
123
+ const items = [item({ id: 'both', roles: ['admin', 'manager'] })];
124
+ expect(ids(filterNavItems(items, { userRoles: ['admin'], requireAllRoles: true }))).toEqual([]);
125
+ expect(ids(filterNavItems(items, { userRoles: ['admin', 'manager'], requireAllRoles: true }))).toEqual(['both']);
126
+ });
127
+ it('honors requireAllPermissions', () => {
128
+ const items = [item({ id: 'both', permissions: ['a:read', 'b:read'] })];
129
+ expect(ids(filterNavItems(items, { userPermissions: ['a:read'], requireAllPermissions: true }))).toEqual([]);
130
+ expect(ids(filterNavItems(items, {
131
+ userPermissions: ['a:read', 'b:read'],
132
+ requireAllPermissions: true,
133
+ }))).toEqual(['both']);
134
+ });
135
+ it('leaves childless items without a children key', () => {
136
+ const filtered = filterNavItems([item({ id: 'a' })], {});
137
+ expect('children' in filtered[0]).toBe(false);
138
+ });
139
+ it('does not mutate the input', () => {
140
+ const items = [item({ id: 'main', children: [item({ id: 'audit', roles: ['admin'] })] })];
141
+ const before = JSON.stringify(items);
142
+ filterNavItems(items, { userRoles: ['agent'] });
143
+ expect(JSON.stringify(items)).toBe(before);
144
+ });
145
+ });
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Sidebar Role-based Expansion Tests
3
+ *
4
+ * Covers which sections start expanded for a given user. This is a UX
5
+ * affordance, not access control — it decides what starts open, not what is
6
+ * visible (that's nav-access).
7
+ */
8
+ import { describe, it, expect } from 'vitest';
9
+ import { sidebarStore } from '../lib/stores/sidebar.svelte.js';
10
+ function section(partial) {
11
+ return { title: partial.id, items: [], ...partial };
12
+ }
13
+ describe('computeInitialExpansions', () => {
14
+ it('expands a section for any matching role', () => {
15
+ const sections = [section({ id: 'admin', expandForRoles: ['admin', 'superuser'] })];
16
+ expect(sidebarStore.computeInitialExpansions(sections, ['admin'])).toEqual(['admin']);
17
+ expect(sidebarStore.computeInitialExpansions(sections, ['superuser'])).toEqual(['admin']);
18
+ });
19
+ it('expands on a secondary role, not just the first held', () => {
20
+ const sections = [section({ id: 'ops', expandForRoles: ['agent'] })];
21
+ expect(sidebarStore.computeInitialExpansions(sections, ['manager', 'agent'])).toEqual(['ops']);
22
+ });
23
+ it('leaves a section collapsed when no role matches', () => {
24
+ const sections = [section({ id: 'admin', expandForRoles: ['admin'] })];
25
+ expect(sidebarStore.computeInitialExpansions(sections, ['agent'])).toEqual([]);
26
+ });
27
+ it('leaves a role-gated section collapsed for a user with no roles', () => {
28
+ const sections = [section({ id: 'admin', expandForRoles: ['admin'] })];
29
+ expect(sidebarStore.computeInitialExpansions(sections, [])).toEqual([]);
30
+ expect(sidebarStore.computeInitialExpansions(sections, undefined)).toEqual([]);
31
+ });
32
+ it('expands non-collapsible sections regardless of roles', () => {
33
+ const sections = [section({ id: 'main', collapsible: false })];
34
+ expect(sidebarStore.computeInitialExpansions(sections, [])).toEqual(['main']);
35
+ });
36
+ it('respects an explicitly collapsed section', () => {
37
+ const sections = [section({ id: 'main', collapsible: true, expanded: false })];
38
+ expect(sidebarStore.computeInitialExpansions(sections, ['admin'])).toEqual([]);
39
+ });
40
+ it('expands a collapsible section that is not explicitly collapsed', () => {
41
+ const sections = [section({ id: 'main', collapsible: true })];
42
+ expect(sidebarStore.computeInitialExpansions(sections, [])).toEqual(['main']);
43
+ });
44
+ it('prefers expandForRoles over the collapsed default', () => {
45
+ // expandForRoles is checked first, so it wins over `expanded: false`.
46
+ const sections = [
47
+ section({ id: 'admin', collapsible: true, expanded: false, expandForRoles: ['admin'] }),
48
+ ];
49
+ expect(sidebarStore.computeInitialExpansions(sections, ['admin'])).toEqual(['admin']);
50
+ });
51
+ it('handles a mixed navigation', () => {
52
+ const sections = [
53
+ section({ id: 'main', collapsible: false }),
54
+ section({ id: 'admin', expandForRoles: ['admin'] }),
55
+ section({ id: 'ops', expandForRoles: ['agent'] }),
56
+ ];
57
+ expect(sidebarStore.computeInitialExpansions(sections, ['agent'])).toEqual(['main', 'ops']);
58
+ });
59
+ });