Files
PaperClipAI/ui/src/pages/apps/AppLogo.test.tsx
T
DottaandPaperclip c90d904779 feat(apps): add connection intent setup experience (#12347)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The backend now turns agent requests into durable connection
intents.
> - Operators need a clear path to inspect, configure, and finish those
requests.
> - The experience must preserve identity, agent access, and interrupted
setup state.
> - This pull request adds the connection intent setup experience across
the app UI.
> - The benefit is one guided flow from agent request to governed
connection.

## Linked Issues or Issue Description

Refs #11965

This is stack 9 of 11. It depends on stack 8 and replaces another
reviewable part of #11965.

## What Changed

- Add connection intent cards and setup flow integration.
- Add browse, connection, app detail, and sidebar experience updates.
- Preserve exact draft identity and access choices across resume and
OAuth recovery.
- Add focused UI, architecture, policy, and end-to-end coverage.
- Keep transient retained-connection lookup failures retryable instead
of misclassifying them as missing targets.
- Align the dark-mode E2E contract with the intentionally hidden
Gateways and Profiles sidebar tabs.

## Verification

- `pnpm -r typecheck`
- Focused UI result: 372 tests passed across 20 files.
- AppsConnect regression suite: 80/80 passed, including failed
connection and application lookups during retained reconnect.
- `pnpm --filter @paperclipai/ui exec vitest run
src/components/AppsSidebar.test.tsx` (1 passed)
- `pnpm check:token-gates`
- `pnpm --filter @paperclipai/db check:migrations`
- `pnpm build`

## Risks

- An interrupted OAuth flow can leave a durable draft that needs resume.
- The UI resumes the exact draft and keeps its identity and agent access
settings.
- Retained reconnect retries refetch connections and applications
together to avoid mixing partial snapshots.
- Gateways and Profiles remain route-accessible but intentionally absent
from the sidebar until their existing ship gate is lifted.
- The change does not add a database migration.

> I checked `ROADMAP.md`. This stack continues the existing app
connection work from #11965 and does not duplicate another planned item.

## Model Used

OpenAI Codex, GPT-5. The runtime model ID and context window were not
exposed. The model used reasoning, tool use, and code execution.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have linked the public source pull request with `Refs #`
- [x] I have not referenced internal or instance-local Paperclip issues
or links
- [x] My branch name describes the change and contains no internal
Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-08-29 12:08:35 -05:00

72 lines
2.2 KiB
TypeScript

// @vitest-environment jsdom
import { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { resetAppBrandManifestCacheForTests } from "@/lib/app-brand-assets";
import { AppLogo } from "./AppLogo";
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
describe("AppLogo", () => {
let container: HTMLDivElement;
let root: ReturnType<typeof createRoot>;
beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
resetAppBrandManifestCacheForTests();
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ schemaVersion: 1, providers: [] }),
}));
});
afterEach(() => {
act(() => root.unmount());
container.remove();
vi.unstubAllGlobals();
});
it("renders decorative local light and dark provider marks", async () => {
await act(async () => {
root.render(
<AppLogo
name="Notion"
logoUrl="/brands/apps/notion.svg"
darkLogoUrl="/brands/apps/notion-dark.svg"
size={36}
/>,
);
await Promise.resolve();
await Promise.resolve();
});
const images = Array.from(container.querySelectorAll("img"));
expect(images).toHaveLength(2);
expect(images.map((image) => image.getAttribute("src"))).toEqual([
"/brands/apps/notion.svg",
"/brands/apps/notion-dark.svg",
]);
expect(images.every((image) => image.alt === "")).toBe(true);
expect(images[0]?.className).toContain("dark:hidden");
expect(images[1]?.className).toContain("dark:block");
});
it("uses the deterministic letter tile only after a runtime image failure", async () => {
await act(async () => {
root.render(<AppLogo name="Jira" logoUrl="/brands/apps/missing.svg" />);
await Promise.resolve();
await Promise.resolve();
});
const image = container.querySelector("img");
expect(image).toBeTruthy();
act(() => image?.dispatchEvent(new Event("error")));
expect(container.querySelector("img")).toBeNull();
expect(container.textContent).toBe("J");
});
});