mirror of
https://github.com/mengxi-ream/read-frog.git
synced 2026-04-30 01:56:46 +00:00
Compare commits
1 commit
main
...
fix/issue-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63235c2d06 |
3 changed files with 106 additions and 4 deletions
5
.changeset/tall-poems-sparkle.md
Normal file
5
.changeset/tall-poems-sparkle.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@read-frog/extension": patch
|
||||
---
|
||||
|
||||
fix: keep AI-aware page translation cache title stable on same-page toggles
|
||||
91
src/utils/host/translate/__tests__/article-context.test.ts
Normal file
91
src/utils/host/translate/__tests__/article-context.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue