362 lines
10 KiB
TypeScript
362 lines
10 KiB
TypeScript
import { JSDOM } from "jsdom";
|
||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||
|
||
import { bootPopup } from "../src/popup/index";
|
||
|
||
function flushTasks(): Promise<void> {
|
||
return new Promise((resolve) => setTimeout(resolve, 0));
|
||
}
|
||
|
||
describe("popup-entry", () => {
|
||
let dom: JSDOM;
|
||
|
||
beforeEach(() => {
|
||
dom = new JSDOM("<!doctype html><html><body></body></html>");
|
||
});
|
||
|
||
test("renders a sign-in button when unauthenticated", async () => {
|
||
dom.window.document.body.innerHTML = "<main id='app'></main>";
|
||
|
||
await bootPopup({
|
||
document: dom.window.document,
|
||
sendMessage: vi.fn(async () => ({
|
||
ok: true,
|
||
type: "auth:state",
|
||
value: { isAuthenticated: false }
|
||
}))
|
||
});
|
||
|
||
expect(
|
||
dom.window.document.querySelector('[data-popup-shell="root"]')
|
||
).not.toBeNull();
|
||
expect(
|
||
dom.window.document.querySelector('[data-popup-account="card"]')
|
||
).not.toBeNull();
|
||
expect(dom.window.document.querySelector("button")?.textContent).toContain(
|
||
"登录"
|
||
);
|
||
});
|
||
|
||
test("renders the dev auth panel when enabled", async () => {
|
||
dom.window.document.body.innerHTML = "<main id='app'></main>";
|
||
|
||
await bootPopup({
|
||
config: { enableDevAuthPanel: true },
|
||
document: dom.window.document,
|
||
sendMessage: vi.fn(async () => ({
|
||
ok: true,
|
||
type: "auth:state",
|
||
value: {
|
||
accessTokenExpiresAt: 1700000000000,
|
||
isAuthenticated: true,
|
||
resource: "https://api.example.test",
|
||
scopes: ["openid", "profile"],
|
||
tokenAvailable: true,
|
||
userInfo: { email: "dev@example.com", name: "Dev" }
|
||
}
|
||
}))
|
||
});
|
||
|
||
expect(
|
||
dom.window.document.querySelector('[data-popup-header="root"]')
|
||
).not.toBeNull();
|
||
expect(
|
||
dom.window.document.querySelector('[data-popup-account="card"]')
|
||
).not.toBeNull();
|
||
expect(dom.window.document.body.textContent).toContain("resource");
|
||
expect(dom.window.document.body.textContent).toContain("token");
|
||
});
|
||
|
||
test("shows available extension updates in the popup", async () => {
|
||
const fetchUpdateManifest = vi.fn(async () => ({
|
||
guideUrl: "https://cos.example.com/guide.pdf",
|
||
latestVersion: "0.2.0421.3",
|
||
minSupportedVersion: "0.2.0421.2",
|
||
publishedAt: "2026-05-19",
|
||
releaseNotes: ["支持检查更新"],
|
||
zipUrl: "https://cos.example.com/plugin.zip"
|
||
}));
|
||
|
||
dom.window.document.body.innerHTML = "<main id='app'></main>";
|
||
|
||
await bootPopup({
|
||
currentVersion: "0.2.0421.2",
|
||
document: dom.window.document,
|
||
fetchUpdateManifest,
|
||
sendMessage: vi.fn(async () => ({
|
||
ok: true,
|
||
type: "auth:state",
|
||
value: {
|
||
isAuthenticated: true,
|
||
userInfo: { name: "Dev" }
|
||
}
|
||
}))
|
||
});
|
||
await flushTasks();
|
||
await flushTasks();
|
||
|
||
expect(fetchUpdateManifest).toHaveBeenCalledTimes(1);
|
||
expect(dom.window.document.body.textContent).toContain("当前版本:0.2.0421.2");
|
||
expect(dom.window.document.body.textContent).toContain("发现新版本:0.2.0421.3");
|
||
expect(dom.window.document.body.textContent).toContain("支持检查更新");
|
||
expect(
|
||
dom.window.document.querySelector('[data-popup-update="card"]')
|
||
).not.toBeNull();
|
||
expect(
|
||
dom.window.document.querySelector('[data-popup-download-update="button"]')
|
||
).not.toBeNull();
|
||
expect(
|
||
dom.window.document.querySelector('[data-popup-download-guide="button"]')
|
||
).not.toBeNull();
|
||
});
|
||
|
||
test("downloads update assets from popup buttons", async () => {
|
||
const sendMessage = vi
|
||
.fn()
|
||
.mockResolvedValueOnce({
|
||
ok: true,
|
||
type: "auth:state",
|
||
value: {
|
||
isAuthenticated: true,
|
||
userInfo: { name: "Dev" }
|
||
}
|
||
})
|
||
.mockResolvedValue({ ok: true, type: "update:download-ack" });
|
||
|
||
dom.window.document.body.innerHTML = "<main id='app'></main>";
|
||
|
||
await bootPopup({
|
||
currentVersion: "0.2.0421.2",
|
||
document: dom.window.document,
|
||
fetchUpdateManifest: vi.fn(async () => ({
|
||
guideUrl: "https://cos.example.com/guide.pdf",
|
||
latestVersion: "0.2.0421.3",
|
||
minSupportedVersion: "0.2.0421.2",
|
||
publishedAt: "2026-05-19",
|
||
releaseNotes: [],
|
||
zipUrl: "https://cos.example.com/plugin.zip"
|
||
})),
|
||
sendMessage
|
||
});
|
||
await flushTasks();
|
||
await flushTasks();
|
||
|
||
(
|
||
dom.window.document.querySelector(
|
||
'[data-popup-download-update="button"]'
|
||
) as HTMLButtonElement | null
|
||
)?.click();
|
||
(
|
||
dom.window.document.querySelector(
|
||
'[data-popup-download-guide="button"]'
|
||
) as HTMLButtonElement | null
|
||
)?.click();
|
||
|
||
await Promise.resolve();
|
||
await Promise.resolve();
|
||
|
||
expect(sendMessage).toHaveBeenCalledWith({
|
||
filename: "star-chart-search-enhancer-internal.zip",
|
||
type: "update:download",
|
||
url: "https://cos.example.com/plugin.zip"
|
||
});
|
||
expect(sendMessage).toHaveBeenCalledWith({
|
||
filename: "星图增强插件-超简单安装使用指南.pdf",
|
||
type: "update:download",
|
||
url: "https://cos.example.com/guide.pdf"
|
||
});
|
||
});
|
||
|
||
test("renders a protected api test button in the dev panel", async () => {
|
||
dom.window.document.body.innerHTML = "<main id='app'></main>";
|
||
|
||
await bootPopup({
|
||
config: { enableDevAuthPanel: true },
|
||
document: dom.window.document,
|
||
sendMessage: vi.fn(async () => ({
|
||
ok: true,
|
||
type: "auth:state",
|
||
value: {
|
||
isAuthenticated: true,
|
||
tokenAvailable: true
|
||
}
|
||
}))
|
||
});
|
||
|
||
expect(
|
||
dom.window.document.querySelector('[data-popup-test-protected-api="button"]')
|
||
).not.toBeNull();
|
||
});
|
||
|
||
test("clicking the dev button runs the protected api client and prints the result", async () => {
|
||
const fetchProtectedApi = vi.fn(async () => ({
|
||
message: "authorized",
|
||
ok: true,
|
||
source: "mock-protected-api"
|
||
}));
|
||
const sendMessage = vi.fn(async () => ({
|
||
ok: true,
|
||
type: "auth:state",
|
||
value: {
|
||
isAuthenticated: true,
|
||
tokenAvailable: true
|
||
}
|
||
}));
|
||
|
||
dom.window.document.body.innerHTML = "<main id='app'></main>";
|
||
|
||
await bootPopup({
|
||
config: { enableDevAuthPanel: true },
|
||
document: dom.window.document,
|
||
fetchProtectedApi,
|
||
sendMessage
|
||
});
|
||
|
||
(
|
||
dom.window.document.querySelector(
|
||
'[data-popup-test-protected-api="button"]'
|
||
) as HTMLButtonElement | null
|
||
)?.click();
|
||
|
||
await Promise.resolve();
|
||
|
||
expect(fetchProtectedApi).toHaveBeenCalledTimes(1);
|
||
expect(dom.window.document.body.textContent).toContain("authorized");
|
||
expect(dom.window.document.body.textContent).toContain("mock-protected-api");
|
||
});
|
||
|
||
test("clicking sign-out sends the auth:sign-out message", async () => {
|
||
const sendMessage = vi
|
||
.fn()
|
||
.mockResolvedValueOnce({
|
||
ok: true,
|
||
type: "auth:state",
|
||
value: {
|
||
isAuthenticated: true,
|
||
userInfo: { email: "dev@example.com", name: "Dev" }
|
||
}
|
||
})
|
||
.mockResolvedValueOnce({
|
||
ok: true,
|
||
type: "auth:ack"
|
||
})
|
||
.mockResolvedValueOnce({
|
||
ok: true,
|
||
type: "auth:state",
|
||
value: {
|
||
isAuthenticated: false
|
||
}
|
||
});
|
||
|
||
dom.window.document.body.innerHTML = "<main id='app'></main>";
|
||
|
||
await bootPopup({
|
||
document: dom.window.document,
|
||
sendMessage
|
||
});
|
||
|
||
(
|
||
dom.window.document.querySelector('[data-popup-sign-out="button"]') as
|
||
| HTMLButtonElement
|
||
| null
|
||
)?.click();
|
||
|
||
await Promise.resolve();
|
||
|
||
expect(sendMessage).toHaveBeenCalledWith({ type: "auth:sign-out" });
|
||
});
|
||
|
||
test("shows the latest state without update actions", async () => {
|
||
dom.window.document.body.innerHTML = "<main id='app'></main>";
|
||
|
||
await bootPopup({
|
||
currentVersion: "0.0525.5",
|
||
document: dom.window.document,
|
||
fetchUpdateManifest: vi.fn(async () => ({
|
||
guideUrl: "https://cos.example.com/guide.pdf",
|
||
latestVersion: "0.0525.5",
|
||
minSupportedVersion: "0.0525.5",
|
||
publishedAt: "2026-05-19",
|
||
releaseNotes: ["支持检查更新"],
|
||
zipUrl: "https://cos.example.com/plugin.zip"
|
||
})),
|
||
sendMessage: vi.fn(async () => ({
|
||
ok: true,
|
||
type: "auth:state",
|
||
value: {
|
||
isAuthenticated: true,
|
||
userInfo: { name: "Dev" }
|
||
}
|
||
}))
|
||
});
|
||
await flushTasks();
|
||
await flushTasks();
|
||
|
||
expect(dom.window.document.body.textContent).toContain("当前已是最新版本");
|
||
expect(
|
||
dom.window.document.querySelector('[data-popup-download-update="button"]')
|
||
).toBeNull();
|
||
});
|
||
|
||
test("shows a readable error state when the manifest fetch fails", async () => {
|
||
dom.window.document.body.innerHTML = "<main id='app'></main>";
|
||
|
||
await bootPopup({
|
||
currentVersion: "0.0525.5",
|
||
document: dom.window.document,
|
||
fetchUpdateManifest: vi.fn(async () => {
|
||
throw new Error("network down");
|
||
}),
|
||
sendMessage: vi.fn(async () => ({
|
||
ok: true,
|
||
type: "auth:state",
|
||
value: {
|
||
isAuthenticated: true,
|
||
userInfo: { name: "Dev" }
|
||
}
|
||
}))
|
||
});
|
||
await flushTasks();
|
||
await flushTasks();
|
||
|
||
expect(dom.window.document.body.textContent).toContain("暂时无法检查更新");
|
||
expect(dom.window.document.body.textContent).toContain("network down");
|
||
});
|
||
|
||
test("shows the auth error when sign-in fails", async () => {
|
||
const sendMessage = vi
|
||
.fn()
|
||
.mockResolvedValueOnce({
|
||
ok: true,
|
||
type: "auth:state",
|
||
value: {
|
||
isAuthenticated: false
|
||
}
|
||
})
|
||
.mockResolvedValueOnce({
|
||
error: "redirect_uri_mismatch",
|
||
ok: false,
|
||
type: "auth:error"
|
||
});
|
||
|
||
dom.window.document.body.innerHTML = "<main id='app'></main>";
|
||
|
||
await bootPopup({
|
||
document: dom.window.document,
|
||
sendMessage
|
||
});
|
||
|
||
(
|
||
dom.window.document.querySelector('[data-popup-sign-in="button"]') as
|
||
| HTMLButtonElement
|
||
| null
|
||
)?.click();
|
||
|
||
await Promise.resolve();
|
||
|
||
expect(dom.window.document.body.textContent).toContain(
|
||
"redirect_uri_mismatch"
|
||
);
|
||
});
|
||
});
|