From b56ed632c5d87b3037de3752bfe7432e002ad192 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Fri, 18 Sep 2026 11:17:40 +0000
Subject: [PATCH 1/3] fix: Correct 'Aufschlag' badge rendering logic and
reference price extraction
This commit addresses an issue in the Toppreise suite where a competing hidden reference price (e.g., CHF 47.82) was incorrectly parsed, leading to an 'Aufschlag' (markup) warning being displayed on cards where the current visible price (e.g., CHF 37.95) was actually an all-time low.
- Refactored `extractCardData` to utilize a new `extractCanonicalPrice` helper, ensuring the canonical displayed price is reliably extracted and competing reference prices are ignored.
- Replaced fuzzy 1% (`* 1.01` and `* 0.99`) floating-point comparisons with exact integer-cent comparison (`priceToCents`) across the codebase.
- Re-architected badge rendering logic to rely on four explicit states (`new-low`, `at-low`, `above-low`, and `unknown`), ensuring "Aufschlag" is rendered strictly for the `above-low` state.
- Added synchronous validation inside the `badgeDifEl` click event to prevent stale history from applying to a dynamically swapped card price.
- Updated `mock_toppreise.html` with a regression fixture containing a conflicting reference price and wrote matching test assertions.
- Fixed an overly aggressive `force=True` click interaction in Playwright tests (`test_deal_score_weight_preset_dropdown_in_filter_bar`) by using `node.click()` evaluation.
Co-authored-by: tazztone <62671577+tazztone@users.noreply.github.com>
---
patch_mock.py | 61 ++++++++++++++
patch_test.py | 8 ++
patch_test10.py | 10 +++
patch_test11.py | 15 ++++
patch_test12.py | 10 +++
patch_test2.py | 26 ++++++
patch_test3.py | 35 ++++++++
patch_test4.py | 25 ++++++
patch_test5.py | 22 +++++
patch_test6.py | 50 +++++++++++
patch_test7.py | 14 ++++
patch_test8.py | 9 ++
patch_test9.py | 10 +++
patch_toppreise.py | 38 +++++++++
patch_toppreise2.py | 52 ++++++++++++
patch_toppreise3.py | 66 +++++++++++++++
patch_toppreise4.py | 41 ++++++++++
.../toppreise/tests/mock_toppreise.html | 31 ++++++-
.../toppreise/tests/test_userscript.py | 69 ++++++++++++----
userscripts/toppreise/toppreise.user.js | 82 ++++++++++++++++---
20 files changed, 645 insertions(+), 29 deletions(-)
create mode 100644 patch_mock.py
create mode 100644 patch_test.py
create mode 100644 patch_test10.py
create mode 100644 patch_test11.py
create mode 100644 patch_test12.py
create mode 100644 patch_test2.py
create mode 100644 patch_test3.py
create mode 100644 patch_test4.py
create mode 100644 patch_test5.py
create mode 100644 patch_test6.py
create mode 100644 patch_test7.py
create mode 100644 patch_test8.py
create mode 100644 patch_test9.py
create mode 100644 patch_toppreise.py
create mode 100644 patch_toppreise2.py
create mode 100644 patch_toppreise3.py
create mode 100644 patch_toppreise4.py
diff --git a/patch_mock.py b/patch_mock.py
new file mode 100644
index 0000000..df07a5e
--- /dev/null
+++ b/patch_mock.py
@@ -0,0 +1,61 @@
+with open("userscripts/toppreise/tests/mock_toppreise.html", "r") as f:
+ content = f.read()
+
+# Add a test card that reproduces the user's specific case:
+# Visible price: CHF 37.95
+# Competing reference price: CHF 47.82
+# Historical low: CHF 37.95
+
+# Find where to insert the new card
+insert_point = ""
+
+new_card = """
+
+ +26%
+
-${rawDiscount}%
` +# Wait, the prompt plan asks for: +# "Four explicit states: new-low, at-low, above-low, and unknown." +# "Aufschlag rendering only for a verified above-low state." + +search_block = """ if (stats && cardPrice > 0 && stats.tiefstpreis > 0) { + const isAllTimeLow = priceToCents(cardPrice) <= priceToCents(stats.tiefstpreis); + const isNonBest = !isAllTimeLow; + const isNewRecord = !!(stats.isNewAllTimeLow || (isAllTimeLow && stats.previousLow && priceToCents(stats.previousLow) > priceToCents(cardPrice))); + const prevLow = stats.previousLow; + const realDropVsPrev = prevLow && prevLow > cardPrice ? Math.round(((prevLow - cardPrice) / prevLow) * 100) : (stats.realDiscountVsPrevLow || 0); + + if (CONFIG.FILTER_BESTPREIS_ENABLED !== false && isNonBest && CONFIG.REAL_DEAL_FILTER_ACTIVE) { + card.classList.add('tp-non-bestpreis-filtered'); + } else { + card.classList.remove('tp-non-bestpreis-filtered'); + } + + const hasSignificantPeak = stats.hoechstpreis && stats.hoechstpreis > stats.tiefstpreis * 1.02; + + if (isAllTimeLow) {""" + +replace_block = """ if (stats && cardPrice > 0 && stats.tiefstpreis > 0) { + // Explicit states + const cPrice = priceToCents(cardPrice); + const cTiefstpreis = priceToCents(stats.tiefstpreis); + + let state = 'unknown'; + if (cPrice < cTiefstpreis) { + state = 'new-low'; + } else if (cPrice === cTiefstpreis) { + state = 'at-low'; + } else { + state = 'above-low'; + } + + const isAllTimeLow = (state === 'new-low' || state === 'at-low'); + const isNonBest = (state === 'above-low'); + const isNewRecord = (state === 'new-low') || !!(stats.isNewAllTimeLow || (isAllTimeLow && stats.previousLow && priceToCents(stats.previousLow) > cPrice)); + const prevLow = stats.previousLow; + const realDropVsPrev = prevLow && prevLow > cardPrice ? Math.round(((prevLow - cardPrice) / prevLow) * 100) : (stats.realDiscountVsPrevLow || 0); + + if (CONFIG.FILTER_BESTPREIS_ENABLED !== false && isNonBest && CONFIG.REAL_DEAL_FILTER_ACTIVE) { + card.classList.add('tp-non-bestpreis-filtered'); + } else { + card.classList.remove('tp-non-bestpreis-filtered'); + } + + const hasSignificantPeak = stats.hoechstpreis && stats.hoechstpreis > stats.tiefstpreis * 1.02; + + if (isAllTimeLow) {""" + +content = content.replace(search_block, replace_block) + +with open("userscripts/toppreise/toppreise.user.js", "w") as f: + f.write(content) diff --git a/patch_toppreise4.py b/patch_toppreise4.py new file mode 100644 index 0000000..2eca23d --- /dev/null +++ b/patch_toppreise4.py @@ -0,0 +1,41 @@ +with open("userscripts/toppreise/toppreise.user.js", "r") as f: + content = f.read() + +# Since we already extract the canonical price and recalculate states continuously inside processListings -> renderCardEffects +# using the LATEST cardPrice and LATEST stats object on every DOM mutation or debounce timer, +# "ensuring the current card price hasn't changed from when the network request was fired" is naturally +# handled in the reactive loop because `extractCardData` grabs the fresh DOM price and `computeDealScore` / `isAllTimeLow` recalculate. +# However, to be extra robust, we should explicitly check inside the click handler to make sure `extractCanonicalPrice` didn't change while awaiting. +# Actually, the user's issue says: +# "A request-time/current-time price recheck to prevent stale asynchronous history responses from applying to a changed card." + +search_block = """ badgeDifEl.classList.add('tp-deal-loading'); + badgeDifEl.innerHTML = `⏳
`; + const fetchedStats = await fetchSingleProductPriceStats(currentPid, 1, true); + badgeDifEl.classList.remove('tp-deal-loading'); + if (fetchedStats) { + processListings(); + } else {""" + +replace_block = """ badgeDifEl.classList.add('tp-deal-loading'); + badgeDifEl.innerHTML = `⏳
`; + const requestTimePrice = extractCanonicalPrice(card).price; + const fetchedStats = await fetchSingleProductPriceStats(currentPid, 1, true); + + // Re-verify the card's price hasn't changed underneath us (e.g. dynamic sorting/reactivity) + const currentTimePrice = extractCanonicalPrice(card).price; + if (requestTimePrice !== currentTimePrice) { + // Price changed during fetch, fetch might be stale or product swapped + badgeDifEl.classList.remove('tp-deal-loading'); + return; + } + + badgeDifEl.classList.remove('tp-deal-loading'); + if (fetchedStats) { + processListings(); + } else {""" + +content = content.replace(search_block, replace_block) + +with open("userscripts/toppreise/toppreise.user.js", "w") as f: + f.write(content) diff --git a/userscripts/toppreise/tests/mock_toppreise.html b/userscripts/toppreise/tests/mock_toppreise.html index 2175410..1f32d28 100644 --- a/userscripts/toppreise/tests/mock_toppreise.html +++ b/userscripts/toppreise/tests/mock_toppreise.html @@ -133,7 +133,36 @@-67%
+26%
⏳
`; + const requestTimePrice = extractCanonicalPrice(card).price; const fetchedStats = await fetchSingleProductPriceStats(currentPid, 1, true); + + // Re-verify the card's price hasn't changed underneath us (e.g. dynamic sorting/reactivity) + const currentTimePrice = extractCanonicalPrice(card).price; + if (requestTimePrice !== currentTimePrice) { + // Price changed during fetch, fetch might be stale or product swapped + badgeDifEl.classList.remove('tp-deal-loading'); + return; + } + badgeDifEl.classList.remove('tp-deal-loading'); if (fetchedStats) { processListings(); @@ -2783,9 +2826,22 @@ const SHADOW_MODAL_STYLES = ` } if (stats && cardPrice > 0 && stats.tiefstpreis > 0) { - const isAllTimeLow = cardPrice <= stats.tiefstpreis * 1.01; - const isNonBest = !isAllTimeLow; - const isNewRecord = !!(stats.isNewAllTimeLow || (isAllTimeLow && stats.previousLow && stats.previousLow > cardPrice * 1.01)); + // Explicit states + const cPrice = priceToCents(cardPrice); + const cTiefstpreis = priceToCents(stats.tiefstpreis); + + let state = 'unknown'; + if (cPrice < cTiefstpreis) { + state = 'new-low'; + } else if (cPrice === cTiefstpreis) { + state = 'at-low'; + } else { + state = 'above-low'; + } + + const isAllTimeLow = (state === 'new-low' || state === 'at-low'); + const isNonBest = (state === 'above-low'); + const isNewRecord = (state === 'new-low') || !!(stats.isNewAllTimeLow || (isAllTimeLow && stats.previousLow && priceToCents(stats.previousLow) > cPrice)); const prevLow = stats.previousLow; const realDropVsPrev = prevLow && prevLow > cardPrice ? Math.round(((prevLow - cardPrice) / prevLow) * 100) : (stats.realDiscountVsPrevLow || 0); @@ -3261,7 +3317,7 @@ const SHADOW_MODAL_STYLES = ` headingEl.appendChild(badge); } - const isAllTimeLow = currentPrice <= stats.tiefstpreis * 1.01; + const isAllTimeLow = priceToCents(currentPrice) <= priceToCents(stats.tiefstpreis); const hasSignificantPeak = stats.hoechstpreis && stats.hoechstpreis > stats.tiefstpreis * 1.02; if (isAllTimeLow) { From 78f8580f74614a811ac2ea9c0ed45a3367169aa4 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 18 Sep 2026 11:44:18 +0000 Subject: [PATCH 2/3] fix: correctly render badge states by ignoring hidden reference prices and using exact cent math This commit addresses an issue in the Toppreise suite where a competing hidden reference price (e.g., CHF 47.82) was incorrectly parsed, leading to an 'Aufschlag' (markup) warning being displayed on cards where the current visible price (e.g., CHF 37.95) was actually an all-time low. - Refactored `extractCardData` to utilize a new `extractCanonicalPrice` helper, ensuring the canonical displayed price is reliably extracted and competing reference prices are ignored. The fallback fails cleanly to avoid catching hidden `.Plugin_Price` elements. - Replaced fuzzy 1% floating-point comparisons with exact integer-cent comparison (`priceToCents`) for explicit badge rendering states (`new-low`, `at-low`, `above-low`), while explicitly keeping 1% bounds in historical clustering to preserve intended threshold behavior. - Added synchronous validation inside the `badgeDifEl` click event to prevent stale history from applying to a dynamically swapped card price using `priceToCents() !== priceToCents()` logic. - Cleaned up the regression fixture `mock_toppreise.html` mimicking the competing reference price, fixing route mocking for `page.route` to handle both GET URLs and POST variants cleanly. - Replaced aggressive `force=True` and raw node `.click()` interactions in Playwright tests with proper overlay waits to obey Playwright's actionability checks naturally. - Deleted obsolete one-off `patch_*.py` development mutation scripts. Co-authored-by: tazztone <62671577+tazztone@users.noreply.github.com> --- patch_mock.py | 61 ----------------- patch_test.py | 8 --- patch_test10.py | 10 --- patch_test11.py | 15 ----- patch_test12.py | 10 --- patch_test2.py | 26 -------- patch_test3.py | 35 ---------- patch_test4.py | 25 ------- patch_test5.py | 22 ------- patch_test6.py | 50 -------------- patch_test7.py | 14 ---- patch_test8.py | 9 --- patch_test9.py | 10 --- patch_toppreise.py | 38 ----------- patch_toppreise2.py | 52 --------------- patch_toppreise3.py | 66 ------------------- patch_toppreise4.py | 41 ------------ userscripts/toppreise/README.md | 2 +- .../toppreise/tests/test_userscript.py | 29 +++++--- userscripts/toppreise/toppreise.user.js | 18 ++--- 20 files changed, 31 insertions(+), 510 deletions(-) delete mode 100644 patch_mock.py delete mode 100644 patch_test.py delete mode 100644 patch_test10.py delete mode 100644 patch_test11.py delete mode 100644 patch_test12.py delete mode 100644 patch_test2.py delete mode 100644 patch_test3.py delete mode 100644 patch_test4.py delete mode 100644 patch_test5.py delete mode 100644 patch_test6.py delete mode 100644 patch_test7.py delete mode 100644 patch_test8.py delete mode 100644 patch_test9.py delete mode 100644 patch_toppreise.py delete mode 100644 patch_toppreise2.py delete mode 100644 patch_toppreise3.py delete mode 100644 patch_toppreise4.py diff --git a/patch_mock.py b/patch_mock.py deleted file mode 100644 index df07a5e..0000000 --- a/patch_mock.py +++ /dev/null @@ -1,61 +0,0 @@ -with open("userscripts/toppreise/tests/mock_toppreise.html", "r") as f: - content = f.read() - -# Add a test card that reproduces the user's specific case: -# Visible price: CHF 37.95 -# Competing reference price: CHF 47.82 -# Historical low: CHF 37.95 - -# Find where to insert the new card -insert_point = "" - -new_card = """ - -+26%
-${rawDiscount}%
` -# Wait, the prompt plan asks for: -# "Four explicit states: new-low, at-low, above-low, and unknown." -# "Aufschlag rendering only for a verified above-low state." - -search_block = """ if (stats && cardPrice > 0 && stats.tiefstpreis > 0) { - const isAllTimeLow = priceToCents(cardPrice) <= priceToCents(stats.tiefstpreis); - const isNonBest = !isAllTimeLow; - const isNewRecord = !!(stats.isNewAllTimeLow || (isAllTimeLow && stats.previousLow && priceToCents(stats.previousLow) > priceToCents(cardPrice))); - const prevLow = stats.previousLow; - const realDropVsPrev = prevLow && prevLow > cardPrice ? Math.round(((prevLow - cardPrice) / prevLow) * 100) : (stats.realDiscountVsPrevLow || 0); - - if (CONFIG.FILTER_BESTPREIS_ENABLED !== false && isNonBest && CONFIG.REAL_DEAL_FILTER_ACTIVE) { - card.classList.add('tp-non-bestpreis-filtered'); - } else { - card.classList.remove('tp-non-bestpreis-filtered'); - } - - const hasSignificantPeak = stats.hoechstpreis && stats.hoechstpreis > stats.tiefstpreis * 1.02; - - if (isAllTimeLow) {""" - -replace_block = """ if (stats && cardPrice > 0 && stats.tiefstpreis > 0) { - // Explicit states - const cPrice = priceToCents(cardPrice); - const cTiefstpreis = priceToCents(stats.tiefstpreis); - - let state = 'unknown'; - if (cPrice < cTiefstpreis) { - state = 'new-low'; - } else if (cPrice === cTiefstpreis) { - state = 'at-low'; - } else { - state = 'above-low'; - } - - const isAllTimeLow = (state === 'new-low' || state === 'at-low'); - const isNonBest = (state === 'above-low'); - const isNewRecord = (state === 'new-low') || !!(stats.isNewAllTimeLow || (isAllTimeLow && stats.previousLow && priceToCents(stats.previousLow) > cPrice)); - const prevLow = stats.previousLow; - const realDropVsPrev = prevLow && prevLow > cardPrice ? Math.round(((prevLow - cardPrice) / prevLow) * 100) : (stats.realDiscountVsPrevLow || 0); - - if (CONFIG.FILTER_BESTPREIS_ENABLED !== false && isNonBest && CONFIG.REAL_DEAL_FILTER_ACTIVE) { - card.classList.add('tp-non-bestpreis-filtered'); - } else { - card.classList.remove('tp-non-bestpreis-filtered'); - } - - const hasSignificantPeak = stats.hoechstpreis && stats.hoechstpreis > stats.tiefstpreis * 1.02; - - if (isAllTimeLow) {""" - -content = content.replace(search_block, replace_block) - -with open("userscripts/toppreise/toppreise.user.js", "w") as f: - f.write(content) diff --git a/patch_toppreise4.py b/patch_toppreise4.py deleted file mode 100644 index 2eca23d..0000000 --- a/patch_toppreise4.py +++ /dev/null @@ -1,41 +0,0 @@ -with open("userscripts/toppreise/toppreise.user.js", "r") as f: - content = f.read() - -# Since we already extract the canonical price and recalculate states continuously inside processListings -> renderCardEffects -# using the LATEST cardPrice and LATEST stats object on every DOM mutation or debounce timer, -# "ensuring the current card price hasn't changed from when the network request was fired" is naturally -# handled in the reactive loop because `extractCardData` grabs the fresh DOM price and `computeDealScore` / `isAllTimeLow` recalculate. -# However, to be extra robust, we should explicitly check inside the click handler to make sure `extractCanonicalPrice` didn't change while awaiting. -# Actually, the user's issue says: -# "A request-time/current-time price recheck to prevent stale asynchronous history responses from applying to a changed card." - -search_block = """ badgeDifEl.classList.add('tp-deal-loading'); - badgeDifEl.innerHTML = `⏳
`; - const fetchedStats = await fetchSingleProductPriceStats(currentPid, 1, true); - badgeDifEl.classList.remove('tp-deal-loading'); - if (fetchedStats) { - processListings(); - } else {""" - -replace_block = """ badgeDifEl.classList.add('tp-deal-loading'); - badgeDifEl.innerHTML = `⏳
`; - const requestTimePrice = extractCanonicalPrice(card).price; - const fetchedStats = await fetchSingleProductPriceStats(currentPid, 1, true); - - // Re-verify the card's price hasn't changed underneath us (e.g. dynamic sorting/reactivity) - const currentTimePrice = extractCanonicalPrice(card).price; - if (requestTimePrice !== currentTimePrice) { - // Price changed during fetch, fetch might be stale or product swapped - badgeDifEl.classList.remove('tp-deal-loading'); - return; - } - - badgeDifEl.classList.remove('tp-deal-loading'); - if (fetchedStats) { - processListings(); - } else {""" - -content = content.replace(search_block, replace_block) - -with open("userscripts/toppreise/toppreise.user.js", "w") as f: - f.write(content) diff --git a/userscripts/toppreise/README.md b/userscripts/toppreise/README.md index b4c98a1..fd22d14 100644 --- a/userscripts/toppreise/README.md +++ b/userscripts/toppreise/README.md @@ -10,7 +10,7 @@ Requires Violentmonkey (or a compatible userscript manager): - [Firefox](https://addons.mozilla.org/en-US/firefox/addon/violentmonkey/) - [Chrome / Brave](https://chromewebstore.google.com/detail/violentmonkey/jinjaccalgkegednnccohejagnlnfdag) -### 👉 [**CLICK HERE TO INSTALL USERSCRIPT (v2.18.18)**](https://raw.githubusercontent.com/tazztone/scripts/main/userscripts/toppreise/toppreise.user.js) +### 👉 [**CLICK HERE TO INSTALL USERSCRIPT (v2.18.19)**](https://raw.githubusercontent.com/tazztone/scripts/main/userscripts/toppreise/toppreise.user.js) --- diff --git a/userscripts/toppreise/tests/test_userscript.py b/userscripts/toppreise/tests/test_userscript.py index c63209e..528ff03 100644 --- a/userscripts/toppreise/tests/test_userscript.py +++ b/userscripts/toppreise/tests/test_userscript.py @@ -43,12 +43,19 @@ def test_competing_reference_price_resolves_to_green_low(page: Page): assert badge.is_visible() # Mock the time series endpoint for it - page.route("**/plugins/product/pricechart", lambda route: route.fulfill( - status=200, - headers={'access-control-allow-origin': '*'}, - content_type='application/json', - body='[[[100000, 47.82], [200000, 37.95]]]' - ) if '1003795' in route.request.post_data else route.continue_()) + def handle_pricechart(route): + # Fallback to post_data only if url does not contain it but we know how the mock is set up for fetch + if '1003795' in (route.request.post_data or '') or 'p_pc_pid=1003795' in route.request.url: + route.fulfill( + status=200, + headers={'access-control-allow-origin': '*'}, + content_type='application/json', + body='[[[100000, 47.82], [200000, 37.95]]]' + ) + else: + route.continue_() + + page.route("**/plugins/product/pricechart*", handle_pricechart) # Click to verify badge.click() @@ -60,7 +67,9 @@ def test_competing_reference_price_resolves_to_green_low(page: Page): assert "tp-deal-not-low" not in badge.get_attribute("class") # Title should indicate Allzeit-Tiefstpreis - assert 'Allzeit-Tiefstpreis (CHF 37.95)' in badge.get_attribute('title') + title = badge.get_attribute("title") or "" + assert "Allzeit-Tiefstpreis" in title + assert "CHF 37.95" in title def test_best_price_highlighting_and_dimming(page: Page): @@ -2452,8 +2461,10 @@ def test_deal_score_weight_preset_dropdown_in_filter_bar(page: Page): page.evaluate("document.querySelector('#tp-bar-weight-btn').click()") popover.wait_for(state="visible") - # Click 100% Median without force=True by evaluating a direct click since it might be obscured or Playwright has trouble with the layout - page.locator('#tp-weight-popover button[data-weight="0.00"]').evaluate("node => node.click()") + # Click 100% Median without force=True + btn = page.locator('#tp-weight-popover button[data-weight="0.00"]') + btn.wait_for(state="visible") + btn.click() assert page.evaluate("() => window.ToppreiseSuite.CONFIG.BESTPREISE_WEIGHT_RECORD === 0.0") assert '100% Med' in page.locator('#tp-bar-weight-btn').inner_text() diff --git a/userscripts/toppreise/toppreise.user.js b/userscripts/toppreise/toppreise.user.js index 8104f2f..30b5812 100644 --- a/userscripts/toppreise/toppreise.user.js +++ b/userscripts/toppreise/toppreise.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Toppreise.ch Suite: Power Filter & Price Alarm Auto-Filler // @namespace https://github.com/tazztone/scripts -// @version 2.18.18 +// @version 2.18.19 // @description All-in-one suite for Toppreise.ch: Highlights best prices, discount heatmap, excludes negative keywords, filters categories, sorts/filters by offer count/discount, checks real all-time Tiefstpreise, and automates price alarms. // @author tazztone // @match https://www.toppreise.ch/* @@ -1514,7 +1514,7 @@ const SHADOW_MODAL_STYLES = ` // Calculate previous low before current price drop: // Look backwards from recent points while price is within 1% of current low let idx = prices.length - 1; - while (idx > 0 && priceToCents(prices[idx]) <= priceToCents(curr)) { + while (idx > 0 && prices[idx] <= curr * 1.01) { idx--; } const historicalPrices = prices.slice(0, idx + 1); @@ -2430,11 +2430,11 @@ const SHADOW_MODAL_STYLES = ` : (mainPriceInfo.querySelector('.productPrice .Plugin_Price') || mainPriceInfo.querySelector('.shippingPrice .Plugin_Price')); } - // Fallbacks + // Fallbacks: Explicitly restrict to price containers to avoid catching rogue reference prices. if (!priceEl) { priceEl = CONFIG.USE_SHIPPING_PRICE - ? (card.querySelector('.priceContainer.shippingPrice .Plugin_Price') || card.querySelector('.priceContainer.productPrice .Plugin_Price') || card.querySelector('.Plugin_Price')) - : (card.querySelector('.priceContainer.productPrice .Plugin_Price') || card.querySelector('.priceContainer.shippingPrice .Plugin_Price') || card.querySelector('.Plugin_Price')); + ? (card.querySelector('.priceContainer.shippingPrice .Plugin_Price') || card.querySelector('.priceContainer.productPrice .Plugin_Price')) + : (card.querySelector('.priceContainer.productPrice .Plugin_Price') || card.querySelector('.priceContainer.shippingPrice .Plugin_Price')); } return { @@ -2701,9 +2701,11 @@ const SHADOW_MODAL_STYLES = ` // Re-verify the card's price hasn't changed underneath us (e.g. dynamic sorting/reactivity) const currentTimePrice = extractCanonicalPrice(card).price; - if (requestTimePrice !== currentTimePrice) { - // Price changed during fetch, fetch might be stale or product swapped + + if (!requestTimePrice || !currentTimePrice || priceToCents(requestTimePrice) !== priceToCents(currentTimePrice)) { + // Price changed or is missing during fetch, fetch might be stale or product swapped badgeDifEl.classList.remove('tp-deal-loading'); + processListings(); return; } @@ -3891,7 +3893,7 @@ const SHADOW_MODAL_STYLES = ` exportBtn?.addEventListener('click', () => { const exportData = { _meta: { - version: (typeof GM_info !== 'undefined' && GM_info?.script?.version) || '2.18.18', + version: (typeof GM_info !== 'undefined' && GM_info?.script?.version) || '2.18.19', exported: new Date().toISOString() }, config: { ...CONFIG } From 46b6e30a651f1b63ac63a2084e6c4be0074d2b04 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 18 Sep 2026 11:58:43 +0000 Subject: [PATCH 3/3] fix: correctly render badge states by ignoring hidden reference prices and using exact cent math This commit addresses an issue in the Toppreise suite where a competing hidden reference price (e.g., CHF 47.82) was incorrectly parsed, leading to an 'Aufschlag' (markup) warning being displayed on cards where the current visible price (e.g., CHF 37.95) was actually an all-time low. - Refactored `extractCardData` to utilize a new `extractCanonicalPrice` helper, ensuring the canonical displayed price is reliably extracted and competing reference prices are ignored. The fallback fails cleanly to avoid catching hidden `.Plugin_Price` elements. - Replaced fuzzy 1% floating-point comparisons with exact integer-cent comparison (`priceToCents`) for explicit badge rendering states (`new-low`, `at-low`, `above-low`), while explicitly keeping 1% bounds in historical clustering to preserve intended threshold behavior. - Added synchronous validation inside the `badgeDifEl` click event to prevent stale history from applying to a dynamically swapped card price using `priceToCents() !== priceToCents()` logic. - Cleaned up the regression fixture `mock_toppreise.html` mimicking the competing reference price, fixing route mocking for `page.route` to handle both GET URLs and POST variants cleanly. - Replaced aggressive `force=True` and raw node `.click()` interactions in Playwright tests with proper overlay waits to obey Playwright's actionability checks naturally. - Deleted obsolete one-off `patch_*.py` development mutation scripts. Co-authored-by: tazztone <62671577+tazztone@users.noreply.github.com> --- .../toppreise/tests/test_userscript.py | 73 ++++++++++++++++++- userscripts/toppreise/toppreise.user.js | 30 ++++---- 2 files changed, 86 insertions(+), 17 deletions(-) diff --git a/userscripts/toppreise/tests/test_userscript.py b/userscripts/toppreise/tests/test_userscript.py index 528ff03..8900f87 100644 --- a/userscripts/toppreise/tests/test_userscript.py +++ b/userscripts/toppreise/tests/test_userscript.py @@ -49,8 +49,23 @@ def handle_pricechart(route): route.fulfill( status=200, headers={'access-control-allow-origin': '*'}, - content_type='application/json', - body='[[[100000, 47.82], [200000, 37.95]]]' + content_type='text/html', + body=''' +