Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions src/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,21 @@

"validatingToken": { "message": "Validating token…" },
"tokenValid": {
"message": "Valid — authenticated as $user$",
"placeholders": { "user": { "content": "$1", "example": "octocat" } }
"message": "Saved — $user$ · $expiration$",
"placeholders": {
"user": { "content": "$1", "example": "octocat" },
"expiration": { "content": "$2", "example": "expires 2026-07-08" }
}
},
"tokenExpirationDate": {
"message": "expires $date$",
"placeholders": { "date": { "content": "$1", "example": "2026-07-08" } }
},
"tokenExpirationNone": { "message": "no expiration date" },
"tokenInvalid": { "message": "Invalid token — not saved" },
"tokenStoredInvalid": {
"message": "Saved token is invalid — update it to keep token-only features working"
},
"tokenInvalid": { "message": "Invalid token — authentication failed" },
"tokenValidationFailed": {
"message": "Validation failed (HTTP $status$)",
"placeholders": { "status": { "content": "$1", "example": "500" } }
Expand Down
17 changes: 14 additions & 3 deletions src/_locales/zh_CN/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,21 @@

"validatingToken": { "message": "正在验证令牌…" },
"tokenValid": {
"message": "有效 — 已认证为 $user$",
"placeholders": { "user": { "content": "$1", "example": "octocat" } }
"message": "已保存 — $user$ · $expiration$",
"placeholders": {
"user": { "content": "$1", "example": "octocat" },
"expiration": { "content": "$2", "example": "有效期至 2026-07-08" }
}
},
"tokenExpirationDate": {
"message": "有效期至 $date$",
"placeholders": { "date": { "content": "$1", "example": "2026-07-08" } }
},
"tokenExpirationNone": { "message": "无过期时间" },
"tokenInvalid": { "message": "无效令牌 — 未保存" },
"tokenStoredInvalid": {
"message": "已保存的令牌无效 — 请更新令牌,否则需要令牌的功能无法使用"
},
"tokenInvalid": { "message": "无效令牌 — 认证失败" },
"tokenValidationFailed": {
"message": "验证失败(HTTP $status$)",
"placeholders": { "status": { "content": "$1", "example": "500" } }
Expand Down
17 changes: 14 additions & 3 deletions src/_locales/zh_TW/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,21 @@

"validatingToken": { "message": "正在驗證權杖…" },
"tokenValid": {
"message": "有效 — 已驗證為 $user$",
"placeholders": { "user": { "content": "$1", "example": "octocat" } }
"message": "已儲存 — $user$ · $expiration$",
"placeholders": {
"user": { "content": "$1", "example": "octocat" },
"expiration": { "content": "$2", "example": "有效期至 2026-07-08" }
}
},
"tokenExpirationDate": {
"message": "有效期至 $date$",
"placeholders": { "date": { "content": "$1", "example": "2026-07-08" } }
},
"tokenExpirationNone": { "message": "無過期時間" },
"tokenInvalid": { "message": "無效權杖 — 未儲存" },
"tokenStoredInvalid": {
"message": "已儲存的權杖無效 — 請更新權杖,否則需要權杖的功能無法使用"
},
"tokenInvalid": { "message": "無效權杖 — 驗證失敗" },
"tokenValidationFailed": {
"message": "驗證失敗(HTTP $status$)",
"placeholders": { "status": { "content": "$1", "example": "500" } }
Expand Down
5 changes: 4 additions & 1 deletion src/lib/i18n.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,14 @@ describe("i18n", () => {

it("substitutes positional placeholders in both locales", () => {
setLocale("en");
expect(t("tokenValid", "octocat")).toBe("Valid — authenticated as octocat");
expect(t("tokenValid", ["octocat", "expires 2026-07-08"])).toBe(
"Saved — octocat · expires 2026-07-08",
);
expect(t("diffStatsTitleWithFiles", ["1,234", "56", "3 files"])).toBe(
"1,234 additions, 56 deletions across 3 files",
);
setLocale("zh_CN");
expect(t("tokenExpirationNone")).toBe("无过期时间");
expect(t("commitTagTitle", "v1.0.0")).toBe("Tag: v1.0.0");
});

Expand Down
91 changes: 59 additions & 32 deletions src/options.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";

const FEATURE_KEYS = [
"feature-pr-branch-names",
Expand All @@ -23,14 +23,10 @@ interface ChromeStub {
}

function buildDom(): void {
const checkboxes = FEATURE_KEYS.map(
(k) => `<input type="checkbox" id="${k}" />`,
).join("");
const checkboxes = FEATURE_KEYS.map((k) => `<input type="checkbox" id="${k}" />`).join("");
document.body.innerHTML = `
<input type="password" id="token" />
<div id="tokenStatus" class="token-status"></div>
<button id="save">Save</button>
<div class="status" id="status"></div>
<button id="searchBtn"></button>
<div id="searchBar" class="search-bar"></div>
<input type="text" id="searchInput" />
Expand Down Expand Up @@ -84,44 +80,47 @@ const $ = <T extends HTMLElement>(id: string) => document.getElementById(id) as

describe("options page", () => {
afterEach(() => {
vi.useRealTimers();
vi.unstubAllGlobals();
vi.restoreAllMocks();
document.body.innerHTML = "";
});

it("reflects stored settings into the form on load", async () => {
it("reflects stored settings and validates a stored token on load", async () => {
vi.stubGlobal(
"fetch",
vi.fn().mockResolvedValue(
new Response(JSON.stringify({ login: "octocat" }), {
status: 200,
headers: { "GitHub-Authentication-Token-Expiration": "2026-07-08 12:00:00 UTC" },
}),
),
);

await loadOptions({ githubToken: "ghp_abc", "feature-default-sort": false });

expect($<HTMLInputElement>("token").value).toBe("ghp_abc");
// Absent flag defaults to enabled; an explicit false renders unchecked.
expect($<HTMLInputElement>("feature-pr-branch-names").checked).toBe(true);
expect($<HTMLInputElement>("feature-default-sort").checked).toBe(false);
expect($("footer").innerHTML).toContain("9.9.9");
await vi.waitFor(() =>
expect($("tokenStatus").textContent).toBe("Saved — octocat · expires 2026-07-08"),
);
});

it("persists the token and every flag when Save is clicked", async () => {
const chrome = await loadOptions({});
$<HTMLInputElement>("token").value = " ghp_new ";
$<HTMLInputElement>("feature-commit-tags").checked = false;

$("save").click();

expect(chrome.set).toHaveBeenCalledTimes(1);
const saved = chrome.set.mock.calls[0][0] as Record<string, unknown>;
expect(saved.githubToken).toBe("ghp_new"); // trimmed
expect(saved["feature-commit-tags"]).toBe(false);
expect(saved["feature-pr-branch-names"]).toBe(true);
expect($("status").textContent).toBe("Saved!");
});

it("surfaces a storage error from Save", async () => {
const chrome = await loadOptions({});
chrome.lastError = { message: "quota exceeded" };

$("save").click();
it("shows stored invalid token copy without clearing the saved token", async () => {
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response(null, { status: 401 })));
const chrome = await loadOptions({ githubToken: "ghp_revoked" });

expect($("status").textContent).toBe("quota exceeded");
expect($("status").className).toContain("error");
await vi.waitFor(() =>
expect($("tokenStatus").textContent).toBe(
"Saved token is invalid — update it to keep token-only features working",
),
);
expect($<HTMLInputElement>("token").value).toBe("ghp_revoked");
expect(chrome.store.githubToken).toBe("ghp_revoked");
expect(chrome.set).not.toHaveBeenCalled();
});

it("auto-saves a single flag when its checkbox is toggled", async () => {
Expand All @@ -134,6 +133,30 @@ describe("options page", () => {
expect(chrome.set).toHaveBeenCalledWith({ "feature-release-tab": false });
});

it("auto-saves a valid token after input validation", async () => {
vi.useFakeTimers();
const chrome = await loadOptions({});
vi.stubGlobal(
"fetch",
vi.fn().mockResolvedValue(
new Response(JSON.stringify({ login: "octocat" }), {
status: 200,
headers: { "GitHub-Authentication-Token-Expiration": "2026-07-08 12:00:00 UTC" },
}),
),
);

const token = $<HTMLInputElement>("token");
token.value = " ghp_valid ";
token.dispatchEvent(new Event("input"));
await vi.advanceTimersByTimeAsync(600);

await vi.waitFor(() =>
expect(chrome.set).toHaveBeenCalledWith({ githubToken: "ghp_valid" }, expect.any(Function)),
);
expect($("tokenStatus").textContent).toBe("Saved — octocat · expires 2026-07-08");
});

it("filters feature items and hides groups with no match while searching", async () => {
await loadOptions({});
const input = $<HTMLInputElement>("searchInput");
Expand All @@ -150,20 +173,24 @@ describe("options page", () => {
});

it("validates the token against the GitHub API on blur", async () => {
await loadOptions({});
const chrome = await loadOptions({});
vi.stubGlobal(
"fetch",
vi.fn().mockResolvedValue(
new Response(JSON.stringify({ login: "octocat" }), { status: 200 }),
new Response(JSON.stringify({ login: "octocat" }), {
status: 200,
headers: { "GitHub-Authentication-Token-Expiration": "2026-07-08 12:00:00 UTC" },
}),
),
);

const token = $<HTMLInputElement>("token");
token.value = "ghp_valid";
token.dispatchEvent(new Event("blur"));
await vi.waitFor(() =>
expect($("tokenStatus").textContent).toBe("Validauthenticated as octocat"),
expect($("tokenStatus").textContent).toBe("Savedoctocat · expires 2026-07-08"),
);
expect($("tokenStatus").className).toContain("valid");
expect(chrome.set).toHaveBeenCalledWith({ githubToken: "ghp_valid" }, expect.any(Function));
});
});
Loading