diff --git a/ui/src/App.test.tsx b/ui/src/App.test.tsx
index d1b14463b0b..ed6bba5d9e6 100644
--- a/ui/src/App.test.tsx
+++ b/ui/src/App.test.tsx
@@ -42,9 +42,6 @@ vi.mock("@/lib/router", () => ({
useParams: () => ({}),
}));
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
-
async function flushReact() {
await Promise.resolve();
await new Promise((resolve) => window.setTimeout(resolve, 0));
@@ -58,6 +55,29 @@ async function waitForText(container: HTMLElement, text: string) {
expect(container.textContent).toContain(text);
}
+function renderGate(container: HTMLElement) {
+ const root = createRoot(container);
+ const queryClient = new QueryClient({
+ defaultOptions: { queries: { retry: false } },
+ });
+
+ flushSync(() => {
+ root.render(
+
+
+ ,
+ );
+ });
+
+ return root;
+}
+
+function unmountRoot(root: ReturnType) {
+ flushSync(() => {
+ root.unmount();
+ });
+}
+
describe("CloudAccessGate", () => {
let container: HTMLDivElement;
@@ -92,26 +112,13 @@ describe("CloudAccessGate", () => {
keyId: null,
});
- const root = createRoot(container);
- const queryClient = new QueryClient({
- defaultOptions: { queries: { retry: false } },
- });
-
- flushSync(() => {
- root.render(
-
-
- ,
- );
- });
+ const root = renderGate(container);
await waitForText(container, "No company access");
expect(container.textContent).toContain("No company access");
expect(container.textContent).not.toContain("Outlet content");
- flushSync(() => {
- root.unmount();
- });
+ unmountRoot(root);
});
it("allows authenticated users with company access through to the board", async () => {
@@ -128,26 +135,13 @@ describe("CloudAccessGate", () => {
keyId: null,
});
- const root = createRoot(container);
- const queryClient = new QueryClient({
- defaultOptions: { queries: { retry: false } },
- });
-
- flushSync(() => {
- root.render(
-
-
- ,
- );
- });
+ const root = renderGate(container);
await waitForText(container, "Outlet content");
expect(container.textContent).toContain("Outlet content");
expect(container.textContent).not.toContain("No company access");
- flushSync(() => {
- root.unmount();
- });
+ unmountRoot(root);
});
it("shows browser sign-in setup for signed-out private bootstrap-pending instances", async () => {
@@ -160,18 +154,7 @@ describe("CloudAccessGate", () => {
});
mockAuthApi.getSession.mockResolvedValue(null);
- const root = createRoot(container);
- const queryClient = new QueryClient({
- defaultOptions: { queries: { retry: false } },
- });
-
- flushSync(() => {
- root.render(
-
-
- ,
- );
- });
+ const root = renderGate(container);
await waitForText(container, "Finish setting up this Paperclip");
expect(container.textContent).toContain("Finish setting up this Paperclip");
@@ -179,9 +162,7 @@ describe("CloudAccessGate", () => {
expect(container.textContent).toContain("pnpm paperclipai auth bootstrap-ceo");
expect(mockAccessApi.getCurrentBoardAccess).not.toHaveBeenCalled();
- flushSync(() => {
- root.unmount();
- });
+ unmountRoot(root);
});
it("shows the claim action for signed-in private bootstrap-pending instances", async () => {
@@ -198,18 +179,7 @@ describe("CloudAccessGate", () => {
});
mockAccessApi.claimBootstrapAdmin.mockResolvedValue({ claimed: true, userId: "user-1" });
- const root = createRoot(container);
- const queryClient = new QueryClient({
- defaultOptions: { queries: { retry: false } },
- });
-
- flushSync(() => {
- root.render(
-
-
- ,
- );
- });
+ const root = renderGate(container);
await waitForText(container, "Claim this instance");
expect(container.textContent).toContain("Claim this instance");
@@ -227,10 +197,9 @@ describe("CloudAccessGate", () => {
expect(mockAccessApi.claimBootstrapAdmin).toHaveBeenCalledTimes(1);
expect(container.textContent).toContain("You're the instance admin");
+ expect(container.textContent).toContain("Continue to dashboard");
- flushSync(() => {
- root.unmount();
- });
+ unmountRoot(root);
});
it("keeps public bootstrap-pending instances invite-only", async () => {
@@ -246,18 +215,7 @@ describe("CloudAccessGate", () => {
user: { id: "user-1", email: "user@example.com", name: "User", image: null },
});
- const root = createRoot(container);
- const queryClient = new QueryClient({
- defaultOptions: { queries: { retry: false } },
- });
-
- flushSync(() => {
- root.render(
-
-
- ,
- );
- });
+ const root = renderGate(container);
await waitForText(container, "This Paperclip is waiting on its first admin");
expect(container.textContent).toContain("This Paperclip is waiting on its first admin");
@@ -266,8 +224,6 @@ describe("CloudAccessGate", () => {
expect(container.textContent).not.toContain("Sign in / Create account");
expect(mockAccessApi.claimBootstrapAdmin).not.toHaveBeenCalled();
- flushSync(() => {
- root.unmount();
- });
+ unmountRoot(root);
});
});
diff --git a/ui/src/bootstrapSetup.ts b/ui/src/bootstrapSetup.ts
new file mode 100644
index 00000000000..83ebeba84f9
--- /dev/null
+++ b/ui/src/bootstrapSetup.ts
@@ -0,0 +1 @@
+export const BOOTSTRAP_FALLBACK_COMMAND = "pnpm paperclipai auth bootstrap-ceo";
diff --git a/ui/src/components/BootstrapPendingPage.tsx b/ui/src/components/BootstrapPendingPage.tsx
index ab3784d2b1a..659ad5723cd 100644
--- a/ui/src/components/BootstrapPendingPage.tsx
+++ b/ui/src/components/BootstrapPendingPage.tsx
@@ -2,10 +2,9 @@ import type { ReactNode } from "react";
import { Loader2, ShieldCheck, Terminal, TriangleAlert } from "lucide-react";
import { Link } from "@/lib/router";
import { Button } from "@/components/ui/button";
+import { BOOTSTRAP_FALLBACK_COMMAND } from "@/bootstrapSetup";
import type { AuthSession } from "@paperclipai/shared";
-const FALLBACK_COMMAND = "pnpm paperclipai auth bootstrap-ceo";
-
type BootstrapPendingPageProps = {
claimAvailable: boolean;
hasActiveInvite?: boolean;
@@ -28,7 +27,7 @@ function CliFallback({ hasActiveInvite = false }: { hasActiveInvite?: boolean })
: "Run this command on the host that runs Paperclip to print a one-time first-admin invite URL:"}
-{FALLBACK_COMMAND}
+{BOOTSTRAP_FALLBACK_COMMAND}
);
@@ -108,6 +107,11 @@ export function BootstrapPendingPage({
Redirecting...
+
);
}
diff --git a/ui/src/pages/BootstrapSetupUxLab.tsx b/ui/src/pages/BootstrapSetupUxLab.tsx
index 22810dbadd3..35d49644c8b 100644
--- a/ui/src/pages/BootstrapSetupUxLab.tsx
+++ b/ui/src/pages/BootstrapSetupUxLab.tsx
@@ -1,5 +1,6 @@
import type { ReactElement, ReactNode } from "react";
import { Loader2, ShieldCheck, Terminal, TriangleAlert } from "lucide-react";
+import { BOOTSTRAP_FALLBACK_COMMAND } from "@/bootstrapSetup";
import { Button } from "@/components/ui/button";
type LabFixtureKey =
@@ -28,8 +29,6 @@ const FIXTURE_ORDER: LabFixtureKey[] = [
"public-invite-only",
];
-const FALLBACK_COMMAND = "pnpm paperclipai auth bootstrap-ceo";
-
function CliFallback({ hasActiveInvite }: { hasActiveInvite: boolean }) {
return (
@@ -43,7 +42,7 @@ function CliFallback({ hasActiveInvite }: { hasActiveInvite: boolean }) {
: "Run this command on the host that runs Paperclip to print a one‑time first‑admin invite URL:"}
-{FALLBACK_COMMAND}
+{BOOTSTRAP_FALLBACK_COMMAND}
);
@@ -170,6 +169,11 @@ function ClaimSuccess() {
Redirecting…
+
);
}