From ee9dfa0d8e423c168dee3f4f95df00380e109932 Mon Sep 17 00:00:00 2001 From: Shubham Padkonde Date: Tue, 27 Oct 2026 17:36:05 +0530 Subject: [PATCH] fix(website): open entry anchors after restoring filters --- website/static/main.js | 10 ++--- website/tests/test_entry_links.py | 63 +++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 5 deletions(-) create mode 100644 website/tests/test_entry_links.py diff --git a/website/static/main.js b/website/static/main.js index 5944dd9882..79151054eb 100644 --- a/website/static/main.js +++ b/website/static/main.js @@ -361,11 +361,6 @@ document.addEventListener("click", function (e) { gtag("event", "sponsor_contact", { tier: link.dataset.tier }); }); -if (location.hash) { - const linkedRow = document.getElementById(decodeURIComponent(location.hash.slice(1))); - if (linkedRow && linkedRow.matches("tr.row")) openEntryRow(linkedRow); -} - if (searchInput) { let searchTimer; searchInput.addEventListener("input", function () { @@ -451,4 +446,9 @@ if (backToTop) { sortRows(); } updateSortIndicators(); + // Filtering collapses rows, so open a linked entry after restoring URL state. + if (location.hash) { + const linkedRow = document.getElementById(decodeURIComponent(location.hash.slice(1))); + if (linkedRow && linkedRow.matches("tr.row")) openEntryRow(linkedRow); + } })(); diff --git a/website/tests/test_entry_links.py b/website/tests/test_entry_links.py new file mode 100644 index 0000000000..e7281dd521 --- /dev/null +++ b/website/tests/test_entry_links.py @@ -0,0 +1,63 @@ +import json +import shutil +import subprocess +from pathlib import Path + +import pytest + +NODE = shutil.which("node") +SCRIPT = Path(__file__).resolve().parents[1] / "static/main.js" +RUNNER = r""" +const fs = require("node:fs"); +const vm = require("node:vm"); +const { script, query } = JSON.parse(fs.readFileSync(0, "utf8")); +const classes = new Set(); +const attributes = {"aria-expanded": "false"}; +const search = {value: "", addEventListener() {}}; +const row = { + hidden: false, textContent: "httpx HTTP client", cells: [{textContent: "1"}], + nextElementSibling: null, + classList: { + contains: name => name === "row" || classes.has(name), + toggle(name, on) { if (on) classes.add(name); else classes.delete(name); } + }, + matches: selector => selector === "tr.row", + setAttribute: (name, value) => attributes[name] = value, + querySelector: () => ({textContent: "httpx"}) +}; +const tbody = { + rows: [row], addEventListener() {}, appendChild() {}, + querySelectorAll: selector => selector === "tr.row.open" ? (classes.has("open") ? [row] : []) : [row] +}; +const table = {dataset: {defaultSort: "editorial"}, classList: {toggle() {}}}; +const document = { + querySelector: selector => ({".search": search, ".table": table, ".table tbody": tbody})[selector] || null, + querySelectorAll: selector => selector === ".table tbody tr.row" ? [row] : [], + getElementById: id => id === "entry-httpx" ? row : null, + createDocumentFragment: () => ({appendChild() {}}), + addEventListener() {} +}; +const location = new URL("https://awesome-python.com/categories/http-clients/" + query + "#entry-httpx"); +vm.runInNewContext(script, { + document, location, + window: {location, matchMedia: () => ({matches: false})}, URLSearchParams +}); +process.stdout.write(JSON.stringify({open: attributes["aria-expanded"], hidden: row.hidden, search: search.value})); +""" + + +@pytest.mark.parametrize( + ("query", "search"), + [("", ""), ("?sort=name&order=asc", ""), ("?q=httpx", "httpx"), ("?q=requests", "")], +) +def test_entry_anchor_opens_after_initial_search_and_sort(query, search): + if NODE is None: + pytest.skip("Entry link script tests require Node.js") + result = subprocess.run( + [NODE, "-e", RUNNER], + input=json.dumps({"script": SCRIPT.read_text(encoding="utf-8"), "query": query}), + capture_output=True, + text=True, + check=True, + ) + assert json.loads(result.stdout) == {"open": "true", "hidden": False, "search": search}