Files
openclaw/ui/src/components/github-link-hovercard.test.ts
2026-07-12 15:00:53 +08:00

248 lines
9.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* @vitest-environment jsdom */
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { GatewayBrowserClient } from "../api/gateway.ts";
import { i18n } from "../i18n/index.ts";
import {
GitHubLinkHovercardProvider,
parseGitHubIssueOrPullRequestLink,
} from "./github-link-hovercard.ts";
const GITHUB_LINK_HOVERCARD_ELEMENT_NAME = `test-openclaw-github-link-hovercard-provider-${crypto.randomUUID()}`;
// The non-isolated UI runner resets modules but not customElements. Register
// the current class graph so locale updates reach the mounted test element.
class TestGitHubLinkHovercardProvider extends GitHubLinkHovercardProvider {}
customElements.define(GITHUB_LINK_HOVERCARD_ELEMENT_NAME, TestGitHubLinkHovercardProvider);
function createLink(href: string, label = "GitHub item") {
const provider = document.createElement(
GITHUB_LINK_HOVERCARD_ELEMENT_NAME,
) as GitHubLinkHovercardProvider;
const anchor = document.createElement("a");
anchor.href = href;
anchor.textContent = label;
provider.append(anchor);
document.body.append(provider);
return { anchor, provider };
}
async function hover(anchor: HTMLAnchorElement): Promise<void> {
anchor.dispatchEvent(new MouseEvent("pointerover", { bubbles: true, composed: true }));
await vi.advanceTimersByTimeAsync(250);
}
function leave(anchor: HTMLAnchorElement): void {
anchor.dispatchEvent(
new MouseEvent("pointerout", {
bubbles: true,
composed: true,
relatedTarget: document.body,
}),
);
}
describe("parseGitHubIssueOrPullRequestLink", () => {
it("parses issue and pull request links with trailing paths", () => {
expect(
parseGitHubIssueOrPullRequestLink(
"https://github.com/openclaw/openclaw/issues/99815#issuecomment-1",
),
).toMatchObject({ kind: "issue", number: 99815, owner: "openclaw", repo: "openclaw" });
expect(
parseGitHubIssueOrPullRequestLink("https://github.com/openclaw/openclaw/pull/99816/files"),
).toMatchObject({ kind: "pull", number: 99816, owner: "openclaw", repo: "openclaw" });
});
it("rejects non-item, non-HTTPS, credentialed, and non-GitHub links", () => {
expect(parseGitHubIssueOrPullRequestLink("https://github.com/openclaw/openclaw")).toBeNull();
expect(
parseGitHubIssueOrPullRequestLink("http://github.com/openclaw/openclaw/issues/1"),
).toBeNull();
expect(
parseGitHubIssueOrPullRequestLink("https://user@github.com/openclaw/openclaw/issues/1"),
).toBeNull();
expect(
parseGitHubIssueOrPullRequestLink("https://example.com/openclaw/openclaw/issues/1"),
).toBeNull();
});
});
describe("openclaw-github-link-hovercard-provider", () => {
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-07-05T10:00:00Z"));
});
afterEach(async () => {
await i18n.setLocale("en");
document.body.replaceChildren();
vi.useRealTimers();
vi.restoreAllMocks();
});
it("renders and caches pull request details without changing the link", async () => {
const request = vi.fn().mockResolvedValue({
additions: 101,
avatarDataUrl:
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9WlY9Z8AAAAASUVORK5CYII=",
changedFiles: 3,
closedAt: "2026-07-04T09:53:52Z",
createdAt: "2026-07-04T05:03:47Z",
deletions: 12,
draft: false,
kind: "pull",
login: "steipete",
mergedAt: "2026-07-04T09:53:52Z",
number: 99816,
owner: "OpenClaw",
repo: "OpenClaw",
state: "closed",
title: "fix(agents): derive conversation scope from trusted group facts",
updatedAt: "2026-07-05T09:55:00Z",
});
const href = "https://github.com/openclaw/openclaw/pull/99816";
const { anchor, provider } = createLink(href, "#99816");
provider.client = { request } as unknown as GatewayBrowserClient;
await hover(anchor);
const card = document.querySelector<HTMLElement>(".github-link-hovercard");
expect(card?.textContent).toContain("Merged");
expect(card?.textContent).toContain("openclaw/openclaw #99816");
expect(card?.textContent).toContain(
"fix(agents): derive conversation scope from trusted group facts",
);
expect(card?.textContent).toContain("steipete");
expect(card?.textContent).toContain("+101");
expect(card?.textContent).toContain("12");
expect(card?.textContent).toContain("3 files");
expect(card?.textContent).toContain("5m ago");
expect(anchor.href).toBe(href);
expect(anchor.getAttribute("aria-describedby")).toBe(card?.id);
expect(request).toHaveBeenCalledWith("controlUi.githubPreview", {
kind: "pull",
number: 99816,
owner: "openclaw",
repo: "openclaw",
});
leave(anchor);
expect(document.querySelector(".github-link-hovercard")).toBeNull();
await hover(anchor);
expect(request).toHaveBeenCalledTimes(1);
});
it("renders issue comments and supports focus plus Escape", async () => {
const request = vi.fn().mockResolvedValue({
closedAt: null,
comments: 4,
createdAt: "2026-07-05T08:00:00Z",
kind: "issue",
login: "octocat",
number: 99815,
owner: "openclaw",
repo: "openclaw",
state: "open",
stateReason: null,
title: "Keep hover previews compact",
updatedAt: "2026-07-05T09:55:00Z",
});
const { anchor, provider } = createLink(
"https://github.com/openclaw/openclaw/issues/99815",
"#99815",
);
provider.client = { request } as unknown as GatewayBrowserClient;
anchor.dispatchEvent(new FocusEvent("focusin", { bubbles: true, composed: true }));
await vi.advanceTimersByTimeAsync(0);
expect(document.querySelector(".github-link-hovercard")?.textContent).toContain("4 comments");
expect(document.querySelector(".github-link-hovercard")?.textContent).toContain("Open");
anchor.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
expect(document.querySelector(".github-link-hovercard")).toBeNull();
});
it("ignores unsupported GitHub links and shows a quiet unavailable state", async () => {
const request = vi.fn().mockRejectedValue(new Error("Not Found"));
const unsupportedLink = createLink("https://github.com/openclaw/openclaw", "repository");
unsupportedLink.provider.client = { request } as unknown as GatewayBrowserClient;
await hover(unsupportedLink.anchor);
expect(request).not.toHaveBeenCalled();
expect(document.querySelector(".github-link-hovercard")).toBeNull();
const missingLink = createLink("https://github.com/openclaw/openclaw/issues/999999", "missing");
missingLink.provider.client = { request } as unknown as GatewayBrowserClient;
await hover(missingLink.anchor);
expect(document.querySelector(".github-link-hovercard")?.textContent).toContain(
"GitHub preview unavailable",
);
});
it("preserves an existing description when hover ends before opening", async () => {
const request = vi.fn();
const { anchor, provider } = createLink("https://github.com/openclaw/openclaw/issues/99815");
provider.client = { request } as unknown as GatewayBrowserClient;
anchor.setAttribute("aria-describedby", "existing-description");
anchor.dispatchEvent(new MouseEvent("pointerover", { bubbles: true, composed: true }));
leave(anchor);
await vi.advanceTimersByTimeAsync(250);
expect(anchor.getAttribute("aria-describedby")).toBe("existing-description");
expect(request).not.toHaveBeenCalled();
});
it("rerenders an open preview when the locale changes", async () => {
const request = vi.fn().mockResolvedValue({
closedAt: null,
comments: 1,
createdAt: "2026-07-05T08:00:00Z",
kind: "issue",
login: "octocat",
number: 99815,
owner: "openclaw",
repo: "openclaw",
state: "open",
stateReason: null,
title: "Keep hover previews compact",
updatedAt: "2026-07-05T09:55:00Z",
});
const { anchor, provider } = createLink(
"https://github.com/openclaw/openclaw/issues/99815",
"#99815",
);
provider.client = { request } as unknown as GatewayBrowserClient;
await hover(anchor);
i18n.registerTranslation("pt-BR", {
githubPreview: {
loading: "Carregando detalhes do GitHub…",
unavailable: "Prévia do GitHub indisponível",
states: {
merged: "Mesclado",
draft: "Rascunho",
open: "Aberto",
closed: "Fechado",
notPlanned: "Não planejado",
},
file: "{count} arquivo",
files: "{count} arquivos",
comment: "{count} comentário",
comments: "{count} comentários",
pullRequest: "pull request",
issue: "issue",
ariaLabel: "{state} {kind} {repo} #{number}: {title}, por {author}",
},
});
await i18n.setLocale("pt-BR");
const card = document.querySelector<HTMLElement>(".github-link-hovercard");
expect(card?.textContent).toContain("Aberto");
expect(card?.textContent).toContain("1 comentário");
expect(card?.getAttribute("aria-label")).toContain("por octocat");
});
});