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();