System decks/notes are owned by a reserved sentinel user id, reusing all existing per-user store/index plumbing unchanged. Regular users see them under a separate "Official Decks" section, can study them with fully independent per-user FSRS progress (lazily materialized on first visit), but can never edit or archive them - enforced both in the client (no Edit/Archive controls) and server-side (ownership guard on every mutating endpoint). Includes a small idempotent seed command (`dotnet run -- seed-flashcards`) for testing, since there's no in-app authoring UI yet.
108 lines
4.7 KiB
GDScript
108 lines
4.7 KiB
GDScript
extends SceneTree
|
|
|
|
# Exercises the client-side "system vs. user" flashcard split added to the hub screen: deck
|
|
# summaries splitting into My/Official groups in the deck selector, Edit/Archive controls being
|
|
# absent on system-owned note rows, and "New Card" refusing to target a system deck. Mirrors
|
|
# flashcard_theme_responsive_test.gd's instantiation pattern (added to the tree so _ready() runs).
|
|
|
|
var failures := 0
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("_run")
|
|
|
|
|
|
func _run() -> void:
|
|
var host := Control.new()
|
|
host.size = Vector2(1280, 800)
|
|
root.add_child(host)
|
|
|
|
var Hub := load("res://scripts/ui/flashcards_hub.gd")
|
|
var hub = Hub.new()
|
|
hub.size = host.size
|
|
host.add_child(hub)
|
|
await process_frame
|
|
|
|
# ---------- Deck selector splits into "My Decks" / "Official Decks" ----------
|
|
var summaries := [
|
|
{"deck": {"id": "deck-mine", "name": "My Cardio Deck", "isSystem": false}, "total": 5, "mastered": 0, "due": 1, "new": 2},
|
|
{"deck": {"id": "deck-system", "name": "Step 3 Essentials", "isSystem": true}, "total": 10, "mastered": 3, "due": 0, "new": 4},
|
|
]
|
|
hub._on_decks_changed(summaries)
|
|
_assert(hub.deck_ids.size() == hub.deck_selector.item_count,
|
|
"deck_ids should stay index-aligned with the OptionButton's items (including separators)")
|
|
|
|
var my_header_index := -1
|
|
var official_header_index := -1
|
|
var mine_item_index := -1
|
|
var system_item_index := -1
|
|
for i in hub.deck_selector.item_count:
|
|
if hub.deck_selector.is_item_separator(i):
|
|
if hub.deck_selector.get_item_text(i) == "My Decks":
|
|
my_header_index = i
|
|
elif hub.deck_selector.get_item_text(i) == "Official Decks":
|
|
official_header_index = i
|
|
elif hub.deck_selector.get_item_text(i) == "My Cardio Deck":
|
|
mine_item_index = i
|
|
elif hub.deck_selector.get_item_text(i) == "Step 3 Essentials":
|
|
system_item_index = i
|
|
|
|
_assert(my_header_index >= 0, "a 'My Decks' separator header should be present")
|
|
_assert(official_header_index >= 0, "an 'Official Decks' separator header should be present")
|
|
_assert(mine_item_index > my_header_index, "the user's own deck should be listed under the 'My Decks' header")
|
|
_assert(system_item_index > official_header_index, "the system deck should be listed under the 'Official Decks' header")
|
|
_assert(str(hub.deck_ids[mine_item_index]) == "deck-mine", "deck_ids should still resolve the correct id for the user's own deck")
|
|
_assert(str(hub.deck_ids[system_item_index]) == "deck-system", "deck_ids should still resolve the correct id for the system deck")
|
|
_assert(hub._is_system_deck("deck-system"), "_is_system_deck should report true for the seeded system deck")
|
|
_assert(not hub._is_system_deck("deck-mine"), "_is_system_deck should report false for the user's own deck")
|
|
|
|
# ---------- System note rows have no Edit/Archive controls, plus an "Official" badge ----------
|
|
hub.notes = [
|
|
{"id": "note-mine", "type": "basic", "prompt": "Mine", "answer": "A", "tags": [], "isSystem": false},
|
|
{"id": "note-system", "type": "basic", "prompt": "System", "answer": "A", "tags": [], "isSystem": true},
|
|
]
|
|
hub._render_notes()
|
|
await process_frame
|
|
|
|
var mine_row := hub.list_container.get_child(0) as BoxContainer
|
|
var mine_actions := mine_row.get_child(2) as HBoxContainer
|
|
_assert(mine_actions.get_child_count() == 2, "the user's own note row should show both Edit and Archive")
|
|
|
|
var system_row := hub.list_container.get_child(2) as BoxContainer
|
|
var system_actions := system_row.get_child(2) as HBoxContainer
|
|
_assert(system_actions.get_child_count() == 0, "a system note row should show no Edit/Archive controls at all")
|
|
|
|
var system_header_row := (system_row.get_child(1) as VBoxContainer).get_child(0) as HBoxContainer
|
|
var found_official_badge := false
|
|
for child in system_header_row.get_children():
|
|
if child is Label and (child as Label).text == "Official":
|
|
found_official_badge = true
|
|
_assert(found_official_badge, "a system note row should show an 'Official' badge")
|
|
|
|
# ---------- "New Card" against a system-selected deck falls back to the default deck ----------
|
|
hub.decks = summaries
|
|
var system_index: int = hub.deck_ids.find("deck-system")
|
|
hub.deck_selector.select(system_index)
|
|
hub._open_editor("", {})
|
|
_assert(hub.editor_target_deck_id.is_empty(),
|
|
"opening the editor for a brand-new card while a system deck is selected should fall back to the default deck instead")
|
|
hub._close_editor()
|
|
|
|
var mine_index: int = hub.deck_ids.find("deck-mine")
|
|
hub.deck_selector.select(mine_index)
|
|
hub._open_editor("", {})
|
|
_assert(hub.editor_target_deck_id == "deck-mine",
|
|
"opening the editor for a brand-new card while the user's own deck is selected should target that deck")
|
|
|
|
hub.free()
|
|
host.queue_free()
|
|
await process_frame
|
|
quit(1 if failures > 0 else 0)
|
|
|
|
|
|
func _assert(condition: bool, message: String) -> void:
|
|
if condition:
|
|
return
|
|
failures += 1
|
|
push_error(message)
|