mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-23 13:21:12 +00:00
248 lines
9.1 KiB
TypeScript
248 lines
9.1 KiB
TypeScript
/* @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");
|
||
});
|
||
});
|