feat(FN-3792): add /tasks deep-link support with theme-data URL resolution
Adds a `/tasks` deep-link that rewrites legacy hash-style URLs on the client and redirects root-absolute paths on the server (FN-3792), along with a new `fusion-plugin-reports` scaffold including manifest, settings, and notification service improvements (FN-3778, FN-3790); also adds a session switch Fusion-Task-Id: FN-3792
This commit is contained in:
@@ -36,7 +36,14 @@ describe("useDeepLink", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
window.history.replaceState = vi.fn();
|
||||
window.history.replaceState = vi.fn((_state, _unused, url) => {
|
||||
if (typeof url === "string" && url.length > 0) {
|
||||
Object.defineProperty(window, "location", {
|
||||
configurable: true,
|
||||
value: new URL(url, "http://localhost:3000"),
|
||||
});
|
||||
}
|
||||
}) as typeof window.history.replaceState;
|
||||
Object.defineProperty(window, "location", {
|
||||
configurable: true,
|
||||
value: new URL("http://localhost:3000/"),
|
||||
@@ -83,7 +90,51 @@ describe("useDeepLink", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("fetches and opens task detail for task deep-link", async () => {
|
||||
it("rewrites /tasks/:id path and opens detail", async () => {
|
||||
Object.defineProperty(window, "location", {
|
||||
configurable: true,
|
||||
value: new URL("http://localhost:3000/tasks/FN-9999"),
|
||||
});
|
||||
|
||||
const { openTaskDetail } = renderUseDeepLink();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.history.replaceState).toHaveBeenCalledWith(expect.anything(), "", "/?task=FN-9999");
|
||||
expect(mockFetchTaskDetail).toHaveBeenCalledWith("FN-9999", "proj_123");
|
||||
expect(openTaskDetail).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves project query when rewriting /tasks/:id path", async () => {
|
||||
Object.defineProperty(window, "location", {
|
||||
configurable: true,
|
||||
value: new URL("http://localhost:3000/tasks/FN-9999?project=proj_456"),
|
||||
});
|
||||
|
||||
const { setCurrentProject } = renderUseDeepLink();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.history.replaceState).toHaveBeenCalledWith(expect.anything(), "", "/?project=proj_456&task=FN-9999");
|
||||
expect(setCurrentProject).toHaveBeenCalledWith(otherProject);
|
||||
expect(mockFetchTaskDetail).toHaveBeenCalledWith("FN-9999", "proj_456");
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores invalid /tasks/:id path", async () => {
|
||||
Object.defineProperty(window, "location", {
|
||||
configurable: true,
|
||||
value: new URL("http://localhost:3000/tasks/not-a-task-id"),
|
||||
});
|
||||
|
||||
renderUseDeepLink();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.history.replaceState).not.toHaveBeenCalled();
|
||||
expect(mockFetchTaskDetail).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("fetches and opens task detail for existing ?task deep-link without path rewrite", async () => {
|
||||
Object.defineProperty(window, "location", {
|
||||
configurable: true,
|
||||
value: new URL("http://localhost:3000/?task=FN-123"),
|
||||
@@ -95,6 +146,8 @@ describe("useDeepLink", () => {
|
||||
expect(mockFetchTaskDetail).toHaveBeenCalledWith("FN-123", "proj_123");
|
||||
expect(openTaskDetail).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
expect(window.history.replaceState).not.toHaveBeenCalledWith(expect.anything(), "", "/?task=FN-123");
|
||||
});
|
||||
|
||||
it("switches project and uses project param for task fetch", async () => {
|
||||
|
||||
@@ -649,8 +649,7 @@ describe("useTheme", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("resolves theme-data.css relative to document.baseURI for HTTP paths", () => {
|
||||
// Simulate a non-root HTTP path like http://localhost:3000/some/path/
|
||||
it("resolves theme-data.css to origin root for HTTP sub-paths", () => {
|
||||
Object.defineProperty(document, "baseURI", {
|
||||
value: "http://localhost:3000/some/path/",
|
||||
configurable: true,
|
||||
@@ -662,12 +661,12 @@ describe("useTheme", () => {
|
||||
result.current.setColorTheme("ocean");
|
||||
});
|
||||
|
||||
const link = document.getElementById("theme-data");
|
||||
const link = document.getElementById("theme-data") as HTMLLinkElement | null;
|
||||
expect(link).not.toBeNull();
|
||||
// URL resolution should work with non-root base path
|
||||
expect(link?.getAttribute("href")?.endsWith("theme-data.css")).toBe(true);
|
||||
const resolved = new URL(link!.href);
|
||||
expect(resolved.origin).toBe("http://localhost:3000");
|
||||
expect(resolved.pathname).toBe("/theme-data.css");
|
||||
|
||||
// Clean up
|
||||
Object.defineProperty(document, "baseURI", {
|
||||
value: "http://localhost:3000/",
|
||||
configurable: true,
|
||||
@@ -779,8 +778,8 @@ describe("useTheme", () => {
|
||||
// The link should exist and href should be updated to the correct value
|
||||
const link = document.getElementById("theme-data") as HTMLLinkElement;
|
||||
expect(link).not.toBeNull();
|
||||
// href should be updated to resolve correctly for the new baseURI
|
||||
expect(link?.href).toBe("http://localhost:3000/some/nested/path/theme-data.css");
|
||||
// HTTP(S) always resolves to origin-root stylesheet path.
|
||||
expect(link?.href).toBe("http://localhost:3000/theme-data.css");
|
||||
|
||||
// Clean up
|
||||
link?.remove();
|
||||
@@ -1210,17 +1209,42 @@ describe("getThemeInitScript", () => {
|
||||
expect(script).toContain("effectiveMode");
|
||||
});
|
||||
|
||||
it("index.html uses correct URL replacement pattern", () => {
|
||||
// Verify that the inline script in index.html uses the correct URL replacement
|
||||
// pattern (handle both directory paths and filename paths) rather than buggy concatenation
|
||||
it("pre-hydration script resolves theme-data path like runtime loader", () => {
|
||||
const script = getThemeInitScript();
|
||||
const runScript = () => {
|
||||
window.eval(script);
|
||||
};
|
||||
|
||||
localStorage.setItem(COLOR_THEME_STORAGE_KEY, "ocean");
|
||||
|
||||
Object.defineProperty(document, "baseURI", {
|
||||
value: "http://localhost:4040/tasks/FN-3773",
|
||||
configurable: true,
|
||||
});
|
||||
runScript();
|
||||
let link = document.getElementById("theme-data") as HTMLLinkElement | null;
|
||||
expect(link).not.toBeNull();
|
||||
expect(new URL(link!.href).origin).toBe("http://localhost:4040");
|
||||
expect(new URL(link!.href).pathname).toBe("/theme-data.css");
|
||||
|
||||
link?.remove();
|
||||
Object.defineProperty(document, "baseURI", {
|
||||
value: "file:///Users/me/Projects/kb/packages/dashboard/dist/client/index.html",
|
||||
configurable: true,
|
||||
});
|
||||
runScript();
|
||||
link = document.getElementById("theme-data") as HTMLLinkElement | null;
|
||||
expect(link).not.toBeNull();
|
||||
expect(link!.href).toBe("file:///Users/me/Projects/kb/packages/dashboard/dist/client/theme-data.css");
|
||||
});
|
||||
|
||||
it("index.html uses HTTP root-absolute and file-relative theme URL logic", () => {
|
||||
const indexHtml = readFileSync(resolve(PACKAGE_ROOT, "app/index.html"), "utf8");
|
||||
|
||||
// The correct pattern: check if base ends with '/' and use slice or replace accordingly
|
||||
// The buggy pattern: base.substring(0, 7) + dirPath + 'theme-data.css'
|
||||
expect(indexHtml).toContain("new URL('/theme-data.css', base)");
|
||||
expect(indexHtml).toContain("base.endsWith('/')");
|
||||
expect(indexHtml).toContain("base.slice(0, -1)");
|
||||
|
||||
// Ensure the buggy pattern is NOT present
|
||||
expect(indexHtml).not.toContain("base.substring(0, 7)");
|
||||
expect(indexHtml).not.toContain("pathMatch");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user