Files
multica/packages/views/workspace/no-access-page.test.tsx
marovole baf8b215cb Fix workspace recovery for desktop and web (MUL-2894) (#3436)
* fix(workspace): recover from stale workspace state

* fix(workspace): apply review nits for recovery flow

- no-access-page: navigate via nav.replace so a browser Back doesn't
  land the user back on NoAccessPage with the dead slug
- no-access-page: refresh the stale cookie-clear comment — the recovery
  button no longer routes through `/`; the clear now guards other `/`
  entry points (manual nav, Back into `/`, fresh page load)
- tab-store: drop the redundant `as string | undefined` cast (the Set
  value is already string | undefined under TS 5.9)
- tab-store.test: cover the route-layout heal path (all stale groups
  dropped, then seed a fresh tab for a valid slug) and assert the
  dropped group's router is disposed

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-02 14:04:27 +08:00

96 lines
2.9 KiB
TypeScript

import type { ReactNode } from "react";
import { describe, expect, it, vi, beforeEach } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import { I18nProvider } from "@multica/core/i18n/react";
import enCommon from "../locales/en/common.json";
import enWorkspace from "../locales/en/workspace.json";
import { NoAccessPage } from "./no-access-page";
const TEST_RESOURCES = {
en: { common: enCommon, workspace: enWorkspace },
};
const navigate = vi.fn();
const logout = vi.fn();
const mockWorkspaces = vi.hoisted(() => [{ slug: "valid-team" }]);
vi.mock("../navigation", () => ({
useNavigation: () => ({ push: navigate, replace: navigate }),
}));
vi.mock("../auth", () => ({
useLogout: () => logout,
}));
vi.mock("@multica/core/paths", async () => {
const actual =
await vi.importActual<typeof import("@multica/core/paths")>(
"@multica/core/paths",
);
return {
...actual,
useHasOnboarded: () => true,
};
});
vi.mock("@tanstack/react-query", () => ({
useQuery: () => ({ data: mockWorkspaces }),
}));
vi.mock("@multica/core/workspace/queries", () => ({
workspaceListOptions: () => ({ queryKey: ["workspaces", "list"] }),
}));
function I18nWrapper({ children }: { children: ReactNode }) {
return (
<I18nProvider locale="en" resources={TEST_RESOURCES}>
{children}
</I18nProvider>
);
}
function renderPage() {
return render(<NoAccessPage />, { wrapper: I18nWrapper });
}
describe("NoAccessPage", () => {
beforeEach(() => {
navigate.mockReset();
logout.mockReset();
});
it("renders generic message that doesn't leak existence", () => {
renderPage();
expect(
screen.getByText(/doesn't exist or you don't have access/i),
).toBeInTheDocument();
});
it("navigates to the first accessible workspace on 'Go to my workspaces'", () => {
renderPage();
fireEvent.click(screen.getByRole("button", { name: /go to my workspaces/i }));
expect(navigate).toHaveBeenCalledWith("/valid-team/issues");
});
it("clears last_workspace_slug cookie on mount so the proxy stops looping us back", () => {
document.cookie = "last_workspace_slug=stale; path=/";
renderPage();
// Assert empty value, not just absence of "stale" — the proxy reads any
// truthy value as a redirect target, so a buggy clear that left e.g.
// `last_workspace_slug=other` would still trap users.
const value = document.cookie.match(/last_workspace_slug=([^;]*)/)?.[1];
expect(value ?? "").toBe("");
});
it("fully logs out on 'Sign in as a different user' instead of just navigating", () => {
renderPage();
fireEvent.click(
screen.getByRole("button", { name: /sign in as a different user/i }),
);
expect(logout).toHaveBeenCalledTimes(1);
// Should NOT just navigate to /login — that would leave the session
// cookie + auth state intact and AuthInitializer would re-auth.
expect(navigate).not.toHaveBeenCalledWith("/login");
});
});