feat: implement global loading indicators and refactor activity feed tests
This commit is contained in:
@@ -31,7 +31,8 @@ describe("/activity feed", () => {
|
||||
}
|
||||
|
||||
function assertSignedInAndLanded() {
|
||||
cy.contains(/live feed/i, { timeout: 30_000 }).should("be.visible");
|
||||
cy.waitForAppLoaded();
|
||||
cy.contains(/live feed/i).should("be.visible");
|
||||
}
|
||||
|
||||
it("auth negative: signed-out user cannot access /activity", () => {
|
||||
@@ -70,7 +71,6 @@ describe("/activity feed", () => {
|
||||
cy.visit("/activity");
|
||||
assertSignedInAndLanded();
|
||||
|
||||
cy.wait("@activityList");
|
||||
cy.contains("CI hardening").should("be.visible");
|
||||
cy.contains("Hello world").should("be.visible");
|
||||
});
|
||||
@@ -91,7 +91,6 @@ describe("/activity feed", () => {
|
||||
cy.visit("/activity");
|
||||
assertSignedInAndLanded();
|
||||
|
||||
cy.wait("@activityList");
|
||||
cy.contains(/waiting for new comments/i).should("be.visible");
|
||||
});
|
||||
|
||||
@@ -111,7 +110,6 @@ describe("/activity feed", () => {
|
||||
cy.visit("/activity");
|
||||
assertSignedInAndLanded();
|
||||
|
||||
cy.wait("@activityList");
|
||||
cy.contains(/unable to load feed|boom/i).should("be.visible");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ describe("Clerk login", () => {
|
||||
|
||||
// After login, user should be able to access protected route.
|
||||
cy.visit("/activity");
|
||||
cy.contains(/live feed/i, { timeout: 30_000 }).should("be.visible");
|
||||
cy.waitForAppLoaded();
|
||||
cy.contains(/live feed/i).should("be.visible");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,7 +14,8 @@ describe("Organizations (PR #61)", () => {
|
||||
cy.clerkSignIn({ strategy: "email_code", identifier: email });
|
||||
|
||||
cy.visit("/organization");
|
||||
cy.contains(/members\s*&\s*invites/i, { timeout: 30_000 }).should("be.visible");
|
||||
cy.waitForAppLoaded();
|
||||
cy.contains(/members\s*&\s*invites/i).should("be.visible");
|
||||
|
||||
// Deterministic assertion across roles:
|
||||
// - if user is admin: invite button enabled
|
||||
|
||||
@@ -6,6 +6,8 @@ type ClerkOtpLoginOptions = {
|
||||
otp: string;
|
||||
};
|
||||
|
||||
const APP_LOAD_TIMEOUT_MS = 30_000;
|
||||
|
||||
function getEnv(name: string, fallback?: string): string {
|
||||
const value = Cypress.env(name) as string | undefined;
|
||||
if (value) return value;
|
||||
@@ -40,6 +42,16 @@ function normalizeOrigin(value: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
Cypress.Commands.add("waitForAppLoaded", () => {
|
||||
cy.get("[data-cy='route-loader']", {
|
||||
timeout: APP_LOAD_TIMEOUT_MS,
|
||||
}).should("not.exist");
|
||||
|
||||
cy.get("[data-cy='global-loader']", {
|
||||
timeout: APP_LOAD_TIMEOUT_MS,
|
||||
}).should("have.attr", "aria-hidden", "true");
|
||||
});
|
||||
|
||||
Cypress.Commands.add("loginWithClerkOtp", () => {
|
||||
const clerkOrigin = normalizeOrigin(
|
||||
getEnv("CLERK_ORIGIN", clerkOriginFromPublishableKey()),
|
||||
@@ -194,6 +206,11 @@ declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
/**
|
||||
* Waits for route-level and global app loaders to disappear.
|
||||
*/
|
||||
waitForAppLoaded(): Chainable<void>;
|
||||
|
||||
/**
|
||||
* Logs in via the real Clerk SignIn page using deterministic OTP credentials.
|
||||
*
|
||||
|
||||
@@ -7,6 +7,7 @@ import { DM_Serif_Display, IBM_Plex_Sans, Sora } from "next/font/google";
|
||||
|
||||
import { AuthProvider } from "@/components/providers/AuthProvider";
|
||||
import { QueryProvider } from "@/components/providers/QueryProvider";
|
||||
import { GlobalLoader } from "@/components/ui/global-loader";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "OpenClaw Mission Control",
|
||||
@@ -41,7 +42,10 @@ export default function RootLayout({ children }: { children: ReactNode }) {
|
||||
className={`${bodyFont.variable} ${headingFont.variable} ${displayFont.variable} min-h-screen bg-app text-strong antialiased`}
|
||||
>
|
||||
<AuthProvider>
|
||||
<QueryProvider>{children}</QueryProvider>
|
||||
<QueryProvider>
|
||||
<GlobalLoader />
|
||||
{children}
|
||||
</QueryProvider>
|
||||
</AuthProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
13
frontend/src/app/loading.tsx
Normal file
13
frontend/src/app/loading.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div
|
||||
data-cy="route-loader"
|
||||
className="flex min-h-screen items-center justify-center bg-app px-6"
|
||||
>
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className="h-10 w-10 animate-spin rounded-full border-2 border-slate-200 border-t-[var(--accent)]" />
|
||||
<p className="text-sm text-slate-500">Loading mission control...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
29
frontend/src/components/ui/global-loader.tsx
Normal file
29
frontend/src/components/ui/global-loader.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import { useIsFetching, useIsMutating } from "@tanstack/react-query";
|
||||
|
||||
export function GlobalLoader() {
|
||||
const fetchingCount = useIsFetching({
|
||||
predicate: (query) =>
|
||||
query.state.fetchStatus === "fetching" && query.state.data === undefined,
|
||||
});
|
||||
const mutatingCount = useIsMutating();
|
||||
const visible = fetchingCount + mutatingCount > 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
data-cy="global-loader"
|
||||
className={`pointer-events-none fixed inset-x-0 top-0 z-[120] h-1 transition-opacity duration-200 ${
|
||||
visible ? "opacity-100" : "opacity-0"
|
||||
}`}
|
||||
aria-hidden={!visible}
|
||||
data-state={visible ? "visible" : "hidden"}
|
||||
role="status"
|
||||
>
|
||||
<div className="h-full w-full overflow-hidden bg-[var(--accent-soft)]">
|
||||
<div className="h-full w-full animate-progress-shimmer bg-[linear-gradient(90deg,transparent_0%,var(--accent)_50%,transparent_100%)]" />
|
||||
</div>
|
||||
<span className="sr-only">Loading</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user