feat(FN-1739): merge fusion/fn-1739
This commit is contained in:
@@ -260,6 +260,7 @@ Dashboard SSE (`/api/events`) streams plugin lifecycle events as normalized `plu
|
||||
- `mission-store.test.ts` has a flaky test (`getMissionHealth computes mission metrics and latest error context`) that fails intermittently when timestamps collide in the same millisecond — this is pre-existing and not related to dashboard changes.
|
||||
- **SettingsModal sidebar reordering**: When reordering sections in `SETTINGS_SECTIONS`, update all tests that assume a specific section is the default. Tests using `screen.getByText("SectionName")` may fail with "multiple elements found" when the section heading also appears in the content area alongside the sidebar item. Use `screen.getAllByText("SectionName")[0]` or navigate to the section explicitly before accessing its fields.
|
||||
- **Test isolation with temp directories**: Tests that create filesystem state (like agent files under `.fusion/agents/`) should use per-test temp directories via `mkdtempSync(join(os.tmpdir(), 'fn-test-'))` and clean up in `afterEach` with `rmSync(dir, { recursive: true, force: true })`. Shared temp paths cause state leakage between tests, leading to noisy/flaky behavior. See `in-process-runtime.test.ts` for the pattern.
|
||||
- **xterm.js WebGL on mobile (FN-1739)**: The `@xterm/addon-webgl` addon causes garbled/overlapping Unicode text on mobile browsers (especially iOS Safari/WebKit) due to rendering artifacts. Always wrap WebGL addon loading in a `!isMobileDevice()` check, falling back to canvas rendering for mobile. Use the project's monospace font stack (`ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace`) for better Unicode coverage on all platforms.
|
||||
|
||||
- When adding light-theme overrides for CSS components that already use `var(--*)` tokens, most selectors inherit correctly from the light-theme root variable redefinitions. Only add explicit `[data-theme="light"]` overrides where fine-tuning is needed (e.g., slightly different opacity values, subtle box-shadows for contrast).
|
||||
- `--surface-hover` is used but never defined as a CSS custom property in the root or light theme blocks — it resolves to invalid/empty. Components using `var(--surface-hover)` (like `.github-import-tab:hover`) get no background. Either define it in the theme roots or use fallbacks like `var(--surface-hover, rgba(0,0,0,0.03))`.
|
||||
|
||||
@@ -357,7 +357,7 @@ export function TerminalModal({ isOpen, onClose, initialCommand, projectId }: Te
|
||||
cursorBlink: true,
|
||||
cursorStyle: "block",
|
||||
fontSize: 14,
|
||||
fontFamily: "monospace",
|
||||
fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
|
||||
theme: {
|
||||
background: "#1e1e1e",
|
||||
foreground: "#d4d4d4",
|
||||
@@ -384,15 +384,19 @@ export function TerminalModal({ isOpen, onClose, initialCommand, projectId }: Te
|
||||
terminal.loadAddon(webLinksAddon);
|
||||
|
||||
// Try to load WebGL addon for better performance
|
||||
try {
|
||||
const { WebglAddon } = await import("@xterm/addon-webgl");
|
||||
const webglAddon = new WebglAddon();
|
||||
webglAddon.onContextLoss(() => {
|
||||
webglAddon.dispose();
|
||||
});
|
||||
terminal.loadAddon(webglAddon);
|
||||
} catch {
|
||||
// WebGL not available, fallback to canvas
|
||||
// Skip WebGL on mobile devices to avoid rendering artifacts (e.g., garbled
|
||||
// Unicode characters in powerline prompt symbols on iOS Safari/WebKit).
|
||||
if (!isMobileDevice()) {
|
||||
try {
|
||||
const { WebglAddon } = await import("@xterm/addon-webgl");
|
||||
const webglAddon = new WebglAddon();
|
||||
webglAddon.onContextLoss(() => {
|
||||
webglAddon.dispose();
|
||||
});
|
||||
terminal.loadAddon(webglAddon);
|
||||
} catch {
|
||||
// WebGL not available, fallback to canvas
|
||||
}
|
||||
}
|
||||
|
||||
// Open terminal in container
|
||||
|
||||
@@ -865,6 +865,84 @@ describe("TerminalModal", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// --- FN-1739 mobile WebGL skip regression tests ---
|
||||
describe("mobile WebGL skip (FN-1739)", () => {
|
||||
let savedInnerWidth: typeof window.innerWidth;
|
||||
let savedOntouchstart: typeof window.ontouchstart;
|
||||
let savedNavigator: typeof navigator;
|
||||
|
||||
beforeEach(() => {
|
||||
savedInnerWidth = window.innerWidth;
|
||||
savedOntouchstart = window.ontouchstart;
|
||||
savedNavigator = navigator;
|
||||
|
||||
// Mock WebGL addon to track if it's loaded
|
||||
vi.mock("@xterm/addon-webgl", () => ({
|
||||
WebglAddon: vi.fn(() => ({
|
||||
onContextLoss: vi.fn(),
|
||||
dispose: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(window, "innerWidth", {
|
||||
value: savedInnerWidth,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
Object.defineProperty(window, "ontouchstart", {
|
||||
value: savedOntouchstart,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
Object.defineProperty(navigator, "maxTouchPoints", {
|
||||
value: (savedNavigator as any).maxTouchPoints,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
});
|
||||
|
||||
function simulateMobileDevice() {
|
||||
Object.defineProperty(window, "innerWidth", {
|
||||
value: 375,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
Object.defineProperty(window, "ontouchstart", {
|
||||
value: undefined,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
Object.defineProperty(navigator, "maxTouchPoints", {
|
||||
value: 2,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
|
||||
it("does not load WebGL addon when device is mobile", async () => {
|
||||
simulateMobileDevice();
|
||||
|
||||
// Import WebGL addon mock to get reference for assertions
|
||||
const webglModule = await import("@xterm/addon-webgl");
|
||||
const loadAddonSpy = vi.spyOn(mockTerminalInstance, "loadAddon");
|
||||
|
||||
render(<TerminalModal isOpen={true} onClose={mockOnClose} />);
|
||||
|
||||
// Wait for xterm initialization to complete
|
||||
await waitFor(() => {
|
||||
expect(mockTerminalInstance.open).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// WebGL addon constructor should NOT have been called
|
||||
expect(webglModule.WebglAddon).not.toHaveBeenCalled();
|
||||
|
||||
// loadAddon should NOT have been called with WebGL addon
|
||||
expect(loadAddonSpy).not.toHaveBeenCalledWith(expect.any(Object));
|
||||
});
|
||||
});
|
||||
|
||||
describe("xterm import MIME type retry", () => {
|
||||
function isXtermImportBatch(values: Iterable<unknown>): values is Promise<unknown>[] {
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user