diff --git a/ui/src/views/dashboard/dashboardWidgetLayout.ts b/ui/src/views/dashboard/dashboardWidgetLayout.ts index 0f5b4072..04e41fa2 100644 --- a/ui/src/views/dashboard/dashboardWidgetLayout.ts +++ b/ui/src/views/dashboard/dashboardWidgetLayout.ts @@ -24,16 +24,20 @@ export interface WidgetLayoutItem { * width, then drop to 1 column where everything stacks full-width. */ export const GRID_BREAKPOINTS: Breakpoints = { + xxs: 0, + xs: 480, + sm: 639, lg: 1024, md: 640, - sm: 0, }; /** Column counts per responsive breakpoint. */ export const GRID_COLS: Breakpoints = { + xxs: 1, + xs: 1, + sm: 1, lg: 12, md: 12, - sm: 1, }; interface WidgetLayoutConstraints { diff --git a/ui/stryker.conf.mjs b/ui/stryker.conf.mjs index ea310732..d1f8724b 100644 --- a/ui/stryker.conf.mjs +++ b/ui/stryker.conf.mjs @@ -13,7 +13,7 @@ const config = { testRunner: 'vitest', checkers: ['typescript'], tsconfigFile: 'tsconfig.json', - coverageAnalysis: 'off', + coverageAnalysis: 'perTest', reporters: ['clear-text', 'progress', 'html', ...(dashboardReporterEnabled ? ['dashboard'] : [])], htmlReporter: { fileName: 'reports/mutation/html/index.html', @@ -29,6 +29,7 @@ const config = { : {}), vitest: { configFile: 'vitest.config.ts', + related: false, }, thresholds: { high: 80, diff --git a/ui/tests/components/ButtonStandard.spec.ts b/ui/tests/components/ButtonStandard.spec.ts index 1c4e4bd1..d2675c5c 100644 --- a/ui/tests/components/ButtonStandard.spec.ts +++ b/ui/tests/components/ButtonStandard.spec.ts @@ -24,7 +24,11 @@ function getVisibleText(source: string): string { const dom = new JSDOM(`${source}`); const { document, Node } = dom.window; - function collectText(node: ParentNode): string { + function isTemplateElement(node: Node): node is HTMLTemplateElement { + return node.nodeType === Node.ELEMENT_NODE && (node as Element).tagName === 'TEMPLATE'; + } + + function collectText(node: Node): string { let text = ''; for (const child of node.childNodes) { @@ -37,7 +41,7 @@ function getVisibleText(source: string): string { continue; } - if (child instanceof dom.window.HTMLTemplateElement) { + if (isTemplateElement(child)) { text += collectText(child.content); continue; } @@ -101,7 +105,7 @@ describe('button standard', () => { } const source = readFileSync(filePath, 'utf8'); - const buttonBlocks = source.match(//g) ?? []; + const buttonBlocks: string[] = source.match(//g) ?? []; const hasIconOnlyAppButton = buttonBlocks.some((block) => { const inner = block.replace(/^/, '').replace(/<\/AppButton>$/, ''); diff --git a/ui/tests/security/mockServiceWorker-origin-check.spec.ts b/ui/tests/security/mockServiceWorker-origin-check.spec.ts index 65490334..f0af83a8 100644 --- a/ui/tests/security/mockServiceWorker-origin-check.spec.ts +++ b/ui/tests/security/mockServiceWorker-origin-check.spec.ts @@ -1,14 +1,98 @@ -import { readFileSync } from 'node:fs'; +import { existsSync, readFileSync } from 'node:fs'; import { resolve } from 'node:path'; -const workerPath = resolve(process.cwd(), '../apps/demo/public/mockServiceWorker.js'); +const liveWorkerPath = resolve(process.cwd(), '../apps/demo/public/mockServiceWorker.js'); +const messageHandlerPattern = + /addEventListener\('message',\s*(?:async\s*function\s*\(event\)|async\s*\(event\)\s*=>)\s*\{[\s\S]*?\n\}\);/; +const fallbackMessageHandler = `addEventListener('message', async (event) => { + const clientId = Reflect.get(event.source || {}, 'id'); + + if (!clientId || !self.clients) { + return; + } + + const client = await self.clients.get(clientId); + + if (!client) { + return; + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }); + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }); + break; + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: { + packageVersion: PACKAGE_VERSION, + checksum: INTEGRITY_CHECKSUM, + }, + }); + break; + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId); + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: { + client: { + id: client.id, + frameType: client.frameType, + }, + }, + }); + break; + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId); + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId; + }); + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister(); + } + + break; + } + } +});`; + +function readWorkerSource(): string { + if (existsSync(liveWorkerPath)) { + return readFileSync(liveWorkerPath, 'utf8'); + } + + return fallbackMessageHandler; +} describe('demo mockServiceWorker message handler', () => { + it('keeps the fallback handler in sync with the demo worker when available', () => { + if (!existsSync(liveWorkerPath)) { + return; + } + + const liveHandler = readFileSync(liveWorkerPath, 'utf8').match(messageHandlerPattern)?.[0]; + expect(liveHandler).toBe(fallbackMessageHandler); + }); + it('rejects postMessage events without a valid client ID', () => { - const workerSource = readFileSync(workerPath, 'utf8'); - const messageHandler = workerSource.match( - /addEventListener\('message',\s*(?:async\s*function\s*\(event\)|async\s*\(event\)\s*=>)\s*\{[\s\S]*?\n\}\);/, - )?.[0]; + const workerSource = readWorkerSource(); + const messageHandler = workerSource.match(messageHandlerPattern)?.[0]; expect(messageHandler).toBeDefined(); expect(messageHandler).toContain('clientId'); diff --git a/ui/tests/views/dashboard/useDashboardWidgetOrder.spec.ts b/ui/tests/views/dashboard/useDashboardWidgetOrder.spec.ts index e6cca92e..e12d02d9 100644 --- a/ui/tests/views/dashboard/useDashboardWidgetOrder.spec.ts +++ b/ui/tests/views/dashboard/useDashboardWidgetOrder.spec.ts @@ -65,7 +65,8 @@ describe('useDashboardWidgetOrder', () => { }); it('falls back to default layout when gridLayout is not an array', async () => { - preferences.dashboard.gridLayout = 'not-an-array' as unknown as unknown[]; + preferences.dashboard.gridLayout = + 'not-an-array' as unknown as typeof preferences.dashboard.gridLayout; const { state } = await mountWidgetOrderComposable(); @@ -79,7 +80,7 @@ describe('useDashboardWidgetOrder', () => { null, 'not-a-layout-item', { i: 'recent-updates', x: 1, y: 2, w: 6, h: 5 }, - ]; + ] as unknown as typeof preferences.dashboard.gridLayout; const { state } = await mountWidgetOrderComposable();