Compare commits

...

1 commit

Author SHA1 Message Date
GuaGua
63235c2d06 fix: stabilize AI-aware page translation cache title 2026-04-04 04:37:46 -07:00
3 changed files with 106 additions and 4 deletions

View file

@ -0,0 +1,5 @@
---
"@read-frog/extension": patch
---
fix: keep AI-aware page translation cache title stable on same-page toggles

View file

@ -0,0 +1,91 @@
// @vitest-environment jsdom
import { beforeEach, describe, expect, it, vi } from "vitest"
const mockParse = vi.fn()
const mockRemoveDummyNodes = vi.fn()
const mockWarn = vi.fn()
vi.mock("@mozilla/readability", () => ({
Readability: vi.fn().mockImplementation(() => ({
parse: mockParse,
})),
}))
vi.mock("@/utils/content/utils", () => ({
removeDummyNodes: mockRemoveDummyNodes,
}))
vi.mock("@/utils/logger", () => ({
logger: {
warn: mockWarn,
},
}))
async function loadModule() {
vi.resetModules()
return await import("../article-context")
}
describe("getOrFetchArticleData", () => {
beforeEach(() => {
mockParse.mockReset()
mockRemoveDummyNodes.mockReset()
mockWarn.mockReset()
mockParse.mockReturnValue({ textContent: "Readable article body" })
mockRemoveDummyNodes.mockResolvedValue(undefined)
document.title = "Original Title"
document.body.innerHTML = "<main>Article body</main>"
window.history.replaceState({}, "", "/article")
})
it("keeps the original title stable for AI-aware context on the same URL", async () => {
const { getOrFetchArticleData } = await loadModule()
const first = await getOrFetchArticleData(true)
document.title = "Translated Browser Title"
const second = await getOrFetchArticleData(true)
expect(first?.title).toBe("Original Title")
expect(first?.textContent).toBeTruthy()
expect(second).toEqual({
title: "Original Title",
textContent: first?.textContent,
})
})
it("still returns the live title when AI content aware is disabled", async () => {
const { getOrFetchArticleData } = await loadModule()
const first = await getOrFetchArticleData(false)
document.title = "Updated Live Title"
const second = await getOrFetchArticleData(false)
expect(first).toEqual({ title: "Original Title" })
expect(second).toEqual({ title: "Updated Live Title" })
expect(mockParse).not.toHaveBeenCalled()
expect(mockRemoveDummyNodes).not.toHaveBeenCalled()
})
it("refreshes the cached title and text content after the URL changes", async () => {
const { getOrFetchArticleData } = await loadModule()
const first = await getOrFetchArticleData(true)
document.title = "Next Article Title"
document.body.innerHTML = "<main>Next article body</main>"
mockParse.mockReturnValueOnce({ textContent: "Next readable article body" })
window.history.replaceState({}, "", "/article-2")
const second = await getOrFetchArticleData(true)
expect(first?.title).toBe("Original Title")
expect(first?.textContent).toBeTruthy()
expect(second?.title).toBe("Next Article Title")
expect(second?.textContent).toBeTruthy()
expect(second?.textContent).not.toBe(first?.textContent)
})
})

View file

@ -2,7 +2,7 @@ import { Readability } from "@mozilla/readability"
import { removeDummyNodes } from "@/utils/content/utils"
import { logger } from "@/utils/logger"
let cachedTextContent: { url: string, textContent: string } | null = null
let cachedArticleData: { url: string, title: string, textContent: string } | null = null
async function fetchPageTextContent(): Promise<string> {
try {
@ -29,12 +29,18 @@ export async function getOrFetchArticleData(
return { title }
const currentUrl = window.location.href
if (cachedTextContent?.url === currentUrl) {
return { title, textContent: cachedTextContent.textContent }
if (cachedArticleData?.url === currentUrl) {
// Keep article context stable for the lifetime of a page URL. During page translation,
// document.title can be mutated to the translated title, and re-reading that live value
// would drift the prompt/context and the AI-aware cache key for the same page.
return {
title: cachedArticleData.title,
textContent: cachedArticleData.textContent,
}
}
const textContent = await fetchPageTextContent()
cachedTextContent = { url: currentUrl, textContent }
cachedArticleData = { url: currentUrl, title, textContent }
return { title, textContent }
}