From b2d6d2b4e46b3759bb0a19023b0e16d94ab5409b Mon Sep 17 00:00:00 2001 From: shawn Date: Thu, 23 Jul 2026 09:55:25 +0800 Subject: [PATCH 1/5] fix: support new repository action dom --- src/features/watch-fork-star-popup.test.ts | 20 ++++++ src/features/watch-fork-star-popup.ts | 76 ++++++++++++++++------ 2 files changed, 77 insertions(+), 19 deletions(-) diff --git a/src/features/watch-fork-star-popup.test.ts b/src/features/watch-fork-star-popup.test.ts index 37f5620..1fbb518 100644 --- a/src/features/watch-fork-star-popup.test.ts +++ b/src/features/watch-fork-star-popup.test.ts @@ -40,6 +40,26 @@ describe("watch/fork/star popup", () => { expect(document.querySelector(".bg-wfs-popup")).toBeNull(); }); + it("finds repository actions after GitHub replaces the legacy wrappers and counter classes", () => { + document.body.innerHTML = ` +
+
+
+
+
+ `; + + injectWatchForkStarPopup(); + + expect(document.querySelectorAll(".bg-wfs-counter-wrap")).toHaveLength(3); + expect(document.querySelectorAll(".bg-wfs-popup")).toHaveLength(3); + expect( + [...document.querySelectorAll(".bg-wfs-popup-header span:last-child")].map( + (count) => count.textContent, + ), + ).toEqual(["4", "2", "9"]); + }); + it("loads and renders the user list when a counter is hovered", async () => { vi.useFakeTimers(); vi.mocked(fetchWatchers).mockResolvedValue([ diff --git a/src/features/watch-fork-star-popup.ts b/src/features/watch-fork-star-popup.ts index ffc1e30..1734205 100644 --- a/src/features/watch-fork-star-popup.ts +++ b/src/features/watch-fork-star-popup.ts @@ -202,6 +202,33 @@ function reapOrphanedPopups(): void { } } +function findActionTargets( + root: Element, + oldSelector: string, + iconSelector: string, +): HTMLElement[] { + const targets = new Set(root.querySelectorAll(oldSelector)); + for (const icon of root.querySelectorAll(iconSelector)) { + const control = icon.closest("a, button") ?? icon.parentElement; + if (!control) continue; + const action = control.closest("li") ?? control.parentElement ?? control; + targets.add(action.querySelector('[class*="Counter"], strong') ?? control); + } + return [...targets]; +} + +function getCountText(target: HTMLElement): string { + for (const value of [ + target.textContent, + target.getAttribute("title"), + target.getAttribute("aria-label"), + ]) { + const count = value?.match(/\d[\d,.]*[kKmM]?/)?.[0]; + if (count) return count; + } + return "0"; +} + function attachPopup( counter: HTMLElement, config: PopupConfig, @@ -240,17 +267,23 @@ export function injectWatchForkStarPopup(): void { const { owner, repo } = repoInfo; - // Check that pagehead-actions exist (repo page) - const actions = document.querySelector("ul.pagehead-actions"); + const actions = + document.querySelector("#repository-container-header") ?? + document.querySelector("ul.pagehead-actions") ?? + document.querySelector("#repo-content-pjax-container"); if (!actions) return; - // Watch counter (Primer React component — don't move it, attach in-place) - const watchCounter = actions.querySelector('[class*="CounterLabel"]') as HTMLElement | null; - if (watchCounter) { - const countText = watchCounter.textContent?.trim() || "0"; - attachPopup(watchCounter, { + // Prefer the old counter selectors, then fall back to the action icon so + // GitHub can change its wrapper/component classes without disabling hover. + const watchTargets = findActionTargets( + actions, + '[class*="CounterLabel"]', + 'svg[class*="octicon-bell"], svg[class*="octicon-eye"]', + ); + for (const watchTarget of watchTargets) { + attachPopup(watchTarget, { title: t("watchers"), - countText, + countText: getCountText(watchTarget), viewAllUrl: `/${owner}/${repo}/watchers`, }, async (list) => { const data = await fetchWatchers(owner, repo); @@ -258,13 +291,15 @@ export function injectWatchForkStarPopup(): void { }); } - // Fork counter - const forkCounter = actions.querySelector("#fork-button .Counter") as HTMLElement | null; - if (forkCounter) { - const countText = forkCounter.textContent?.trim() || "0"; - attachPopup(forkCounter, { + const forkTargets = findActionTargets( + actions, + "#fork-button .Counter", + 'svg[class*="octicon-repo-forked"]', + ); + for (const forkTarget of forkTargets) { + attachPopup(forkTarget, { title: t("forks"), - countText, + countText: getCountText(forkTarget), viewAllUrl: `/${owner}/${repo}/forks`, }, async (list) => { const data = await fetchForks(owner, repo); @@ -276,12 +311,15 @@ export function injectWatchForkStarPopup(): void { // so the popup works regardless of toggle state. // Share fetched data so toggling star state doesn't re-fetch. let starData: Awaited> | null = null; - const starCounters = actions.querySelectorAll(".Counter.js-social-count"); - for (const starCounter of starCounters) { - const countText = starCounter.textContent?.trim() || "0"; - attachPopup(starCounter as HTMLElement, { + const starTargets = findActionTargets( + actions, + ".Counter.js-social-count", + 'svg[class*="octicon-star"]', + ); + for (const starTarget of starTargets) { + attachPopup(starTarget, { title: t("stargazers"), - countText, + countText: getCountText(starTarget), viewAllUrl: `/${owner}/${repo}/stargazers`, }, async (list) => { if (!starData) starData = await fetchStargazers(owner, repo); From 808f660934b53293f975fb66c3214be12c35fca5 Mon Sep 17 00:00:00 2001 From: shawn Date: Thu, 23 Jul 2026 10:09:18 +0800 Subject: [PATCH 2/5] fix: target redesigned repository counters --- src/features/watch-fork-star-popup.test.ts | 14 +++-- src/features/watch-fork-star-popup.ts | 62 +++++----------------- 2 files changed, 22 insertions(+), 54 deletions(-) diff --git a/src/features/watch-fork-star-popup.test.ts b/src/features/watch-fork-star-popup.test.ts index 1fbb518..0b25410 100644 --- a/src/features/watch-fork-star-popup.test.ts +++ b/src/features/watch-fork-star-popup.test.ts @@ -42,10 +42,16 @@ describe("watch/fork/star popup", () => { it("finds repository actions after GitHub replaces the legacy wrappers and counter classes", () => { document.body.innerHTML = ` -
-
-
-
+
+ + + Fork 2 + +
`; diff --git a/src/features/watch-fork-star-popup.ts b/src/features/watch-fork-star-popup.ts index 1734205..e19d7b4 100644 --- a/src/features/watch-fork-star-popup.ts +++ b/src/features/watch-fork-star-popup.ts @@ -202,33 +202,6 @@ function reapOrphanedPopups(): void { } } -function findActionTargets( - root: Element, - oldSelector: string, - iconSelector: string, -): HTMLElement[] { - const targets = new Set(root.querySelectorAll(oldSelector)); - for (const icon of root.querySelectorAll(iconSelector)) { - const control = icon.closest("a, button") ?? icon.parentElement; - if (!control) continue; - const action = control.closest("li") ?? control.parentElement ?? control; - targets.add(action.querySelector('[class*="Counter"], strong') ?? control); - } - return [...targets]; -} - -function getCountText(target: HTMLElement): string { - for (const value of [ - target.textContent, - target.getAttribute("title"), - target.getAttribute("aria-label"), - ]) { - const count = value?.match(/\d[\d,.]*[kKmM]?/)?.[0]; - if (count) return count; - } - return "0"; -} - function attachPopup( counter: HTMLElement, config: PopupConfig, @@ -267,23 +240,14 @@ export function injectWatchForkStarPopup(): void { const { owner, repo } = repoInfo; - const actions = - document.querySelector("#repository-container-header") ?? - document.querySelector("ul.pagehead-actions") ?? - document.querySelector("#repo-content-pjax-container"); - if (!actions) return; - - // Prefer the old counter selectors, then fall back to the action icon so - // GitHub can change its wrapper/component classes without disabling hover. - const watchTargets = findActionTargets( - actions, - '[class*="CounterLabel"]', - 'svg[class*="octicon-bell"], svg[class*="octicon-eye"]', + const watchTargets = document.querySelectorAll( + 'ul.pagehead-actions [class*="CounterLabel"], ' + + '[data-testid="notifications-subscriptions-menu-button"] [data-component="CounterLabel"]', ); for (const watchTarget of watchTargets) { attachPopup(watchTarget, { title: t("watchers"), - countText: getCountText(watchTarget), + countText: watchTarget.textContent?.trim() || "0", viewAllUrl: `/${owner}/${repo}/watchers`, }, async (list) => { const data = await fetchWatchers(owner, repo); @@ -291,15 +255,14 @@ export function injectWatchForkStarPopup(): void { }); } - const forkTargets = findActionTargets( - actions, - "#fork-button .Counter", - 'svg[class*="octicon-repo-forked"]', + const forkTargets = document.querySelectorAll( + 'ul.pagehead-actions #fork-button .Counter, ' + + '[data-testid="fork-button"] [data-component="CounterLabel"]', ); for (const forkTarget of forkTargets) { attachPopup(forkTarget, { title: t("forks"), - countText: getCountText(forkTarget), + countText: forkTarget.textContent?.trim() || "0", viewAllUrl: `/${owner}/${repo}/forks`, }, async (list) => { const data = await fetchForks(owner, repo); @@ -311,15 +274,14 @@ export function injectWatchForkStarPopup(): void { // so the popup works regardless of toggle state. // Share fetched data so toggling star state doesn't re-fetch. let starData: Awaited> | null = null; - const starTargets = findActionTargets( - actions, - ".Counter.js-social-count", - 'svg[class*="octicon-star"]', + const starTargets = document.querySelectorAll( + 'ul.pagehead-actions .Counter.js-social-count, ' + + '[data-testid="star-button"] [data-component="CounterLabel"]', ); for (const starTarget of starTargets) { attachPopup(starTarget, { title: t("stargazers"), - countText: getCountText(starTarget), + countText: starTarget.textContent?.trim() || "0", viewAllUrl: `/${owner}/${repo}/stargazers`, }, async (list) => { if (!starData) starData = await fetchStargazers(owner, repo); From d9648f59dae8cdaf0083899f5b0f40474a143b33 Mon Sep 17 00:00:00 2001 From: shawn Date: Thu, 23 Jul 2026 10:25:13 +0800 Subject: [PATCH 3/5] fix: avoid restricted social list requests --- src/_locales/en/messages.json | 1 + src/_locales/zh_CN/messages.json | 1 + src/_locales/zh_TW/messages.json | 1 + src/features/watch-fork-star-popup.test.ts | 20 ++++++++ src/features/watch-fork-star-popup.ts | 27 +++++++++- src/lib/github-api.ts | 16 ++++-- src/lib/messages.ts | 2 + src/service-worker.test.ts | 29 +++++++++-- src/service-worker.ts | 57 +++++++++++++++++++--- 9 files changed, 136 insertions(+), 18 deletions(-) diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index 96fb51d..1b1f634 100644 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -167,6 +167,7 @@ "noStargazers": { "message": "No stargazers yet" }, "noForks": { "message": "No forks yet" }, "failedToLoad": { "message": "Failed to load" }, + "githubAccessRestrictions": { "message": "GitHub access restrictions" }, "collapseTree": { "message": "Collapse tree" }, "expandTree": { "message": "Expand tree" }, diff --git a/src/_locales/zh_CN/messages.json b/src/_locales/zh_CN/messages.json index fbd736e..d0296a9 100644 --- a/src/_locales/zh_CN/messages.json +++ b/src/_locales/zh_CN/messages.json @@ -167,6 +167,7 @@ "noStargazers": { "message": "暂无 Stargazers" }, "noForks": { "message": "暂无 Forks" }, "failedToLoad": { "message": "加载失败" }, + "githubAccessRestrictions": { "message": "GitHub 访问限制说明" }, "collapseTree": { "message": "折叠目录树" }, "expandTree": { "message": "展开目录树" }, diff --git a/src/_locales/zh_TW/messages.json b/src/_locales/zh_TW/messages.json index 8fa4ce4..425a886 100644 --- a/src/_locales/zh_TW/messages.json +++ b/src/_locales/zh_TW/messages.json @@ -167,6 +167,7 @@ "noStargazers": { "message": "尚無 Stargazers" }, "noForks": { "message": "尚無 Forks" }, "failedToLoad": { "message": "載入失敗" }, + "githubAccessRestrictions": { "message": "GitHub 存取限制說明" }, "collapseTree": { "message": "摺疊目錄樹" }, "expandTree": { "message": "展開目錄樹" }, diff --git a/src/features/watch-fork-star-popup.test.ts b/src/features/watch-fork-star-popup.test.ts index 0b25410..88bd376 100644 --- a/src/features/watch-fork-star-popup.test.ts +++ b/src/features/watch-fork-star-popup.test.ts @@ -90,6 +90,26 @@ describe("watch/fork/star popup", () => { vi.useRealTimers(); }); + it("shows GitHub's restriction notice instead of a list for non-collaborators", async () => { + vi.useFakeTimers(); + vi.mocked(fetchWatchers).mockResolvedValue({ restricted: true }); + + injectWatchForkStarPopup(); + document.getElementById("watch-counter")!.dispatchEvent(new Event("mouseenter")); + await vi.advanceTimersByTimeAsync(300); + + expect(document.querySelector(".bg-wfs-popup-empty")?.textContent).toBe("—"); + const link = [...document.querySelectorAll(".bg-wfs-popup-footer a")].find( + (candidate) => candidate.target === "_blank", + )!; + expect(link.href).toBe( + "https://github.blog/changelog/2026-06-30-upcoming-access-restrictions-to-public-api-endpoints-and-ui-views/", + ); + expect(link.rel).toBe("noopener noreferrer"); + + vi.useRealTimers(); + }); + it("portals the popup to , out of the stargazers anchor, so clicks can't leak to it", async () => { vi.useFakeTimers(); // GitHub nests the star counter inside an ``. diff --git a/src/features/watch-fork-star-popup.ts b/src/features/watch-fork-star-popup.ts index e19d7b4..2edd33c 100644 --- a/src/features/watch-fork-star-popup.ts +++ b/src/features/watch-fork-star-popup.ts @@ -6,6 +6,8 @@ import type { ForkInfo } from "../lib/github-api"; const WRAP_CLASS = "bg-wfs-counter-wrap"; const POPUP_CLASS = "bg-wfs-popup"; +const ACCESS_RESTRICTIONS_URL = + "https://github.blog/changelog/2026-06-30-upcoming-access-restrictions-to-public-api-endpoints-and-ui-views/"; // Must match `.bg-wfs-popup { width }` in content.css — the popup is portaled to // and positioned with fixed coordinates, so JS needs the width to right- @@ -100,6 +102,27 @@ function renderUserList( `).join(""); } +function renderSocialList( + list: HTMLElement, + data: Awaited>, + emptyMsg: string, +): void { + if (Array.isArray(data)) { + renderUserList(list, data, emptyMsg); + return; + } + + list.innerHTML = '
'; + const footerLink = list.parentElement?.querySelector( + ".bg-wfs-popup-footer a", + ); + if (!footerLink) return; + footerLink.href = ACCESS_RESTRICTIONS_URL; + footerLink.target = "_blank"; + footerLink.rel = "noopener noreferrer"; + footerLink.textContent = t("githubAccessRestrictions"); +} + function renderForks(list: HTMLElement, items: ForkInfo[]): void { if (items.length === 0) { list.innerHTML = `
${t("noForks")}
`; @@ -251,7 +274,7 @@ export function injectWatchForkStarPopup(): void { viewAllUrl: `/${owner}/${repo}/watchers`, }, async (list) => { const data = await fetchWatchers(owner, repo); - renderUserList(list, data, t("noWatchers")); + renderSocialList(list, data, t("noWatchers")); }); } @@ -285,7 +308,7 @@ export function injectWatchForkStarPopup(): void { viewAllUrl: `/${owner}/${repo}/stargazers`, }, async (list) => { if (!starData) starData = await fetchStargazers(owner, repo); - renderUserList(list, starData, t("noStargazers")); + renderSocialList(list, starData, t("noStargazers")); }); } } diff --git a/src/lib/github-api.ts b/src/lib/github-api.ts index 80b6691..79817ad 100644 --- a/src/lib/github-api.ts +++ b/src/lib/github-api.ts @@ -9,6 +9,7 @@ import type { TagInfo, StargazerInfo, WatcherInfo, + SocialList, ForkInfo, ContributorInfo, } from "./messages"; @@ -47,6 +48,7 @@ export type { TagInfo, StargazerInfo, WatcherInfo, + SocialList, ForkInfo, ContributorInfo, }; @@ -197,9 +199,12 @@ export async function approvePR( } } -export async function fetchStargazers(owner: string, repo: string): Promise { +export async function fetchStargazers( + owner: string, + repo: string, +): Promise> { try { - return await sendMessage({ + return await sendMessage>({ type: "FETCH_STARGAZERS", owner, repo, @@ -210,9 +215,12 @@ export async function fetchStargazers(owner: string, repo: string): Promise { +export async function fetchWatchers( + owner: string, + repo: string, +): Promise> { try { - return await sendMessage({ + return await sendMessage>({ type: "FETCH_WATCHERS", owner, repo, diff --git a/src/lib/messages.ts b/src/lib/messages.ts index d0a483f..1b2f243 100644 --- a/src/lib/messages.ts +++ b/src/lib/messages.ts @@ -61,6 +61,8 @@ export interface WatcherInfo { name: string | null; } +export type SocialList = T[] | { restricted: true }; + export interface ForkInfo { owner: string; ownerAvatarUrl: string; diff --git a/src/service-worker.test.ts b/src/service-worker.test.ts index 072d438..f1a2f78 100644 --- a/src/service-worker.test.ts +++ b/src/service-worker.test.ts @@ -409,12 +409,12 @@ describe("service worker", () => { it("maps the stargazers REST payload", async () => { const state = await loadWorker("token"); - vi.mocked(fetch).mockResolvedValue( - jsonResponse([ + vi.mocked(fetch) + .mockResolvedValueOnce(jsonResponse({ permissions: { admin: true } })) + .mockResolvedValueOnce(jsonResponse([ { user: { login: "octocat", avatar_url: "https://a/o.png", name: "Octo Cat" }, starred_at: "2026-01-01T00:00:00Z" }, { user: { login: "mona", avatar_url: "https://a/m.png", name: null }, starred_at: "2026-01-02T00:00:00Z" }, - ]), - ); + ])); const response = await sendMessage(state.messageListeners[0], { type: "FETCH_STARGAZERS", @@ -431,6 +431,27 @@ describe("service worker", () => { }); }); + it.each([ + ["FETCH_STARGAZERS", "/stargazers"], + ["FETCH_WATCHERS", "/subscribers"], + ] as const)("skips %s for repositories without collaborator access", async (type, path) => { + const state = await loadWorker("token"); + vi.mocked(fetch).mockResolvedValueOnce( + jsonResponse({ permissions: { pull: true, push: false } }), + ); + + const response = await sendMessage(state.messageListeners[0], { + type, + owner: "owner", + repo: "repo", + }); + + expect(response).toEqual({ ok: true, data: { restricted: true } }); + expect(fetch).toHaveBeenCalledTimes(1); + expect(vi.mocked(fetch).mock.calls[0][0]).toBe("https://api.github.com/repos/owner/repo"); + expect(vi.mocked(fetch).mock.calls.some(([url]) => String(url).includes(path))).toBe(false); + }); + it("maps the forks REST payload", async () => { const state = await loadWorker("token"); vi.mocked(fetch).mockResolvedValue( diff --git a/src/service-worker.ts b/src/service-worker.ts index f6dce4d..b21f382 100644 --- a/src/service-worker.ts +++ b/src/service-worker.ts @@ -10,6 +10,7 @@ import type { TagInfo, StargazerInfo, WatcherInfo, + SocialList, ForkInfo, ContributorInfo, } from "./lib/messages"; @@ -485,10 +486,44 @@ async function fetchRepoTags(owner: string, repo: string): Promise { }); } -async function fetchStargazers(owner: string, repo: string): Promise { +async function canAccessSocialLists(owner: string, repo: string, token: string): Promise { + if (!token) return false; + + const cacheKey = `cache:social-list-access:${owner}/${repo}`; + const result = await cachedFetch<{ allowed: boolean }>(cacheKey, async () => { + const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`, { + headers: restHeaders(token), + }); + if (!response.ok) return { allowed: false }; + + const data: { + permissions?: { admin?: boolean; maintain?: boolean; push?: boolean; triage?: boolean }; + } = await response.json(); + const permissions = data.permissions; + // `pull` is true for every public repository visitor, so only elevated + // permissions prove that the token belongs to an admin or collaborator. + return { + allowed: Boolean( + permissions?.admin || + permissions?.maintain || + permissions?.push || + permissions?.triage, + ), + }; + }); + return result.allowed; +} + +async function fetchStargazers( + owner: string, + repo: string, +): Promise> { + const token = await getToken(); + if (!(await canAccessSocialLists(owner, repo, token))) return { restricted: true }; + const cacheKey = `cache:stargazers:${owner}/${repo}`; - return cachedFetch(cacheKey, async () => { - const headers = restHeaders(await getToken(), "application/vnd.github.star+json"); + return cachedFetch>(cacheKey, async () => { + const headers = restHeaders(token, "application/vnd.github.star+json"); const url = `https://api.github.com/repos/${owner}/${repo}/stargazers?per_page=30`; const response = await fetch(url, { headers }); @@ -496,7 +531,7 @@ async function fetchStargazers(owner: string, repo: string): Promise { +async function fetchWatchers( + owner: string, + repo: string, +): Promise> { + const token = await getToken(); + if (!(await canAccessSocialLists(owner, repo, token))) return { restricted: true }; + const cacheKey = `cache:watchers:${owner}/${repo}`; - return cachedFetch(cacheKey, async () => { - const headers = restHeaders(await getToken()); + return cachedFetch>(cacheKey, async () => { + const headers = restHeaders(token); const url = `https://api.github.com/repos/${owner}/${repo}/subscribers?per_page=30`; const response = await fetch(url, { headers }); @@ -523,7 +564,7 @@ async function fetchWatchers(owner: string, repo: string): Promise = From 46a7d8c129a8695122e269bc08c27c79dc81f984 Mon Sep 17 00:00:00 2001 From: shawn Date: Thu, 23 Jul 2026 10:44:00 +0800 Subject: [PATCH 4/5] fix: keep restriction link inside popup body --- src/features/watch-fork-star-popup.test.ts | 12 ++++++++---- src/features/watch-fork-star-popup.ts | 15 ++++++--------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/features/watch-fork-star-popup.test.ts b/src/features/watch-fork-star-popup.test.ts index 88bd376..ff28a5a 100644 --- a/src/features/watch-fork-star-popup.test.ts +++ b/src/features/watch-fork-star-popup.test.ts @@ -98,14 +98,18 @@ describe("watch/fork/star popup", () => { document.getElementById("watch-counter")!.dispatchEvent(new Event("mouseenter")); await vi.advanceTimersByTimeAsync(300); - expect(document.querySelector(".bg-wfs-popup-empty")?.textContent).toBe("—"); - const link = [...document.querySelectorAll(".bg-wfs-popup-footer a")].find( - (candidate) => candidate.target === "_blank", - )!; + const empty = document.querySelector(".bg-wfs-popup-empty")!; + expect(empty.textContent).toContain("—"); + const link = empty.querySelector("a")!; expect(link.href).toBe( "https://github.blog/changelog/2026-06-30-upcoming-access-restrictions-to-public-api-endpoints-and-ui-views/", ); expect(link.rel).toBe("noopener noreferrer"); + expect( + [...document.querySelectorAll(".bg-wfs-popup-footer a")].some( + (candidate) => candidate.getAttribute("href") === "/owner/repo/watchers", + ), + ).toBe(true); vi.useRealTimers(); }); diff --git a/src/features/watch-fork-star-popup.ts b/src/features/watch-fork-star-popup.ts index 2edd33c..638b478 100644 --- a/src/features/watch-fork-star-popup.ts +++ b/src/features/watch-fork-star-popup.ts @@ -112,15 +112,12 @@ function renderSocialList( return; } - list.innerHTML = '
'; - const footerLink = list.parentElement?.querySelector( - ".bg-wfs-popup-footer a", - ); - if (!footerLink) return; - footerLink.href = ACCESS_RESTRICTIONS_URL; - footerLink.target = "_blank"; - footerLink.rel = "noopener noreferrer"; - footerLink.textContent = t("githubAccessRestrictions"); + list.innerHTML = `
`; } function renderForks(list: HTMLElement, items: ForkInfo[]): void { From fc751a06623d8bb0cff5327458b320e5008b1990 Mon Sep 17 00:00:00 2001 From: shawn Date: Thu, 23 Jul 2026 12:05:26 +0800 Subject: [PATCH 5/5] fix: remove restricted popup placeholder --- src/features/watch-fork-star-popup.test.ts | 1 - src/features/watch-fork-star-popup.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/features/watch-fork-star-popup.test.ts b/src/features/watch-fork-star-popup.test.ts index ff28a5a..02fc87b 100644 --- a/src/features/watch-fork-star-popup.test.ts +++ b/src/features/watch-fork-star-popup.test.ts @@ -99,7 +99,6 @@ describe("watch/fork/star popup", () => { await vi.advanceTimersByTimeAsync(300); const empty = document.querySelector(".bg-wfs-popup-empty")!; - expect(empty.textContent).toContain("—"); const link = empty.querySelector("a")!; expect(link.href).toBe( "https://github.blog/changelog/2026-06-30-upcoming-access-restrictions-to-public-api-endpoints-and-ui-views/", diff --git a/src/features/watch-fork-star-popup.ts b/src/features/watch-fork-star-popup.ts index 638b478..2f67050 100644 --- a/src/features/watch-fork-star-popup.ts +++ b/src/features/watch-fork-star-popup.ts @@ -113,7 +113,6 @@ function renderSocialList( } list.innerHTML = `