mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-10-03 10:13:47 +10:00
fix(settings): show application names and separate scrolling
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { i18n } from "@lingui/core";
|
||||
import { I18nProvider } from "@lingui/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
const oauth2 = vi.hoisted(() => ({ getConsents: vi.fn(), publicClient: vi.fn(), deleteConsent: vi.fn() }));
|
||||
vi.mock("@/libs/auth/client", () => ({ authClient: { oauth2 } }));
|
||||
|
||||
i18n.loadAndActivate({ locale: "en", messages: {} });
|
||||
const { ConnectedAppsSection } = await import("./connected-apps");
|
||||
|
||||
it("shows registered application names, keeps unnamed or unavailable clients revocable, and revokes the consent", async () => {
|
||||
oauth2.getConsents.mockResolvedValue({
|
||||
data: [
|
||||
{ id: "consent-1", clientId: "codex-client-id", scopes: ["api:read"] },
|
||||
{ id: "consent-2", clientId: "unnamed-client-id", scopes: ["api:read"] },
|
||||
{ id: "consent-3", clientId: "unavailable-client-id", scopes: ["api:read"] },
|
||||
],
|
||||
error: null,
|
||||
});
|
||||
oauth2.publicClient.mockImplementation(({ query }: { query: { client_id: string } }) => {
|
||||
if (query.client_id === "unavailable-client-id") return Promise.reject(new Error("Network unavailable"));
|
||||
return Promise.resolve({
|
||||
data: { client_name: query.client_id === "codex-client-id" ? "Codex" : null },
|
||||
error: null,
|
||||
});
|
||||
});
|
||||
oauth2.deleteConsent.mockResolvedValue({ error: null });
|
||||
|
||||
render(
|
||||
<I18nProvider i18n={i18n}>
|
||||
<QueryClientProvider client={new QueryClient({ defaultOptions: { queries: { retry: false } } })}>
|
||||
<ConnectedAppsSection />
|
||||
</QueryClientProvider>
|
||||
</I18nProvider>,
|
||||
);
|
||||
|
||||
expect(await screen.findByText("Codex")).toBeTruthy();
|
||||
expect(screen.queryByText("codex-client-id")).toBeNull();
|
||||
expect(screen.getByText("unnamed-client-id")).toBeTruthy();
|
||||
expect(screen.getByText("unavailable-client-id")).toBeTruthy();
|
||||
const connection = screen.getByText("Codex").closest("li");
|
||||
if (!connection) throw new Error("Application name is missing its connection row");
|
||||
fireEvent.click(within(connection).getByRole("button", { name: "Revoke access" }));
|
||||
await waitFor(() => expect(oauth2.deleteConsent).toHaveBeenCalledWith({ id: "consent-1" }));
|
||||
});
|
||||
@@ -18,7 +18,14 @@ export function ConnectedAppsSection() {
|
||||
queryFn: async () => {
|
||||
const { data, error } = await authClient.oauth2.getConsents();
|
||||
if (error) throw new Error(t`Could not load connected applications.`);
|
||||
return data ?? [];
|
||||
return Promise.all(
|
||||
(data ?? []).map(async (connection) => {
|
||||
const client = await authClient.oauth2
|
||||
.publicClient({ query: { client_id: connection.clientId } })
|
||||
.catch(() => null);
|
||||
return { ...connection, clientName: client?.data?.client_name?.trim() || connection.clientId };
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
const revoke = useMutation({
|
||||
@@ -48,7 +55,7 @@ export function ConnectedAppsSection() {
|
||||
{connections.map((connection) => (
|
||||
<li key={connection.id} className="flex items-center justify-between gap-3 py-3">
|
||||
<div className="min-w-0 text-sm">
|
||||
<p className="font-medium break-all">{connection.clientId}</p>
|
||||
<p className="font-medium break-all">{connection.clientName}</p>
|
||||
<p className="break-words text-ink-3">{connection.scopes.join(", ")}</p>
|
||||
</div>
|
||||
<Button
|
||||
|
||||
@@ -35,10 +35,10 @@ function RouteComponent() {
|
||||
const isRoot = pathname.replace(/\/$/, "") === "/dashboard/settings";
|
||||
|
||||
return (
|
||||
<div className="grid min-h-full content-start lg:grid-cols-[220px_minmax(0,1fr)] lg:content-stretch">
|
||||
<div className="grid min-h-full content-start sm:h-svh sm:min-h-0 sm:grid-rows-[auto_minmax(0,1fr)] sm:overflow-hidden lg:grid-cols-[220px_minmax(0,1fr)] lg:grid-rows-1 lg:content-stretch">
|
||||
<nav
|
||||
aria-label={t`Settings`}
|
||||
className="flex flex-col gap-1 border-line [view-transition-name:settings-nav] max-lg:border-b max-sm:hidden lg:border-e lg:py-7 lg:ps-6 lg:pe-3"
|
||||
className="flex flex-col gap-1 border-line [view-transition-name:settings-nav] max-lg:border-b max-sm:hidden lg:min-h-0 lg:overflow-y-auto lg:overscroll-contain lg:border-e lg:py-7 lg:ps-6 lg:pe-3"
|
||||
>
|
||||
<h1 className="ms-2 mb-3.5 font-display text-[26px] leading-8 font-medium max-lg:hidden">
|
||||
<Trans>Settings</Trans>
|
||||
@@ -70,7 +70,7 @@ function RouteComponent() {
|
||||
</p>
|
||||
</nav>
|
||||
|
||||
<div className="min-w-0 px-12 pt-8 pb-16 max-lg:px-6 max-sm:px-4 max-sm:pt-4">
|
||||
<div className="min-w-0 px-12 pt-8 pb-16 max-lg:px-6 max-sm:px-4 max-sm:pt-4 sm:min-h-0 sm:overflow-y-auto sm:overscroll-contain">
|
||||
{!isRoot && (
|
||||
<Link
|
||||
to="/dashboard/settings"
|
||||
|
||||
Reference in New Issue
Block a user