@countermeasure-platform/web-components 1.3.3-dev.33.1 → 1.3.4
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.
- package/README.md +31 -0
- package/dist/component-D5sRm1fq.js +389 -0
- package/dist/component-D5sRm1fq.js.map +1 -0
- package/dist/components/index.js +27 -27
- package/dist/icons/index.d.ts +12 -0
- package/dist/icons/index.d.ts.map +1 -1
- package/dist/icons/index.js +12 -0
- package/dist/icons/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +127 -126
- package/dist/layout/app-shell.d.ts +50 -3
- package/dist/layout/app-shell.d.ts.map +1 -1
- package/dist/layout/app-shell.js +142 -13
- package/dist/layout/app-shell.js.map +1 -1
- package/dist/layout/core-app-chrome.d.ts +81 -0
- package/dist/layout/core-app-chrome.d.ts.map +1 -0
- package/dist/layout/core-app-chrome.js +349 -0
- package/dist/layout/core-app-chrome.js.map +1 -0
- package/dist/layout/index.d.ts +4 -2
- package/dist/layout/index.d.ts.map +1 -1
- package/dist/layout/index.js +88 -87
- package/dist/layout/index.js.map +1 -1
- package/dist/react/primitives/badge.d.ts +1 -1
- package/dist/react/primitives/button.d.ts +2 -2
- package/dist/react/primitives/copy-button.d.ts +1 -1
- package/dist/react/primitives/stat-card.d.ts +1 -1
- package/dist/react/primitives/toggle.d.ts +1 -1
- package/dist/react/sidebar.d.ts +4 -4
- package/dist/react/sidebar.d.ts.map +1 -1
- package/dist/react/sidebar.js +18 -16
- package/dist/react/sidebar.js.map +1 -1
- package/dist/sidebar/component.d.ts +24 -2
- package/dist/sidebar/component.d.ts.map +1 -1
- package/dist/sidebar/enhance.d.ts +8 -0
- package/dist/sidebar/enhance.d.ts.map +1 -1
- package/dist/sidebar/index.d.ts +3 -0
- package/dist/sidebar/index.d.ts.map +1 -1
- package/dist/sidebar/index.js +81 -28
- package/dist/sidebar/index.js.map +1 -1
- package/dist/sidebar/types.d.ts +126 -4
- package/dist/sidebar/types.d.ts.map +1 -1
- package/dist/styles/layout.css +252 -0
- package/dist/styles/sidebar.css +313 -5
- package/package.json +6 -1
- package/src/icons/icons.test.ts +9 -0
- package/src/icons/index.ts +12 -0
- package/src/index.ts +11 -0
- package/src/layout/app-shell.test.ts +204 -0
- package/src/layout/app-shell.ts +362 -3
- package/src/layout/core-app-chrome.test.ts +507 -0
- package/src/layout/core-app-chrome.ts +662 -0
- package/src/layout/index.ts +36 -2
- package/src/react/sidebar.test.tsx +104 -3
- package/src/react/sidebar.tsx +26 -4
- package/src/sidebar/component.test.ts +395 -1
- package/src/sidebar/component.ts +661 -86
- package/src/sidebar/enhance.ts +118 -0
- package/src/sidebar/index.ts +144 -0
- package/src/sidebar/types.ts +143 -4
- package/src/styles/layout.css +252 -0
- package/src/styles/sidebar.css +313 -5
- package/dist/component-Bxhxf21c.js +0 -167
- package/dist/component-Bxhxf21c.js.map +0 -1
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
import { describe, expect, it, beforeEach, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { SidebarComponent } from '../sidebar/component'
|
|
4
|
+
import {
|
|
5
|
+
createCoreAppChromePreset,
|
|
6
|
+
createCoreAppSidebarConfig,
|
|
7
|
+
createCoreAppSidebarSections,
|
|
8
|
+
createCoreAppTopbarActions,
|
|
9
|
+
createCoreAppTopbarConfig,
|
|
10
|
+
getCoreAppRouteLabel,
|
|
11
|
+
mountCoreAppChrome,
|
|
12
|
+
mountCoreAppChromeFromDom,
|
|
13
|
+
} from './core-app-chrome'
|
|
14
|
+
|
|
15
|
+
describe('core app chrome preset', () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
localStorage.clear()
|
|
18
|
+
window.history.replaceState({}, '', '/app')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('creates the Threat Library sidebar sections under the native /app base path', () => {
|
|
22
|
+
const sections = createCoreAppSidebarSections('/app')
|
|
23
|
+
|
|
24
|
+
expect(sections).toHaveLength(5)
|
|
25
|
+
expect(sections[0]?.label).toBe('Dashboards')
|
|
26
|
+
expect(sections[0]?.items[0]).toMatchObject({
|
|
27
|
+
id: 'library',
|
|
28
|
+
label: 'Library',
|
|
29
|
+
href: '/app',
|
|
30
|
+
activeMatch: 'exact',
|
|
31
|
+
end: true,
|
|
32
|
+
})
|
|
33
|
+
expect(sections[0]?.items[1]).toMatchObject({
|
|
34
|
+
id: 'capacity',
|
|
35
|
+
href: '/app/capacity',
|
|
36
|
+
activeMatch: 'prefix',
|
|
37
|
+
})
|
|
38
|
+
expect(sections[3]?.items[1]).toMatchObject({
|
|
39
|
+
id: 'jobs',
|
|
40
|
+
href: '/app/jobs',
|
|
41
|
+
icon: 'jobs',
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('maps native app routes to topbar labels', () => {
|
|
46
|
+
expect(getCoreAppRouteLabel('/app')).toBe('Platform Overview')
|
|
47
|
+
expect(getCoreAppRouteLabel('/app/dashboards/library')).toBe('Library Dashboard')
|
|
48
|
+
expect(getCoreAppRouteLabel('/app/jobs')).toBe('Jobs')
|
|
49
|
+
expect(getCoreAppRouteLabel('/app/api-access/tokens')).toBe('API Access')
|
|
50
|
+
expect(getCoreAppRouteLabel('/app/unknown')).toBe('Threat Library')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('builds a product sidebar config with tenant scope and native behavior defaults', () => {
|
|
54
|
+
const config = createCoreAppSidebarConfig({
|
|
55
|
+
basePath: '/app',
|
|
56
|
+
scope: {
|
|
57
|
+
label: 'Tenant',
|
|
58
|
+
value: 'all',
|
|
59
|
+
options: [{ id: 'all', label: 'All Tenants' }],
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
expect(config.variant).toBe('cm')
|
|
64
|
+
expect(config.brand).toMatchObject({ label: 'CounterMeasure', href: '/app' })
|
|
65
|
+
expect(config.scope?.options[0]?.label).toBe('All Tenants')
|
|
66
|
+
expect(config.collapsible).toBe(true)
|
|
67
|
+
expect(config.collapseButtonVisible).toBe(true)
|
|
68
|
+
expect(config.storageKey).toBe('cm-sidebar-collapsed')
|
|
69
|
+
expect(config.mobileBackdrop).toBe(true)
|
|
70
|
+
expect(config.closeOnNavigate).toBe(true)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('creates topbar defaults with native action hooks', () => {
|
|
74
|
+
const actions = createCoreAppTopbarActions()
|
|
75
|
+
const config = createCoreAppTopbarConfig({ pathname: '/app/jobs' })
|
|
76
|
+
|
|
77
|
+
expect(actions).toHaveLength(3)
|
|
78
|
+
expect(config.product).toBe('CounterMeasure Core')
|
|
79
|
+
expect(config.current).toBe('Jobs')
|
|
80
|
+
expect(config.actions?.[0]).toMatchObject({
|
|
81
|
+
id: 'search',
|
|
82
|
+
label: 'Find...',
|
|
83
|
+
icon: 'search',
|
|
84
|
+
attributes: { 'data-app-search-trigger': true },
|
|
85
|
+
})
|
|
86
|
+
expect(config.actions?.[1]).toMatchObject({
|
|
87
|
+
id: 'feedback',
|
|
88
|
+
attributes: { 'data-feedback-toggle': true },
|
|
89
|
+
})
|
|
90
|
+
expect(config.actions?.[2]).toMatchObject({
|
|
91
|
+
id: 'help',
|
|
92
|
+
icon: 'help',
|
|
93
|
+
attributes: { 'data-help-toggle': true },
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('supports custom topbar action sets and optional topbar config fields', () => {
|
|
98
|
+
const container = document.createElement('div')
|
|
99
|
+
const beforeActions = document.createElement('span')
|
|
100
|
+
const afterActions = document.createElement('span')
|
|
101
|
+
const user = { name: 'Wyatt Roersma', detail: 'Core' }
|
|
102
|
+
|
|
103
|
+
beforeActions.setAttribute('data-before', '')
|
|
104
|
+
afterActions.setAttribute('data-after', '')
|
|
105
|
+
|
|
106
|
+
expect(
|
|
107
|
+
createCoreAppTopbarActions({
|
|
108
|
+
includeSearchAction: false,
|
|
109
|
+
includeFeedbackAction: false,
|
|
110
|
+
includeHelpAction: false,
|
|
111
|
+
})
|
|
112
|
+
).toEqual([])
|
|
113
|
+
|
|
114
|
+
const customAction = { id: 'custom', label: 'Custom', attributes: { 'data-custom': true } }
|
|
115
|
+
const config = createCoreAppTopbarConfig({
|
|
116
|
+
container,
|
|
117
|
+
product: 'Core',
|
|
118
|
+
current: 'Custom Page',
|
|
119
|
+
breadcrumbs: [{ label: 'Platform' }],
|
|
120
|
+
actions: [customAction],
|
|
121
|
+
beforeActions,
|
|
122
|
+
afterActions,
|
|
123
|
+
user,
|
|
124
|
+
iconSet: 'lucide',
|
|
125
|
+
className: 'core-extra',
|
|
126
|
+
includeSearchAction: false,
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
expect(config.container).toBe(container)
|
|
130
|
+
expect(config.product).toBe('Core')
|
|
131
|
+
expect(config.current).toBe('Custom Page')
|
|
132
|
+
expect(config.actions).toEqual([customAction])
|
|
133
|
+
expect(config.beforeActions).toBe(beforeActions)
|
|
134
|
+
expect(config.afterActions).toBe(afterActions)
|
|
135
|
+
expect(config.user).toBe(user)
|
|
136
|
+
expect(config.iconSet).toBe('lucide')
|
|
137
|
+
expect(config.className).toBe('core-extra')
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('copies custom sidebar sections and applies optional sidebar config fields', () => {
|
|
141
|
+
const beforeNav = document.createElement('div')
|
|
142
|
+
const afterNav = document.createElement('div')
|
|
143
|
+
const onNavigate = vi.fn()
|
|
144
|
+
const sections = [
|
|
145
|
+
{
|
|
146
|
+
id: 'custom',
|
|
147
|
+
items: [{ id: 'custom-item', label: 'Custom', href: '/custom' }],
|
|
148
|
+
},
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
const config = createCoreAppSidebarConfig({
|
|
152
|
+
basePath: '/',
|
|
153
|
+
sections,
|
|
154
|
+
variant: 'threat-library',
|
|
155
|
+
brand: { label: 'Library', href: '/library' },
|
|
156
|
+
beforeNav,
|
|
157
|
+
afterNav,
|
|
158
|
+
user: { name: 'Analyst' },
|
|
159
|
+
footerActions: [{ id: 'settings', label: 'Settings', icon: 'settings' }],
|
|
160
|
+
iconSet: 'lucide',
|
|
161
|
+
collapsed: true,
|
|
162
|
+
activeItemId: 'custom-item',
|
|
163
|
+
pathname: '/custom',
|
|
164
|
+
persistCollapsed: false,
|
|
165
|
+
mobileBreakpoint: 640,
|
|
166
|
+
open: true,
|
|
167
|
+
onNavigate,
|
|
168
|
+
onCollapse: vi.fn(),
|
|
169
|
+
onOpenChange: vi.fn(),
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
sections[0]?.items.push({ id: 'later', label: 'Later', href: '/later' })
|
|
173
|
+
|
|
174
|
+
expect(config.variant).toBe('threat-library')
|
|
175
|
+
expect(config.sections).toHaveLength(1)
|
|
176
|
+
expect(config.sections[0]?.items).toHaveLength(1)
|
|
177
|
+
expect(config.brand).toMatchObject({ label: 'Library', href: '/library' })
|
|
178
|
+
expect(config.beforeNav).toBe(beforeNav)
|
|
179
|
+
expect(config.afterNav).toBe(afterNav)
|
|
180
|
+
expect(config.user?.name).toBe('Analyst')
|
|
181
|
+
expect(config.footerActions?.[0]?.id).toBe('settings')
|
|
182
|
+
expect(config.iconSet).toBe('lucide')
|
|
183
|
+
expect(config.collapsed).toBe(true)
|
|
184
|
+
expect(config.activeItemId).toBe('custom-item')
|
|
185
|
+
expect(config.pathname).toBe('/custom')
|
|
186
|
+
expect(config.persistCollapsed).toBe(false)
|
|
187
|
+
expect(config.mobileBreakpoint).toBe(640)
|
|
188
|
+
expect(config.open).toBe(true)
|
|
189
|
+
expect(config.onNavigate).toBe(onNavigate)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('renders the preset as an active product sidebar for native /app routes', () => {
|
|
193
|
+
window.history.replaceState({}, '', '/')
|
|
194
|
+
const container = document.createElement('div')
|
|
195
|
+
document.body.appendChild(container)
|
|
196
|
+
|
|
197
|
+
const preset = createCoreAppChromePreset({ basePath: '/app', pathname: '/app/jobs' })
|
|
198
|
+
const sidebar = new SidebarComponent({ container, ...preset.sidebar })
|
|
199
|
+
|
|
200
|
+
const aside = container.querySelector<HTMLElement>('.sidebar')
|
|
201
|
+
const active = container.querySelector<HTMLElement>('[data-item-id="jobs"]')
|
|
202
|
+
const library = container.querySelector<HTMLElement>('[data-item-id="library"]')
|
|
203
|
+
|
|
204
|
+
expect(preset.topbar.current).toBe('Jobs')
|
|
205
|
+
expect(preset.sidebar.pathname).toBe('/app/jobs')
|
|
206
|
+
expect(aside?.classList.contains('sidebar--product')).toBe(true)
|
|
207
|
+
expect(aside?.getAttribute('data-sidebar')).toBe('cm')
|
|
208
|
+
expect(active?.classList.contains('sidebar__item--active')).toBe(true)
|
|
209
|
+
expect(active?.getAttribute('aria-current')).toBe('page')
|
|
210
|
+
expect(library?.classList.contains('sidebar__item--active')).toBe(false)
|
|
211
|
+
|
|
212
|
+
sidebar.destroy()
|
|
213
|
+
container.remove()
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('mounts and destroys native app chrome in server-rendered containers', () => {
|
|
217
|
+
window.history.replaceState({}, '', '/')
|
|
218
|
+
const sidebarContainer = document.createElement('div')
|
|
219
|
+
const topbarContainer = document.createElement('div')
|
|
220
|
+
const serverPlaceholder = document.createElement('span')
|
|
221
|
+
const notificationAction = document.createElement('button')
|
|
222
|
+
|
|
223
|
+
serverPlaceholder.textContent = 'server sidebar'
|
|
224
|
+
notificationAction.type = 'button'
|
|
225
|
+
notificationAction.setAttribute('data-notification-toggle', '')
|
|
226
|
+
notificationAction.textContent = 'Notifications'
|
|
227
|
+
sidebarContainer.appendChild(serverPlaceholder)
|
|
228
|
+
document.body.append(sidebarContainer, topbarContainer)
|
|
229
|
+
|
|
230
|
+
const mounted = mountCoreAppChrome({
|
|
231
|
+
sidebarContainer,
|
|
232
|
+
topbarContainer,
|
|
233
|
+
basePath: '/app',
|
|
234
|
+
pathname: '/app/jobs',
|
|
235
|
+
scope: {
|
|
236
|
+
label: 'Tenant',
|
|
237
|
+
value: 'all',
|
|
238
|
+
options: [{ id: 'all', label: 'All Tenants', status: 'neutral' }],
|
|
239
|
+
},
|
|
240
|
+
topbarActions: [notificationAction],
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
const aside = sidebarContainer.querySelector<HTMLElement>('.sidebar')
|
|
244
|
+
const topbar = topbarContainer.querySelector<HTMLElement>('.cmm-topbar')
|
|
245
|
+
const active = sidebarContainer.querySelector<HTMLElement>('[data-item-id="jobs"]')
|
|
246
|
+
|
|
247
|
+
expect(serverPlaceholder.isConnected).toBe(false)
|
|
248
|
+
expect(aside?.classList.contains('sidebar--product')).toBe(true)
|
|
249
|
+
expect(active?.classList.contains('sidebar__item--active')).toBe(true)
|
|
250
|
+
expect(topbar?.querySelector('.cmm-topbar__current')?.textContent).toBe('Jobs')
|
|
251
|
+
expect(topbar?.querySelector('[data-notification-toggle]')).toBe(notificationAction)
|
|
252
|
+
expect(sidebarContainer.querySelector('.sidebar__scope-current')?.textContent).toBe(
|
|
253
|
+
'All Tenants'
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
mounted.destroy()
|
|
257
|
+
expect(sidebarContainer.querySelector('.sidebar')).toBeNull()
|
|
258
|
+
expect(topbarContainer.querySelector('.cmm-topbar')).toBeNull()
|
|
259
|
+
|
|
260
|
+
sidebarContainer.remove()
|
|
261
|
+
topbarContainer.remove()
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it('preserves explicitly marked native topbar action nodes during mount', () => {
|
|
265
|
+
const sidebarContainer = document.createElement('div')
|
|
266
|
+
const topbarContainer = document.createElement('div')
|
|
267
|
+
const action = document.createElement('button')
|
|
268
|
+
const beforeActions = document.createElement('span')
|
|
269
|
+
|
|
270
|
+
action.type = 'button'
|
|
271
|
+
action.setAttribute('data-core-app-topbar-action', '')
|
|
272
|
+
action.setAttribute('data-notification-toggle', '')
|
|
273
|
+
action.textContent = 'Notifications'
|
|
274
|
+
beforeActions.setAttribute('data-before-actions', '')
|
|
275
|
+
topbarContainer.appendChild(action)
|
|
276
|
+
document.body.append(sidebarContainer, topbarContainer)
|
|
277
|
+
|
|
278
|
+
const mounted = mountCoreAppChrome({
|
|
279
|
+
sidebarContainer,
|
|
280
|
+
topbarContainer,
|
|
281
|
+
pathname: '/app',
|
|
282
|
+
topbar: { afterActions: beforeActions },
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
const topbar = topbarContainer.querySelector<HTMLElement>('.cmm-topbar')
|
|
286
|
+
expect(topbar?.querySelector('[data-app-search-trigger]')).toBeTruthy()
|
|
287
|
+
expect(topbar?.querySelector('[data-before-actions]')).toBe(beforeActions)
|
|
288
|
+
expect(topbar?.querySelector('[data-notification-toggle]')).toBe(action)
|
|
289
|
+
expect(action.isConnected).toBe(true)
|
|
290
|
+
|
|
291
|
+
mounted.destroy()
|
|
292
|
+
sidebarContainer.remove()
|
|
293
|
+
topbarContainer.remove()
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
it('dedupes preserved default action nodes and supports no-topbar mounts', () => {
|
|
297
|
+
const sidebarContainer = document.createElement('div')
|
|
298
|
+
const topbarContainer = document.createElement('div')
|
|
299
|
+
const preservedSearch = document.createElement('button')
|
|
300
|
+
const preservedFeedback = document.createElement('button')
|
|
301
|
+
const preservedHelp = document.createElement('button')
|
|
302
|
+
|
|
303
|
+
preservedSearch.setAttribute('data-core-app-topbar-action', '')
|
|
304
|
+
preservedSearch.setAttribute('data-app-search-trigger', '')
|
|
305
|
+
preservedFeedback.setAttribute('data-core-app-topbar-action', '')
|
|
306
|
+
preservedFeedback.setAttribute('data-feedback-toggle', '')
|
|
307
|
+
preservedHelp.setAttribute('data-core-app-topbar-action', '')
|
|
308
|
+
preservedHelp.setAttribute('data-help-toggle', '')
|
|
309
|
+
topbarContainer.append(preservedSearch, preservedFeedback, preservedHelp)
|
|
310
|
+
document.body.append(sidebarContainer, topbarContainer)
|
|
311
|
+
|
|
312
|
+
const mounted = mountCoreAppChrome({
|
|
313
|
+
sidebarContainer,
|
|
314
|
+
topbarContainer,
|
|
315
|
+
pathname: '/app',
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
expect(topbarContainer.querySelectorAll('[data-app-search-trigger]')).toHaveLength(1)
|
|
319
|
+
expect(topbarContainer.querySelectorAll('[data-feedback-toggle]')).toHaveLength(1)
|
|
320
|
+
expect(topbarContainer.querySelectorAll('[data-help-toggle]')).toHaveLength(1)
|
|
321
|
+
mounted.destroy()
|
|
322
|
+
|
|
323
|
+
const sidebarOnlyContainer = document.createElement('div')
|
|
324
|
+
document.body.appendChild(sidebarOnlyContainer)
|
|
325
|
+
const sidebarOnly = mountCoreAppChrome({
|
|
326
|
+
sidebarContainer: sidebarOnlyContainer,
|
|
327
|
+
pathname: '/app',
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
expect(sidebarOnly.topbar).toBeUndefined()
|
|
331
|
+
expect(sidebarOnlyContainer.querySelector('.sidebar')).toBeTruthy()
|
|
332
|
+
|
|
333
|
+
sidebarOnly.destroy()
|
|
334
|
+
sidebarContainer.remove()
|
|
335
|
+
topbarContainer.remove()
|
|
336
|
+
sidebarOnlyContainer.remove()
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
it('honors clear-container and preservation controls during mount', () => {
|
|
340
|
+
const sidebarContainer = document.createElement('div')
|
|
341
|
+
const topbarContainer = document.createElement('div')
|
|
342
|
+
const existingSidebar = document.createElement('p')
|
|
343
|
+
const preserved = document.createElement('button')
|
|
344
|
+
|
|
345
|
+
existingSidebar.textContent = 'existing'
|
|
346
|
+
preserved.setAttribute('data-core-app-topbar-action', '')
|
|
347
|
+
preserved.setAttribute('data-help-toggle', '')
|
|
348
|
+
sidebarContainer.appendChild(existingSidebar)
|
|
349
|
+
topbarContainer.appendChild(preserved)
|
|
350
|
+
document.body.append(sidebarContainer, topbarContainer)
|
|
351
|
+
|
|
352
|
+
const mounted = mountCoreAppChrome({
|
|
353
|
+
sidebarContainer,
|
|
354
|
+
topbarContainer,
|
|
355
|
+
clearContainers: false,
|
|
356
|
+
preserveTopbarActions: false,
|
|
357
|
+
topbar: {
|
|
358
|
+
includeSearchAction: false,
|
|
359
|
+
includeFeedbackAction: false,
|
|
360
|
+
includeHelpAction: true,
|
|
361
|
+
},
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
expect(sidebarContainer.contains(existingSidebar)).toBe(true)
|
|
365
|
+
expect(topbarContainer.contains(preserved)).toBe(true)
|
|
366
|
+
expect(topbarContainer.querySelectorAll('[data-help-toggle]')).toHaveLength(2)
|
|
367
|
+
|
|
368
|
+
mounted.destroy()
|
|
369
|
+
sidebarContainer.remove()
|
|
370
|
+
topbarContainer.remove()
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
it('mounts from declarative DOM placeholders', () => {
|
|
374
|
+
window.history.replaceState({}, '', '/')
|
|
375
|
+
const root = document.createElement('section')
|
|
376
|
+
const sidebarContainer = document.createElement('div')
|
|
377
|
+
const topbarContainer = document.createElement('div')
|
|
378
|
+
const action = document.createElement('button')
|
|
379
|
+
|
|
380
|
+
sidebarContainer.setAttribute('data-core-app-sidebar', '')
|
|
381
|
+
sidebarContainer.setAttribute('data-base-path', '/app')
|
|
382
|
+
sidebarContainer.setAttribute('data-pathname', '/app/api-access/tokens')
|
|
383
|
+
topbarContainer.setAttribute('data-core-app-topbar', '')
|
|
384
|
+
topbarContainer.setAttribute('data-product', 'CounterMeasure Core')
|
|
385
|
+
topbarContainer.setAttribute('data-current', 'API Tokens')
|
|
386
|
+
action.type = 'button'
|
|
387
|
+
action.setAttribute('data-core-app-topbar-action', '')
|
|
388
|
+
action.setAttribute('data-help-toggle', '')
|
|
389
|
+
action.textContent = 'Help'
|
|
390
|
+
topbarContainer.appendChild(action)
|
|
391
|
+
root.append(sidebarContainer, topbarContainer)
|
|
392
|
+
document.body.appendChild(root)
|
|
393
|
+
|
|
394
|
+
const mounted = mountCoreAppChromeFromDom({ root })
|
|
395
|
+
|
|
396
|
+
expect(mounted).not.toBeNull()
|
|
397
|
+
expect(sidebarContainer.querySelector('.sidebar--product')).toBeTruthy()
|
|
398
|
+
expect(
|
|
399
|
+
sidebarContainer.querySelector('[data-item-id="api-access"]')?.getAttribute('aria-current')
|
|
400
|
+
).toBe('page')
|
|
401
|
+
expect(topbarContainer.querySelector('.cmm-topbar__product')?.textContent).toBe(
|
|
402
|
+
'CounterMeasure Core'
|
|
403
|
+
)
|
|
404
|
+
expect(topbarContainer.querySelector('.cmm-topbar__current')?.textContent).toBe('API Tokens')
|
|
405
|
+
expect(topbarContainer.querySelector('[data-help-toggle]')).toBe(action)
|
|
406
|
+
|
|
407
|
+
mounted?.destroy()
|
|
408
|
+
root.remove()
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
it('lets explicit topbar options override declarative labels', () => {
|
|
412
|
+
const root = document.createElement('section')
|
|
413
|
+
const sidebarContainer = document.createElement('div')
|
|
414
|
+
const topbarContainer = document.createElement('div')
|
|
415
|
+
|
|
416
|
+
sidebarContainer.setAttribute('data-core-app-sidebar', '')
|
|
417
|
+
topbarContainer.setAttribute('data-core-app-topbar', '')
|
|
418
|
+
topbarContainer.setAttribute('data-product', 'CounterMeasure Core')
|
|
419
|
+
topbarContainer.setAttribute('data-current', 'Platform Overview')
|
|
420
|
+
root.append(sidebarContainer, topbarContainer)
|
|
421
|
+
document.body.appendChild(root)
|
|
422
|
+
|
|
423
|
+
const mounted = mountCoreAppChromeFromDom({
|
|
424
|
+
root,
|
|
425
|
+
topbar: {
|
|
426
|
+
product: 'Threat Library',
|
|
427
|
+
current: 'Library Dashboard',
|
|
428
|
+
},
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
expect(topbarContainer.querySelector('.cmm-topbar__product')?.textContent).toBe(
|
|
432
|
+
'Threat Library'
|
|
433
|
+
)
|
|
434
|
+
expect(topbarContainer.querySelector('.cmm-topbar__current')?.textContent).toBe(
|
|
435
|
+
'Library Dashboard'
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
mounted?.destroy()
|
|
439
|
+
root.remove()
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
it('mounts from custom selectors and sidebar-provided declarative labels', () => {
|
|
443
|
+
const root = document.createElement('section')
|
|
444
|
+
const sidebarContainer = document.createElement('div')
|
|
445
|
+
const topbarContainer = document.createElement('div')
|
|
446
|
+
const customAction = document.createElement('button')
|
|
447
|
+
|
|
448
|
+
sidebarContainer.setAttribute('data-custom-sidebar', '')
|
|
449
|
+
sidebarContainer.setAttribute('data-base-path', '/native')
|
|
450
|
+
sidebarContainer.setAttribute('data-pathname', '/native/connectors')
|
|
451
|
+
sidebarContainer.setAttribute('data-product', 'Native Core')
|
|
452
|
+
sidebarContainer.setAttribute('data-current', 'Native Connectors')
|
|
453
|
+
topbarContainer.setAttribute('data-custom-topbar', '')
|
|
454
|
+
customAction.setAttribute('data-preserved-action', '')
|
|
455
|
+
customAction.setAttribute('data-app-search-trigger', '')
|
|
456
|
+
topbarContainer.appendChild(customAction)
|
|
457
|
+
root.append(sidebarContainer, topbarContainer)
|
|
458
|
+
document.body.appendChild(root)
|
|
459
|
+
|
|
460
|
+
const mounted = mountCoreAppChromeFromDom({
|
|
461
|
+
root,
|
|
462
|
+
sidebarSelector: '[data-custom-sidebar]',
|
|
463
|
+
topbarSelector: '[data-custom-topbar]',
|
|
464
|
+
topbarActionSelector: '[data-preserved-action]',
|
|
465
|
+
})
|
|
466
|
+
|
|
467
|
+
expect(mounted).not.toBeNull()
|
|
468
|
+
expect(
|
|
469
|
+
sidebarContainer.querySelector('[data-item-id="connectors"]')?.getAttribute('href')
|
|
470
|
+
).toBe('/native/connectors')
|
|
471
|
+
expect(
|
|
472
|
+
sidebarContainer.querySelector('[data-item-id="connectors"]')?.getAttribute('aria-current')
|
|
473
|
+
).toBe('page')
|
|
474
|
+
expect(topbarContainer.querySelector('.cmm-topbar__product')?.textContent).toBe('Native Core')
|
|
475
|
+
expect(topbarContainer.querySelector('.cmm-topbar__current')?.textContent).toBe(
|
|
476
|
+
'Native Connectors'
|
|
477
|
+
)
|
|
478
|
+
expect(topbarContainer.querySelectorAll('[data-app-search-trigger]')).toHaveLength(1)
|
|
479
|
+
|
|
480
|
+
mounted?.destroy()
|
|
481
|
+
root.remove()
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
it('returns null when no declarative sidebar placeholder exists', () => {
|
|
485
|
+
const root = document.createElement('section')
|
|
486
|
+
|
|
487
|
+
expect(mountCoreAppChromeFromDom({ root })).toBeNull()
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
it('mounts declarative sidebar-only chrome when no topbar data is present', () => {
|
|
491
|
+
const root = document.createElement('section')
|
|
492
|
+
const sidebarContainer = document.createElement('div')
|
|
493
|
+
|
|
494
|
+
sidebarContainer.setAttribute('data-core-app-sidebar', '')
|
|
495
|
+
root.appendChild(sidebarContainer)
|
|
496
|
+
document.body.appendChild(root)
|
|
497
|
+
|
|
498
|
+
const mounted = mountCoreAppChromeFromDom({ root })
|
|
499
|
+
|
|
500
|
+
expect(mounted).not.toBeNull()
|
|
501
|
+
expect(mounted?.topbar).toBeUndefined()
|
|
502
|
+
expect(sidebarContainer.querySelector('.sidebar--product')).toBeTruthy()
|
|
503
|
+
|
|
504
|
+
mounted?.destroy()
|
|
505
|
+
root.remove()
|
|
506
|
+
})
|
|
507
|
+
})
|