@open-mercato/ui 0.6.3-develop.3838.1.7f2657a47c → 0.6.3-develop.3857.1.da89d7530c

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.
@@ -2,7 +2,7 @@
2
2
  import { jsx } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { MutationCache, QueryCache, QueryClient, QueryClientProvider } from "@tanstack/react-query";
5
- import { redirectToSessionRefresh, redirectToForbiddenLogin, UnauthorizedError, ForbiddenError, apiFetch, setAuthRedirectConfig } from "../backend/utils/api.js";
5
+ import { redirectToSessionRefresh, notifyForbiddenAccess, UnauthorizedError, ForbiddenError, apiFetch, setAuthRedirectConfig } from "../backend/utils/api.js";
6
6
  function ensureGlobalFetchInterception() {
7
7
  if (typeof window === "undefined") return;
8
8
  const w = window;
@@ -15,17 +15,17 @@ const client = new QueryClient({
15
15
  queryCache: new QueryCache({
16
16
  onError: (error) => {
17
17
  if (error instanceof UnauthorizedError) redirectToSessionRefresh();
18
- else if (error instanceof ForbiddenError) redirectToForbiddenLogin();
18
+ else if (error instanceof ForbiddenError) notifyForbiddenAccess();
19
19
  else if (error?.status === 401) redirectToSessionRefresh();
20
- else if (error?.status === 403) redirectToForbiddenLogin();
20
+ else if (error?.status === 403) notifyForbiddenAccess();
21
21
  }
22
22
  }),
23
23
  mutationCache: new MutationCache({
24
24
  onError: (error) => {
25
25
  if (error instanceof UnauthorizedError) redirectToSessionRefresh();
26
- else if (error instanceof ForbiddenError) redirectToForbiddenLogin();
26
+ else if (error instanceof ForbiddenError) notifyForbiddenAccess();
27
27
  else if (error?.status === 401) redirectToSessionRefresh();
28
- else if (error?.status === 403) redirectToForbiddenLogin();
28
+ else if (error?.status === 403) notifyForbiddenAccess();
29
29
  }
30
30
  })
31
31
  });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/theme/QueryProvider.tsx"],
4
- "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { MutationCache, QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query'\nimport { redirectToSessionRefresh, redirectToForbiddenLogin, UnauthorizedError, ForbiddenError, apiFetch, setAuthRedirectConfig } from '../backend/utils/api'\n\n// Ensure global fetch calls also respect our redirect-on-401/403 policy.\nfunction ensureGlobalFetchInterception() {\n if (typeof window === 'undefined') return\n const w = window as any\n if (w.__omFetchPatched) return\n w.__omFetchPatched = true\n w.__omOriginalFetch = window.fetch\n window.fetch = ((input: RequestInfo | URL, init?: RequestInit) => apiFetch(input, init)) as any\n}\n\nconst client = new QueryClient({\n queryCache: new QueryCache({\n onError: (error) => {\n if (error instanceof UnauthorizedError) redirectToSessionRefresh()\n else if (error instanceof ForbiddenError) redirectToForbiddenLogin()\n // As a fallback, try to detect common cases\n else if ((error as any)?.status === 401) redirectToSessionRefresh()\n else if ((error as any)?.status === 403) redirectToForbiddenLogin()\n },\n }),\n mutationCache: new MutationCache({\n onError: (error) => {\n if (error instanceof UnauthorizedError) redirectToSessionRefresh()\n else if (error instanceof ForbiddenError) redirectToForbiddenLogin()\n else if ((error as any)?.status === 401) redirectToSessionRefresh()\n else if ((error as any)?.status === 403) redirectToForbiddenLogin()\n },\n }),\n})\n\ntype QueryProviderProps = { children: React.ReactNode; defaultForbiddenRoles?: string[] }\n\nexport function QueryProvider({ children, defaultForbiddenRoles }: QueryProviderProps) {\n React.useEffect(() => {\n ensureGlobalFetchInterception()\n if (defaultForbiddenRoles && defaultForbiddenRoles.length) {\n setAuthRedirectConfig({ defaultForbiddenRoles })\n }\n }, [])\n return <QueryClientProvider client={client}>{children}</QueryClientProvider>\n}\n"],
5
- "mappings": ";AA4CS;AA3CT,YAAY,WAAW;AACvB,SAAS,eAAe,YAAY,aAAa,2BAA2B;AAC5E,SAAS,0BAA0B,0BAA0B,mBAAmB,gBAAgB,UAAU,6BAA6B;AAGvI,SAAS,gCAAgC;AACvC,MAAI,OAAO,WAAW,YAAa;AACnC,QAAM,IAAI;AACV,MAAI,EAAE,iBAAkB;AACxB,IAAE,mBAAmB;AACrB,IAAE,oBAAoB,OAAO;AAC7B,SAAO,SAAS,CAAC,OAA0B,SAAuB,SAAS,OAAO,IAAI;AACxF;AAEA,MAAM,SAAS,IAAI,YAAY;AAAA,EAC7B,YAAY,IAAI,WAAW;AAAA,IACzB,SAAS,CAAC,UAAU;AAClB,UAAI,iBAAiB,kBAAmB,0BAAyB;AAAA,eACxD,iBAAiB,eAAgB,0BAAyB;AAAA,eAEzD,OAAe,WAAW,IAAK,0BAAyB;AAAA,eACxD,OAAe,WAAW,IAAK,0BAAyB;AAAA,IACpE;AAAA,EACF,CAAC;AAAA,EACD,eAAe,IAAI,cAAc;AAAA,IAC/B,SAAS,CAAC,UAAU;AAClB,UAAI,iBAAiB,kBAAmB,0BAAyB;AAAA,eACxD,iBAAiB,eAAgB,0BAAyB;AAAA,eACzD,OAAe,WAAW,IAAK,0BAAyB;AAAA,eACxD,OAAe,WAAW,IAAK,0BAAyB;AAAA,IACpE;AAAA,EACF,CAAC;AACH,CAAC;AAIM,SAAS,cAAc,EAAE,UAAU,sBAAsB,GAAuB;AACrF,QAAM,UAAU,MAAM;AACpB,kCAA8B;AAC9B,QAAI,yBAAyB,sBAAsB,QAAQ;AACzD,4BAAsB,EAAE,sBAAsB,CAAC;AAAA,IACjD;AAAA,EACF,GAAG,CAAC,CAAC;AACL,SAAO,oBAAC,uBAAoB,QAAiB,UAAS;AACxD;",
4
+ "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { MutationCache, QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query'\nimport { redirectToSessionRefresh, notifyForbiddenAccess, UnauthorizedError, ForbiddenError, apiFetch, setAuthRedirectConfig } from '../backend/utils/api'\n\n// Ensure global fetch calls also flow through apiFetch so 401 session-refresh\n// and 403 access-denied flash banners fire consistently.\nfunction ensureGlobalFetchInterception() {\n if (typeof window === 'undefined') return\n const w = window as any\n if (w.__omFetchPatched) return\n w.__omFetchPatched = true\n w.__omOriginalFetch = window.fetch\n window.fetch = ((input: RequestInfo | URL, init?: RequestInit) => apiFetch(input, init)) as any\n}\n\nconst client = new QueryClient({\n queryCache: new QueryCache({\n onError: (error) => {\n if (error instanceof UnauthorizedError) redirectToSessionRefresh()\n else if (error instanceof ForbiddenError) notifyForbiddenAccess()\n // As a fallback, try to detect common cases\n else if ((error as any)?.status === 401) redirectToSessionRefresh()\n else if ((error as any)?.status === 403) notifyForbiddenAccess()\n },\n }),\n mutationCache: new MutationCache({\n onError: (error) => {\n if (error instanceof UnauthorizedError) redirectToSessionRefresh()\n else if (error instanceof ForbiddenError) notifyForbiddenAccess()\n else if ((error as any)?.status === 401) redirectToSessionRefresh()\n else if ((error as any)?.status === 403) notifyForbiddenAccess()\n },\n }),\n})\n\ntype QueryProviderProps = { children: React.ReactNode; defaultForbiddenRoles?: string[] }\n\nexport function QueryProvider({ children, defaultForbiddenRoles }: QueryProviderProps) {\n React.useEffect(() => {\n ensureGlobalFetchInterception()\n if (defaultForbiddenRoles && defaultForbiddenRoles.length) {\n setAuthRedirectConfig({ defaultForbiddenRoles })\n }\n }, [])\n return <QueryClientProvider client={client}>{children}</QueryClientProvider>\n}\n"],
5
+ "mappings": ";AA6CS;AA5CT,YAAY,WAAW;AACvB,SAAS,eAAe,YAAY,aAAa,2BAA2B;AAC5E,SAAS,0BAA0B,uBAAuB,mBAAmB,gBAAgB,UAAU,6BAA6B;AAIpI,SAAS,gCAAgC;AACvC,MAAI,OAAO,WAAW,YAAa;AACnC,QAAM,IAAI;AACV,MAAI,EAAE,iBAAkB;AACxB,IAAE,mBAAmB;AACrB,IAAE,oBAAoB,OAAO;AAC7B,SAAO,SAAS,CAAC,OAA0B,SAAuB,SAAS,OAAO,IAAI;AACxF;AAEA,MAAM,SAAS,IAAI,YAAY;AAAA,EAC7B,YAAY,IAAI,WAAW;AAAA,IACzB,SAAS,CAAC,UAAU;AAClB,UAAI,iBAAiB,kBAAmB,0BAAyB;AAAA,eACxD,iBAAiB,eAAgB,uBAAsB;AAAA,eAEtD,OAAe,WAAW,IAAK,0BAAyB;AAAA,eACxD,OAAe,WAAW,IAAK,uBAAsB;AAAA,IACjE;AAAA,EACF,CAAC;AAAA,EACD,eAAe,IAAI,cAAc;AAAA,IAC/B,SAAS,CAAC,UAAU;AAClB,UAAI,iBAAiB,kBAAmB,0BAAyB;AAAA,eACxD,iBAAiB,eAAgB,uBAAsB;AAAA,eACtD,OAAe,WAAW,IAAK,0BAAyB;AAAA,eACxD,OAAe,WAAW,IAAK,uBAAsB;AAAA,IACjE;AAAA,EACF,CAAC;AACH,CAAC;AAIM,SAAS,cAAc,EAAE,UAAU,sBAAsB,GAAuB;AACrF,QAAM,UAAU,MAAM;AACpB,kCAA8B;AAC9B,QAAI,yBAAyB,sBAAsB,QAAQ;AACzD,4BAAsB,EAAE,sBAAsB,CAAC;AAAA,IACjD;AAAA,EACF,GAAG,CAAC,CAAC;AACL,SAAO,oBAAC,uBAAoB,QAAiB,UAAS;AACxD;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/ui",
3
- "version": "0.6.3-develop.3838.1.7f2657a47c",
3
+ "version": "0.6.3-develop.3857.1.da89d7530c",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -141,13 +141,13 @@
141
141
  "remark-gfm": "^4.0.1"
142
142
  },
143
143
  "peerDependencies": {
144
- "@open-mercato/shared": "0.6.3-develop.3838.1.7f2657a47c",
144
+ "@open-mercato/shared": "0.6.3-develop.3857.1.da89d7530c",
145
145
  "react": ">=18.0.0",
146
146
  "react-dom": ">=18.0.0",
147
147
  "react-is": ">=18.0.0"
148
148
  },
149
149
  "devDependencies": {
150
- "@open-mercato/shared": "0.6.3-develop.3838.1.7f2657a47c",
150
+ "@open-mercato/shared": "0.6.3-develop.3857.1.da89d7530c",
151
151
  "@testing-library/dom": "^10.4.1",
152
152
  "@testing-library/jest-dom": "^6.9.1",
153
153
  "@testing-library/react": "^16.3.1",
@@ -66,6 +66,7 @@ export type AppShellProps = {
66
66
  productName?: string
67
67
  logo?: ShellLogo
68
68
  email?: string
69
+ canManageUpgradeActions?: boolean
69
70
  groups: {
70
71
  id?: string
71
72
  name: string
@@ -416,7 +417,7 @@ export function AppShell(props: AppShellProps) {
416
417
  )
417
418
  }
418
419
 
419
- function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, children, sidebarCollapsedDefault = false, currentTitle, breadcrumb, version, settingsSectionTitle, settingsPathPrefixes = [], settingsSections, profileSections, profileSectionTitle, profilePathPrefixes = [], mobileSidebarSlot }: AppShellProps) {
420
+ function AppShellBody({ productName, logo, email, canManageUpgradeActions = false, groups, rightHeaderSlot, children, sidebarCollapsedDefault = false, currentTitle, breadcrumb, version, settingsSectionTitle, settingsPathPrefixes = [], settingsSections, profileSections, profileSectionTitle, profilePathPrefixes = [], mobileSidebarSlot }: AppShellProps) {
420
421
  const pathname = usePathname()
421
422
  const searchParams = useSearchParams()
422
423
  const t = useT()
@@ -1341,7 +1342,7 @@ function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, child
1341
1342
  <InjectionSpot spotId={BACKEND_LAYOUT_TOP_INJECTION_SPOT_ID} context={injectionContext} />
1342
1343
  <FlashMessages />
1343
1344
  <PartialIndexBanner />
1344
- <UpgradeActionBanner />
1345
+ {canManageUpgradeActions ? <UpgradeActionBanner /> : null}
1345
1346
  <LastOperationBanner />
1346
1347
  <InjectionSpot spotId={BACKEND_RECORD_CURRENT_INJECTION_SPOT_ID} context={injectionContext} />
1347
1348
  <InjectionSpot
@@ -4109,6 +4109,7 @@ const FieldControl = React.memo(function FieldControlImpl({
4109
4109
  : undefined
4110
4110
  }
4111
4111
  allowCustomValues={builtin?.allowCustomValues ?? true}
4112
+ clearable={!field.required}
4112
4113
  disabled={disabled}
4113
4114
  />
4114
4115
  )}
@@ -243,6 +243,33 @@ describe('AppShell', () => {
243
243
  })
244
244
  })
245
245
 
246
+ it('renders the upgrade action banner only for users who can manage upgrade actions', () => {
247
+ const { rerender } = renderWithProviders(
248
+ <AppShell
249
+ email="demo@example.com"
250
+ groups={groups}
251
+ canManageUpgradeActions={false}
252
+ >
253
+ <div>Child content</div>
254
+ </AppShell>,
255
+ { dict },
256
+ )
257
+
258
+ expect(screen.queryByTestId('upgrade-action-banner')).not.toBeInTheDocument()
259
+
260
+ rerender(
261
+ <AppShell
262
+ email="demo@example.com"
263
+ groups={groups}
264
+ canManageUpgradeActions
265
+ >
266
+ <div>Child content</div>
267
+ </AppShell>,
268
+ )
269
+
270
+ expect(screen.getByTestId('upgrade-action-banner')).toBeInTheDocument()
271
+ })
272
+
246
273
  it('resets breadcrumb to server-provided values when pathname changes', async () => {
247
274
  mockPathname = '/backend/users'
248
275
 
@@ -0,0 +1,146 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+
5
+ import * as React from 'react'
6
+ import { act, screen, waitFor } from '@testing-library/react'
7
+ import { UpgradeActionBanner } from '../upgrades/UpgradeActionBanner'
8
+ import { useBackendChrome } from '../BackendChromeProvider'
9
+ import { apiCall } from '../utils/apiCall'
10
+ import { renderWithProviders } from '@open-mercato/shared/lib/testing/renderWithProviders'
11
+
12
+ jest.mock('../utils/apiCall', () => ({
13
+ apiCall: jest.fn(),
14
+ }))
15
+
16
+ jest.mock('../FlashMessages', () => ({
17
+ flash: jest.fn(),
18
+ }))
19
+
20
+ jest.mock('../BackendChromeProvider', () => ({
21
+ BackendChromeProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
22
+ useBackendChrome: jest.fn(),
23
+ }))
24
+
25
+ const UPGRADE_ACTIONS_RESPONSE = {
26
+ version: '0.3.4',
27
+ actions: [
28
+ {
29
+ id: 'seed-catalog',
30
+ version: '0.3.4',
31
+ message: 'Install example products',
32
+ ctaLabel: 'Install now',
33
+ },
34
+ ],
35
+ }
36
+
37
+ function mockChrome(opts: { isReady?: boolean; grantedFeatures?: string[] } = {}) {
38
+ const isReady = opts.isReady ?? true
39
+ ;(useBackendChrome as jest.Mock).mockReturnValue({
40
+ payload: opts.grantedFeatures !== undefined ? { grantedFeatures: opts.grantedFeatures } : null,
41
+ isReady,
42
+ isLoading: !isReady,
43
+ refresh: jest.fn(),
44
+ })
45
+ }
46
+
47
+ beforeAll(() => {
48
+ if (typeof globalThis.fetch === 'undefined') {
49
+ Object.defineProperty(globalThis, 'fetch', { value: jest.fn(), writable: true, configurable: true })
50
+ }
51
+ })
52
+
53
+ beforeEach(() => {
54
+ jest.clearAllMocks()
55
+ process.env.NEXT_PUBLIC_UPGRADE_ACTIONS_ENABLED = 'true'
56
+ mockChrome()
57
+ })
58
+
59
+ afterEach(() => {
60
+ delete process.env.NEXT_PUBLIC_UPGRADE_ACTIONS_ENABLED
61
+ })
62
+
63
+ describe('UpgradeActionBanner — feature guard', () => {
64
+ it('renders null and does not call apiCall when payload is null (no configs.manage)', async () => {
65
+ ;(apiCall as jest.Mock).mockResolvedValue({ ok: true, result: UPGRADE_ACTIONS_RESPONSE })
66
+
67
+ renderWithProviders(<UpgradeActionBanner />)
68
+
69
+ await act(async () => {
70
+ await new Promise((r) => setTimeout(r, 50))
71
+ })
72
+
73
+ expect(screen.queryByText('Install now')).toBeNull()
74
+ expect(apiCall).not.toHaveBeenCalled()
75
+ })
76
+
77
+ it('renders null and does not call apiCall when grantedFeatures is empty (no configs.manage)', async () => {
78
+ mockChrome({ isReady: true, grantedFeatures: [] })
79
+ ;(apiCall as jest.Mock).mockResolvedValue({ ok: true, result: UPGRADE_ACTIONS_RESPONSE })
80
+
81
+ renderWithProviders(<UpgradeActionBanner />)
82
+
83
+ await act(async () => {
84
+ await new Promise((r) => setTimeout(r, 50))
85
+ })
86
+
87
+ expect(screen.queryByText('Install now')).toBeNull()
88
+ expect(apiCall).not.toHaveBeenCalled()
89
+ })
90
+
91
+ it('calls apiCall and renders banner when grantedFeatures includes configs.manage', async () => {
92
+ mockChrome({ isReady: true, grantedFeatures: ['configs.manage'] })
93
+ ;(apiCall as jest.Mock).mockResolvedValue({ ok: true, result: UPGRADE_ACTIONS_RESPONSE })
94
+
95
+ renderWithProviders(<UpgradeActionBanner />)
96
+
97
+ await waitFor(() => {
98
+ expect(apiCall).toHaveBeenCalledWith(
99
+ '/api/configs/upgrade-actions',
100
+ { headers: { 'x-om-forbidden-redirect': '0' } },
101
+ )
102
+ })
103
+
104
+ await waitFor(() => {
105
+ expect(screen.getByRole('button', { name: 'Install now' })).toBeInTheDocument()
106
+ })
107
+ })
108
+
109
+ it('calls apiCall when grantedFeatures includes configs.* wildcard', async () => {
110
+ mockChrome({ isReady: true, grantedFeatures: ['configs.*'] })
111
+ ;(apiCall as jest.Mock).mockResolvedValue({ ok: true, result: UPGRADE_ACTIONS_RESPONSE })
112
+
113
+ renderWithProviders(<UpgradeActionBanner />)
114
+
115
+ await waitFor(() => {
116
+ expect(apiCall).toHaveBeenCalledWith(
117
+ '/api/configs/upgrade-actions',
118
+ { headers: { 'x-om-forbidden-redirect': '0' } },
119
+ )
120
+ })
121
+ })
122
+
123
+ it('does not call apiCall while chrome is still loading (isReady=false), then fires after ready', async () => {
124
+ mockChrome({ isReady: false })
125
+ ;(apiCall as jest.Mock).mockResolvedValue({ ok: true, result: UPGRADE_ACTIONS_RESPONSE })
126
+
127
+ const { rerender } = renderWithProviders(<UpgradeActionBanner />)
128
+
129
+ await act(async () => {
130
+ await new Promise((r) => setTimeout(r, 50))
131
+ })
132
+
133
+ expect(screen.queryByText('Install now')).toBeNull()
134
+ expect(apiCall).not.toHaveBeenCalled()
135
+
136
+ mockChrome({ isReady: true, grantedFeatures: ['configs.manage'] })
137
+ rerender(<UpgradeActionBanner />)
138
+
139
+ await waitFor(() => {
140
+ expect(apiCall).toHaveBeenCalledWith(
141
+ '/api/configs/upgrade-actions',
142
+ { headers: { 'x-om-forbidden-redirect': '0' } },
143
+ )
144
+ })
145
+ })
146
+ })
@@ -129,4 +129,77 @@ describe('settings navigation helpers', () => {
129
129
 
130
130
  expect(entries.map((item) => item.href)).toContain('/backend/customer_accounts/users')
131
131
  })
132
+
133
+ // Regression for GH #2070: an employee without any `configs.*` grant must not
134
+ // see the system-status page in the sidebar (including settings sections),
135
+ // even when modules with adjacent prefixes are granted.
136
+ it('hides routes whose required features are not granted, including settings-context pages', async () => {
137
+ const employeeGrants = [
138
+ 'sales.orders.view',
139
+ 'sales.quotes.view',
140
+ 'inbox_ops.proposals.view',
141
+ ]
142
+ const entries = await buildAdminNav(
143
+ [
144
+ {
145
+ id: 'sales',
146
+ backendRoutes: [
147
+ {
148
+ pattern: '/backend/sales/orders',
149
+ title: 'Orders',
150
+ requireFeatures: ['sales.orders.view'],
151
+ },
152
+ {
153
+ pattern: '/backend/sales/quotes',
154
+ title: 'Quotes',
155
+ requireFeatures: ['sales.quotes.view'],
156
+ },
157
+ ],
158
+ },
159
+ {
160
+ id: 'inbox_ops',
161
+ backendRoutes: [
162
+ {
163
+ pattern: '/backend/inbox-ops',
164
+ title: 'Proposals',
165
+ requireFeatures: ['inbox_ops.proposals.view'],
166
+ },
167
+ ],
168
+ },
169
+ {
170
+ id: 'configs',
171
+ backendRoutes: [
172
+ {
173
+ pattern: '/backend/config/system-status',
174
+ title: 'System status',
175
+ requireFeatures: ['configs.system_status.view'],
176
+ pageContext: 'settings',
177
+ group: 'System',
178
+ groupKey: 'settings.sections.system',
179
+ },
180
+ ],
181
+ },
182
+ ],
183
+ { auth: { roles: [] } },
184
+ undefined,
185
+ undefined,
186
+ {
187
+ checkFeatures: async (features: string[]) =>
188
+ features.filter((feature) => employeeGrants.includes(feature)),
189
+ },
190
+ )
191
+
192
+ const hrefs = entries.map((item) => item.href)
193
+ expect(hrefs).toContain('/backend/sales/orders')
194
+ expect(hrefs).toContain('/backend/sales/quotes')
195
+ expect(hrefs).toContain('/backend/inbox-ops')
196
+ expect(hrefs).not.toContain('/backend/config/system-status')
197
+
198
+ // The filtered settings entry must also be absent from any settings section.
199
+ const settingsSections = buildSettingsSections(entries, { system: 1 })
200
+ const allSettingsHrefs = settingsSections.flatMap((section) =>
201
+ section.items.map((item) => item.href),
202
+ )
203
+ expect(allSettingsHrefs).not.toContain('/backend/config/system-status')
204
+ })
132
205
  })