From 8dc38fd8339c740252960e28dc462aabf8c9f05d Mon Sep 17 00:00:00 2001 From: AlanRockefeller Date: Tue, 14 Apr 2026 00:00:41 -0700 Subject: [PATCH 1/2] Fix console qml warning --- faststack/qml/Main.qml | 86 ++++++++++++++++--- .../tests/test_main_qml_runtime_guards.py | 17 ++++ faststack/tests/test_recycle_bin_tracking.py | 20 +++++ 3 files changed, 112 insertions(+), 11 deletions(-) create mode 100644 faststack/tests/test_main_qml_runtime_guards.py diff --git a/faststack/qml/Main.qml b/faststack/qml/Main.qml index dae1187..330ef32 100644 --- a/faststack/qml/Main.qml +++ b/faststack/qml/Main.qml @@ -123,6 +123,63 @@ ApplicationWindow { } } + function toArray(value) { + if (value === null || value === undefined) { + return [] + } + + if (Array.isArray(value)) { + return value + } + + var valueType = typeof value + if (valueType === "string" || valueType === "number" + || valueType === "boolean" || valueType === "function") { + return [] + } + + if (typeof value.length === "number") { + var result = [] + for (var i = 0; i < value.length; ++i) { + result.push(value[i]) + } + return result + } + + return [] + } + + function stringOrEmpty(value) { + if (typeof value === "string") { + return value + } + + if (value === null || value === undefined) { + return "" + } + + if (typeof value === "number" || typeof value === "boolean") { + return String(value) + } + + // Avoid rendering unexpected objects as "[object Object]". + return "" + } + + function itemsWithStatus(items, status) { + var source = root.toArray(items) + var result = [] + + for (var i = 0; i < source.length; ++i) { + var item = source[i] + if (item && item.status === status) { + result.push(item) + } + } + + return result + } + // -------- CUSTOM TITLE BAR -------- property int titleBarHeight: 36 @@ -1209,8 +1266,10 @@ ApplicationWindow { color: root.currentTextColor } Label { - visible: (root.uiStateRef && root.uiStateRef.imageCount > 0 && root.uiStateRef.exifBrief && root.uiStateRef.exifBrief.length > 0) - text: root.uiStateRef ? (root.uiStateRef.exifBrief || "") : "" + visible: root.uiStateRef + && root.uiStateRef.imageCount > 0 + && root.stringOrEmpty(root.uiStateRef.exifBrief).length > 0 + text: root.uiStateRef ? root.stringOrEmpty(root.uiStateRef.exifBrief) : "" color: root.currentTextColor } Label { @@ -1318,11 +1377,16 @@ ApplicationWindow { } // Variant badges (loupe view only, when multiple variants exist) Row { + id: variantBadgeRow + property var badgeItems: root.toArray(root.uiStateRef ? root.uiStateRef.variantBadges : null) + spacing: 4 - visible: root.uiStateRef && !root.uiStateRef.isGridViewActive && root.uiStateRef.variantBadges.length > 1 + visible: root.uiStateRef + && !root.uiStateRef.isGridViewActive + && variantBadgeRow.badgeItems.length > 1 Repeater { - model: root.uiStateRef ? root.uiStateRef.variantBadges : [] + model: variantBadgeRow.badgeItems delegate: Rectangle { id: variantBadge @@ -1767,6 +1831,9 @@ ApplicationWindow { // Single source of truth for per-bin restore info. // Populated on open and after each restore action. property var binInfo: [] + property var binInfoItems: root.toArray(binInfo) + property var restorableBins: root.itemsWithStatus(binInfoItems, "restorable") + property var unavailableBins: root.itemsWithStatus(binInfoItems, "unavailable") function refreshBinInfo() { if (root.uiStateRef) { @@ -1826,7 +1893,7 @@ ApplicationWindow { // ---- Per-bin restore rows (restorable bins only) ---- Repeater { id: restorableRepeater - model: recycleBinCleanupDialog.binInfo.filter(function(b) { return b.status === "restorable" }) + model: recycleBinCleanupDialog.restorableBins delegate: Rectangle { id: restorableBin @@ -1907,7 +1974,7 @@ ApplicationWindow { root.uiStateRef.restoreSingleBin(restorableBin.modelData.bin_path) recycleBinCleanupDialog.refreshBinInfo() // Auto-close if nothing left - if (recycleBinCleanupDialog.binInfo.length === 0) { + if (recycleBinCleanupDialog.binInfoItems.length === 0) { recycleBinCleanupDialog.close() } } @@ -1924,10 +1991,7 @@ ApplicationWindow { Column { width: dialogContent.width - 40 spacing: 6 - visible: { - var items = recycleBinCleanupDialog.binInfo.filter(function(b) { return b.status === "unavailable" }) - return items.length > 0 - } + visible: recycleBinCleanupDialog.unavailableBins.length > 0 Label { text: "Not auto-restorable (legacy format)" @@ -1944,7 +2008,7 @@ ApplicationWindow { } Repeater { - model: recycleBinCleanupDialog.binInfo.filter(function(b) { return b.status === "unavailable" }) + model: recycleBinCleanupDialog.unavailableBins delegate: Label { id: unavailableBin diff --git a/faststack/tests/test_main_qml_runtime_guards.py b/faststack/tests/test_main_qml_runtime_guards.py new file mode 100644 index 0000000..d1ae59b --- /dev/null +++ b/faststack/tests/test_main_qml_runtime_guards.py @@ -0,0 +1,17 @@ +from pathlib import Path + + +def test_main_qml_coerces_runtime_values_before_array_operations(): + """Keep startup bindings from calling JS array APIs on raw backend values.""" + qml_path = Path(__file__).resolve().parents[1] / "qml" / "Main.qml" + qml_text = qml_path.read_text(encoding="utf-8") + + assert "function toArray(value)" in qml_text + assert "function itemsWithStatus(items, status)" in qml_text + assert "value === null || value === undefined" in qml_text + assert "if (!value)" not in qml_text + + assert "root.uiStateRef.variantBadges.length" not in qml_text + assert "recycleBinCleanupDialog.binInfo.filter(" not in qml_text + assert "recycleBinCleanupDialog.binInfo.length" not in qml_text + assert "root.stringOrEmpty(root.uiStateRef.exifBrief)" in qml_text diff --git a/faststack/tests/test_recycle_bin_tracking.py b/faststack/tests/test_recycle_bin_tracking.py index 36b6f1d..a084eec 100644 --- a/faststack/tests/test_recycle_bin_tracking.py +++ b/faststack/tests/test_recycle_bin_tracking.py @@ -151,3 +151,23 @@ def test_get_recycle_bin_stats_untracked_existing_bin(app_controller): assert stats[0]["count"] == 1 # Check that it was auto-added to active_recycle_bins for future cleanup assert recycle_bin in app_controller.active_recycle_bins + + +def test_get_per_bin_restore_info_returns_fresh_values(app_controller): + """Each refresh should produce fresh containers so QML bindings can update.""" + recycle_bin = app_controller.image_dir / "image recycle bin" + recycle_bin.mkdir(parents=True) + (recycle_bin / "photo._fs_deadbeef.jpg").touch() + app_controller.active_recycle_bins.add(recycle_bin) + + first = app_controller.get_per_bin_restore_info() + second = app_controller.get_per_bin_restore_info() + + assert first == second + assert first is not second + assert first[0] is not second[0] + assert second[0]["status"] == "restorable" + + first[0]["status"] = "mutated" + + assert second[0]["status"] == "restorable" From 8f323ae2bc7636efd46f3ccd6d1834183b6eda14 Mon Sep 17 00:00:00 2001 From: AlanRockefeller Date: Tue, 14 Apr 2026 09:33:57 -0700 Subject: [PATCH 2/2] fix test --- faststack/tests/test_main_qml_runtime_guards.py | 1 + 1 file changed, 1 insertion(+) diff --git a/faststack/tests/test_main_qml_runtime_guards.py b/faststack/tests/test_main_qml_runtime_guards.py index d1ae59b..1624b78 100644 --- a/faststack/tests/test_main_qml_runtime_guards.py +++ b/faststack/tests/test_main_qml_runtime_guards.py @@ -7,6 +7,7 @@ def test_main_qml_coerces_runtime_values_before_array_operations(): qml_text = qml_path.read_text(encoding="utf-8") assert "function toArray(value)" in qml_text + assert "function stringOrEmpty(value)" in qml_text assert "function itemsWithStatus(items, status)" in qml_text assert "value === null || value === undefined" in qml_text assert "if (!value)" not in qml_text