Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

chore(site): update and refactor all custom hook tests that rely on React Router#12219

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
Parkreiner merged 20 commits intomainfrommes/hook-test-revamps-4
Mar 8, 2024
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
Show all changes
20 commits
Select commitHold shift + click to select a range
0b186bf
chore: rename useTab to useSearchParamsKey and add test
ParkreinerFeb 19, 2024
a7a0944
chore: mark old renderHookWithAuth as deprecated (temp)
ParkreinerFeb 19, 2024
ec3ba4f
fix: update imports for useResourcesNav
ParkreinerFeb 19, 2024
6d7ef9f
Merge branch 'main' into mes/hook-test-revamps-4
ParkreinerMar 3, 2024
afffb26
refactor: change API for useSearchParamsKey
ParkreinerMar 3, 2024
590f02b
chore: let user pass in their own URLSearchParams value
ParkreinerMar 3, 2024
9e00ea6
refactor: clean up comments for clarity
ParkreinerMar 3, 2024
d7110c5
fix: update import
ParkreinerMar 3, 2024
2cc63d7
wip: commit progress on useWorkspaceDuplication revamp
ParkreinerMar 3, 2024
26089e3
chore: migrate duplication test to new helper
ParkreinerMar 3, 2024
8057397
refactor: update code for clarity
ParkreinerMar 3, 2024
f8f87a3
refactor: reorder test cases for clarity
ParkreinerMar 3, 2024
dcc89dc
refactor: split off hook helper into separate file
ParkreinerMar 3, 2024
1ab6f03
refactor: remove reliance on internal React Router state property
ParkreinerMar 4, 2024
b32603f
refactor: move variables around for more clarity
ParkreinerMar 4, 2024
b0fabde
refactor: more updates for clarity
ParkreinerMar 4, 2024
b94decc
refactor: reorganize test cases for clarity
ParkreinerMar 4, 2024
3e41877
refactor: clean up test cases for useWorkspaceDupe
ParkreinerMar 8, 2024
1fcf9e2
refactor: clean up test cases for useWorkspaceDupe
ParkreinerMar 8, 2024
13f5e53
Merge branch 'main' into mes/hook-test-revamps-4
ParkreinerMar 8, 2024
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
1 change: 0 additions & 1 deletionsite/src/hooks/index.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,4 +2,3 @@ export * from "./useClickable";
export * from "./useClickableTableRow";
export * from "./useClipboard";
export * from "./usePagination";
export * from "./useTab";
12 changes: 8 additions & 4 deletionssite/src/hooks/usePaginatedQuery.test.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
import { waitFor } from "@testing-library/react";
import { renderHookWithAuth } from "testHelpers/renderHelpers";
import { renderHookWithAuth } from "testHelpers/hooks";
import {
type PaginatedData,
type UsePaginatedQueryOptions,
Expand All@@ -23,9 +23,13 @@ function render<
route?: `/?page=${string}`,
) {
return renderHookWithAuth(({ options }) => usePaginatedQuery(options), {
route,
path: "/",
initialProps: { options },
routingOptions: {
route,
path: "/",
},
renderOptions: {
initialProps: { options },
},
});
}

Expand Down
134 changes: 134 additions & 0 deletionssite/src/hooks/useSearchParamsKey.test.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
import { act, waitFor } from "@testing-library/react";
import { renderHookWithAuth } from "testHelpers/hooks";
import { useSearchParamsKey } from "./useSearchParamsKey";

/**
* Tried to extract the setup logic into one place, but it got surprisingly
* messy. Went with straightforward approach of calling things individually
*
* @todo See if there's a way to test the interaction with the history object
* (particularly, for replace behavior). It's traditionally very locked off, and
* React Router gives you no way of interacting with it directly.
*/
describe(useSearchParamsKey.name, () => {
describe("Render behavior", () => {
it("Returns empty string if hook key does not exist in URL, and there is no default value", async () => {
const { result } = await renderHookWithAuth(
() => useSearchParamsKey({ key: "blah" }),
{ routingOptions: { route: `/` } },
);

expect(result.current.value).toEqual("");
});

it("Returns out 'defaultValue' property if defined while hook key does not exist in URL", async () => {
const defaultValue = "dogs";
const { result } = await renderHookWithAuth(
() => useSearchParamsKey({ key: "blah", defaultValue }),
{ routingOptions: { route: `/` } },
);

expect(result.current.value).toEqual(defaultValue);
});

it("Returns out URL value if key exists in URL (always ignoring default value)", async () => {
const key = "blah";
const value = "cats";

const { result } = await renderHookWithAuth(
() => useSearchParamsKey({ key, defaultValue: "I don't matter" }),
{ routingOptions: { route: `/?${key}=${value}` } },
);

expect(result.current.value).toEqual(value);
});

it("Does not have methods change previous values if 'key' argument changes during re-renders", async () => {
const readonlyKey = "readonlyKey";
const mutableKey = "mutableKey";
const initialReadonlyValue = "readonly";
const initialMutableValue = "mutable";

const { result, rerender, getLocationSnapshot } =
await renderHookWithAuth(({ key }) => useSearchParamsKey({ key }), {
routingOptions: {
route: `/?${readonlyKey}=${initialReadonlyValue}&${mutableKey}=${initialMutableValue}`,
},
renderOptions: { initialProps: { key: readonlyKey } },
});

const swapValue = "dogs";
await rerender({ key: mutableKey });
act(() => result.current.setValue(swapValue));
await waitFor(() => expect(result.current.value).toEqual(swapValue));

const snapshot1 = getLocationSnapshot();
expect(snapshot1.search.get(readonlyKey)).toEqual(initialReadonlyValue);
expect(snapshot1.search.get(mutableKey)).toEqual(swapValue);

act(() => result.current.deleteValue());
await waitFor(() => expect(result.current.value).toEqual(""));

const snapshot2 = getLocationSnapshot();
expect(snapshot2.search.get(readonlyKey)).toEqual(initialReadonlyValue);
expect(snapshot2.search.get(mutableKey)).toEqual(null);
});
});

describe("setValue method", () => {
it("Updates state and URL when called with a new value", async () => {
const key = "blah";
const initialValue = "cats";

const { result, getLocationSnapshot } = await renderHookWithAuth(
() => useSearchParamsKey({ key }),
{ routingOptions: { route: `/?${key}=${initialValue}` } },
);

const newValue = "dogs";
act(() => result.current.setValue(newValue));
await waitFor(() => expect(result.current.value).toEqual(newValue));

const { search } = getLocationSnapshot();
expect(search.get(key)).toEqual(newValue);
});
});

describe("deleteValue method", () => {
it("Clears value for the given key from the state and URL when called", async () => {
const key = "blah";
const initialValue = "cats";

const { result, getLocationSnapshot } = await renderHookWithAuth(
() => useSearchParamsKey({ key }),
{ routingOptions: { route: `/?${key}=${initialValue}` } },
);

act(() => result.current.deleteValue());
await waitFor(() => expect(result.current.value).toEqual(""));

const { search } = getLocationSnapshot();
expect(search.get(key)).toEqual(null);
});
});

describe("Override behavior", () => {
it("Will dispatch state changes through custom URLSearchParams value if provided", async () => {
const key = "love";
const initialValue = "dogs";
const customParams = new URLSearchParams({ [key]: initialValue });

const { result } = await renderHookWithAuth(
({ key }) => useSearchParamsKey({ key, searchParams: customParams }),
{
routingOptions: { route: `/?=${key}=${initialValue}` },
renderOptions: { initialProps: { key } },
},
);

const newValue = "all animals";
act(() => result.current.setValue(newValue));
await waitFor(() => expect(customParams.get(key)).toEqual(newValue));
});
});
});
41 changes: 41 additions & 0 deletionssite/src/hooks/useSearchParamsKey.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
import { useSearchParams } from "react-router-dom";

export type UseSearchParamsKeyConfig = Readonly<{
key: string;
searchParams?: URLSearchParams;
defaultValue?: string;
replace?: boolean;
}>;

export type UseSearchParamKeyResult = Readonly<{
value: string;
setValue: (newValue: string) => void;
deleteValue: () => void;
}>;

export const useSearchParamsKey = (
config: UseSearchParamsKeyConfig,
): UseSearchParamKeyResult => {
// Cannot use function update form for setSearchParams, because by default, it
// will always be linked to innerSearchParams, ignoring the config's params
const [innerSearchParams, setSearchParams] = useSearchParams();

const {
key,
searchParams = innerSearchParams,
defaultValue = "",
replace = true,
} = config;

return {
value: searchParams.get(key) ?? defaultValue,
setValue: (newValue) => {
searchParams.set(key, newValue);
setSearchParams(searchParams, { replace });
},
deleteValue: () => {
searchParams.delete(key);
setSearchParams(searchParams, { replace });
},
};
};
19 changes: 0 additions & 19 deletionssite/src/hooks/useTab.ts
View file
Open in desktop

This file was deleted.

View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,45 @@
import { waitFor } from "@testing-library/react";
import {act,waitFor } from "@testing-library/react";
import type { Workspace } from "api/typesGenerated";
import * as M from "testHelpers/entities";
import { renderHookWithAuth } from "testHelpers/renderHelpers";
import { MockWorkspace } from "testHelpers/entities";
import {
type GetLocationSnapshot,
renderHookWithAuth,
} from "testHelpers/hooks";
import * as M from "../../testHelpers/entities";
import CreateWorkspacePage from "./CreateWorkspacePage";
import { useWorkspaceDuplication } from "./useWorkspaceDuplication";

function render(workspace?: Workspace) {
return renderHookWithAuth(
({ workspace }) => {
return useWorkspaceDuplication(workspace);
},
({ workspace }) => useWorkspaceDuplication(workspace),
{
initialProps: { workspace },
extraRoutes: [
{
path: "/templates/:template/workspace",
element: <CreateWorkspacePage />,
},
],
renderOptions: { initialProps: { workspace } },
routingOptions: {
extraRoutes: [
{
path: "/templates/:template/workspace",
element: <CreateWorkspacePage />,
},
],
},
},
);
}

type RenderResult = Awaited<ReturnType<typeof render>>;
type RenderHookResult = RenderResult["result"];

async function performNavigation(
result:RenderResult["result"],
router: RenderResult["router"],
result:RenderHookResult,
getLocationSnapshot: GetLocationSnapshot,
) {
await waitFor(() => expect(result.current.isDuplicationReady).toBe(true));
result.current.duplicateWorkspace();
act(() =>result.current.duplicateWorkspace());

const templateName = MockWorkspace.template_name;
return waitFor(() => {
expect(router.state.location.pathname).toEqual(
`/templates/${M.MockWorkspace.template_name}/workspace`,
);
const { pathname } = getLocationSnapshot();
expect(pathname).toEqual(`/templates/${templateName}/workspace`);
});
}

Expand All@@ -44,28 +49,23 @@ describe(`${useWorkspaceDuplication.name}`, () => {
expect(result.current.isDuplicationReady).toBe(false);

for (let i = 0; i < 10; i++) {
rerender({ workspace: undefined });
awaitrerender({ workspace: undefined });
expect(result.current.isDuplicationReady).toBe(false);
}
});

it("Will become ready when workspace is provided and build params are successfully fetched", async () => {
const { result } = await render(M.MockWorkspace);

const { result } = await render(MockWorkspace);
expect(result.current.isDuplicationReady).toBe(false);
await waitFor(() => expect(result.current.isDuplicationReady).toBe(true));
});

it("Is able to navigate the user to the workspace creation page", async () => {
const { result,router } = await render(M.MockWorkspace);
await performNavigation(result,router);
const { result,getLocationSnapshot } = await render(MockWorkspace);
await performNavigation(result,getLocationSnapshot);
});

test("Navigating populates the URL search params with the workspace's build params", async () => {
const { result, router } = await render(M.MockWorkspace);
await performNavigation(result, router);

const parsedParams = new URLSearchParams(router.state.location.search);
const mockBuildParams = [
M.MockWorkspaceBuildParameter1,
M.MockWorkspaceBuildParameter2,
Expand All@@ -74,25 +74,29 @@ describe(`${useWorkspaceDuplication.name}`, () => {
M.MockWorkspaceBuildParameter5,
];

const { result, getLocationSnapshot } = await render(MockWorkspace);
await performNavigation(result, getLocationSnapshot);

const { search } = getLocationSnapshot();
for (const { name, value } of mockBuildParams) {
const key = `param.${name}`;
expect(parsedParams.get(key)).toEqual(value);
expect(search.get(key)).toEqual(value);
}
});

test("Navigating appends other necessary metadata to the search params", async () => {
const { result, router } = await render(M.MockWorkspace);
await performNavigation(result, router);

const parsedParams = new URLSearchParams(router.state.location.search);
const extraMetadataEntries = [
const extraMetadataEntries: readonly [string, string][] = [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

why the switch fromas const?

Copy link
MemberAuthor

@ParkreinerParkreinerMar 8, 2024
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I felt like it would surface any accidental errors in a better way, and that it does a better job of communicating intent

With the previous code, I could do something like this:

constextraMetadataEntries=[["mode","duplicate"],["name",`${MockWorkspace.name}-copy`],["version",MockWorkspace.template_active_version_id],functionobviouslyWrong(){},null,["forgotToAddTheSecondTupleElement"]]asconst;

And this would be valid still – the underlying type would get broadened. So while the type mismatch would get caught down the line, redefining the type to be an array of string tuples tells you what types should go in, and is more defensive against typos

["mode", "duplicate"],
["name", `${M.MockWorkspace.name}-copy`],
["version", M.MockWorkspace.template_active_version_id],
] as const;
["name", `${MockWorkspace.name}-copy`],
["version", MockWorkspace.template_active_version_id],
];

const { result, getLocationSnapshot } = await render(MockWorkspace);
await performNavigation(result, getLocationSnapshot);

const { search } = getLocationSnapshot();
for (const [key, value] of extraMetadataEntries) {
expect(parsedParams.get(key)).toBe(value);
expect(search.get(key)).toBe(value);
}
});
});
Loading

[8]ページ先頭

©2009-2025 Movatter.jp