From 92854f4d6b24d98e41c29be105d99f00f17fa188 Mon Sep 17 00:00:00 2001 From: KoloMl Date: Fri, 28 Feb 2025 02:40:19 +0400 Subject: [PATCH 1/3] Renamed hooks to TS --- src/{hooks.js => hooks.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{hooks.js => hooks.ts} (100%) diff --git a/src/hooks.js b/src/hooks.ts similarity index 100% rename from src/hooks.js rename to src/hooks.ts From f687389516187628629831cf55bddd94a3ab2bf2 Mon Sep 17 00:00:00 2001 From: KoloMl Date: Fri, 28 Feb 2025 03:18:18 +0400 Subject: [PATCH 2/3] Implemented routing to be more compatible for extension popup --- src/hooks.ts | 11 ++++++--- src/lib/popup-links.ts | 48 +++++++++++++++++++++++++++++++++++++++ src/routes/+layout.svelte | 8 +++++++ 3 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 src/lib/popup-links.ts diff --git a/src/hooks.ts b/src/hooks.ts index 3ca7fe9..6555677 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -1,8 +1,13 @@ -/** @type {import('@sveltejs/kit').Reroute} */ -export function reroute({url}) { +import type { Reroute } from "@sveltejs/kit"; + +export const reroute: Reroute = ({url}) => { // Reroute index.html as just / for the root. // Browser extension starts from with the index.html file in the pathname which is not correct for the router. if (url.pathname === '/index.html') { + if (url.searchParams.has('path')) { + return url.searchParams.get('path')!; + } + return "/"; } -} \ No newline at end of file +}; diff --git a/src/lib/popup-links.ts b/src/lib/popup-links.ts new file mode 100644 index 0000000..5287696 --- /dev/null +++ b/src/lib/popup-links.ts @@ -0,0 +1,48 @@ +function resolveReplaceableLink(target: EventTarget | null = null): HTMLAnchorElement | null { + if (!(target instanceof HTMLElement)) { + return null; + } + + const closestLink = target.closest('a'); + + if ( + closestLink instanceof HTMLAnchorElement + && !closestLink.search + && closestLink.origin === location.origin + ) { + return closestLink; + } + + return null; +} + +function replaceLink(linkElement: HTMLAnchorElement) { + const params = new URLSearchParams([ + ['path', linkElement.pathname] + ]); + + linkElement.search = params.toString(); + linkElement.pathname = "/index.html"; +} + +export function initializeLinksReplacement(): () => void { + const abortController = new AbortController(); + const replacementHandler = (event: Event) => { + const closestLink = resolveReplaceableLink(event.target); + + if (closestLink) { + replaceLink(closestLink); + } + } + + // Dynamically replace the links from the Svelte default links to the links usable for the popup. + document.body.addEventListener('mousedown', replacementHandler, { + signal: abortController.signal, + }); + + document.body.addEventListener('click', replacementHandler, { + signal: abortController.signal, + }) + + return () => abortController.abort(); +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 9d1f851..a7b87e2 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -2,6 +2,8 @@ import "../styles/popup.scss"; import Header from "$components/layout/Header.svelte"; import Footer from "$components/layout/Footer.svelte"; + import { initializeLinksReplacement } from "$lib/popup-links"; + import { onDestroy } from "svelte"; interface Props { children?: import('svelte').Snippet; @@ -12,6 +14,12 @@ // Sort of a hack, detect if we rendered in the browser tab or in the popup. // Popup will always should have fixed 320px size, otherwise we consider it opened in the tab. document.body.classList.toggle('is-in-tab', window.innerWidth > 320); + + const disconnectLinkReplacement = initializeLinksReplacement(); + + onDestroy(() => { + disconnectLinkReplacement(); + })
From a2d884c9692de71bd316da50b3e59db4d6f6aaf7 Mon Sep 17 00:00:00 2001 From: KoloMl Date: Fri, 28 Feb 2025 03:50:08 +0400 Subject: [PATCH 3/3] Added tests for the link replacement logic --- tests/lib/popup-links.spec.ts | 75 +++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 tests/lib/popup-links.spec.ts diff --git a/tests/lib/popup-links.spec.ts b/tests/lib/popup-links.spec.ts new file mode 100644 index 0000000..5a6d772 --- /dev/null +++ b/tests/lib/popup-links.spec.ts @@ -0,0 +1,75 @@ +import { randomString } from "$tests/utils"; +import { initializeLinksReplacement } from "$lib/popup-links"; + +describe('popup-links', () => { + let expectedPath = ''; + let testLink: HTMLAnchorElement = document.createElement('a'); + let disconnectCallback: (() => void) | null = null; + + function fireEventAt(target: EventTarget, eventName: string) { + target.dispatchEvent(new Event(eventName, {bubbles: true})); + } + + beforeEach(() => { + expectedPath = `/test/${randomString()}`; + testLink.href = expectedPath; + document.body.append(testLink); + }); + + afterEach(() => { + if (disconnectCallback) { + disconnectCallback(); + disconnectCallback = null; + } + }); + + it('should replace link on any mouse button down', () => { + disconnectCallback = initializeLinksReplacement(); + fireEventAt(testLink, "mousedown"); + + const resultUrl = new URL(testLink.href); + + expect(resultUrl.searchParams.get('path')).toBe(expectedPath); + }); + + it('should replace link when link is pressed by keyboard or clicked', () => { + disconnectCallback = initializeLinksReplacement(); + fireEventAt(testLink, "click"); + + const resultUrl = new URL(testLink.href); + + expect(resultUrl.searchParams.get('path')).toBe(expectedPath); + }); + + it('should not replace already replaced links', () => { + disconnectCallback = initializeLinksReplacement(); + fireEventAt(testLink, "click"); + const hrefAfterFirstClick = testLink.href; + + fireEventAt(testLink, "click"); + const hrefAfterSecondClick = testLink.href; + + expect(hrefAfterFirstClick).toBe(hrefAfterSecondClick); + }); + + it('should stop replacing links once disconnect is called', () => { + const hrefBefore = testLink.href; + + disconnectCallback = initializeLinksReplacement(); + disconnectCallback(); + fireEventAt(testLink, "mousedown"); + fireEventAt(testLink, "click"); + + expect(hrefBefore).toBe(testLink.href); + }); + + it('should not touch links with different origin', () => { + testLink.href = "https://external.example.com/" + randomString() + "/"; + + const hrefBefore = testLink.href; + disconnectCallback = initializeLinksReplacement(); + fireEventAt(testLink, "click"); + + expect(testLink.href).toBe(hrefBefore); + }); +});