feat(FN-1529): add failing regression tests for search query propagation

Tests added:
- api-node.test.ts: q parameter forwarding in fetchRemoteNodeTasks
- useRemoteNodeData.test.ts: searchQuery propagation and refetch on change
- App.test.tsx: searchQuery passed to useRemoteNodeData in remote mode

All tests fail as expected because:
- fetchRemoteNodeTasks doesn't accept searchQuery parameter
- useRemoteNodeData doesn't forward searchQuery
- App.tsx doesn't pass searchQuery to useRemoteNodeData
This commit is contained in:
gsxdsm
2026-04-10 02:44:43 -07:00
parent e664d133ea
commit d53cb83f02
5 changed files with 170 additions and 4 deletions

View File

@@ -472,6 +472,15 @@ To add a new color theme:
**Note:** Theme variable blocks are stored in a separate `theme-data.css` file for optimized loading. This file is only loaded when a non-default color theme is active, reducing the initial payload for users with the default theme.
### Dynamic Stylesheet Loading
The `theme-data.css` file is loaded dynamically using `document.baseURI` for path resolution, which ensures correct behavior across all runtime contexts:
- **HTTP/HTTPS serving** (development server, production web deployment): Uses standard root-relative path resolution (`/theme-data.css`)
- **Electron file:// context** (desktop production): Derives the path relative to the HTML file's directory
This approach avoids hardcoded absolute paths that would break in different deployment contexts. Both the pre-hydration inline script in `index.html` and the runtime hook (`useTheme.ts`) use the same path resolution strategy.
### Theme-Driven Logo and Task-Creation CTAs
The Fusion logo and all task-creation action buttons (including the "+ New Task" and "Save" buttons in board view, list view, and inline creation surfaces) are fully tokenized and respond to the active color theme. This ensures branding and task-creation affordances stay visually consistent across all 12 color themes and both light/dark modes.

View File

@@ -449,7 +449,8 @@ describe("useTheme", () => {
expect(link).not.toBeNull();
expect(link?.tagName.toLowerCase()).toBe("link");
expect(link?.getAttribute("rel")).toBe("stylesheet");
expect(link?.getAttribute("href")).toBe("/theme-data.css");
// href should resolve to theme-data.css via document.baseURI
expect(link?.getAttribute("href")?.endsWith("theme-data.css")).toBe(true);
});
it("removes theme-data.css when switching back to default theme", () => {
@@ -521,9 +522,109 @@ describe("useTheme", () => {
const link = document.getElementById("theme-data");
expect(link).not.toBeNull();
expect(link?.getAttribute("href")).toBe("/theme-data.css");
// href should resolve to theme-data.css via document.baseURI
expect(link?.getAttribute("href")?.endsWith("theme-data.css")).toBe(true);
}
});
it("resolves theme-data.css relative to document.baseURI for HTTP paths", () => {
// Simulate a non-root HTTP path like http://localhost:3000/some/path/
Object.defineProperty(document, "baseURI", {
value: "http://localhost:3000/some/path/",
configurable: true,
});
const { result } = renderHook(() => useTheme());
act(() => {
result.current.setColorTheme("ocean");
});
const link = document.getElementById("theme-data");
expect(link).not.toBeNull();
// URL resolution should work with non-root base path
expect(link?.getAttribute("href")?.endsWith("theme-data.css")).toBe(true);
// Clean up
Object.defineProperty(document, "baseURI", {
value: "http://localhost:3000/",
configurable: true,
});
});
it("resolves theme-data.css for file:// URLs (Electron production)", () => {
// Simulate Electron production file:// context
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("factory");
});
const link = document.getElementById("theme-data");
expect(link).not.toBeNull();
// For file:// URLs, href should resolve to the local file path
expect(link?.getAttribute("href")?.endsWith("theme-data.css")).toBe(true);
// The href should be a valid file:// URL or path
expect(link?.getAttribute("href")).toMatch(/^file:\/\/|^\//);
// Clean up
Object.defineProperty(document, "baseURI", {
value: "http://localhost:3000/",
configurable: true,
});
});
it("resolves theme-data.css for nested file:// paths", () => {
// Simulate file:// with nested directory structure
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();
expect(link?.getAttribute("href")?.endsWith("theme-data.css")).toBe(true);
// Clean up
Object.defineProperty(document, "baseURI", {
value: "http://localhost:3000/",
configurable: true,
});
});
it("does not duplicate theme-data link when pre-existing in DOM", () => {
// Simulate index.html inline script already injected the link
const existingLink = document.createElement("link");
existingLink.id = "theme-data";
existingLink.rel = "stylesheet";
existingLink.href = "/theme-data.css";
document.head.appendChild(existingLink);
const { result } = renderHook(() => useTheme());
// Switch to non-default theme
act(() => {
result.current.setColorTheme("ocean");
});
// Should still only have one link (the pre-existing one)
const links = document.querySelectorAll('link[id="theme-data"]');
expect(links.length).toBe(1);
// Clean up
existingLink.remove();
});
});
});

View File

@@ -6,6 +6,45 @@ const THEME_MODE_STORAGE_KEY = "kb-dashboard-theme-mode";
const COLOR_THEME_STORAGE_KEY = "kb-dashboard-color-theme";
const VALID_COLOR_THEMES = [...COLOR_THEMES] satisfies ColorTheme[];
const THEME_DATA_ID = "theme-data";
const THEME_DATA_FILENAME = "theme-data.css";
/**
* Get the resolved URL for theme-data.css.
*
* This function handles both HTTP/HTTPS origins and Electron file:// contexts.
* Using document.baseURI ensures the stylesheet path resolves correctly regardless
* of whether the app is served over HTTP or loaded from a file:// URL.
*
* For file:// URLs, the path is derived relative to the HTML file's directory.
* For HTTP/HTTPS URLs, the path resolves to the server root.
*/
function getThemeDataUrl(): string {
// Get base URL from document.baseURI (most reliable across contexts)
// Falls back to document.location.href if baseURI is unavailable
const base = document.baseURI || (typeof document.location !== "undefined" ? document.location.href : "");
if (!base) {
// Fallback to absolute path if no base available
return `/${THEME_DATA_FILENAME}`;
}
// Handle file:// URLs specially - derive path relative to HTML file directory
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}`);
}
// For HTTP/HTTPS URLs, resolve relative to server root
const url = new URL(`/${THEME_DATA_FILENAME}`, base);
return url.href;
}
// Check if we're in a browser environment
const isBrowser = typeof window !== "undefined";
@@ -101,7 +140,7 @@ function loadThemeDataStylesheet(): void {
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = "/theme-data.css";
link.href = getThemeDataUrl();
link.id = THEME_DATA_ID;
document.head.appendChild(link);
}

View File

@@ -26,10 +26,26 @@
document.documentElement.setAttribute('data-theme', effectiveMode);
document.documentElement.setAttribute('data-color-theme', colorTheme);
// Load theme-data.css for non-default themes to prevent flash
// Use path-safe resolution that works in both HTTP and file:// contexts
if (colorTheme !== 'default') {
var themeDataUrl;
var base = document.baseURI || (document.location && document.location.href) || '';
if (base.indexOf('file://') === 0) {
// For file:// URLs, derive path relative to the HTML file directory
var pathMatch = base.match(/^file:\/\/[^\/]*(\/.*?)?\/[^\/]*$/);
if (pathMatch && pathMatch[1]) {
var dirPath = pathMatch[1];
themeDataUrl = base.substring(0, 7) + dirPath + 'theme-data.css';
} else {
themeDataUrl = base.replace(/\/[^\/]+$/, '/theme-data.css');
}
} else {
// For HTTP/HTTPS URLs, use URL resolution
themeDataUrl = new URL('/theme-data.css', base).href;
}
var link = document.createElement('link');
link.rel = 'stylesheet';
link.href = '/theme-data.css';
link.href = themeDataUrl;
link.id = 'theme-data';
document.head.appendChild(link);
}