fix(frontend): show toasts through message.useMessage, never the static API

Invariant: an antd toast lives inside the React tree of the component that
raised it, so it takes the theme from ConfigProvider and unmounts with it.

Four components called antd's static message.* (Happ settings, TUIC form,
routing tab, command palette); the other 40 use message.useMessage. A static
toast renders in a detached root that RTL's cleanup never unmounts, so its
rAF-driven close timer kept updating state for ~3s after a test file ended.
happ-routing-editor.test.tsx logged 419 act() warnings, 226 of them after
its last test; on a slow runner the worker closed first and vitest failed
the job with "Closing rpc while onUserConsoleLog was pending" (CI runs
37075337071, 37303631843). In the product the same toasts ignored the
dark/ultra theme.

The command palette keeps its holder mounted while closed: restartXray
closes the palette before its success toast. The Happ editor test now
asserts its toast unmounts with the component (red on the static API), and
no-static-message.test.ts fails on any static call in src. The spy that
silenced message.success in happ-settings-presets.test.tsx intercepted
nothing after the change and is gone.
This commit is contained in:
MHSanaei
2026-10-05 14:01:47 +02:00
parent 5819a01cdc
commit d1b60799ec
7 changed files with 71 additions and 19 deletions
@@ -0,0 +1,31 @@
import { readFileSync, readdirSync, statSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { join, relative, resolve } from 'node:path';
import { describe, expect, it } from 'vitest';
const srcRoot = resolve(fileURLToPath(import.meta.url), '../..');
const staticCall = /(?<![\w.$])message\.(success|error|warning|info|loading|open)\(/;
function sourceFiles(dir: string): string[] {
return readdirSync(dir).flatMap((name) => {
const path = join(dir, name);
if (statSync(path).isDirectory()) return name === 'test' ? [] : sourceFiles(path);
return /\.tsx?$/.test(name) ? [path] : [];
});
}
// antd's static message renders outside React: it ignores the theme and its
// timers outlive the component, which broke CI after the Happ tests tore down.
describe('antd message', () => {
it('is only used through message.useMessage()', () => {
const offenders = sourceFiles(srcRoot).flatMap((file) =>
readFileSync(file, 'utf8')
.split('\n')
.flatMap((line, i) =>
staticCall.test(line) ? [`${relative(srcRoot, file)}:${i + 1}: ${line.trim()}`] : [],
),
);
expect(offenders).toEqual([]);
});
});