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

feat: allow users to duplicate workspaces by parameters#10362

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 24 commits intomainfrommes/workspace-clone-feat
Nov 3, 2023
Merged
Show file tree
Hide file tree
Changes from1 commit
Commits
Show all changes
24 commits
Select commitHold shift + click to select a range
9e4f999
chore: add queries for workspace build info
ParkreinerOct 20, 2023
112bc95
refactor: clean up logic for CreateWorkspacePage to support multiple …
ParkreinerOct 20, 2023
15fdfbf
chore: add custom workspace duplication hook
ParkreinerOct 20, 2023
d007b86
chore: integrate mode into CreateWorkspacePageView
ParkreinerOct 20, 2023
294156e
fix: add mode to CreateWorkspacePageView stories
ParkreinerOct 20, 2023
4554895
refactor: extract workspace duplication outside CreateWorkspacePage file
ParkreinerOct 20, 2023
25bacf2
chore: integrate useWorkspaceDuplication into WorkspaceActions
ParkreinerOct 20, 2023
0947031
chore: delete unnecessary function
ParkreinerOct 20, 2023
1d4d4d7
Merge branch 'main' into mes/workspace-clone-feat
ParkreinerOct 31, 2023
d71acf6
refactor: swap useReducer for useState
ParkreinerOct 31, 2023
c0a8c56
fix: swap warning alert for info alert
ParkreinerOct 31, 2023
0b3e954
refactor: move info alert message
ParkreinerOct 31, 2023
7a763a9
refactor: simplify UI logic for mode alerts
ParkreinerOct 31, 2023
da488fa
fix: prevent dismissed Alerts from affecting layouts
ParkreinerOct 31, 2023
5c7242f
fix: remove unnecessary prop binding
ParkreinerOct 31, 2023
98d1b1b
docs: reword comment for clarity
ParkreinerOct 31, 2023
aeacda5
chore: update msw build params to return multiple params
ParkreinerOct 31, 2023
230a4f1
chore: rename duplicationReady to isDuplicationReady
ParkreinerOct 31, 2023
75b1839
chore: expose root component for testing/re-rendering
ParkreinerNov 1, 2023
7cf446f
chore: get tests in place (still have act warnings)
ParkreinerNov 1, 2023
bf21656
refactor: move stuff around for clarity
ParkreinerNov 1, 2023
38ba3b2
chore: finish tests
ParkreinerNov 1, 2023
923d080
chore: revamp tests
ParkreinerNov 3, 2023
8b3d4dd
chore: merge main into branch
ParkreinerNov 3, 2023
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
PrevPrevious commit
NextNext commit
chore: revamp tests
  • Loading branch information
@Parkreiner
Parkreiner committedNov 3, 2023
commit923d080dc105a5adf68d95077e0039aec860546d
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,36 @@
import { waitFor, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { createMemoryRouter } from "react-router-dom";
import { renderWithRouter } from "testHelpers/renderHelpers";

import { waitFor } from "@testing-library/react";
import * as M from "../../testHelpers/entities";
import { type Workspace } from "api/typesGenerated";
import { useWorkspaceDuplication } from "./useWorkspaceDuplication";
import { MockWorkspace } from "testHelpers/entities";
import CreateWorkspacePage from "./CreateWorkspacePage";
import { renderHookWithAuth } from "testHelpers/renderHelpers";

// Tried really hard to get these tests working with RTL's renderHook, but I
// kept running into weird function mismatches, mostly stemming from the fact
// that React Router's RouteProvider does not accept children, meaning that you
// can't inject values into it with renderHook's wrapper
function WorkspaceMock({ workspace }: { workspace?: Workspace }) {
const { duplicateWorkspace, isDuplicationReady } =
useWorkspaceDuplication(workspace);

return (
<button onClick={duplicateWorkspace} disabled={!isDuplicationReady}>
Click me!
</button>
);
}

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

return renderWithRouter(router);
);
}

type RenderResult = Awaited<ReturnType<typeof render>>;

async function performNavigation(
button: HTMLElement,
router:ReturnType<typeof createMemoryRouter>,
result: RenderResult["result"],
router:RenderResult["router"],
) {
await waitFor(() => expect(button).not.toBeDisabled());
await userEvent.click(button);
await waitFor(() => expect(result.current.isDuplicationReady).toBe(true));
result.current.duplicateWorkspace();

return waitFor(() => {
expect(router.state.location.pathname).toEqual(
Expand All@@ -52,34 +41,30 @@ async function performNavigation(

describe(`${useWorkspaceDuplication.name}`, () => {
it("Will never be ready when there is no workspace passed in", async () => {
const { rootComponent, rerender } = renderInMemory(undefined);
const button = await screen.findByRole("button");
expect(button).toBeDisabled();
const { result, rerender } = await render(undefined);
expect(result.current.isDuplicationReady).toBe(false);

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

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

expect(button).toBeDisabled();
await waitFor(() => expect(button).not.toBeDisabled());
expect(result.current.isDuplicationReady).toBe(false);
await waitFor(() => expect(result.current.isDuplicationReady).toBe(true));
});

it("duplicateWorkspace navigates the user to the workspace creation page", async () => {
const { router } = renderInMemory(MockWorkspace);
const button = await screen.findByRole("button");
await performNavigation(button, router);
it("Is able to navigate the user to the workspace creation page", async () => {
const { result, router } = await render(MockWorkspace);
await performNavigation(result, router);
});

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

const parsedParams = new URLSearchParams(router.state.location.search);
const mockBuildParams = [
Expand All@@ -97,9 +82,8 @@ describe(`${useWorkspaceDuplication.name}`, () => {
});

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

const parsedParams = new URLSearchParams(router.state.location.search);
const extraMetadataEntries = [
Expand Down
114 changes: 98 additions & 16 deletionssite/src/testHelpers/renderHelpers.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
import { render as tlRender, screen, waitFor } from "@testing-library/react";
import {
render as tlRender,
screen,
waitFor,
renderHook,
} from "@testing-library/react";
import { AppProviders, ThemeProviders } from "App";
import { DashboardLayout } from "components/Dashboard/DashboardLayout";
import { TemplateSettingsLayout } from "pages/TemplateSettingsPage/TemplateSettingsLayout";
Expand All@@ -10,15 +15,13 @@ import {
} from "react-router-dom";
import { RequireAuth } from "../components/RequireAuth/RequireAuth";
import { MockUser } from "./entities";
import { ReactNode } from "react";
import { ReactNode, useState } from "react";
import { QueryClient } from "react-query";

export const renderWithRouter = (
router: ReturnType<typeof createMemoryRouter>,
) => {
// Create one query client for each render isolate it avoid other
// tests to be affected
const queryClient = new QueryClient({
function createTestQueryClient() {
// Helps create one query client for each test case, to make sure that tests
// are isolated and can't affect each other
return new QueryClient({
defaultOptions: {
queries: {
retry: false,
Expand All@@ -28,16 +31,19 @@ export const renderWithRouter = (
},
},
});
}

const rootComponent = (
<AppProviders queryClient={queryClient}>
<RouterProvider router={router} />
</AppProviders>
);
export const renderWithRouter = (
router: ReturnType<typeof createMemoryRouter>,
) => {
const queryClient = createTestQueryClient();

return {
...tlRender(rootComponent),
rootComponent,
...tlRender(
<AppProviders queryClient={queryClient}>
<RouterProvider router={router} />
</AppProviders>,
),
router,
};
};
Expand All@@ -56,7 +62,7 @@ export const render = (element: ReactNode) => {
);
};

type RenderWithAuthOptions = {
exporttype RenderWithAuthOptions = {
// The current URL, /workspaces/123
route?: string;
// The route path, /workspaces/:workspaceId
Expand DownExpand Up@@ -98,6 +104,82 @@ export function renderWithAuth(
};
}

type RenderHookWithAuthOptions<Props> = Partial<
Readonly<
Omit<RenderWithAuthOptions, "children"> & {
initialProps: Props;
}
>
>;

/**
* Custom version of renderHook that is aware of all our App providers.
*
* Had to do some nasty, cursed things in the implementation to make sure that
* the tests using this function remained simple.
*
* @see {@link https://github.com/coder/coder/pull/10362#discussion_r1380852725}
*/
export async function renderHookWithAuth<Result, Props>(
render: (initialProps: Props) => Result,
{
initialProps,
path = "/",
extraRoutes = [],
}: RenderHookWithAuthOptions<Props> = {},
) {
const queryClient = createTestQueryClient();

// Easy to miss – there's an evil definite assignment via the !
let escapedRouter!: ReturnType<typeof createMemoryRouter>;

const { result, rerender, unmount } = renderHook(render, {
initialProps,
wrapper: ({ children }) => {
/**
* Unfortunately, there isn't a way to define the router outside the
* wrapper while keeping it aware of children, meaning that we need to
* define the router as readonly state in the component instance. This
* ensures the value remains stable across all re-renders
*/
// eslint-disable-next-line react-hooks/rules-of-hooks -- This is actually processed as a component; the linter just isn't aware of that
const [readonlyStatefulRouter] = useState(() => {
return createMemoryRouter([
{ path, element: <>{children}</> },
...extraRoutes,
]);
});

/**
* Leaks the wrapper component's state outside React's render cycles.
*/
escapedRouter = readonlyStatefulRouter;

return (
<AppProviders queryClient={queryClient}>
<RouterProvider router={readonlyStatefulRouter} />
</AppProviders>
);
},
});

/**
* This is necessary to get around some providers in AppProviders having
* conditional rendering and not always rendering their children immediately.
*
* The hook result won't actually exist until the children defined via wrapper
* render in full.
*/
await waitFor(() => expect(result.current).not.toBe(null));

return {
result,
rerender,
unmount,
router: escapedRouter,
} as const;
}

export function renderWithTemplateSettingsLayout(
element: JSX.Element,
{
Expand Down

[8]ページ先頭

©2009-2025 Movatter.jp