From 35b19cbb51771393316b985b61fa4450acb5b4fc Mon Sep 17 00:00:00 2001 From: "Jokob @NetAlertX" <96159884+jokob-sk@users.noreply.github.com> Date: Thu, 2 Jul 2026 04:32:01 +0000 Subject: [PATCH] Refactor cache handling for plugins: update pluginsData initialization and improve fetch logic; enhance UI tests for translation loading --- front/js/cache.js | 39 +++++++++++++++++++++++++-------- front/pluginsCore.php | 2 +- test/ui/test_ui_translations.py | 18 ++++++++------- 3 files changed, 41 insertions(+), 18 deletions(-) diff --git a/front/js/cache.js b/front/js/cache.js index 4412a622..9d6e9539 100644 --- a/front/js/cache.js +++ b/front/js/cache.js @@ -178,8 +178,10 @@ function cacheApiConfig() { // Module-level declaration so cachePluginStrings() and other callers can // safely reference pluginsData even before cacheSettings() has fetched it. -// Populated (or repopulated) by cacheSettings() on every full fetch. -var pluginsData = []; +// null = fetch not yet attempted or fetch failed (unknown state). +// [] = fetch succeeded and confirmed no plugins are configured. +// [...] = fetch succeeded and plugins are present. +var pluginsData = null; function cacheSettings() { @@ -192,12 +194,18 @@ function cacheSettings() // so the extra fetch would be pointless on every subsequent warm load. if (getCache(CACHE_KEYS.initFlag('cachePluginStrings_v1')) !== 'true') { fetchJson('plugins.json') - .catch(() => []) .then((pluginsArr) => { - if (Array.isArray(pluginsArr) && pluginsArr.length > 0) { + // Only update pluginsData on a successful fetch so that a network + // failure is not misread as "no plugins configured" (null sentinel). + if (Array.isArray(pluginsArr)) { pluginsData = pluginsArr; } resolve(); + }) + .catch(() => { + // Fetch failed — leave pluginsData as null (unknown state) so + // cachePluginStrings() retries rather than treating it as no-op. + resolve(); }); return; } @@ -312,10 +320,20 @@ function cacheStrings() { return new Promise((resolve, reject) => { if(getCache(CACHE_KEYS.initFlag('cacheStrings_v2')) === "true") { - // Core strings are already cached. Plugin strings are now handled by - // the dedicated cachePluginStrings() step — nothing to do here. - resolve(); - return; + // Verify STRINGS_COUNT still matches the current language configuration. + // A language switch without a full cache clear leaves the flag set but + // the count wrong, causing isAppInitialized() to deadlock. + const expectedCount = getLangCode() === 'en_us' ? 1 : 2; + const currentCount = parseInt(getCache(CACHE_KEYS.STRINGS_COUNT)) || 0; + if (currentCount === expectedCount) { + resolve(); + return; + } + // Mismatch — reset count and fall through to reload strings for the + // current language set. The flag stays set; handleSuccess re-increments. + console.log(`[cacheStrings] STRINGS_COUNT mismatch (stored: ${currentCount}, expected: ${expectedCount}) — reloading strings.`); + setCache(CACHE_KEYS.STRINGS_COUNT, 0); + // Do NOT return — fall through to full load path below. } // Create a promise for each language (include en_us by default as fallback) @@ -386,7 +404,10 @@ function cachePluginStrings() { // No plugin strings returned. If plugins are loaded the file may not // be ready yet — reject so retryStep() retries. If no plugins are // loaded at all, empty strings are expected: succeed. - const hasPlugins = Array.isArray(pluginsData) && pluginsData.length > 0; + // null = plugins.json fetch failed (unknown) — retry to be safe. + // [] = confirmed no plugins — accept empty strings as valid. + // [...] = plugins present — retry, file may not be ready yet. + const hasPlugins = pluginsData === null || (Array.isArray(pluginsData) && pluginsData.length > 0); if (hasPlugins) { console.warn('[cachePluginStrings] 0 entries returned but plugins are loaded — file may not be ready yet. Will retry.'); reject(new Error('Plugin language strings empty')); diff --git a/front/pluginsCore.php b/front/pluginsCore.php index 70ca8322..a8f36686 100755 --- a/front/pluginsCore.php +++ b/front/pluginsCore.php @@ -312,7 +312,7 @@ async function getData() { // module-level fetchJson defined in cache.js, which returns res['data'] and // is used by cachePluginStrings() during app initialization. async function fetchPluginJson(filename) { - const response = await fetch(`php/server/query_json.php?file=${filename}`); + const response = await fetch(`php/server/query_json.php?file=${encodeURIComponent(filename)}&nocache=${Date.now()}`); if (!response.ok) throw new Error(`Failed to load ${filename}`); return await response.json(); } diff --git a/test/ui/test_ui_translations.py b/test/ui/test_ui_translations.py index 7bbca4cf..aa775e9d 100644 --- a/test/ui/test_ui_translations.py +++ b/test/ui/test_ui_translations.py @@ -9,7 +9,6 @@ PRD: Reliable Loading of Plugin Translation Strings import sys import os -import time from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait @@ -161,16 +160,19 @@ def test_plugins_page_no_undefined_strings_after_cache_clear(driver): _wait_for_app_init(driver) - # Give the tabs a moment to render after init completes - time.sleep(2) + # Wait for getData() to complete: either tab links or the empty-state + # paragraph will appear once generateTabs() has run. + WebDriverWait(driver, 10).until( + lambda d: len(d.find_elements( + By.CSS_SELECTOR, + "#tabs-location a, #tabs-content-location p.text-muted" + )) > 0 + ) - # Allow the word 'undefined' inside code/technical blocks but not as a - # standalone visible label replacing a translation string. - # We check that it does not appear as a trimmed standalone word in headings/tabs. tab_labels = driver.find_elements(By.CSS_SELECTOR, "#tabs-location a, .tab-label, h5") for el in tab_labels: label = el.text.strip() assert label != "undefined", ( - f"Tab/heading label is literally 'undefined' — plugin strings were not " - f"loaded before getData() rendered the tabs on plugins.php." + "Tab/heading label is literally 'undefined' — plugin strings were not " + "loaded before getData() rendered the tabs on plugins.php." )