feat(FN-1535): fix theme-data.css URL resolution for file:// contexts

- Fix CSS URL resolution in memory when loading with file:// protocol
- Add DASHBOARD_URL re-export from renderer module for backward compatibility
- Update main.test.ts to use correct renderer mock reference (rendererMocks vs mocks)
- Document the URL resolution bug fix in memory
This commit is contained in:
gsxdsm
2026-04-10 08:37:28 -07:00
parent 05bd2f6623
commit 2ee6d246d4
5 changed files with 169 additions and 43 deletions

View File

@@ -625,6 +625,123 @@ describe("useTheme", () => {
// Clean up
existingLink.remove();
});
it("resolves concrete path for deep nested file:// URL", () => {
// Simulate a deeply nested Electron production path
Object.defineProperty(document, "baseURI", {
value: "file:///Users/me/Projects/kb/packages/dashboard/dist/client/index.html",
configurable: true,
});
const { result } = renderHook(() => useTheme());
act(() => {
result.current.setColorTheme("ocean");
});
const link = document.getElementById("theme-data");
expect(link).not.toBeNull();
const href = link?.getAttribute("href");
// Must have concrete path ending with theme-data.css
expect(href).toBe("file:///Users/me/Projects/kb/packages/dashboard/dist/client/theme-data.css");
// Regression: ensure no malformed concatenation (missing slash before filename)
expect(href).not.toMatch(/clienttheme-data/);
// Clean up
Object.defineProperty(document, "baseURI", {
value: "http://localhost:3000/",
configurable: true,
});
});
it("resolves concrete path for shallow file:// URL", () => {
// Simulate a shallow Electron production path
Object.defineProperty(document, "baseURI", {
value: "file:///app/index.html",
configurable: true,
});
const { result } = renderHook(() => useTheme());
act(() => {
result.current.setColorTheme("factory");
});
const link = document.getElementById("theme-data");
expect(link).not.toBeNull();
const href = link?.getAttribute("href");
// Must resolve to the correct path with proper slash separator
expect(href).toBe("file:///app/theme-data.css");
// Regression: ensure no malformed concatenation (missing slash before filename)
expect(href).not.toMatch(/apptheme-data/);
// Clean up
Object.defineProperty(document, "baseURI", {
value: "http://localhost:3000/",
configurable: true,
});
});
it("resolves concrete path for medium nested file:// URL", () => {
// Simulate Electron path with medium nesting
Object.defineProperty(document, "baseURI", {
value: "file:///app/fusion/node/dashboard/dist/index.html",
configurable: true,
});
const { result } = renderHook(() => useTheme());
act(() => {
result.current.setColorTheme("nord");
});
const link = document.getElementById("theme-data");
expect(link).not.toBeNull();
const href = link?.getAttribute("href");
// Must resolve to the correct path with proper slash separator
expect(href).toBe("file:///app/fusion/node/dashboard/dist/theme-data.css");
// Regression: ensure no malformed concatenation (missing slash before filename)
expect(href).not.toMatch(/disttheme-data/);
// Clean up
Object.defineProperty(document, "baseURI", {
value: "http://localhost:3000/",
configurable: true,
});
});
it("rejects malformed file:// URL with missing slash before filename", () => {
// This test documents the bug that was fixed: URLs like file:///apptheme-data.css
// should never be produced. The fix ensures directory and filename are always
// separated by a slash.
Object.defineProperty(document, "baseURI", {
value: "file:///app/index.html",
configurable: true,
});
const { result } = renderHook(() => useTheme());
act(() => {
result.current.setColorTheme("dracula");
});
const link = document.getElementById("theme-data");
expect(link).not.toBeNull();
const href = link?.getAttribute("href");
// The buggy implementation would produce file:///apptheme-data.css
// The correct implementation produces file:///app/theme-data.css
// These regexes catch the malformed pattern
expect(href).not.toMatch(/apptheme-data\.css$/);
expect(href).not.toMatch(/theme-data\.css$/ && !/\/theme-data\.css$/.test(href || ""));
// Verify it's actually a valid file URL
expect(href).toMatch(/^file:\/\/.*\/theme-data\.css$/);
// Clean up
Object.defineProperty(document, "baseURI", {
value: "http://localhost:3000/",
configurable: true,
});
});
});
});
@@ -671,4 +788,18 @@ describe("getThemeInitScript", () => {
expect(script).toContain("systemDark");
expect(script).toContain("effectiveMode");
});
it("index.html uses correct file:// URL replacement pattern", () => {
// Verify that the inline script in index.html uses the correct URL replacement
// pattern (replace filename with theme-data.css) rather than buggy concatenation
const indexHtml = readFileSync("app/index.html", "utf8");
// The correct pattern: base.replace(/\/[^\/]+$/, '/theme-data.css')
// The buggy pattern: base.substring(0, 7) + dirPath + 'theme-data.css'
expect(indexHtml).toContain("base.replace(/\\/[^\\/]+$/, '/theme-data.css')");
// Ensure the buggy pattern is NOT present
expect(indexHtml).not.toContain("base.substring(0, 7)");
expect(indexHtml).not.toContain("pathMatch");
});
});

View File

@@ -28,16 +28,10 @@ function getThemeDataUrl(): string {
return `/${THEME_DATA_FILENAME}`;
}
// Handle file:// URLs specially - derive path relative to HTML file directory
// Handle file:// URLs - derive path relative to HTML file directory
// Replace the filename at the end of the path with theme-data.css
// This produces correct paths like: file:///path/to/app/theme-data.css
if (base.startsWith("file://")) {
// Extract directory from file:// path (e.g., file:///path/to/app/index.html → /path/to/app/)
const pathMatch = base.match(/^file:\/\/[^\/]*(\/.*?)?\/[^\/]*$/);
if (pathMatch && pathMatch[1]) {
// base ends with filename, use the directory portion
const dirPath = pathMatch[1];
return `${base.substring(0, 7)}${dirPath}${THEME_DATA_FILENAME}`;
}
// Fallback: just use the base without the filename
return base.replace(/\/[^\/]+$/, `/${THEME_DATA_FILENAME}`);
}