@bongos/core 1.24.79 → 1.24.80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +495 -350
- package/.claude/skills/ask-for-help/SKILL.md +1 -1
- package/.claude/skills/builder-sequence/SKILL.md +1 -1
- package/.claude/skills/design/SKILL.md +1 -1
- package/.claude/skills/goal-create/SKILL.md +1 -1
- package/.claude/skills/planning-session/SKILL.md +1 -1
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +8 -0
- package/clients/bongos-client/index.cjs +8 -0
- package/clients/bongos-client/index.d.ts +11 -0
- package/clients/bongos-client/index.mjs +8 -0
- package/docs/adr/0274-one-kernel-three-role-packs.md +2 -2
- package/docs/adr/0399-the-visor-is-glass-over-the-scene-and-owns-no-camera.md +91 -0
- package/docs/adr/0400-a-chain-reserves-work-and-each-sub-quest-is-still-claimed-alone.md +109 -0
- package/docs/adr/0401-one-options-menu-opens-every-settings-screen-in-a-frame.md +53 -0
- package/docs/adr/README.md +3 -0
- package/docs/api/openapi.json +195 -3
- package/docs/api-reference.md +8 -4
- package/docs/architecture.md +7 -3
- package/docs/copy-inventory.md +390 -370
- package/docs/copy-registry.json +719 -518
- package/docs/design/characters/character-sprites-source.webp +0 -0
- package/docs/file-map.md +7 -8
- package/docs/module-api-changelog.md +2 -0
- package/docs/modules-contract.md +5 -0
- package/docs/onboarding/slash-commands.md +1 -1
- package/docs/packs/{engineer.md → artisan.md} +5 -5
- package/docs/packs/artist.md +50 -4
- package/docs/page-inventory.json +15 -4
- package/docs/page-readings.json +3182 -3336
- package/docs/{packs/ideator.md → recipes/ideation-playbook.md} +5 -48
- package/migrations/core_296_crafts_artisan_artist.sql +72 -0
- package/migrations/core_297_builder_character.sql +30 -0
- package/modules/copy-desk/proposals.js +1 -1
- package/modules/economy/credits-by-craft.js +29 -15
- package/modules/hall-ui/public/ai-studio.html +4 -4
- package/modules/hall-ui/public/atlas.html +5 -5
- package/modules/hall-ui/public/atlas.js +3 -1
- package/modules/hall-ui/public/blockers.html +5 -5
- package/modules/hall-ui/public/board-room.html +4 -4
- package/modules/hall-ui/public/characters/artisan.png +0 -0
- package/modules/hall-ui/public/characters/artist.png +0 -0
- package/modules/hall-ui/public/characters/patron.png +0 -0
- package/modules/hall-ui/public/city.css +2 -1
- package/modules/hall-ui/public/city.html +9 -6
- package/modules/hall-ui/public/city.js +7 -1
- package/modules/hall-ui/public/collab.html +4 -4
- package/modules/hall-ui/public/commands.html +4 -4
- package/modules/hall-ui/public/copy-desk.html +4 -4
- package/modules/hall-ui/public/craft-mode.js +258 -64
- package/modules/hall-ui/public/deploy.html +4 -4
- package/modules/hall-ui/public/diagram-viewer.js +2 -1
- package/modules/hall-ui/public/dom-utils.js +1 -1
- package/modules/hall-ui/public/goal-map.html +9 -6
- package/modules/hall-ui/public/goals.html +5 -5
- package/modules/hall-ui/public/government.html +4 -4
- package/modules/hall-ui/public/hall-render.js +3 -1
- package/modules/hall-ui/public/home-city-draw.js +3 -3
- package/modules/hall-ui/public/home-city.css +4 -0
- package/modules/hall-ui/public/home-city.js +17 -9
- package/modules/hall-ui/public/home-gallery.js +8 -1
- package/modules/hall-ui/public/home-idea-box.css +2 -2
- package/modules/hall-ui/public/home-idea-box.js +3 -3
- package/modules/hall-ui/public/idea.html +5 -5
- package/modules/hall-ui/public/ideas.html +5 -5
- package/modules/hall-ui/public/index.html +11 -7
- package/modules/hall-ui/public/market-watch-item.html +4 -4
- package/modules/hall-ui/public/market-watch-rules.html +4 -4
- package/modules/hall-ui/public/market-watch.html +6 -9
- package/modules/hall-ui/public/market-watch.js +22 -45
- package/modules/hall-ui/public/modules.html +4 -4
- package/modules/hall-ui/public/money.html +4 -4
- package/modules/hall-ui/public/my-agents.html +4 -4
- package/modules/hall-ui/public/options-menu.js +409 -0
- package/modules/hall-ui/public/paintbrush.js +3 -3
- package/modules/hall-ui/public/palette.js +11 -13
- package/modules/hall-ui/public/primer.html +5 -5
- package/modules/hall-ui/public/profile-crafts.js +1 -1
- package/modules/hall-ui/public/profile-roles.js +23 -19
- package/modules/hall-ui/public/profile.css +1 -1
- package/modules/hall-ui/public/profile.html +19 -18
- package/modules/hall-ui/public/profile.js +19 -18
- package/modules/hall-ui/public/profile.states.json +4 -4
- package/modules/hall-ui/public/project-settings.html +4 -4
- package/modules/hall-ui/public/roster.html +4 -4
- package/modules/hall-ui/public/roster.js +3 -3
- package/modules/hall-ui/public/settings.css +24 -4
- package/modules/hall-ui/public/settings.html +21 -17
- package/modules/hall-ui/public/settings.js +44 -44
- package/modules/hall-ui/public/shell-game.css +65 -0
- package/modules/hall-ui/public/shell-game.js +283 -3
- package/modules/hall-ui/public/shell.js +7 -26
- package/modules/hall-ui/public/sky-interaction.js +7 -0
- package/modules/hall-ui/public/speciality-tools.js +1 -2
- package/modules/hall-ui/public/street-inside.js +3 -4
- package/modules/hall-ui/public/street.html +4 -4
- package/modules/hall-ui/public/studio.html +5 -5
- package/modules/hall-ui/public/style.css +73 -47
- package/modules/hall-ui/public/task-detail.js +5 -4
- package/modules/hall-ui/public/task.html +5 -5
- package/modules/hall-ui/public/thinking.css +10 -0
- package/modules/hall-ui/public/thinking.html +9 -6
- package/modules/hall-ui/public/tweak-editor.html +5 -5
- package/modules/hall-ui/public/tweak-editor.js +5 -1
- package/modules/hall-ui/public/update-pill.js +91 -2
- package/modules/hall-ui/public/visor.css +80 -0
- package/modules/hall-ui/public/visor.js +316 -0
- package/modules/hall-ui/public/watch-repo-invite.js +159 -0
- package/modules/hall-ui/public/watch.html +6 -5
- package/modules/hall-ui/public/watch.js +17 -2
- package/modules/hall-ui/public/welcome-boot.js +15 -6
- package/modules/hall-ui/public/work.html +5 -5
- package/modules/hall-ui/records/builders-roster.md +2 -2
- package/modules/hall-ui/records/craft-mode.md +13 -11
- package/modules/hall-ui/records/game-mode.md +2 -2
- package/modules/hall-ui/records/governor-street.md +1 -1
- package/modules/hall-ui/records/home-city.md +3 -1
- package/modules/hall-ui/records/market-watch-settings.md +2 -0
- package/modules/hall-ui/records/options-menu.md +15 -0
- package/modules/hall-ui/records/profile-roles.md +11 -11
- package/modules/hall-ui/records/sky.md +3 -3
- package/modules/hall-ui/records/start-screen.md +21 -0
- package/modules/hall-ui/records/tweak-editor.md +1 -1
- package/modules/hall-ui/records/update-pill.md +12 -0
- package/modules/hall-ui/records/visor.md +23 -0
- package/modules/hall-ui/records/welcome-tower.md +5 -5
- package/modules/ideas/ideator-stats.js +3 -1
- package/modules/ideas/inbox.js +2 -2
- package/modules/lifecycle/activity.js +1 -1
- package/modules/lifecycle/cascade.js +4 -4
- package/modules/lifecycle/goal-spec-task.js +1 -1
- package/modules/lifecycle/help-requests.js +19 -15
- package/modules/lifecycle/kickoff-checklist.js +3 -3
- package/modules/lifecycle/kickoff-seed.js +1 -1
- package/modules/lifecycle/migrations/lifecycle_021_crafts_artisan_artist.sql +29 -0
- package/modules/lifecycle/mingle.js +4 -4
- package/modules/lifecycle/page-tweak-write.js +1 -1
- package/modules/lifecycle/routes/task-write-routes.js +3 -1
- package/modules/lifecycle/routes/tasks.js +3 -3
- package/modules/lifecycle/ship-card.js +2 -3
- package/modules/lifecycle/task-classifier.js +39 -29
- package/modules/lifecycle/tests/cascade.mjs +7 -7
- package/modules/onboarding/founding-mode.js +3 -0
- package/modules/onboarding/genesis-home.js +21 -1
- package/modules/onboarding/routes/onboarding.js +2 -0
- package/modules/page-notes/notes.js +3 -2
- package/modules/page-notes/split.js +1 -1
- package/modules/platform-identity/craft-rollup.js +35 -7
- package/modules/platform-identity/migrations/platform_identity_036_crafts_artisan_artist.sql +45 -0
- package/modules/platform-identity/platform-identity.js +2 -2
- package/modules/platform-identity/routes/community-leaderboard.js +2 -1
- package/modules/platform-identity/routes/community-search.js +2 -1
- package/modules/platform-identity/routes/sso.js +2 -1
- package/modules/platform-identity/tests/platform-identity.mjs +6 -6
- package/modules/public-landing/public/community.html +3 -5
- package/modules/public-landing/public/index.html +1 -1
- package/modules/public-landing/public/projects.html +1 -1
- package/modules/specialities/migrations/specialities_004_crafts_artisan_artist.sql +30 -0
- package/modules/specialities/routes/specialities.js +2 -2
- package/modules/status-ui/public/status.js +1 -1
- package/modules/ui-design/kit/fixtures/community-leaderboard.json +10 -10
- package/modules/ui-design/kit/fixtures/community-search.json +2 -2
- package/modules/ui-design/kit/fixtures/scouting-recruiter.json +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +70 -0
- package/scripts/bongos/autobongos-loop.js +1 -1
- package/scripts/bongos/discipline-modes.json +7 -12
- package/scripts/bongos/fitness.js +1 -1
- package/scripts/bongos/gen-hall-sitemap.js +7 -7
- package/scripts/bongos/grade-smoke.js +2 -2
- package/scripts/bongos/init.js +2 -2
- package/scripts/bongos/newcomer-floor.config.json +1 -2
- package/scripts/bongos/newcomer-floor.js +4 -3
- package/scripts/bongos/newcomer-floor.templates.json +10 -12
- package/scripts/bongos/newcomer-floor.test.js +47 -38
- package/scripts/bongos/role-pack-guard.js +5 -4
- package/scripts/bongos/seed-tasks.example.json +3 -3
- package/scripts/bongos/setup.js +20 -107
- package/scripts/bongos/start.js +2 -2
- package/scripts/bongos/task.js +5 -4
- package/scripts/bongos/triage.js +1 -1
- package/scripts/bongos/worktree.js +16 -4
- package/scripts/setup-builder.js +6 -46
- package/src/bongos/auth-admission.js +8 -8
- package/src/bongos/craft-mode.js +2 -0
- package/src/bongos/db.js +39 -1
- package/src/bongos/github-repos.js +78 -0
- package/src/bongos/module-scope-map.js +22 -0
- package/src/bongos/role-stats.js +27 -11
- package/src/bongos/routes/github.js +100 -2
- package/src/bongos/routes/me.js +33 -0
- package/src/bongos/serve-internal.js +26 -9
- package/src/crafts.js +66 -0
- package/src/module-api.js +6 -1
- package/src/module-loader/manifest-schema.js +16 -0
- package/src/modules.js +2 -2
- package/src/role-responsibilities.js +6 -5
- package/tests/acclaim_feed_evidence_db.mjs +1 -1
- package/tests/activity_rollup_as_of.mjs +1 -1
- package/tests/api_alias_caller_ratchet.mjs +3 -3
- package/tests/application_lifecycle.mjs +1 -1
- package/tests/artist_review.mjs +1 -1
- package/tests/artist_review_goal.mjs +1 -1
- package/tests/blocker_hall_live_proof.mjs +1 -1
- package/tests/blocker_tasks_named_db.mjs +2 -2
- package/tests/canonical_profile_url.mjs +1 -1
- package/tests/cap_newcomer_tasks.mjs +13 -13
- package/tests/character_creation.mjs +294 -0
- package/tests/claim_gate_open_blocker.mjs +2 -2
- package/tests/claim_gate_rebase.mjs +6 -6
- package/tests/claude_materialize.mjs +1 -1
- package/tests/clawback_confirmed_backlog.mjs +3 -3
- package/tests/collab_page.mjs +8 -9
- package/tests/collab_self_settling.mjs +2 -2
- package/tests/community_leaderboard_route.mjs +13 -5
- package/tests/community_proof_db.mjs +3 -0
- package/tests/community_search_route.mjs +7 -6
- package/tests/conductor.mjs +1 -1
- package/tests/conductor_main_e2e.mjs +8 -8
- package/tests/copy_desk_flags_db.mjs +1 -1
- package/tests/copy_desk_page_approve.mjs +2 -2
- package/tests/copy_desk_page_claim.mjs +2 -2
- package/tests/copy_desk_page_next.mjs +1 -1
- package/tests/copy_desk_page_release.mjs +1 -1
- package/tests/copy_desk_page_submit.mjs +2 -2
- package/tests/core_258_box_session_claims_db.mjs +1 -1
- package/tests/craft_mode.mjs +48 -34
- package/tests/craft_mode_home.mjs +20 -20
- package/tests/craft_mode_looks.mjs +16 -15
- package/tests/craft_mode_nav.mjs +31 -29
- package/tests/craft_review_mode.mjs +46 -41
- package/tests/crafts_vocabulary.mjs +70 -0
- package/tests/cross_discipline_priority_direction.mjs +5 -5
- package/tests/fitness.mjs +7 -11
- package/tests/funded_project_pays_proof_db.mjs +1 -1
- package/tests/genesis_home.mjs +21 -1
- package/tests/github_chip.mjs +3 -3
- package/tests/github_repo_invite.mjs +379 -0
- package/tests/goal_criterion_confirm_ui.mjs +1 -1
- package/tests/goal_invite_consent.mjs +1 -1
- package/tests/goal_map_page.mjs +2 -2
- package/tests/goal_sort_staleness.mjs +1 -1
- package/tests/goal_spec_task.mjs +1 -1
- package/tests/goal_spec_task_db.mjs +3 -3
- package/tests/grade_engine.mjs +2 -2
- package/tests/grader_no_artifact.mjs +1 -1
- package/tests/grader_prompt_fencing.mjs +1 -1
- package/tests/grader_prompt_window_bound.mjs +1 -1
- package/tests/grader_reader_lens.mjs +2 -2
- package/tests/grader_severity_gate.mjs +1 -1
- package/tests/grader_unbacked_score.mjs +1 -1
- package/tests/hall_approval_queue.mjs +1 -1
- package/tests/hall_board_world.mjs +2 -2
- package/tests/hall_builders_page_pass.mjs +1 -1
- package/tests/hall_city.mjs +1 -1
- package/tests/hall_fullscreen.mjs +27 -3
- package/tests/hall_kit_charts.mjs +1 -1
- package/tests/hall_landing_boot.mjs +1 -1
- package/tests/hall_my_agents.mjs +16 -16
- package/tests/hall_nav.mjs +22 -15
- package/tests/hall_options_menu.mjs +247 -0
- package/tests/hall_oversight_world.mjs +1 -1
- package/tests/hall_palette.mjs +8 -9
- package/tests/hall_profile_world.mjs +3 -3
- package/tests/hall_record_world.mjs +2 -2
- package/tests/hall_settings_world.mjs +2 -1
- package/tests/hall_sitemap.mjs +6 -5
- package/tests/hall_start_screen.mjs +244 -0
- package/tests/hall_street.mjs +2 -2
- package/tests/hall_street_inside.mjs +17 -9
- package/tests/hall_studio_world.mjs +3 -3
- package/tests/hall_visor.mjs +224 -0
- package/tests/help_requests.mjs +47 -15
- package/tests/helpers.mjs +4 -4
- package/tests/home_city.mjs +6 -4
- package/tests/home_city_widget.mjs +33 -14
- package/tests/home_gallery_widget.mjs +4 -4
- package/tests/home_idea_box.mjs +6 -6
- package/tests/hub_community_page.mjs +3 -3
- package/tests/hub_craft_rollup.mjs +75 -36
- package/tests/hub_recent_ships.mjs +3 -2
- package/tests/idea_cascade.mjs +2 -2
- package/tests/idea_credit_streams_db.mjs +1 -1
- package/tests/idea_detail_page.mjs +1 -1
- package/tests/idea_ideate_full.mjs +5 -2
- package/tests/idea_spark_hall.mjs +1 -1
- package/tests/init.mjs +3 -3
- package/tests/instance_manifest.mjs +2 -2
- package/tests/kickoff_checklist.mjs +1 -1
- package/tests/leaderboard_roles.mjs +34 -25
- package/tests/lifecycle_claim_ladder_refs.mjs +55 -0
- package/tests/lightbulb.mjs +14 -10
- package/tests/main_discipline.mjs +8 -8
- package/tests/markdown_line_endings.mjs +1 -1
- package/tests/market_watch_pages.mjs +52 -74
- package/tests/me_streak_leaderboard_queries.mjs +3 -3
- package/tests/migration_allocation.mjs +1 -1
- package/tests/mingle.mjs +36 -21
- package/tests/module-scope-map.mjs +22 -2
- package/tests/module_api.mjs +1 -0
- package/tests/module_contributions.mjs +13 -13
- package/tests/module_manifest.mjs +29 -0
- package/tests/module_skills_hook.mjs +2 -2
- package/tests/modules.mjs +14 -7
- package/tests/nav_permission_atoms.mjs +2 -1
- package/tests/needs_rebase.mjs +1 -1
- package/tests/newcomer_restock.mjs +13 -13
- package/tests/page_notes_panel.mjs +22 -22
- package/tests/page_tweak_no_block.mjs +1 -1
- package/tests/paintbrush.mjs +7 -7
- package/tests/privacy_matrix.mjs +1 -0
- package/tests/profile_activity.mjs +20 -23
- package/tests/profile_earnings.mjs +1 -1
- package/tests/profile_nudge_prefs.mjs +4 -4
- package/tests/profile_role_headline.mjs +73 -46
- package/tests/profile_role_stats.mjs +55 -45
- package/tests/profile_rollup_consent.mjs +19 -6
- package/tests/profile_route.mjs +12 -12
- package/tests/profile_ui_cross_project.mjs +4 -4
- package/tests/project_invite_suggestions.mjs +2 -2
- package/tests/promote_goal_id.mjs +1 -1
- package/tests/publish_manifest.mjs +1 -1
- package/tests/rank_floor_wiring.mjs +1 -1
- package/tests/regrade_actual_minutes.mjs +1 -1
- package/tests/removal_proposal.mjs +2 -2
- package/tests/reward_gate.mjs +4 -4
- package/tests/reward_mode_cost_plus_only.mjs +1 -1
- package/tests/role_responsibilities.mjs +18 -12
- package/tests/role_stats_db.mjs +17 -16
- package/tests/scouting_rollup_fanout.mjs +5 -5
- package/tests/seed_tasks.mjs +5 -2
- package/tests/session_earnings_mirror_db.mjs +1 -1
- package/tests/session_reward_on_land_db.mjs +1 -1
- package/tests/session_reward_reconcile_db.mjs +2 -2
- package/tests/sessions_kit_pager.mjs +1 -1
- package/tests/settings_art_key_absent.mjs +2 -2
- package/tests/settings_main_role.mjs +21 -20
- package/tests/setup_flow.mjs +3 -3
- package/tests/setup_smoke.mjs +20 -0
- package/tests/ship_card.mjs +6 -6
- package/tests/specialities.mjs +16 -4
- package/tests/speciality_db_shape.mjs +6 -6
- package/tests/speciality_injection.mjs +17 -17
- package/tests/speciality_offer_ui.mjs +5 -5
- package/tests/speciality_offered.mjs +6 -5
- package/tests/speciality_session_skills.mjs +7 -7
- package/tests/speciality_skills.mjs +11 -11
- package/tests/speciality_walkthrough_ui.mjs +2 -2
- package/tests/sso_checkin_identity_spoof.mjs +2 -1
- package/tests/task_classifier_vocab.mjs +10 -0
- package/tests/task_cli.mjs +11 -3
- package/tests/task_criteria_unlink_db.mjs +2 -2
- package/tests/task_discipline_required.mjs +38 -16
- package/tests/task_goal_required.mjs +4 -4
- package/tests/task_needs_db.mjs +1 -1
- package/tests/tweak_replaces_artist_review.mjs +1 -1
- package/tests/update_pill.mjs +153 -2
- package/tests/upkeep_home.mjs +1 -1
- package/tests/v3_c1_options_proof.mjs +145 -0
- package/tests/visual_review.mjs +3 -3
- package/tests/watch_applications_queue.mjs +8 -5
- package/tests/watch_sealed_floor.mjs +3 -1
- package/tests/welcome_boot.mjs +12 -9
- package/tests/welcome_boot_browser.mjs +13 -13
- package/tests/worktree_helper.mjs +16 -1
package/tests/hall_sitemap.mjs
CHANGED
|
@@ -21,9 +21,10 @@ const model = gen.build();
|
|
|
21
21
|
test('the parse found the hall, so nothing below passes against an empty map', () => {
|
|
22
22
|
const files = fs.readdirSync(path.join(ROOT, 'modules/hall-ui/public')).filter((f) => f.endsWith('.html'));
|
|
23
23
|
assert.equal(model.pages.length, files.length, 'one row per page file');
|
|
24
|
-
//
|
|
25
|
-
assert.ok(model.nav.length >=
|
|
26
|
-
|
|
24
|
+
// 20 since task 1005144 moved four settings pages into the options menu.
|
|
25
|
+
assert.ok(model.nav.length >= 20, `expected the shell's nav, found ${model.nav.length} items`);
|
|
26
|
+
// task 1005141: three crafts — Artisan, Artist (Artistry + Ideating), Patron
|
|
27
|
+
for (const c of ['artisan', 'artist', 'patron']) {
|
|
27
28
|
assert.ok(model.crafts.tabs[c].length > model.crafts.always.length, `${c} lists tabs of its own`);
|
|
28
29
|
}
|
|
29
30
|
assert.ok(model.chrome.includes('shell.js') && model.chrome.includes('craft-mode.js'), 'the chrome is the shell');
|
|
@@ -87,10 +88,10 @@ test('the Primer links to Repo Atlas and Commands, and in the hall they open tho
|
|
|
87
88
|
assert.equal(gen.classify('/no-such-page', urls), false);
|
|
88
89
|
});
|
|
89
90
|
|
|
90
|
-
test('the street is
|
|
91
|
+
test('the street is Patron\'s home: no nav item, reached by clicking from the city', () => {
|
|
91
92
|
const street = model.pages.find((p) => p.url === '/street');
|
|
92
93
|
assert.ok(street, 'the street is a hall page');
|
|
93
94
|
assert.deepEqual(street.nav, []);
|
|
94
|
-
assert.ok(street.reachIn.includes('
|
|
95
|
+
assert.ok(street.reachIn.includes('patron'));
|
|
95
96
|
assert.ok(street.in.includes('/city'), 'the city links to the street');
|
|
96
97
|
});
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
// tests/hall_start_screen.mjs — the game's start screen and load screen (task 1005143, BONGOS-V3 R03).
|
|
2
|
+
//
|
|
3
|
+
// WHY. Done-when: a sign-in passes through both before the world. That is a chain of hand-offs a
|
|
4
|
+
// reader would only notice breaking as "the hall looks like it used to": the welcome hands a fresh
|
|
5
|
+
// sign-in to shell-game.js, Start turns game mode on, the load screen builds the city week by week
|
|
6
|
+
// from the one bounded city read, and the craft picker waits until the world is showing. So this
|
|
7
|
+
// RUNS the real welcome-boot.js and shell-game.js: the hand-off and the pure parts in a vm, and
|
|
8
|
+
// the two screens in a real browser over Home's own markup (skipped where there is no browser).
|
|
9
|
+
//
|
|
10
|
+
// Run: node tests/hall_start_screen.mjs
|
|
11
|
+
import assert from 'node:assert/strict';
|
|
12
|
+
import { test, after } from 'node:test';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import vm from 'node:vm';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
|
|
19
|
+
const require = createRequire(import.meta.url);
|
|
20
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
21
|
+
const HALL = path.join(ROOT, 'modules', 'hall-ui', 'public');
|
|
22
|
+
const read = (f) => fs.readFileSync(path.join(HALL, f), 'utf8');
|
|
23
|
+
const CITY = fs.readFileSync(path.join(ROOT, 'scripts', 'hall-preview', 'fixtures', 'home__city.json'), 'utf8');
|
|
24
|
+
|
|
25
|
+
// ---------- in a vm: the pure parts and the welcome's hand-off ----------
|
|
26
|
+
function sandbox({ store = {}, game = true } = {}) {
|
|
27
|
+
const started = [], asked = [];
|
|
28
|
+
const window = {
|
|
29
|
+
location: { search: '', pathname: '/', hash: '' },
|
|
30
|
+
__BRANDING__: { identity: { worldName: 'Harbor' } },
|
|
31
|
+
localStorage: {
|
|
32
|
+
getItem: (k) => (k in store ? store[k] : null),
|
|
33
|
+
setItem: (k, v) => { store[k] = String(v); },
|
|
34
|
+
removeItem: (k) => { delete store[k]; },
|
|
35
|
+
},
|
|
36
|
+
sessionStorage: { getItem: () => null, setItem() {} },
|
|
37
|
+
OTBCraftMode: { markAsked: () => asked.push('marked'), onAccess: (a, b) => asked.push(['picker', a.authed, b.id]) },
|
|
38
|
+
};
|
|
39
|
+
const document = { documentElement: { dataset: {}, classList: { add() {}, remove() {} } }, addEventListener() {} };
|
|
40
|
+
const box = { window, document, URLSearchParams, setTimeout, clearTimeout, console };
|
|
41
|
+
vm.createContext(box);
|
|
42
|
+
if (game) {
|
|
43
|
+
vm.runInContext(read('shell-game.js'), box, { filename: 'shell-game.js' });
|
|
44
|
+
window.OTBShellGame.start = (o) => { started.push(o); return true; };
|
|
45
|
+
}
|
|
46
|
+
vm.runInContext(read('welcome-boot.js'), box, { filename: 'welcome-boot.js' });
|
|
47
|
+
return { window, store, started, asked };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
test('the menu is Start, Options, Achievements, Extras, Quit, in that order', () => {
|
|
51
|
+
const { window } = sandbox();
|
|
52
|
+
const MENU = JSON.parse(JSON.stringify(window.OTBShellGame.MENU)); // out of the vm's realm
|
|
53
|
+
assert.deepEqual(MENU.map((m) => m.label), ['Start', 'Options', 'Achievements', 'Extras', 'Quit']);
|
|
54
|
+
const links = MENU.filter((m) => m.href).map((m) => [m.id, m.href]);
|
|
55
|
+
assert.deepEqual(links, [['options', '/settings'], ['achievements', '/profile']]);
|
|
56
|
+
for (const [, href] of links) assert.ok(fs.existsSync(path.join(HALL, href.slice(1) + '.html')), href + ' is a hall page');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('the load screen walks the weeks oldest first and lands exactly on now', () => {
|
|
60
|
+
const { window } = sandbox();
|
|
61
|
+
const G = window.OTBShellGame;
|
|
62
|
+
const stops = Array.from(G.historySteps(10, 30));
|
|
63
|
+
assert.equal(stops.length, 31);
|
|
64
|
+
assert.equal(stops[0], 0);
|
|
65
|
+
assert.equal(stops[30], 9, 'the last stop is the latest week, not a rounding short of it');
|
|
66
|
+
assert.ok(stops.every((v, i) => i === 0 || v >= stops[i - 1]), 'never backwards');
|
|
67
|
+
assert.deepEqual(Array.from(G.historySteps(1, 30)).filter((v) => v !== 0), [], 'a one-week project stands still');
|
|
68
|
+
assert.equal(G.whenLabel(10, 0), 'The city, 9 weeks ago');
|
|
69
|
+
assert.equal(G.whenLabel(10, 8), 'The city, 1 week ago');
|
|
70
|
+
assert.equal(G.whenLabel(10, 9), 'The city, now');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('the tips rotate, and every one is a sentence', () => {
|
|
74
|
+
const { window } = sandbox();
|
|
75
|
+
const G = window.OTBShellGame;
|
|
76
|
+
const n = G.TIPS.length;
|
|
77
|
+
assert.ok(n >= 3);
|
|
78
|
+
assert.equal(G.tipAt(0), G.TIPS[0]);
|
|
79
|
+
assert.equal(G.tipAt(n), G.TIPS[0], 'wraps');
|
|
80
|
+
assert.equal(G.tipAt(n + 1), G.TIPS[1]);
|
|
81
|
+
assert.equal(G.tipAt('garbage'), G.TIPS[0]);
|
|
82
|
+
for (const t of G.TIPS) assert.match(t, /^[A-Z].*\.$/);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('a fresh sign-in is handed to the start screen with the still, and the craft picker waits for the world', () => {
|
|
86
|
+
const s = sandbox({ store: { 'hall-welcome-signin': '1' } });
|
|
87
|
+
const access = { authed: true }, builder = { id: 7, display_name: 'Ada' };
|
|
88
|
+
assert.equal(s.window.OTBWelcome.onAccess(access, builder), true, 'the shell is told a screen is up, so it asks no picker');
|
|
89
|
+
assert.equal(s.started.length, 1);
|
|
90
|
+
assert.equal(s.started[0].name, 'Harbor');
|
|
91
|
+
assert.match(s.started[0].art, /^\/builders\/welcome-(day|night)\.webp$/);
|
|
92
|
+
assert.equal(s.store['hall-welcome-signin'], undefined, 'the flag is spent');
|
|
93
|
+
assert.deepEqual(s.asked, [], 'nothing is asked while the start screen is up');
|
|
94
|
+
s.started[0].done();
|
|
95
|
+
assert.deepEqual(s.asked, [['picker', true, 7]], 'the picker is asked once the world is showing');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('a visit that is not a fresh sign-in keeps the welcome, and a hall without shell-game.js greets a sign-in as before', () => {
|
|
99
|
+
const daily = sandbox();
|
|
100
|
+
assert.equal(daily.window.OTBWelcome.onAccess({ authed: true }, { id: 7 }), true);
|
|
101
|
+
assert.deepEqual([daily.started.length, daily.asked], [0, ['marked']], 'the day\'s first visit is the welcome\'s');
|
|
102
|
+
const bare = sandbox({ store: { 'hall-welcome-signin': '1' }, game: false });
|
|
103
|
+
assert.equal(bare.window.OTBWelcome.onAccess({ authed: true }, { id: 7 }), true);
|
|
104
|
+
assert.deepEqual(bare.asked, ['marked']);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('Home loads the sheet and the script, and the sheet types in no size, corner or breakpoint', () => {
|
|
108
|
+
const html = read('index.html');
|
|
109
|
+
assert.match(html, /<link rel="stylesheet" href="\/builders\/shell-game\.css[^"]*">/);
|
|
110
|
+
assert.ok(html.indexOf('welcome-boot.js') < html.indexOf('shell.js?'), 'the welcome, which hands over, loads before the shell');
|
|
111
|
+
const css = read('shell-game.css').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
112
|
+
assert.doesNotMatch(css, /font-size:\s*[\d.]/);
|
|
113
|
+
assert.doesNotMatch(css, /border-radius:\s*[\d.]/);
|
|
114
|
+
assert.doesNotMatch(css, /@media/);
|
|
115
|
+
assert.doesNotMatch(css, /#[0-9a-fA-F]{3,8}\b/, 'tokens, or black and white: no private palette');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// ---------- in a browser: the two screens over Home's own markup ----------
|
|
119
|
+
const lib = require(path.join(ROOT, 'modules', 'ui-design', 'kit', 'lib.js'));
|
|
120
|
+
let browser = null;
|
|
121
|
+
try { browser = await lib.launchBrowser(); } catch (e) { if (!(e instanceof lib.NoBrowserError)) throw e; }
|
|
122
|
+
after(() => browser && browser.close());
|
|
123
|
+
const skip = !browser && 'no system Edge or Chrome';
|
|
124
|
+
|
|
125
|
+
const ORIGIN = 'http://hall.test';
|
|
126
|
+
const TYPES = { '.js': 'text/javascript', '.css': 'text/css', '.webp': 'image/webp' };
|
|
127
|
+
const INDEX = read('index.html').replace(/<script\b[^>]*>[\s\S]*?<\/script>/g, '').replace(/\{\{[^}]*\}\}/g, '');
|
|
128
|
+
const STUB = '<script>window.__BRANDING__ = { identity: { worldName: "Harbor" } }; window.BongosClient = { createClient: () => ({ request: async () => ({ ok: false, status: 404, data: null }) }) };</script>';
|
|
129
|
+
const SCRIPTS = ['dom-utils.js', 'welcome-boot.js', 'shell-game.js', 'shell-trail.js', 'craft-mode.js', 'shell.js', 'city-draw.js', 'home-city-draw.js']
|
|
130
|
+
.map((f) => `<script src="/builders/${f}"></script>`).join('');
|
|
131
|
+
const PAGE = INDEX.replace('</body>', STUB + SCRIPTS + '</body>');
|
|
132
|
+
|
|
133
|
+
async function hall({ city = true, reducedMotion = 'no-preference' } = {}) {
|
|
134
|
+
const ctx = await browser.newContext({ viewport: { width: 1280, height: 800 }, reducedMotion });
|
|
135
|
+
await ctx.route(ORIGIN + '/**', (route) => {
|
|
136
|
+
const p = new URL(route.request().url()).pathname;
|
|
137
|
+
if (p === '/' || p === '/index.html') return route.fulfill({ contentType: 'text/html', body: PAGE });
|
|
138
|
+
if (p === '/api/bongos/home/city') return city ? route.fulfill({ contentType: 'application/json', body: CITY }) : route.fulfill({ status: 500, body: '' });
|
|
139
|
+
const ext = path.extname(p);
|
|
140
|
+
const f = path.join(HALL, p.replace(/^\/builders\//, ''));
|
|
141
|
+
if (p.startsWith('/builders/') && TYPES[ext] && fs.existsSync(f)) return route.fulfill({ contentType: TYPES[ext], body: fs.readFileSync(f) });
|
|
142
|
+
return route.fulfill({ status: 404, body: '' });
|
|
143
|
+
});
|
|
144
|
+
return ctx;
|
|
145
|
+
}
|
|
146
|
+
// A page that has just come back from signing in: the flag the sign-in click left, then /me.
|
|
147
|
+
async function signedIn(ctx) {
|
|
148
|
+
const page = await ctx.newPage();
|
|
149
|
+
const errors = [];
|
|
150
|
+
page.on('pageerror', (e) => errors.push(String(e.message)));
|
|
151
|
+
await page.addInitScript(() => { try { localStorage.setItem('hall-welcome-signin', '1'); } catch (_) { /* no storage */ } });
|
|
152
|
+
await page.goto(ORIGIN + '/');
|
|
153
|
+
await page.waitForFunction(() => !!window.OTBShell && !!window.OTBWelcome && !!window.OTBShellGame);
|
|
154
|
+
await page.evaluate(() => {
|
|
155
|
+
const me = { authed: true, rank: 'metic', builder: { id: 7, display_name: 'Ada', github_login: 'ada' } };
|
|
156
|
+
window.OTBShell.applyAccess(me);
|
|
157
|
+
window.OTBShell.applyAccess(me);
|
|
158
|
+
});
|
|
159
|
+
return { page, errors };
|
|
160
|
+
}
|
|
161
|
+
const state = (page) => page.evaluate(() => ({
|
|
162
|
+
start: !!document.getElementById('game-start'),
|
|
163
|
+
welcome: !!document.getElementById('welcome-boot'),
|
|
164
|
+
picker: !!document.getElementById('craft-picker'),
|
|
165
|
+
game: document.documentElement.dataset.game || null,
|
|
166
|
+
full: document.documentElement.dataset.fullscreen || null,
|
|
167
|
+
}));
|
|
168
|
+
|
|
169
|
+
test('a sign-in passes through the start screen and the load screen before the world', { skip }, async () => {
|
|
170
|
+
const ctx = await hall();
|
|
171
|
+
const { page, errors } = await signedIn(ctx);
|
|
172
|
+
await page.waitForSelector('#game-start');
|
|
173
|
+
assert.deepEqual(await state(page), { start: true, welcome: false, picker: false, game: null, full: null },
|
|
174
|
+
'the start screen, not the welcome and not the picker; game mode is not on until Start');
|
|
175
|
+
const menu = await page.evaluate(() => ({
|
|
176
|
+
title: document.getElementById('gs-title').textContent,
|
|
177
|
+
items: Array.from(document.querySelectorAll('.gs__items:not(.gs__items--extras) .gs__item')).map((e) => e.textContent),
|
|
178
|
+
focus: document.activeElement.textContent,
|
|
179
|
+
small: Array.from(document.querySelectorAll('.gs__items:not(.gs__items--extras) .gs__item')).filter((e) => e.getBoundingClientRect().height < 46).length,
|
|
180
|
+
}));
|
|
181
|
+
assert.deepEqual(menu, { title: 'Harbor', items: ['Start', 'Options', 'Achievements', 'Extras', 'Quit'], focus: 'Start', small: 0 });
|
|
182
|
+
|
|
183
|
+
// The arrows walk the menu; Escape stays with the screen and does not flip the shell underneath.
|
|
184
|
+
await page.keyboard.press('ArrowDown');
|
|
185
|
+
assert.equal(await page.evaluate(() => document.activeElement.textContent), 'Options');
|
|
186
|
+
await page.keyboard.press('ArrowUp');
|
|
187
|
+
await page.keyboard.press('Escape');
|
|
188
|
+
await page.evaluate(() => new Promise((r) => setTimeout(r, 50)));
|
|
189
|
+
assert.equal((await state(page)).start, true, 'Escape does not dismiss the start screen');
|
|
190
|
+
|
|
191
|
+
// Extras opens its links; Escape folds them away again.
|
|
192
|
+
await page.click('[data-gs="extras"]');
|
|
193
|
+
assert.equal(await page.isVisible('#gs-extras a'), true);
|
|
194
|
+
await page.keyboard.press('Escape');
|
|
195
|
+
assert.equal(await page.isVisible('#gs-extras a'), false);
|
|
196
|
+
|
|
197
|
+
// Start: game mode on, the load screen with a tip, and the city building up to now.
|
|
198
|
+
await page.click('[data-gs="start"]');
|
|
199
|
+
await page.waitForSelector('#gs-load:not([hidden])');
|
|
200
|
+
assert.equal(await page.isVisible('.gs__menu'), false);
|
|
201
|
+
assert.match(await page.textContent('#gs-tip'), /\.$/);
|
|
202
|
+
await page.waitForFunction(() => /weeks? ago$/.test(document.getElementById('gs-when').textContent));
|
|
203
|
+
assert.ok(await page.evaluate(() => document.querySelectorAll('#gs-city *').length > 20), 'the city is drawn, not a placeholder');
|
|
204
|
+
await page.waitForFunction(() => { const w = document.getElementById('gs-when'); return !w || w.textContent === 'The city, now'; });
|
|
205
|
+
await page.waitForSelector('#game-start', { state: 'detached' });
|
|
206
|
+
await page.waitForSelector('#craft-picker');
|
|
207
|
+
assert.deepEqual(await state(page), { start: false, welcome: false, picker: true, game: 'on', full: 'on' },
|
|
208
|
+
'the world: game mode on, the bars gone, and only now the craft picker');
|
|
209
|
+
assert.equal(await page.evaluate(() => localStorage.getItem('hall-game')), 'on');
|
|
210
|
+
assert.deepEqual(errors, []);
|
|
211
|
+
await ctx.close();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('Quit closes the tab, and says so when the browser keeps it open', { skip }, async () => {
|
|
215
|
+
const ctx = await hall();
|
|
216
|
+
const { page } = await signedIn(ctx);
|
|
217
|
+
await page.waitForSelector('#game-start');
|
|
218
|
+
await page.evaluate(() => { window.__closed = 0; window.close = () => { window.__closed += 1; }; });
|
|
219
|
+
await page.click('[data-gs="quit"]');
|
|
220
|
+
await page.waitForSelector('#gs-note:not([hidden])');
|
|
221
|
+
assert.equal(await page.evaluate(() => window.__closed), 1);
|
|
222
|
+
assert.match(await page.textContent('#gs-note'), /Close it to quit/);
|
|
223
|
+
await ctx.close();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test('with no city to draw, or with reduced motion, the still and the tip stand and the world still opens', { skip }, async () => {
|
|
227
|
+
for (const opts of [{ city: false }, { reducedMotion: 'reduce' }]) {
|
|
228
|
+
const ctx = await hall(opts);
|
|
229
|
+
const { page, errors } = await signedIn(ctx);
|
|
230
|
+
await page.waitForSelector('#game-start');
|
|
231
|
+
await page.click('[data-gs="start"]');
|
|
232
|
+
await page.waitForSelector('#gs-load:not([hidden])');
|
|
233
|
+
assert.match(await page.textContent('#gs-tip'), /\.$/);
|
|
234
|
+
if (opts.reducedMotion) {
|
|
235
|
+
await page.waitForFunction(() => { const w = document.getElementById('gs-when'); return !w || !w.hidden; });
|
|
236
|
+
assert.equal(await page.evaluate(() => { const w = document.getElementById('gs-when'); return w ? w.textContent : 'The city, now'; }), 'The city, now',
|
|
237
|
+
'no week-by-week run: the finished city at once');
|
|
238
|
+
}
|
|
239
|
+
await page.waitForSelector('#game-start', { state: 'detached' });
|
|
240
|
+
assert.equal((await state(page)).game, 'on');
|
|
241
|
+
assert.deepEqual(errors, []);
|
|
242
|
+
await ctx.close();
|
|
243
|
+
}
|
|
244
|
+
});
|
package/tests/hall_street.mjs
CHANGED
|
@@ -122,11 +122,11 @@ test('a task not started is the prototype\'s workItem "ready" slab, and the more
|
|
|
122
122
|
const p = new Proto({ mode: 'dark' });
|
|
123
123
|
const T = p.theme('dark');
|
|
124
124
|
const a = []; const b = [];
|
|
125
|
-
p.workItem(a, T, '
|
|
125
|
+
p.workItem(a, T, 'artisan', 'ready', 660, 19, 0);
|
|
126
126
|
S.work(b, S.theme('dark'), 'slab', 660, 0, 19, 6, false);
|
|
127
127
|
assert.deepEqual(b, cityTokens(a), 'the slab, at a base of 0 as workItem draws it');
|
|
128
128
|
const c = []; const d = [];
|
|
129
|
-
p.workItem(c, T, '
|
|
129
|
+
p.workItem(c, T, 'artisan', 'backlog', 660, 40, 0);
|
|
130
130
|
S.work(d, S.theme('dark'), 'stakes', 660, 0, 40, 0, false);
|
|
131
131
|
assert.deepEqual(d, c, 'the stakes');
|
|
132
132
|
});
|
|
@@ -131,7 +131,15 @@ test('every district is the prototype\'s road of work, both modes', () => {
|
|
|
131
131
|
const p = proto(mode, { view: key, ...st });
|
|
132
132
|
const want = p.districtView(p.theme(mode), key);
|
|
133
133
|
const got = I.districtView(S.theme(mode), key, p.districtInfo(key));
|
|
134
|
-
for (const k of ['kicker', 'name', 'line', 'road', 'ground', 'tx', 'ty', 's', 'shapes'
|
|
134
|
+
for (const k of ['kicker', 'name', 'line', 'road', 'ground', 'tx', 'ty', 's', 'shapes']) assert.deepEqual(j(got[k]), j(want[k]), `${mode} ${key} ${k}`);
|
|
135
|
+
// The legend's craft line is the one place the hall leaves the prototype's words
|
|
136
|
+
// (task 1005141): the engineer is the artisan, and the ideator — folded into the
|
|
137
|
+
// artist — no longer has a pavilion of its own. The status line is unchanged.
|
|
138
|
+
assert.deepEqual(j(got.legend), [
|
|
139
|
+
{ key: 'a', text: 'Shape is the craft: tower for artisan, studio roof for artist, glass front for UI.' },
|
|
140
|
+
j(want.legend[1]),
|
|
141
|
+
], `${mode} ${key} legend`);
|
|
142
|
+
assert.equal(want.legend[0].key, 'a', 'the prototype still has the craft line first');
|
|
135
143
|
assert.deepEqual(got.items.map((i) => [i.x, i.y, i.w, i.name, i.meta, i.stage, i.stageColor, i.stageEdge]), want.items.map((i) => [i.x, i.y, i.w, i.name, i.meta, i.stage, i.stageColor, i.stageEdge]), `${mode} ${key} items`);
|
|
136
144
|
}
|
|
137
145
|
}
|
|
@@ -142,7 +150,7 @@ test('workItem and crown are verbatim: every craft at every stage, every crown',
|
|
|
142
150
|
for (const mode of ['dark', 'light']) {
|
|
143
151
|
const p = proto(mode);
|
|
144
152
|
const T = p.theme(mode);
|
|
145
|
-
for (const craft of ['
|
|
153
|
+
for (const craft of ['artisan', 'artist', 'ui']) {
|
|
146
154
|
for (const stage of ['backlog', 'ready', 'being built', 'shipped', 'being graded', 'awaiting a tester']) {
|
|
147
155
|
const a = []; const b = [];
|
|
148
156
|
p.workItem(a, T, craft, stage, 86, 40, 92);
|
|
@@ -418,16 +426,16 @@ test('the lift runs to the chosen floor, and the chosen block flips on every pic
|
|
|
418
426
|
|
|
419
427
|
test('a district is its own goal\'s tasks: shape from the craft, stage from the status, label the kind', () => {
|
|
420
428
|
const t = (id, status, discipline, kind = 'feature', goal_id = '7', extra = {}) => ({ id: String(id), title: `task ${id}`, status, discipline, kind, goal_id, est_minutes: 120, ...extra });
|
|
421
|
-
const tasks = [t(1, 'shipped', '
|
|
429
|
+
const tasks = [t(1, 'shipped', 'artisan'), t(2, 'ready', 'artist'), t(3, 'active', 'ui'), t(4, 'confirmed', 'artist', 'spike'), t(5, 'backlog', null, 'bug'), t(6, 'blocked', 'artisan'), t(7, 'completed', 'artist'), t(8, 'cut', 'artisan'), t(9, 'shipped', 'ui', 'feature', '8')];
|
|
422
430
|
const d = I.districtInfo('7', goal(7), tasks);
|
|
423
431
|
assert.deepEqual(d.items.map((i) => [i[0], i[1], i[2], i[3], i[5]]), [
|
|
424
|
-
['task 1', 'feature', '
|
|
425
|
-
['task 4', 'spike', '
|
|
432
|
+
['task 1', 'feature', 'artisan', 'shipped', 'shipped'],
|
|
433
|
+
['task 4', 'spike', 'artist', 'graded · landing', 'graded · landing'],
|
|
426
434
|
['task 7', 'feature', 'artist', 'being graded', 'being graded'],
|
|
427
435
|
['task 3', 'feature', 'ui', 'being built', 'being built'],
|
|
428
436
|
['task 2', 'feature', 'artist', 'ready', 'ready'],
|
|
429
|
-
['task 6', 'feature', '
|
|
430
|
-
['task 5', 'bug', '
|
|
437
|
+
['task 6', 'feature', 'artisan', 'backlog', 'blocked'],
|
|
438
|
+
['task 5', 'bug', 'artisan', 'backlog', 'backlog'],
|
|
431
439
|
], 'in the order work moves; a cut task and another goal\'s are not drawn; no craft is a tower');
|
|
432
440
|
assert.equal(d.items[0][4], 56, 'height from the estimate, never a random');
|
|
433
441
|
assert.equal(d.items[4][4], 0, 'a slab or a stake has no height');
|
|
@@ -439,7 +447,7 @@ test('a district is its own goal\'s tasks: shape from the craft, stage from the
|
|
|
439
447
|
});
|
|
440
448
|
|
|
441
449
|
test('past ten pieces of work the road keeps the ones in flight and counts the rest', () => {
|
|
442
|
-
const tasks = Array.from({ length: 30 }, (_, i) => ({ id: String(i + 1), title: `t${i + 1}`, status: i < 3 ? 'active' : i < 25 ? 'shipped' : 'backlog', discipline: '
|
|
450
|
+
const tasks = Array.from({ length: 30 }, (_, i) => ({ id: String(i + 1), title: `t${i + 1}`, status: i < 3 ? 'active' : i < 25 ? 'shipped' : 'backlog', discipline: 'artisan', kind: 'feature', goal_id: '9', shipped_at: `2026-09-${String(1 + (i % 28)).padStart(2, '0')}T00:00:00Z` }));
|
|
443
451
|
const d = I.districtInfo('9', goal(9), tasks);
|
|
444
452
|
assert.equal(d.items.length, I.MAX_ITEMS);
|
|
445
453
|
assert.equal(d.more, 20);
|
|
@@ -636,7 +644,7 @@ test('a matter is a desk row: its name opens it in place; back to the street ret
|
|
|
636
644
|
});
|
|
637
645
|
|
|
638
646
|
test('a district address reads its goal\'s tasks once and draws them as buildings', async () => {
|
|
639
|
-
const tasks = [{ id: '1', title: 'checkout', status: 'shipped', discipline: '
|
|
647
|
+
const tasks = [{ id: '1', title: 'checkout', status: 'shipped', discipline: 'artisan', kind: 'feature', goal_id: '7' }, { id: '2', title: 'tax forms', status: 'active', discipline: 'ui', kind: 'feature', goal_id: '7' }, { id: '3', title: 'other goal', status: 'ready', goal_id: '8' }];
|
|
640
648
|
const p = await bootPage({ hash: '#district=7', feeds: FEEDS([], { '/api/bongos/tasks?goal_id=7': { tasks } }) });
|
|
641
649
|
await new Promise((r) => setTimeout(r, 0));
|
|
642
650
|
assert.equal(p.calls.filter((c) => c === '/api/bongos/tasks?goal_id=7').length, 1);
|
|
@@ -277,7 +277,7 @@ async function bootStudio({ reviews = [], shipped = {}, rank = 'metic', flags =
|
|
|
277
277
|
// the same reason categoryChip is: an undefined helper throws mid-render.
|
|
278
278
|
roleAxisLabel: () => 'Role',
|
|
279
279
|
roleLabel: (d) => (!d ? '' : d === 'unclassified' ? 'No role yet' : d === 'ui' ? 'UI' : d.charAt(0).toUpperCase() + d.slice(1)),
|
|
280
|
-
roleOptions: () => [{ value: '', label: '— any —' }, { value: '
|
|
280
|
+
roleOptions: () => [{ value: '', label: '— any —' }, { value: 'artisan', label: 'Artisan' }, { value: 'artist', label: 'Artist' }],
|
|
281
281
|
roleIsUnset: (d) => !d || d === 'unclassified',
|
|
282
282
|
ROLE_UNSET: 'unclassified',
|
|
283
283
|
escapeHtml: (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])),
|
|
@@ -592,9 +592,9 @@ function bootShell() {
|
|
|
592
592
|
test('the Studio nav entry is warmed only for an artist-craft builder', () => {
|
|
593
593
|
const { applyAccess, navStudio } = bootShell();
|
|
594
594
|
|
|
595
|
-
applyAccess({ authed: true, rank: 'archon', builder: { display_name: 'Artist A', preferred_disciplines: ['artist'
|
|
595
|
+
applyAccess({ authed: true, rank: 'archon', builder: { display_name: 'Artist A', preferred_disciplines: ['artist'] } });
|
|
596
596
|
assert.ok(navStudio()._classes.has('nav-item--craft'), 'an artist-craft builder gets the warmed entry');
|
|
597
597
|
|
|
598
|
-
applyAccess({ authed: true, rank: 'archon', builder: { display_name: 'Eng E', preferred_disciplines: ['
|
|
598
|
+
applyAccess({ authed: true, rank: 'archon', builder: { display_name: 'Eng E', preferred_disciplines: ['artisan'] } });
|
|
599
599
|
assert.ok(!navStudio()._classes.has('nav-item--craft'), 'and a non-artist builder does not (the class is toggled off)');
|
|
600
600
|
});
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// tests/hall_visor.mjs — the first-person visor (task 1005148, BONGOS-V3 R08; ADR 0399).
|
|
2
|
+
//
|
|
3
|
+
// The checks ADR 0399 §4 names, driven in a real browser over a stand-in scene that
|
|
4
|
+
// behaves like the sky (a canvas that pans on drag, a bubble Escape listener that
|
|
5
|
+
// unpicks and calls preventDefault only when it closed something):
|
|
6
|
+
// - the glass passes pointers through — a drag on the world with the visor up pans it;
|
|
7
|
+
// - an open sheet's Escape leaves the star picked;
|
|
8
|
+
// - in game mode the last Escape reaches the options menu, never the visor.
|
|
9
|
+
// Plus the criterion's own proof half that is the visor's: an Artisan and an Artist
|
|
10
|
+
// are offered DIFFERENT actions on the glass. The pure parts run in a vm.
|
|
11
|
+
import assert from 'node:assert/strict';
|
|
12
|
+
import { test, after } from 'node:test';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import vm from 'node:vm';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
|
|
19
|
+
const require = createRequire(import.meta.url);
|
|
20
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
21
|
+
const HALL = path.join(ROOT, 'modules/hall-ui/public');
|
|
22
|
+
const read = (f) => fs.readFileSync(path.join(HALL, f), 'utf8');
|
|
23
|
+
const VISOR = read('visor.js');
|
|
24
|
+
|
|
25
|
+
// ---------- the pure parts, in a vm ----------
|
|
26
|
+
function loadPure() {
|
|
27
|
+
const listeners = {};
|
|
28
|
+
const doc = {
|
|
29
|
+
readyState: 'complete', body: null, documentElement: { dataset: {}, classList: { contains: () => false, toggle() {}, remove() {} } },
|
|
30
|
+
addEventListener(ev, fn) { listeners[ev] = fn; }, querySelector: () => null, createElement: () => ({}),
|
|
31
|
+
};
|
|
32
|
+
const win = { document: doc, location: { pathname: '/', assign() {} }, addEventListener() {} };
|
|
33
|
+
win.window = win;
|
|
34
|
+
vm.runInNewContext(VISOR, win);
|
|
35
|
+
return win.OTBVisor;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
test('each character is offered its own actions, and the Artisan\'s and the Artist\'s differ', () => {
|
|
39
|
+
const V = loadPure();
|
|
40
|
+
const ids = (c) => JSON.parse(JSON.stringify(V.actionsFor(c).map((a) => a.id))); // out of the vm's realm
|
|
41
|
+
assert.deepEqual(ids('artisan'), ['claim', 'blockers', 'sky', 'thinking']);
|
|
42
|
+
assert.deepEqual(ids('artist'), ['review', 'words', 'idea', 'studio']);
|
|
43
|
+
assert.deepEqual(ids('patron'), ['board', 'city', 'blockers', 'watch']);
|
|
44
|
+
assert.equal(ids('artisan').filter((id) => ids('artist').includes(id)).length, 0, 'the two glasses share no action');
|
|
45
|
+
assert.equal(V.actionsFor('nobody').length, 0, 'an unknown character gets nothing, not a default');
|
|
46
|
+
for (const c of ['artisan', 'artist', 'patron']) {
|
|
47
|
+
const keys = V.actionsFor(c).map((a) => a.key);
|
|
48
|
+
assert.equal(new Set(keys).size, keys.length, `${c}: one key per action`);
|
|
49
|
+
assert.ok(!keys.includes('a'), `${c}: A is the sheet's`);
|
|
50
|
+
for (const k of keys) assert.ok(!['l', '0', '/', '?', 'f', 'k'].includes(k), `${c}: ${k} is not a key a scene already binds`);
|
|
51
|
+
for (const a of V.actionsFor(c)) assert.ok(a.href || a.tool, `${c}: ${a.id} opens a page or a tool the hall already has`);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('a key means an action only when nothing else spent it', () => {
|
|
56
|
+
const V = loadPure();
|
|
57
|
+
const ev = (key, extra = {}) => ({ key, defaultPrevented: false, ctrlKey: false, metaKey: false, altKey: false, ...extra });
|
|
58
|
+
assert.equal(V.keyFor('artisan', ev('c')).id, 'claim');
|
|
59
|
+
assert.equal(V.keyFor('artisan', ev('C')).id, 'claim', 'case does not matter');
|
|
60
|
+
assert.equal(V.keyFor('artisan', ev('a')), 'sheet');
|
|
61
|
+
assert.equal(V.keyFor('artisan', ev('r')), null, 'the Artist\'s key is not the Artisan\'s');
|
|
62
|
+
assert.equal(V.keyFor('artisan', ev('c', { defaultPrevented: true })), null, 'a scene that used the key keeps it');
|
|
63
|
+
for (const m of ['ctrlKey', 'metaKey', 'altKey']) assert.equal(V.keyFor('artisan', ev('c', { [m]: true })), null, `${m} leaves it to the browser`);
|
|
64
|
+
assert.equal(V.keyFor('artisan', ev('Escape')), null, 'the ambient glass never takes Escape (ADR 0399 §2.4)');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('credits, never a payout: the count reads as credits, with thousands grouped', () => {
|
|
68
|
+
const V = loadPure();
|
|
69
|
+
assert.equal(V.creditsText(56021), '56,021 credits');
|
|
70
|
+
assert.equal(V.creditsText(1), '1 credit');
|
|
71
|
+
assert.equal(V.creditsText(0), '0 credits');
|
|
72
|
+
assert.equal(V.creditsText(null), '');
|
|
73
|
+
assert.doesNotMatch(V.creditsText(56021), /[$€£]/, 'a count of credits, not a sum of money');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('source: the visor loads after the shell on every scene page, with its sheet, and nowhere else', () => {
|
|
77
|
+
for (const f of ['index.html', 'city.html', 'goal-map.html', 'thinking.html']) {
|
|
78
|
+
const html = read(f);
|
|
79
|
+
const at = (s) => html.indexOf(s);
|
|
80
|
+
assert.ok(at('/builders/visor.js') > at('/builders/shell.js?') && at('/builders/shell.js?') > at('/builders/shell-game.js'), `${f}: shell-game, shell, then the visor`);
|
|
81
|
+
assert.match(html, /<link rel="stylesheet" href="\/builders\/visor\.css/, `${f}: links visor.css`);
|
|
82
|
+
}
|
|
83
|
+
for (const f of ['sky-interaction.js', 'city.js', 'home-city.js']) assert.match(read(f), /window\.__visorScene = /, `${f} registers its scene`);
|
|
84
|
+
assert.doesNotMatch(VISOR, /cam\.(x|y|k)\s*=/, 'the visor writes no camera (ADR 0399 §2.2)');
|
|
85
|
+
assert.doesNotMatch(VISOR, /setAttribute\('hidden'|\.hidden = true;[^\n]*nav/, 'it hides nothing but itself');
|
|
86
|
+
assert.match(read('visor.css'), /html\[data-game="on"\] \.visor/, 'game mode only');
|
|
87
|
+
assert.match(read('visor.css'), /pointer-events: none/, 'the glass lets the pointer through');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// ---------- in a browser: the glass over a stand-in scene ----------
|
|
91
|
+
const lib = require(path.join(ROOT, 'modules', 'ui-design', 'kit', 'lib.js'));
|
|
92
|
+
let browser = null;
|
|
93
|
+
try { browser = await lib.launchBrowser(); } catch (e) { if (!(e instanceof lib.NoBrowserError)) throw e; }
|
|
94
|
+
after(() => browser && browser.close());
|
|
95
|
+
const skip = !browser && 'no system Edge or Chrome';
|
|
96
|
+
|
|
97
|
+
const ORIGIN = 'http://hall.test';
|
|
98
|
+
const TYPES = { '.js': 'text/javascript', '.css': 'text/css', '.png': 'image/png' };
|
|
99
|
+
// The stand-in scene: a canvas filling the window that pans on drag, and the sky's Escape rule.
|
|
100
|
+
const SCENE = `<script>
|
|
101
|
+
window.cam = { x: 0, y: 0 }; window.picked = true; window.optionsOpened = 0;
|
|
102
|
+
window.OTBOptions = { escape() { window.optionsOpened++; } };
|
|
103
|
+
const cv = document.getElementById('world'); let drag = null;
|
|
104
|
+
cv.addEventListener('pointerdown', (e) => { drag = { x: e.clientX, y: e.clientY, cx: cam.x, cy: cam.y }; });
|
|
105
|
+
cv.addEventListener('pointermove', (e) => { if (drag) { cam.x = drag.cx - (e.clientX - drag.x); cam.y = drag.cy - (e.clientY - drag.y); } });
|
|
106
|
+
cv.addEventListener('pointerup', () => { drag = null; });
|
|
107
|
+
addEventListener('keydown', (e) => { if (e.key === 'Escape' && window.picked) { e.preventDefault(); window.picked = false; } });
|
|
108
|
+
window.OTBCraftMode = { held: 'artisan', character() { return this.held; }, current() { return null; },
|
|
109
|
+
me() { return { builder: { total_credits: 56021 } }; }, spriteSrc(id) { return '/builders/characters/' + id + '.png'; }, openTool(id) { window.toolOpened = id; } };
|
|
110
|
+
window.__visorScene = { name: 'test', place: 'Test city', actions: [{ label: 'Back to centre', key: '0', run() { window.centred = true; } }], keys: [{ label: 'Look around', key: 'drag' }] };
|
|
111
|
+
</script>`;
|
|
112
|
+
const PAGE = (game) => `<!doctype html><html${game ? ' data-game="on"' : ''}><head><meta charset="utf-8">
|
|
113
|
+
<link rel="stylesheet" href="/builders/visor.css"><style>:root{--ink:#111;--ink-soft:#555;--ink-faint:#777;--rule:#ccc;--rule-soft:#ddd;--bg-card:#fff;--bg-deep:#eee;--accent:#fb9569;--accent-on:#111;--accent-deep:#a3481a;--focus:#a3481a}
|
|
114
|
+
body{margin:0}#world{position:fixed;inset:0;width:100vw;height:100vh;display:block}</style></head>
|
|
115
|
+
<body><canvas id="world"></canvas>${SCENE}<script src="/builders/shell-game.js"></script><script src="/builders/visor.js"></script></body></html>`;
|
|
116
|
+
|
|
117
|
+
async function open({ game = true } = {}) {
|
|
118
|
+
const ctx = await browser.newContext({ viewport: { width: 1280, height: 800 } });
|
|
119
|
+
await ctx.route(ORIGIN + '/**', (route) => {
|
|
120
|
+
const p = new URL(route.request().url()).pathname;
|
|
121
|
+
if (p === '/' ) return route.fulfill({ contentType: 'text/html', body: PAGE(game) });
|
|
122
|
+
const f = path.join(HALL, p.replace(/^\/builders\//, ''));
|
|
123
|
+
if (p.startsWith('/builders/') && TYPES[path.extname(p)] && fs.existsSync(f)) return route.fulfill({ contentType: TYPES[path.extname(p)], body: fs.readFileSync(f) });
|
|
124
|
+
return route.fulfill({ status: 404, body: '' });
|
|
125
|
+
});
|
|
126
|
+
const page = await ctx.newPage();
|
|
127
|
+
const errors = [];
|
|
128
|
+
page.on('pageerror', (e) => errors.push(String(e.message)));
|
|
129
|
+
await page.addInitScript((g) => { try { localStorage.setItem('hall-game', g ? 'on' : 'off'); window.OTB = { escapeHtml: (s) => String(s) }; } catch (_) { /* none */ } }, game);
|
|
130
|
+
await page.goto(ORIGIN + '/');
|
|
131
|
+
await page.waitForFunction(() => !!window.OTBVisor);
|
|
132
|
+
return { ctx, page, errors };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
test('outside game mode the hall is as it was: no glass', { skip }, async () => {
|
|
136
|
+
const { ctx, page, errors } = await open({ game: false });
|
|
137
|
+
assert.equal(await page.evaluate(() => getComputedStyle(document.getElementById('visor')).display), 'none');
|
|
138
|
+
assert.deepEqual(errors, []);
|
|
139
|
+
await ctx.close();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('in game mode the glass shows the place, the main action, the sprite and the credits', { skip }, async () => {
|
|
143
|
+
const { ctx, page, errors } = await open();
|
|
144
|
+
const g = await page.evaluate(() => ({
|
|
145
|
+
shown: getComputedStyle(document.getElementById('visor')).display,
|
|
146
|
+
place: document.querySelector('.visor__place').textContent,
|
|
147
|
+
main: document.querySelector('.visor__pill--main').textContent,
|
|
148
|
+
who: document.querySelector('.visor__name').textContent,
|
|
149
|
+
credits: document.querySelector('.visor__credits').textContent,
|
|
150
|
+
sprite: document.querySelector('.visor__sprite').getAttribute('src'),
|
|
151
|
+
}));
|
|
152
|
+
assert.equal(g.shown, 'block');
|
|
153
|
+
assert.equal(g.place, 'Test city');
|
|
154
|
+
assert.match(g.main, /^Claim a task/);
|
|
155
|
+
assert.equal(g.who, 'Artisan');
|
|
156
|
+
assert.equal(g.credits, '56,021 credits');
|
|
157
|
+
assert.equal(g.sprite, '/builders/characters/artisan.png');
|
|
158
|
+
assert.deepEqual(errors, []);
|
|
159
|
+
await ctx.close();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('the glass passes the pointer through: a drag over the frame pans the world', { skip }, async () => {
|
|
163
|
+
const { ctx, page } = await open();
|
|
164
|
+
// The left side line runs down x≈1.8% of the width; drag straight across it.
|
|
165
|
+
await page.mouse.move(20, 400); await page.mouse.down(); await page.mouse.move(220, 450, { steps: 5 }); await page.mouse.up();
|
|
166
|
+
const cam = await page.evaluate(() => window.cam);
|
|
167
|
+
assert.deepEqual(cam, { x: -200, y: -50 });
|
|
168
|
+
assert.equal(await page.evaluate(() => document.elementFromPoint(640, 400).id), 'world', 'the centre cross is not a target');
|
|
169
|
+
await ctx.close();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('Escape closes one layer, top first: the sheet, then the scene\'s pick, then the options menu', { skip }, async () => {
|
|
173
|
+
const { ctx, page } = await open();
|
|
174
|
+
await page.keyboard.press('a');
|
|
175
|
+
assert.equal(await page.evaluate(() => OTBVisor.isOpen()), true, 'A opens the sheet');
|
|
176
|
+
await page.keyboard.press('Escape');
|
|
177
|
+
let s = await page.evaluate(() => ({ open: OTBVisor.isOpen(), picked: window.picked, menu: window.optionsOpened }));
|
|
178
|
+
assert.deepEqual(s, { open: false, picked: true, menu: 0 }, 'the sheet took the press and stopped it: the star stays picked');
|
|
179
|
+
await page.keyboard.press('Escape');
|
|
180
|
+
s = await page.evaluate(() => ({ picked: window.picked, menu: window.optionsOpened }));
|
|
181
|
+
assert.deepEqual(s, { picked: false, menu: 0 }, 'the scene took the next one');
|
|
182
|
+
await page.keyboard.press('Escape');
|
|
183
|
+
await page.waitForTimeout(50);
|
|
184
|
+
assert.equal(await page.evaluate(() => window.optionsOpened), 1, 'nothing left open: the options menu has it, not the glass');
|
|
185
|
+
await ctx.close();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('the sheet lists the character\'s actions, then this place, and runs a scene action', { skip }, async () => {
|
|
189
|
+
const { ctx, page } = await open();
|
|
190
|
+
await page.click('[data-visor-sheet]');
|
|
191
|
+
const rows = await page.$$eval('.visor__sheet .visor__row', (r) => r.map((x) => [...x.children].map((c) => c.textContent).join(' ')));
|
|
192
|
+
assert.deepEqual(rows, ['Claim a task C', 'Blockers B', 'Goal sky G', 'Your thinking T', 'Back to centre 0', 'Look around drag', 'Options Esc']);
|
|
193
|
+
await page.click('[data-visor-scene="0"]');
|
|
194
|
+
assert.deepEqual(await page.evaluate(() => ({ centred: window.centred, open: OTBVisor.isOpen() })), { centred: true, open: false });
|
|
195
|
+
await ctx.close();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('an Artist\'s glass offers other actions, and its keys open the Artist\'s tools', { skip }, async () => {
|
|
199
|
+
const { ctx, page } = await open();
|
|
200
|
+
await page.evaluate(() => { OTBCraftMode.held = 'artist'; OTBVisor.render(); });
|
|
201
|
+
assert.match(await page.textContent('.visor__pill--main'), /^Review this page/);
|
|
202
|
+
await page.keyboard.press('e');
|
|
203
|
+
assert.equal(await page.evaluate(() => window.toolOpened), 'paintbrush');
|
|
204
|
+
await ctx.close();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('the keys wait while someone types, or while a dialog is up', { skip }, async () => {
|
|
208
|
+
const { ctx, page } = await open();
|
|
209
|
+
await page.evaluate(() => { const i = document.createElement('input'); i.id = 'field'; document.body.appendChild(i); });
|
|
210
|
+
await page.focus('#field');
|
|
211
|
+
await page.keyboard.press('a');
|
|
212
|
+
assert.equal(await page.evaluate(() => OTBVisor.isOpen()), false, 'typing A in a field is typing');
|
|
213
|
+
await page.evaluate(() => { document.activeElement.blur(); const d = document.createElement('div'); d.setAttribute('aria-modal', 'true'); document.body.appendChild(d); });
|
|
214
|
+
await page.keyboard.press('a');
|
|
215
|
+
assert.equal(await page.evaluate(() => OTBVisor.isOpen()), false, 'a modal dialog has the keyboard');
|
|
216
|
+
await ctx.close();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('no character, or no scene: no glass', { skip }, async () => {
|
|
220
|
+
const { ctx, page } = await open();
|
|
221
|
+
await page.evaluate(() => { OTBCraftMode.held = null; OTBVisor.render(); });
|
|
222
|
+
assert.equal(await page.evaluate(() => document.getElementById('visor').hidden), true);
|
|
223
|
+
await ctx.close();
|
|
224
|
+
});
|