diff --git a/.gitignore b/.gitignore index 1395cf5..0b6985c 100644 --- a/.gitignore +++ b/.gitignore @@ -237,3 +237,6 @@ userscripts/topp-alarm/bell.png userscripts/topp-alarm/alert.png .specstory/ python/put_files_into_folder_by_extension/mock_dir/ +node_modules/ +package-lock.json +package.json diff --git a/parse_test_v2.js b/parse_test_v2.js new file mode 100644 index 0000000..a8d9404 --- /dev/null +++ b/parse_test_v2.js @@ -0,0 +1,51 @@ +const parsePrice = str => { + if (!str) return 0; + + // Replace terminal dashes with .00 (e.g. 12.- -> 12.00) + let clean = str.replace(/[.–\-]\s*$/g, '.00'); + + // Remove spaces and apostrophes (always grouping separators) and extract valid chars + clean = clean.replace(/[^\d,.]/g, '').replace(/['’\s]/g, ''); + + const lastComma = clean.lastIndexOf(','); + const lastDot = clean.lastIndexOf('.'); + + const lastSeparator = Math.max(lastComma, lastDot); + + if (lastSeparator === -1) { + return parseFloat(clean) || 0; + } + + const digitsAfterSeparator = clean.length - lastSeparator - 1; + + if (digitsAfterSeparator === 3) { + // It's a grouping separator (e.g., 1,385 or 1.385.900) + clean = clean.replace(/[.,]/g, ''); + } else { + // It's a decimal separator. Remove all separators before it, replace the last one with '.' + const before = clean.substring(0, lastSeparator).replace(/[.,]/g, ''); + const after = clean.substring(lastSeparator + 1); + clean = before + '.' + after; + } + + return parseFloat(clean) || 0; +}; + +const tests = [ + ["1.385.90", 1385.90], + ["1,385.90", 1385.90], + ["1.385,90", 1385.90], + ["1,385,900", 1385900], + ["1.385.900", 1385900], + ["1,385", 1385], + ["1.385", 1385], + ["1'385.90", 1385.90], + ["CHF 1'433.00", 1433], + ["12.-", 12], + ["Gratis", 0] +]; + +tests.forEach(([input, expected]) => { + const parsed = parsePrice(input); + console.log(`Input: ${input.padEnd(15)} | Parsed: ${parsed} | Expected: ${expected} | ${parsed === expected ? 'PASS' : 'FAIL'}`); +}); diff --git a/userscripts/toppreise/tests/test_userscript.py b/userscripts/toppreise/tests/test_userscript.py index b9fe634..491a56a 100644 --- a/userscripts/toppreise/tests/test_userscript.py +++ b/userscripts/toppreise/tests/test_userscript.py @@ -1636,6 +1636,34 @@ def test_bestpreise_settings_weight_slider(page: Page): assert page.evaluate("() => window.ToppreiseSuite.CONFIG.BESTPREISE_WEIGHT_RECORD") == 0.70 +def test_parse_price_normalization(page: Page): + """ + Validates that the parsePrice function correctly handles varied European and + international grouping and decimal separator conventions based on the + digits after the final separator. + """ + page.evaluate("""() => { + window.parsePrice = window.ToppreiseSuite.parsePrice; + }""") + + test_cases = [ + ("1.385.90", 1385.90), + ("1,385.90", 1385.90), + ("1.385,90", 1385.90), + ("1,385,900", 1385900), + ("1.385.900", 1385900), + ("1,385", 1385), + ("1'385.90", 1385.90), + ("CHF 1'433.00", 1433), + ("12.-", 12), + ("Gratis", 0) + ] + + for input_str, expected in test_cases: + safe_input = input_str.replace("'", "\\'") + result = page.evaluate(f"() => window.parsePrice('{safe_input}')") + assert result == expected, f"Expected parsePrice('{input_str}') to be {expected}, but got {result}" + def test_outlier_spike_rejection(page: Page): # Product: Smartphone normal price ~CHF 1200 # Vendor glitch: 1-day CHF 15 spike on Day 3 @@ -2378,7 +2406,14 @@ def test_deal_score_weight_preset_dropdown_in_filter_bar(page: Page): assert '100% Rek' in page.locator('#tp-bar-weight-btn').inner_text() # Select 100% Median - page.locator('#tp-bar-weight-btn').click() + popover = page.locator('#tp-weight-popover') + + # If the popover is not visible, click the button to show it + if not popover.is_visible(): + page.locator('#tp-bar-weight-btn').click() + + # Explicitly wait for it to be visible based on state, no timeouts or force + popover.wait_for(state="visible") page.locator('#tp-weight-popover button[data-weight="0.00"]').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 78b91bd..8865f20 100644 --- a/userscripts/toppreise/toppreise.user.js +++ b/userscripts/toppreise/toppreise.user.js @@ -1054,11 +1054,25 @@ const SHADOW_MODAL_STYLES = ` if (!str) return 0; let clean = str.replace(/[.–\-]\s*$/g, '.00'); clean = clean.replace(/[^\d,.]/g, '').replace(/['’\s]/g, ''); - if (/\d+\.\d{3},\d{2}/.test(clean)) { - clean = clean.replace(/\./g, '').replace(',', '.'); + + const lastComma = clean.lastIndexOf(','); + const lastDot = clean.lastIndexOf('.'); + const lastSeparator = Math.max(lastComma, lastDot); + + if (lastSeparator === -1) { + return parseFloat(clean) || 0; + } + + const digitsAfterSeparator = clean.length - lastSeparator - 1; + + if (digitsAfterSeparator === 3) { + clean = clean.replace(/[.,]/g, ''); } else { - clean = clean.replace(',', '.'); + const before = clean.substring(0, lastSeparator).replace(/[.,]/g, ''); + const after = clean.substring(lastSeparator + 1); + clean = before + '.' + after; } + return parseFloat(clean) || 0; }; @@ -3219,13 +3233,14 @@ const SHADOW_MODAL_STYLES = ` } else if (!activeFetches.has(pid)) { isProcessingDetail = true; try { - const fetchedStats = await fetchSingleProductPriceStats(pid); - if (fetchedStats) { - processProductDetailPage(); - } + await fetchSingleProductPriceStats(pid); } finally { isProcessingDetail = false; } + const fetchedStats = getCachedPriceStats(pid); + if (fetchedStats) { + processProductDetailPage(); + } } } @@ -3972,6 +3987,7 @@ const SHADOW_MODAL_STYLES = ` runBestpreiseScan, cancelBestpreiseScan, saveConfigKey, + parsePrice, CONFIG }; }