diff --git a/src/observers/link_prefetch_observer.js b/src/observers/link_prefetch_observer.js index 165569dbd..3877d6ee7 100644 --- a/src/observers/link_prefetch_observer.js +++ b/src/observers/link_prefetch_observer.js @@ -9,10 +9,14 @@ import { StreamMessage } from "../core/streams/stream_message" import { FetchMethod, FetchRequest } from "../http/fetch_request" import { prefetchCache, cacheTtl } from "../core/drive/prefetch_cache" +const observedEvents = { + "mouseenter": ["mouseleave"], + "touchstart": ["touchend", "touchcancel"] +} + export class LinkPrefetchObserver { started = false - hoverTriggerEvent = "mouseenter" - touchTriggerEvent = "touchstart" + #linkToPrefetch = null constructor(delegate, eventTarget) { this.delegate = delegate @@ -32,26 +36,34 @@ export class LinkPrefetchObserver { stop() { if (!this.started) return - this.eventTarget.removeEventListener(this.hoverTriggerEvent, this.#tryToPrefetchRequest, { - capture: true, - passive: true - }) - this.eventTarget.removeEventListener(this.touchTriggerEvent, this.#tryToPrefetchRequest, { - capture: true, - passive: true + Object.entries(observedEvents).forEach(([startEventName, stopEventNames]) => { + this.eventTarget.removeEventListener(startEventName, this.#tryToPrefetchRequest, { + capture: true, + passive: true + }) + stopEventNames.forEach((stopEventName) => { + this.eventTarget.removeEventListener(stopEventName, this.#tryToCancelPrefetchRequest, { + capture: true, + passive: true + }) + }) }) this.eventTarget.removeEventListener("turbo:before-fetch-request", this.#tryToUsePrefetchedRequest, true) this.started = false } #enable = () => { - this.eventTarget.addEventListener(this.hoverTriggerEvent, this.#tryToPrefetchRequest, { - capture: true, - passive: true - }) - this.eventTarget.addEventListener(this.touchTriggerEvent, this.#tryToPrefetchRequest, { - capture: true, - passive: true + Object.entries(observedEvents).forEach(([startEventName, stopEventNames]) => { + this.eventTarget.addEventListener(startEventName, this.#tryToPrefetchRequest, { + capture: true, + passive: true + }) + stopEventNames.forEach((stopEventName) => { + this.eventTarget.addEventListener(stopEventName, this.#tryToCancelPrefetchRequest, { + capture: true, + passive: true + }) + }) }) this.eventTarget.addEventListener("turbo:before-fetch-request", this.#tryToUsePrefetchedRequest, true) this.started = true @@ -68,6 +80,7 @@ export class LinkPrefetchObserver { const location = getLocationForLink(link) if (this.delegate.canPrefetchRequestToLocation(link, location)) { + this.#linkToPrefetch = link const fetchRequest = new FetchRequest( this, FetchMethod.get, @@ -77,12 +90,18 @@ export class LinkPrefetchObserver { ) prefetchCache.setLater(location.toString(), fetchRequest, this.#cacheTtl) - - link.addEventListener("mouseleave", () => prefetchCache.clear(), { once: true }) } } } + #tryToCancelPrefetchRequest = (event) => { + if (event.target === this.#linkToPrefetch) { + prefetchCache.clear() + } + + this.#linkToPrefetch = null + } + #tryToUsePrefetchedRequest = (event) => { if (event.target.tagName !== "FORM" && event.detail.fetchOptions.method === "get") { const cached = prefetchCache.get(event.detail.url.toString()) diff --git a/src/tests/functional/link_prefetch_observer_tests.js b/src/tests/functional/link_prefetch_observer_tests.js index 584d673ef..281f51083 100644 --- a/src/tests/functional/link_prefetch_observer_tests.js +++ b/src/tests/functional/link_prefetch_observer_tests.js @@ -226,7 +226,15 @@ test("it resets the cache when a link is hovered", async ({ page }) => { test("it prefetches page on touchstart", async ({ page }) => { await goTo({ page, path: "/hover_to_prefetch.html" }) - await assertPrefetchedOnTouchstart({ page, selector: "#anchor_for_prefetch" }) + + let requestMade = false + const link = page.locator("#anchor_for_prefetch") + page.on("request", (request) => (requestMade = true)) + + await link.tap() + await sleep(100) + + assertRequestMade(requestMade) }) test("it does not make a network request when clicking on a link that has been prefetched", async ({ page }) => { @@ -248,26 +256,6 @@ test("it follows the link using the cached response when clicking on a link that assert.equal(await page.title(), "Prefetched Page") }) -const assertPrefetchedOnTouchstart = async ({ page, selector, callback }) => { - let requestMade = false - - page.on("request", (request) => { - callback && callback(request) - requestMade = true - }) - - const selectorXY = await page.$eval(selector, (el) => { - const { x, y } = el.getBoundingClientRect() - return { x, y } - }) - - await page.touchscreen.tap(selectorXY.x, selectorXY.y) - - await sleep(100) - - assertRequestMade(requestMade) -} - const assertPrefetchedOnHover = async ({ page, selector, callback }) => { let requestMade = false