PlayerHUD

Modular gamification block for Moodle — full documentation.

English | Português

View on GitHub Download .zip

🧪 Automated Tests

PlayerHUD ships with an extensive test suite covering both business logic (PHPUnit) and browser acceptance (Behat). Every CI push runs against the full matrix (Moodle 4.5 → 5.x, PostgreSQL & MariaDB).

PHPUnit — Unit & Integration Tests

Test file Cases What is covered
ai/generator_test.php 2 save_item() (reached via reflection, no network): clamps an overlong AI-provided name; coerces non-string fields before persisting
backup_restore_test.php 3 Backup/restore step definitions cover all RPG tables; full course round-trip (incl. a real activity) preserves RPG class/chapter/story data, item powers (action_type/action_value), class emoji tiers, and a TYPE_SPECIFIC_TRADE quest’s requirement remapped against the restored trade rather than the item mapping; a pinned deadline_extension cmid and a TYPE_ACTIVITY quest’s requirement are both remapped to the restored course’s own activity
collection_tab_test.php 8 Collection tab: filter_type mapping (avatar/deadline/none), power_hint_avatar shown for unowned non-secret item and hidden for secret item, is_equipped flag; origin classification for an inventory row’s source (map is recognised as PlayerHUD’s own; anything outside the 4 known sources falls back to a generic “game” origin)
content_crud_test.php 13 Item, chapter and trade CRUD: create persists all fields, update changes fields, delete removes record, listing scoped to instance
cross_instance_security_test.php 12 Cross-instance isolation: item, quest, chapter and trade guards accept own-instance IDs and reject foreign ones without modifying the target record
drop_guard_test.php 7 Collection limits, trade-consumed items, cooldown enforcement
game_test.php 36 get_game_stats() totals XP/level plus quest XP inclusion (and exclusion when the quest is disabled), cross-checked against analytics::economy_health()’s own total; collection anti-farm and cooldown; get_avatar_item (enabled, disabled, foreign instance, not found); XP award on finite drop; leaderboard manager exclusion; level-up, beat-the-game and first-PlayerCoin milestone flags on collection; xp_to_level; player auto-creation, gamification and ranking-visibility toggles, inventory (revoked/consumed excluded), has_item; get_user_rank XP order, tie-break by arrival, manager and enrolment exclusion; get_full_trades requirement/reward hydration, empty case, and availability gating when either side’s item is disabled; trade-suggestion heuristics (discounted avatars, covered-avatar skip, prerequisites) and persistence; change_xp emits the xp_changed event on award, on deduction (floored at zero) and stays silent on a true no-op
gamemaster_test.php 6 Grant/revoke/delete item and quest while preserving leaderboard timestamps; XP floor at zero
instance_delete_test.php 1 Deleting a block instance cleans every one of this plugin’s own tables (instance_cleanup)
item_delete_cascade_test.php 17 Trade orphan detection when item deleted (sole req, one-of-two, sole reward, combined req+reward); bulk orphan checks; cross-instance isolation; delete removes item record and cascades orphaned trades without touching non-orphaned ones; deleting an item (single or bulk) reverts XP only for copies that actually earned it, leaving infinite-drop (zero-XP) copies untouched
karma_test.php 11 Karma read/write, positive/negative deltas, clamping at ±999 boundaries, successive accumulation
privacy_provider_test.php 10 GDPR full coverage: context/user discovery (get_contexts_for_userid, get_users_in_context); export_user_data across all six subtrees (profile, RPG, inventory, quests, trades, AI logs); per-user, multi-user and whole-context deletion with isolation guarantees; export/delete of every API-key and avatar preference; metadata declaration; non-block context guards are no-ops
quest_test.php 34 Completion checks (level, XP, items, trades, activity completion); claim rewards; disabled quest; idempotency; level-up and beat-the-game celebration flags on reward claim; has_claimable_quests across every requirement type incl. activity completion, with claimed/unclaimed short-circuit; build_record_from_suggestion mapping, item-id carrying and XP override floor; get_heuristic_suggestions level/collection/economy/activity milestones with duplicate skipping; a completion-tracked activity offered as a heuristic quest is detected as fulfilled once the activity is actually completed
rpg_classes_test.php 7 Class assignment, duplicate guard, karma initialisation, portrait tier boundaries
story_manager_test.php 15 Scene loading, progress persistence, choice navigation, karma delta, chapter completion, error cases
suggest_trades_state_test.php 4 Suggest Trades button: disabled without prereqs, disabled with coin only, disabled when all avatars covered, enabled on partial coverage
trade_test.php 8 Trade assembly, insufficient funds, atomic success, one-time limit, group restriction; a trade referencing a disabled reward item is rejected outright even with sufficient funds
utils_test.php 4 get_avatar_html: emoji produces ph-avatar-emoji div with aria-hidden span; HTTP URL produces ph-avatar-img img tag; a null image does not throw for get_avatar_html nor get_items_display_data
Subtotal 198  

Local Business-Logic Tests (tests/local/)

Shared logic reused by more than one entry point (the wizard’s own web services, the manual “Distribute Drops” screen, the Economy Health panel), tested directly rather than only indirectly through whichever controller happens to call it.

Test file Cases What is covered
analytics_test.php 11 Economy Health: total earnable XP vs ceiling ratio (empty/hard/perfect/easy), quest rewards and infinite/dropless items in the breakdown, zero-ceiling guard; level-distribution histogram bucketing, cap overflow (N+) ordering, percent of tallest bar, zero-XP-per-level guard, empty player set produces no rows; balance_context()’s current XP always matches economy_health()’s own total
audit_log_test.php 5 Shared audit-log query (get_logs()) behind the teacher Reports tab and the student History tab: an item’s xp_gained reflects the recorded xpawarded value at grant time, not the item’s current XP (and matches when never edited); a quest-granted item reports zero xp_gained since its own XP is never paid through that path; a revoked row reports the negative of its originally recorded value, not the item’s current XP; a quest claim’s xp_gained reflects the recorded value, not the quest’s current reward_xp
drop_distribution_test.php 12 Eligible-modules discovery: includes forums, excludes modules pending deletion and the course’s own news forum (reserved for PlayerCoin/Secret Item), empty for an activity-less course; best-name-match suggestion incl. no-match case; inserted-shortcode cmid lookup incl. not-found and empty-input cases; activity-quota splitting always sums to target, caps at activity count, edge cases
external_items_test.php 18 Cross-plugin item API used by other Player-family plugins (e.g. PlayerWords): belongs_to_instance() accepts an item’s own instance (enabled or disabled) and rejects a foreign instance, a nonexistent id, or zero/negative ids without querying the database; grant() inserts one inventory row per unit with its own xpawarded and credits the total XP once, withholds XP when the caller flags the source as unbounded, and is a no-op for a foreign-instance or disabled item; consume() marks the oldest rows consumed on success, returns false when the balance is insufficient, and returns null (not false) for a foreign-instance item so the caller waives the cost instead of blocking the student forever; get_name()/get_xp() resolve for the item’s own instance and return empty/zero for a foreign one; get_available_quantity() counts only active (non-revoked/non-consumed) rows and is zero for a foreign-instance item even if the user holds units of it
wizard_test.php 17 Run manifest: start/finish status; rollback deletes recorded objects across tables, strips the recorded shortcode, reverts XP and clears play history, rejects a mismatched instance; active-runs listing with counts and a limit; per-module “already generated” detection incl. stale runs without content, manifest-only items, AI-logged-only items and Ranking’s config-only check; ensure_config_flag turns a flag on without touching sibling config and is a no-op when already on
xp_budget_test.php 15 Item/mission/chapter counts per journey size incl. fallback to short; distribute_share divides a gap evenly, spreads the remainder on the first elements, caps at the gap when elements outnumber it, edge cases; suggested max-levels mapping; balanced-mission round-robin across types, order preservation within a type, all-selected when the limit covers them, edge cases
Subtotal 78  

Web Services Tests (tests/external/)

One test class per web service function, each validating the external API contract, parameter/return structure conformance (external_api::clean_returnvalue), and capability gates. AI functions are tested without network — with no API key configured, the try/catch path returns success=false, which is asserted directly.

Test file Cases What is covered
chat_message_test.php 2 No API key → moodle_exception; capability guard (manage)
collect_item_test.php 4 Item collected + inventory record created; invalid drop → success=false; limit reached → success=false; capability guard (view)
create_avatar_pack_test.php 6 17 items created; ids and names returned in lockstep; all have action_type=avatar; emoji deduplication; second call creates 0 (idempotency); capability guard
create_class_pack_test.php 7 Creates 3 classes; base-HP tiers match expectations; skips an already-existing name; second call creates 0 (idempotency); different tones produce different names; unknown tone falls back to fantasy; capability guard
create_playercoin_test.php 3 New item created; second call returns existing item (idempotency); capability guard
execute_chat_action_test.php 4 action_open_tab returns redirect URL (deterministic, no AI); unknown action type → success=false; invalid params → success=false; capability guard
generate_ai_content_test.php 2 No API key → success=false; capability guard (manage)
generate_class_oracle_test.php 2 No API key → success=false; capability guard (manage)
generate_story_test.php 2 No API key → success=false; capability guard (manage)
insert_drop_shortcode_test.php 7 Shortcode prepended to module content field; duplicate insert rejected; drop from another instance rejected; drop renamed to the activity it lands in; mode=text with a custom label; unknown mode falls back to card; capability guard
load_recap_test.php 3 Recap HTML returned after scene visit; no history → exception; capability guard (view)
load_scene_test.php 3 Start node and choices returned; invalid chapter → exception; capability guard (view)
make_choice_test.php 3 Advances story to destination node; invalid choice → exception; capability guard (view)
remove_drop_shortcode_test.php 5 Existing shortcode stripped; <br>-separated shortcode stripped; shortcode carrying mode=/text= attributes stripped; absent shortcode is a no-op success; capability guard
setup_playercoin_drop_test.php 6 Success path; no forum → success=false; item from another instance rejected; course not owning the instance rejected; shortcode prepended to existing intro; capability guard
use_item_test.php 6 Capability guard (view); not-owned item → exception; deadline power: no activity selected, no rule found, creates override and consumes item, updates existing override
wizard_apply_suggested_levels_test.php 3 Applies the suggestion when config is at defaults; still applies when config was already customised; preserves every other config field untouched
wizard_generate_helpers_test.php 10 build_step_types() matches selected modules in order, skips auto_distribute when Items’ own distribute flag is off, empty when nothing selected; compute_shared_xp_shares() empty without Items/Missions, Pill/Latepenalty use their own defaults alone, share the budget with Items when combined; resolve_or_create_progress_item() idempotent and creates a complete item when missing; resolve_previous_chapter_context() reads the latest chapter; distribute_drops() caps each activity to its computed quota instead of letting name-matching alone stack every drop onto one activity
wizard_list_runs_test.php 4 Summary for an active run; RPG run summarised; rolled-back runs excluded; capability guard
wizard_run_step_test.php 56 One live-progress step at a time, per mechanic (PlayerCoin, Avatars, Missions, Trade, Knowledge Pill, Secret Item, Ranking, Deadline Extension, RPG, Item RPG, auto-distribute): item/quest/trade creation with manifest recording, idempotent retries, rollback per mechanic, distribute-flag gating, tone/journey-size flavouring, and the news-forum-only placement for PlayerCoin and Secret Item (incl. no-op without a news forum); unknown step type, capability guard, cross-instance runid rejection, failed step does not finish the run, final step reports the economy only when requested
wizard_start_test.php 8 One plan step per selected module; the “slow step” flag reflects whether Next Chapter was selected; XP shares split matches selected modules; Pill’s bonus XP present when selected alone; the story-arc module expands into an outline + one step per chapter, step count grows with journey size, manifest keeps the logical module name; capability guard
Subtotal 146  

Controller Tests (tests/controller/)

These cover the business logic extracted from manage.php into the controllers (MVC refactor), each exercised with explicit inputs and instance isolation.

Test file Cases What is covered
aikeys_test.php 4 AI key storage: keys trimmed and saved as user preferences, empty default for a missing field, legacy keys stripped from block config, clean config left untouched
chapters_test.php 13 Chapter persistence and ordering: save (insert, update, defaults, isolation), delete cascading scenes/choices, reorder/move with full-list renumbering, edge no-op
classes_test.php 7 RPG class persistence: insert (base HP, instance binding, emoji tiers), update preserves base HP, emoji trimming, isolation; delete removes record and tier portraits, isolation, siblings kept
collect_test.php 3 Item collection transaction: finite drop awards XP, infinite drop awards 0 XP (golden rule), zero-XP item stored without XP change
drops_test.php 11 Drop persistence: save (insert + code, unlimited, update preserves ownership, isolation, foreign item); delete single and foreign no-op; bulk deletes only owned with count, empty input; get_owned_item returns for the owning instance and rejects a foreign one
export_test.php 7 Grade export builder: row fields and derived level, XP ordering, level cap, teacher/manager exclusion, localized columns with no players, unenrolled exclusion, XP tie-break by last action
items_test.php 15 Item lifecycle: enable toggle and foreign no-op; grant adds inventory + XP, zero-XP, foreign rejection; revoke deducts XP, infinite-drop preservation, foreign no-op; revoke deducts the XP actually recorded at grant time, not the item’s current XP; surviving-trade detection (trimmed trade, orphaned excluded, unrelated ignored); find_xp_impact aggregates only copies that actually earned XP across all holders, empty for an unheld item, and a no-op for an empty id list
quests_test.php 12 Quest lifecycle: toggle and foreign no-op; delete reverts XP per completion, zero-reward, foreign no-op; delete and bulk-delete revert the XP actually recorded per completion, not the quest’s current reward; bulk deletes only owned with aggregated XP revert and count, empty input; find_xp_impact aggregates only completions that actually earned XP across all claimants, empty for an unclaimed quest, and a no-op for an empty id list
scenes_test.php 6 Story scene/choice persistence: save choices, class assignment with string/int ID normalisation (set_class_id regression), required class, next node, item cost, follow-up node creation
suggestions_test.php 4 Suggestion persistence: only ticked quest suggestions inserted (and none selected), only ticked trade suggestions created with reqs/rewards (and none selected)
trades_test.php 7 Trade persistence: save (insert with reqs + rewards, update replaces, isolation, foreign item filtered); delete cascading reqs/rewards/log, isolation, siblings kept
Subtotal 89  

Output / Renderer Tests (tests/output/)

Test file Cases What is covered
manage/item_delete_confirm_test.php 9 Item-deletion confirmation context: single vs bulk action and id payload, singular/plural/simple confirm labels, surviving-only and orphaned+surviving sections; XP-impact warning shown for a single deletion with a disable-instead link, never shown for a bulk deletion even with a toggle URL supplied, and omitted entirely when there is no XP impact
manage/quest_delete_confirm_test.php 3 Quest-deletion confirmation context: single deletion produces the delete_quest_force action with the XP-impact warning and disable-instead link; bulk deletion produces bulk_delete_quests_force with the id list and never shows the disable-instead link even with a toggle URL supplied; no XP impact omits both the warning and the disable link
manage/tab_chapters_test.php 4 Chapter-card visibility warnings: missing start-scene flag, required-level-above-maximum warning text and bounds
Subtotal 16  
Grand Total 527  
vendor/bin/phpunit --testsuite block_playerhud

Line coverage by class (PHPUnit + Xdebug):

Class Line coverage
ai\generator 6%
controller\aikeys 100%
controller\chapters 40%
controller\classes 41%
controller\collect 13%
controller\drops 20%
controller\export 90%
controller\items 99%
controller\quests 76%
controller\scenes 13%
controller\suggestions 100%
controller\trades 39%
drop_guard 100%
event\xp_changed 43%
external\chat_message 67%
external\collect_item 100%
external\create_avatar_pack 84%
external\create_class_pack 79%
external\create_playercoin 91%
external\execute_chat_action 27%
external\generate_ai_content 77%
external\generate_class_oracle 67%
external\generate_story 75%
external\insert_drop_shortcode 87%
external\load_recap 100%
external\load_scene 79%
external\make_choice 79%
external\remove_drop_shortcode 84%
external\setup_playercoin_drop 90%
external\use_item 75%
external\wizard_apply_suggested_levels 83%
external\wizard_generate 85%
external\wizard_list_runs 100%
external\wizard_run_step 86%
external\wizard_start 99%
game 84%
instance_cleanup 100%
local\analytics 90%
local\audit_log 78%
local\drop_distribution 97%
local\external_items 97%
local\wizard 76%
local\xp_budget 98%
output\manage\item_delete_confirm 100%
output\manage\quest_delete_confirm 100%
output\manage\tab_chapters 7%
output\view\tab_collection 68%
privacy\provider 96%
quest 90%
story_manager 37%
trade_manager 90%
utils 35%
Overall 42%

49 of the plugin’s 82 classes are listed above — the rest (mostly exception classes, event subscribers and thin output wrappers never require‘d during this suite’s run) carry no coverage data at all and are omitted rather than shown as a misleading 0%.

Behat — Acceptance Tests

Feature file Scenarios What is covered
block_playerhud_access.feature 3 Role-based block visibility (teacher adds block, student sees HUD, non-enrolled user cannot)
block_playerhud_student.feature 4 HUD active on first visit, disable/re-enable gamification, dismiss confirmation
block_playerhud_teacher.feature 7 Game Master Panel button, management panel access, tab navigation, return to course; opening a student’s audit log in Reports does not error
block_playerhud_modals.feature 5 Item detail modal open/close, duplicate-open guard, AJAX collect without redirect, no raw placeholders
block_playerhud_celebrations.feature 2 Huddy introduction shown once on the dashboard; first-quest nudge shown once when a reward is claimable
block_playerhud_wizard.feature 6 Wizard opens showing the generation form; Help and External recommendations side views; generating PlayerCoin end-to-end shows the success report; the PlayerCoin card locks after being generated; undoing a run from the History view unlocks it again
Total 27  
php admin/tool/behat/cli/init.php
vendor/bin/behat --tags=@block_playerhud --profile=chrome