Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/hooks/stateSyncManager/hooks/UseStateSync/hook.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { usePathname } from "next/navigation";
import { NextRouter, useRouter } from "next/router";
import { useEffect } from "react";
import { ROUTER_METHOD } from "../../../../providers/exploreState/actions/stateToUrl/types";
Expand All @@ -10,7 +11,8 @@ export const useStateSync = <Action>({
dispatch,
state,
}: UseStateSyncManagerProps<Action>): void => {
const { basePath, isReady, pathname, query } = useRouter();
const { basePath, isReady, query } = useRouter();
const pathname = usePathname() ?? "";
const { onClearPopRef, popRef } = useWasPop();
Comment thread
frano-m marked this conversation as resolved.
Outdated

// Extract the query from the state.
Expand Down
8 changes: 8 additions & 0 deletions src/hooks/useUpdateURLCatalogParam.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ import { useExploreState } from "./useExploreState";
*/
export const useUpdateURLCatalogParams = (): void => {
const { exploreState } = useExploreState();
// `pathname` is intentionally still read from useRouter() here so it stays as
// the route pattern (e.g. `/[entityListType]/[entityId]`). The pattern is
// required by the Router.replace({ pathname, query }) call below — Next.js
// interpolates dynamic-param keys (entityListType, entityId, …) out of the
// query into the path. usePathname() would return the already-resolved URL,
// which on dynamic routes would leave those keys in the query string. The
// migration to usePathname()/router.replace() is tracked in #930 and #931
// and will be done together with the Router.replace refactor.
const { basePath, pathname, query } = useRouter();
const { catalogState } = exploreState;

Expand Down
4 changes: 2 additions & 2 deletions src/views/ExportMethodView/exportMethodView.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useRouter } from "next/router";
import { usePathname } from "next/navigation";
import { JSX } from "react";
import { ComponentCreator } from "../../components/ComponentCreator/ComponentCreator";
import { BackPageView } from "../../components/Layout/components/BackPage/backPageView";
Expand All @@ -8,7 +8,7 @@ import { useUpdateURLSearchParams } from "../../hooks/useUpdateURLSearchParams";

export const ExportMethodView = (): JSX.Element => {
useUpdateURLSearchParams();
const { pathname } = useRouter();
const pathname = usePathname() ?? "";
const { exportMethods, tabs } = useExportConfig();
const { sideColumn } = tabs[0];
const { mainColumn, top } =
Expand Down
133 changes: 133 additions & 0 deletions tests/stateSyncManager_utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import {
hasParams,
isSynced,
stringifyQuery,
wasPop,
} from "../src/hooks/stateSyncManager/hooks/UseStateSync/utils";
import { NextHistoryState } from "../src/services/beforePopState/types";
Comment thread
frano-m marked this conversation as resolved.
Outdated

/**
* Builds a minimal NextHistoryState for tests.
* @param url - The resolved URL the browser is navigating to (the form
* Next.js stores in history state — always the actual URL, never a route
* pattern).
* @returns NextHistoryState with the given url and no-op as/options.
*/
function buildHistoryState(url: string): NextHistoryState {
return { as: url, options: {}, url };
}
Comment thread
frano-m marked this conversation as resolved.

describe("wasPop", () => {
it("returns false when nextHistoryState is undefined", () => {
expect(wasPop("", "/projects", undefined)).toBe(false);
});

it("returns true when pathname matches the path component of the history URL", () => {
expect(wasPop("", "/projects", buildHistoryState("/projects"))).toBe(true);
});

it("strips the query string off the history URL before comparing", () => {
expect(
wasPop("", "/projects", buildHistoryState("/projects?filter=foo")),
).toBe(true);
});

it("returns false when pathname does not match", () => {
expect(wasPop("", "/projects", buildHistoryState("/files"))).toBe(false);
});

it("defaults basePath to empty string when not provided", () => {
expect(wasPop(undefined, "/projects", buildHistoryState("/projects"))).toBe(
true,
);
});

it("prepends basePath to pathname before comparing", () => {
expect(
wasPop("/data", "/projects", buildHistoryState("/data/projects")),
).toBe(true);
});

it("returns false when basePath is set but missing from the history URL", () => {
expect(wasPop("/data", "/projects", buildHistoryState("/projects"))).toBe(
false,
);
});

// Documents the contract the usePathname() migration relies on: pathname is
// the resolved URL (e.g. /anvil-cmg/abc-123), not the route pattern
// (/[entityListType]/[entityId]). The first matches; the second does not.
it("matches when pathname is the resolved URL (post-migration form)", () => {
expect(
wasPop(
"",
"/anvil-cmg/abc-123",
buildHistoryState("/anvil-cmg/abc-123?filter=foo"),
),
).toBe(true);
});

it("does not match when pathname is a route pattern (pre-migration form on dynamic routes)", () => {
expect(
wasPop(
"",
"/[entityListType]/[entityId]",
buildHistoryState("/anvil-cmg/abc-123"),
),
).toBe(false);
});
});

describe("hasParams", () => {
it("returns true when any param key has a defined value in the query", () => {
expect(hasParams({ filter: "foo" }, ["filter"])).toBe(true);
});

it("returns true when at least one of multiple param keys is present", () => {
expect(hasParams({ sort: "asc" }, ["filter", "sort"])).toBe(true);
});

it("returns false when none of the param keys are in the query", () => {
expect(hasParams({ other: "x" }, ["filter", "sort"])).toBe(false);
});

it("returns false for an empty paramKeys list", () => {
expect(hasParams({ filter: "foo" }, [])).toBe(false);
});

it("returns false when a param key is present but undefined", () => {
expect(hasParams({ filter: undefined }, ["filter"])).toBe(false);
});
});

describe("isSynced", () => {
it("returns true for two empty queries", () => {
expect(isSynced({}, {})).toBe(true);
});

it("returns true when queries have the same keys/values in different order", () => {
// eslint-disable-next-line sort-keys -- intentionally unsorted to exercise insertion-order independence.
expect(isSynced({ a: "1", b: "2" }, { b: "2", a: "1" })).toBe(true);
});

it("returns false when queries differ in value", () => {
expect(isSynced({ a: "1" }, { a: "2" })).toBe(false);
});

it("returns false when one query has extra keys", () => {
expect(isSynced({ a: "1" }, { a: "1", b: "2" })).toBe(false);
});
});

describe("stringifyQuery", () => {
it("produces identical output regardless of insertion order", () => {
expect(stringifyQuery({ a: "1", b: "2" })).toBe(
// eslint-disable-next-line sort-keys -- intentionally unsorted to exercise insertion-order independence.
stringifyQuery({ b: "2", a: "1" }),
);
});

it("produces empty-object JSON for an empty query", () => {
expect(stringifyQuery({})).toBe("{}");
});
});
Loading