@bongos/core 1.24.79 → 1.24.81
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 +554 -394
- 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/0402-helm-is-the-halls-look-and-the-hall-is-dark-only.md +142 -0
- package/docs/adr/README.md +4 -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 +393 -378
- package/docs/copy-registry.json +722 -578
- package/docs/design/characters/character-sprites-source.webp +0 -0
- package/docs/file-map.md +7 -8
- package/docs/module-api-changelog.md +4 -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 +16 -7
- package/docs/page-readings.json +3184 -3338
- 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 +6 -4
- package/modules/hall-ui/public/ai-studio.states.json +1 -0
- package/modules/hall-ui/public/atlas.html +5 -5
- package/modules/hall-ui/public/atlas.js +3 -1
- package/modules/hall-ui/public/atlas.states.json +1 -0
- package/modules/hall-ui/public/blockers.html +5 -5
- package/modules/hall-ui/public/board-room.html +4 -4
- package/modules/hall-ui/public/board-room.states.json +1 -0
- 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 +3 -4
- package/modules/hall-ui/public/city.html +9 -9
- package/modules/hall-ui/public/city.js +17 -56
- package/modules/hall-ui/public/city.states.json +7 -32
- package/modules/hall-ui/public/collab.html +4 -4
- package/modules/hall-ui/public/collab.states.json +1 -0
- package/modules/hall-ui/public/commands.html +4 -4
- package/modules/hall-ui/public/commands.states.json +1 -0
- package/modules/hall-ui/public/controls.css +63 -11
- package/modules/hall-ui/public/copy-desk.html +4 -4
- package/modules/hall-ui/public/copy-desk.states.json +1 -0
- package/modules/hall-ui/public/craft-mode.js +258 -64
- package/modules/hall-ui/public/deploy.html +4 -4
- package/modules/hall-ui/public/deploy.states.json +1 -0
- 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/goals.states.json +1 -0
- package/modules/hall-ui/public/government.html +4 -4
- package/modules/hall-ui/public/government.states.json +1 -0
- 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/idea.states.json +1 -0
- package/modules/hall-ui/public/ideas.html +5 -5
- package/modules/hall-ui/public/ideas.states.json +1 -0
- package/modules/hall-ui/public/index-founding.states.json +1 -0
- package/modules/hall-ui/public/index-genesis-decide.states.json +1 -0
- package/modules/hall-ui/public/index-genesis-demo.states.json +1 -0
- package/modules/hall-ui/public/index-genesis.states.json +1 -0
- package/modules/hall-ui/public/index.html +11 -7
- package/modules/hall-ui/public/index.states.json +1 -0
- package/modules/hall-ui/public/market-watch-item.html +4 -4
- package/modules/hall-ui/public/market-watch-item.states.json +1 -0
- package/modules/hall-ui/public/market-watch-rules.html +4 -4
- package/modules/hall-ui/public/market-watch-rules.states.json +1 -0
- 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/market-watch.states.json +1 -0
- package/modules/hall-ui/public/modules.html +4 -4
- package/modules/hall-ui/public/modules.states.json +1 -0
- package/modules/hall-ui/public/money.html +4 -4
- package/modules/hall-ui/public/money.states.json +1 -0
- package/modules/hall-ui/public/my-agents.html +4 -4
- package/modules/hall-ui/public/my-agents.states.json +1 -0
- 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/primer.states.json +1 -0
- 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 +5 -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/roster.states.json +1 -0
- 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/settings.states.json +1 -0
- package/modules/hall-ui/public/shell-game.css +110 -0
- package/modules/hall-ui/public/shell-game.js +316 -3
- package/modules/hall-ui/public/shell.js +15 -66
- 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-card.states.json +1 -0
- package/modules/hall-ui/public/street-inside.js +3 -4
- package/modules/hall-ui/public/street.css +1 -5
- package/modules/hall-ui/public/street.html +6 -10
- package/modules/hall-ui/public/street.js +8 -59
- package/modules/hall-ui/public/street.states.json +7 -32
- package/modules/hall-ui/public/studio.html +5 -5
- package/modules/hall-ui/public/studio.states.json +1 -0
- package/modules/hall-ui/public/style.css +197 -112
- package/modules/hall-ui/public/task-detail.js +5 -4
- package/modules/hall-ui/public/task.html +5 -5
- package/modules/hall-ui/public/task.states.json +1 -0
- package/modules/hall-ui/public/thinking.css +10 -0
- package/modules/hall-ui/public/thinking.html +9 -6
- package/modules/hall-ui/public/thinking.states.json +1 -0
- 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/tweak-editor.states.json +1 -0
- 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/watch.states.json +1 -0
- package/modules/hall-ui/public/welcome-boot.js +15 -6
- package/modules/hall-ui/public/work-founding.states.json +1 -0
- package/modules/hall-ui/public/work.html +5 -5
- package/modules/hall-ui/public/work.states.json +1 -0
- 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 +25 -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/hall-ui/specimens/components.states.json +2 -1
- 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/modules/ui-design/public/brand.html +2 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +76 -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/hall-preview/README.md +2 -3
- 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 +33 -11
- package/src/crafts.js +66 -0
- package/src/hall-helm.js +79 -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/design_reference.mjs +1 -1
- 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 +3 -2
- package/tests/hall_city.mjs +16 -30
- package/tests/hall_controls.mjs +52 -4
- package/tests/hall_fullscreen.mjs +27 -3
- package/tests/hall_helm.mjs +187 -0
- 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 +6 -4
- package/tests/hall_settings_world.mjs +2 -1
- package/tests/hall_sitemap.mjs +6 -5
- package/tests/hall_start_screen.mjs +307 -0
- package/tests/hall_street.mjs +15 -28
- package/tests/hall_street_inside.mjs +17 -9
- package/tests/hall_studio_world.mjs +3 -3
- package/tests/hall_topbar_320.mjs +4 -2
- 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
|
@@ -0,0 +1,307 @@
|
|
|
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('the load screen fills one segment per week, up to the week it is showing (task 1005192)', () => {
|
|
86
|
+
const { window } = sandbox();
|
|
87
|
+
const G = window.OTBShellGame;
|
|
88
|
+
assert.equal(G.segsDone(10, 0), 1, 'the oldest week is the first segment');
|
|
89
|
+
assert.equal(G.segsDone(10, 4.5), 5);
|
|
90
|
+
assert.equal(G.segsDone(10, 9), 10, 'now fills the row');
|
|
91
|
+
assert.equal(G.segsDone(10, 99), 10, 'never more than there are weeks');
|
|
92
|
+
assert.equal(G.segsDone(0, 0), 0);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('a fresh sign-in is handed to the start screen with the still, and the craft picker waits for the world', () => {
|
|
96
|
+
const s = sandbox({ store: { 'hall-welcome-signin': '1' } });
|
|
97
|
+
const access = { authed: true }, builder = { id: 7, display_name: 'Ada' };
|
|
98
|
+
assert.equal(s.window.OTBWelcome.onAccess(access, builder), true, 'the shell is told a screen is up, so it asks no picker');
|
|
99
|
+
assert.equal(s.started.length, 1);
|
|
100
|
+
assert.equal(s.started[0].name, 'Harbor');
|
|
101
|
+
assert.match(s.started[0].art, /^\/builders\/welcome-(day|night)\.webp$/);
|
|
102
|
+
assert.equal(s.store['hall-welcome-signin'], undefined, 'the flag is spent');
|
|
103
|
+
assert.deepEqual(s.asked, [], 'nothing is asked while the start screen is up');
|
|
104
|
+
s.started[0].done();
|
|
105
|
+
assert.deepEqual(s.asked, [['picker', true, 7]], 'the picker is asked once the world is showing');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
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', () => {
|
|
109
|
+
const daily = sandbox();
|
|
110
|
+
assert.equal(daily.window.OTBWelcome.onAccess({ authed: true }, { id: 7 }), true);
|
|
111
|
+
assert.deepEqual([daily.started.length, daily.asked], [0, ['marked']], 'the day\'s first visit is the welcome\'s');
|
|
112
|
+
const bare = sandbox({ store: { 'hall-welcome-signin': '1' }, game: false });
|
|
113
|
+
assert.equal(bare.window.OTBWelcome.onAccess({ authed: true }, { id: 7 }), true);
|
|
114
|
+
assert.deepEqual(bare.asked, ['marked']);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('Home loads the sheet and the script, and the sheet types in no size, corner or breakpoint', () => {
|
|
118
|
+
const html = read('index.html');
|
|
119
|
+
assert.match(html, /<link rel="stylesheet" href="\/builders\/shell-game\.css[^"]*">/);
|
|
120
|
+
assert.ok(html.indexOf('welcome-boot.js') < html.indexOf('shell.js?'), 'the welcome, which hands over, loads before the shell');
|
|
121
|
+
const css = read('shell-game.css').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
122
|
+
assert.doesNotMatch(css, /font-size:\s*[\d.]/);
|
|
123
|
+
assert.doesNotMatch(css, /border-radius:\s*[\d.]/);
|
|
124
|
+
assert.doesNotMatch(css, /@media/);
|
|
125
|
+
assert.doesNotMatch(css, /#[0-9a-fA-F]{3,8}\b/, 'tokens, or black and white: no private palette');
|
|
126
|
+
// Helm (task 1005192, ADR 0402): the literal floor is zero, the voice and the panel are the hall tier's tokens
|
|
127
|
+
assert.doesNotMatch(css, /rgba?\(|hsla?\(|box-shadow:\s*[\d-]|text-shadow:\s*[\d-]|font-family:(?!\s*var\()/, 'no typed colour, shadow or face');
|
|
128
|
+
assert.match(css, /\.gs__title \{[^}]*font-family: var\(--name-face\);[^}]*letter-spacing: var\(--name-space-wide\); text-transform: var\(--name-case\);/, 'the title is the name voice');
|
|
129
|
+
assert.match(css, /\.gs__item \{[^}]*font-family: var\(--name-face\);[^}]*text-transform: var\(--name-case\);/, 'so is every choice');
|
|
130
|
+
assert.match(css, /\.gs__menu, \.gs__load \{[^}]*background: var\(--night-glass\);/, 'both panels are the night over the art');
|
|
131
|
+
assert.match(css, /\.gs__item\[data-gs="start"\] \{\s*background: var\(--accent\); color: var\(--accent-on\);/, 'Start is the one thing lit, under the dark ink');
|
|
132
|
+
assert.match(css, /\.gs__item:focus-visible \{[^}]*box-shadow: var\(--press-focus\);/, 'a row rings inside itself, in two tones');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// ---------- in a browser: the two screens over Home's own markup ----------
|
|
136
|
+
const lib = require(path.join(ROOT, 'modules', 'ui-design', 'kit', 'lib.js'));
|
|
137
|
+
let browser = null;
|
|
138
|
+
try { browser = await lib.launchBrowser(); } catch (e) { if (!(e instanceof lib.NoBrowserError)) throw e; }
|
|
139
|
+
after(() => browser && browser.close());
|
|
140
|
+
const skip = !browser && 'no system Edge or Chrome';
|
|
141
|
+
|
|
142
|
+
const ORIGIN = 'http://hall.test';
|
|
143
|
+
const TYPES = { '.js': 'text/javascript', '.css': 'text/css', '.webp': 'image/webp' };
|
|
144
|
+
const INDEX = read('index.html').replace(/<script\b[^>]*>[\s\S]*?<\/script>/g, '').replace(/\{\{[^}]*\}\}/g, '');
|
|
145
|
+
const STUB = '<script>window.__BRANDING__ = { identity: { worldName: "Harbor" } }; window.BongosClient = { createClient: () => ({ request: async () => ({ ok: false, status: 404, data: null }) }) };</script>';
|
|
146
|
+
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']
|
|
147
|
+
.map((f) => `<script src="/builders/${f}"></script>`).join('');
|
|
148
|
+
const PAGE = INDEX.replace('</body>', STUB + SCRIPTS + '</body>');
|
|
149
|
+
|
|
150
|
+
async function hall({ city = true, reducedMotion = 'no-preference', viewport = { width: 1280, height: 800 } } = {}) {
|
|
151
|
+
const ctx = await browser.newContext({ viewport, reducedMotion });
|
|
152
|
+
await ctx.route(ORIGIN + '/**', (route) => {
|
|
153
|
+
const p = new URL(route.request().url()).pathname;
|
|
154
|
+
if (p === '/' || p === '/index.html') return route.fulfill({ contentType: 'text/html', body: PAGE });
|
|
155
|
+
if (p === '/api/bongos/home/city') return city ? route.fulfill({ contentType: 'application/json', body: CITY }) : route.fulfill({ status: 500, body: '' });
|
|
156
|
+
const ext = path.extname(p);
|
|
157
|
+
const f = path.join(HALL, p.replace(/^\/builders\//, ''));
|
|
158
|
+
if (p.startsWith('/builders/') && TYPES[ext] && fs.existsSync(f)) return route.fulfill({ contentType: TYPES[ext], body: fs.readFileSync(f) });
|
|
159
|
+
return route.fulfill({ status: 404, body: '' });
|
|
160
|
+
});
|
|
161
|
+
return ctx;
|
|
162
|
+
}
|
|
163
|
+
// A page that has just come back from signing in: the flag the sign-in click left, then /me.
|
|
164
|
+
async function signedIn(ctx) {
|
|
165
|
+
const page = await ctx.newPage();
|
|
166
|
+
const errors = [];
|
|
167
|
+
page.on('pageerror', (e) => errors.push(String(e.message)));
|
|
168
|
+
await page.addInitScript(() => { try { localStorage.setItem('hall-welcome-signin', '1'); } catch (_) { /* no storage */ } });
|
|
169
|
+
await page.goto(ORIGIN + '/');
|
|
170
|
+
await page.waitForFunction(() => !!window.OTBShell && !!window.OTBWelcome && !!window.OTBShellGame);
|
|
171
|
+
await page.evaluate(() => {
|
|
172
|
+
const me = { authed: true, rank: 'metic', builder: { id: 7, display_name: 'Ada', github_login: 'ada' } };
|
|
173
|
+
window.OTBShell.applyAccess(me);
|
|
174
|
+
window.OTBShell.applyAccess(me);
|
|
175
|
+
});
|
|
176
|
+
return { page, errors };
|
|
177
|
+
}
|
|
178
|
+
const state = (page) => page.evaluate(() => ({
|
|
179
|
+
start: !!document.getElementById('game-start'),
|
|
180
|
+
welcome: !!document.getElementById('welcome-boot'),
|
|
181
|
+
picker: !!document.getElementById('craft-picker'),
|
|
182
|
+
game: document.documentElement.dataset.game || null,
|
|
183
|
+
full: document.documentElement.dataset.fullscreen || null,
|
|
184
|
+
}));
|
|
185
|
+
|
|
186
|
+
test('a sign-in passes through the start screen and the load screen before the world', { skip }, async () => {
|
|
187
|
+
const ctx = await hall();
|
|
188
|
+
const { page, errors } = await signedIn(ctx);
|
|
189
|
+
await page.waitForSelector('#game-start');
|
|
190
|
+
assert.deepEqual(await state(page), { start: true, welcome: false, picker: false, game: null, full: null },
|
|
191
|
+
'the start screen, not the welcome and not the picker; game mode is not on until Start');
|
|
192
|
+
const menu = await page.evaluate(() => ({
|
|
193
|
+
title: document.getElementById('gs-title').textContent,
|
|
194
|
+
items: Array.from(document.querySelectorAll('.gs__items:not(.gs__items--extras) .gs__item')).map((e) => e.textContent),
|
|
195
|
+
focus: document.activeElement.textContent,
|
|
196
|
+
small: Array.from(document.querySelectorAll('.gs__items:not(.gs__items--extras) .gs__item')).filter((e) => e.getBoundingClientRect().height < 46).length,
|
|
197
|
+
}));
|
|
198
|
+
assert.deepEqual(menu, { title: 'Harbor', items: ['Start', 'Options', 'Achievements', 'Extras', 'Quit'], focus: 'Start', small: 0 });
|
|
199
|
+
|
|
200
|
+
// Helm (task 1005192): one full-height night panel on the left, the mark over the name, a line at the foot.
|
|
201
|
+
const panel = await page.evaluate(() => {
|
|
202
|
+
const box = (sel) => { const r = document.querySelector(sel).getBoundingClientRect(); return { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height) }; };
|
|
203
|
+
const start = document.querySelector('[data-gs="start"]'), title = document.getElementById('gs-title');
|
|
204
|
+
return {
|
|
205
|
+
menu: box('.gs__menu'), start: box('[data-gs="start"]'), view: { w: window.innerWidth, h: window.innerHeight },
|
|
206
|
+
mark: !!document.querySelector('.gs__head .gs__mark'), line: document.getElementById('gs-line').textContent,
|
|
207
|
+
caps: getComputedStyle(title).textTransform, startCaps: getComputedStyle(start).textTransform,
|
|
208
|
+
key: getComputedStyle(start, '::after').content, sideways: document.getElementById('game-start').scrollWidth - document.getElementById('game-start').clientWidth,
|
|
209
|
+
};
|
|
210
|
+
});
|
|
211
|
+
assert.equal(panel.menu.w, 400, 'the panel is 400 wide on a desk');
|
|
212
|
+
assert.ok(panel.menu.x > 0 && panel.menu.x < panel.view.w / 4, 'and stands on the left, off the edge');
|
|
213
|
+
assert.deepEqual([panel.menu.y, panel.menu.h], [0, panel.view.h], 'the full height of the screen');
|
|
214
|
+
assert.ok(panel.start.w >= panel.menu.w - 4, 'Start is a bar the width of the panel');
|
|
215
|
+
assert.equal(panel.mark, true, 'the mark stands over the name');
|
|
216
|
+
assert.match(panel.line, /^[A-Z].*\.$/, 'the foot carries one sentence');
|
|
217
|
+
assert.deepEqual([panel.caps, panel.startCaps], ['uppercase', 'uppercase'], 'names are capitals');
|
|
218
|
+
assert.equal(panel.key, '"Enter"', 'Start names the key that takes it');
|
|
219
|
+
assert.equal(panel.sideways, 0);
|
|
220
|
+
|
|
221
|
+
// The arrows walk the menu; Escape stays with the screen and does not flip the shell underneath.
|
|
222
|
+
await page.keyboard.press('ArrowDown');
|
|
223
|
+
assert.equal(await page.evaluate(() => document.activeElement.textContent), 'Options');
|
|
224
|
+
await page.keyboard.press('ArrowUp');
|
|
225
|
+
await page.keyboard.press('Escape');
|
|
226
|
+
await page.evaluate(() => new Promise((r) => setTimeout(r, 50)));
|
|
227
|
+
assert.equal((await state(page)).start, true, 'Escape does not dismiss the start screen');
|
|
228
|
+
|
|
229
|
+
// Extras opens its links; Escape folds them away again.
|
|
230
|
+
await page.click('[data-gs="extras"]');
|
|
231
|
+
assert.equal(await page.isVisible('#gs-extras a'), true);
|
|
232
|
+
await page.keyboard.press('Escape');
|
|
233
|
+
assert.equal(await page.isVisible('#gs-extras a'), false);
|
|
234
|
+
|
|
235
|
+
// Start: game mode on, the load screen with a tip, and the city building up to now.
|
|
236
|
+
await page.click('[data-gs="start"]');
|
|
237
|
+
await page.waitForSelector('#gs-load:not([hidden])');
|
|
238
|
+
assert.equal(await page.isVisible('.gs__menu'), false);
|
|
239
|
+
assert.match(await page.textContent('#gs-tip'), /\.$/);
|
|
240
|
+
await page.waitForFunction(() => /weeks? ago$/.test(document.getElementById('gs-when').textContent));
|
|
241
|
+
assert.ok(await page.evaluate(() => document.querySelectorAll('#gs-city *').length > 20), 'the city is drawn, not a placeholder');
|
|
242
|
+
const weeks = JSON.parse(CITY).weeks.length;
|
|
243
|
+
assert.equal(await page.evaluate(() => document.querySelectorAll('#gs-segs:not([hidden]) > .seg').length), weeks, 'one segment per week of the history');
|
|
244
|
+
await page.waitForFunction(() => { const w = document.getElementById('gs-when'); return !w || w.textContent === 'The city, now'; });
|
|
245
|
+
const segs = await page.evaluate(() => { const b = document.getElementById('gs-segs'); return b ? [b.querySelectorAll('.seg--done').length, b.getAttribute('aria-hidden')] : null; });
|
|
246
|
+
if (segs) assert.deepEqual(segs, [weeks, 'true'], 'at now every segment is filled; the words beside them say it, so the row is hidden from a screen reader');
|
|
247
|
+
await page.waitForSelector('#game-start', { state: 'detached' });
|
|
248
|
+
await page.waitForSelector('#craft-picker');
|
|
249
|
+
assert.deepEqual(await state(page), { start: false, welcome: false, picker: true, game: 'on', full: 'on' },
|
|
250
|
+
'the world: game mode on, the bars gone, and only now the craft picker');
|
|
251
|
+
assert.equal(await page.evaluate(() => localStorage.getItem('hall-game')), 'on');
|
|
252
|
+
assert.deepEqual(errors, []);
|
|
253
|
+
await ctx.close();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test('on a phone the panel is the whole width, nothing scrolls sideways and every choice clears the floor (task 1005192)', { skip }, async () => {
|
|
257
|
+
for (const [w, h] of [[390, 844], [320, 700]]) {
|
|
258
|
+
const ctx = await hall({ viewport: { width: w, height: h } });
|
|
259
|
+
const { page, errors } = await signedIn(ctx);
|
|
260
|
+
await page.waitForSelector('#game-start');
|
|
261
|
+
const m = await page.evaluate(() => {
|
|
262
|
+
const gs = document.getElementById('game-start'), r = document.querySelector('.gs__menu').getBoundingClientRect();
|
|
263
|
+
return {
|
|
264
|
+
x: Math.round(r.left), w: Math.round(r.width), sideways: gs.scrollWidth - gs.clientWidth,
|
|
265
|
+
small: Array.from(document.querySelectorAll('.gs__item')).filter((e) => !e.closest('[hidden]')).filter((e) => { const b = e.getBoundingClientRect(); return b.height < 24 || b.width < 24 || b.right > window.innerWidth + 1; }).length,
|
|
266
|
+
};
|
|
267
|
+
});
|
|
268
|
+
assert.deepEqual(m, { x: 0, w, sideways: 0, small: 0 }, `at ${w}px`);
|
|
269
|
+
await page.click('[data-gs="start"]');
|
|
270
|
+
await page.waitForSelector('#gs-load:not([hidden])');
|
|
271
|
+
assert.equal(await page.evaluate(() => { const gs = document.getElementById('game-start'); return gs.scrollWidth - gs.clientWidth; }), 0, `the load screen fits ${w}px`);
|
|
272
|
+
assert.deepEqual(errors, []);
|
|
273
|
+
await ctx.close();
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test('Quit closes the tab, and says so when the browser keeps it open', { skip }, async () => {
|
|
278
|
+
const ctx = await hall();
|
|
279
|
+
const { page } = await signedIn(ctx);
|
|
280
|
+
await page.waitForSelector('#game-start');
|
|
281
|
+
await page.evaluate(() => { window.__closed = 0; window.close = () => { window.__closed += 1; }; });
|
|
282
|
+
await page.click('[data-gs="quit"]');
|
|
283
|
+
await page.waitForSelector('#gs-note:not([hidden])');
|
|
284
|
+
assert.equal(await page.evaluate(() => window.__closed), 1);
|
|
285
|
+
assert.match(await page.textContent('#gs-note'), /Close it to quit/);
|
|
286
|
+
await ctx.close();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test('with no city to draw, or with reduced motion, the still and the tip stand and the world still opens', { skip }, async () => {
|
|
290
|
+
for (const opts of [{ city: false }, { reducedMotion: 'reduce' }]) {
|
|
291
|
+
const ctx = await hall(opts);
|
|
292
|
+
const { page, errors } = await signedIn(ctx);
|
|
293
|
+
await page.waitForSelector('#game-start');
|
|
294
|
+
await page.click('[data-gs="start"]');
|
|
295
|
+
await page.waitForSelector('#gs-load:not([hidden])');
|
|
296
|
+
assert.match(await page.textContent('#gs-tip'), /\.$/);
|
|
297
|
+
if (opts.reducedMotion) {
|
|
298
|
+
await page.waitForFunction(() => { const w = document.getElementById('gs-when'); return !w || !w.hidden; });
|
|
299
|
+
assert.equal(await page.evaluate(() => { const w = document.getElementById('gs-when'); return w ? w.textContent : 'The city, now'; }), 'The city, now',
|
|
300
|
+
'no week-by-week run: the finished city at once');
|
|
301
|
+
}
|
|
302
|
+
await page.waitForSelector('#game-start', { state: 'detached' });
|
|
303
|
+
assert.equal((await state(page)).game, 'on');
|
|
304
|
+
assert.deepEqual(errors, []);
|
|
305
|
+
await ctx.close();
|
|
306
|
+
}
|
|
307
|
+
});
|
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
|
});
|
|
@@ -488,7 +488,8 @@ test('the page holds the prototype\'s header, the street, the readings and the c
|
|
|
488
488
|
assert.equal((html.match(/<main\b/g) || []).length, 1);
|
|
489
489
|
assert.equal((html.match(/<h1\b/g) || []).length, 1);
|
|
490
490
|
assert.match(html, /<a class="street-mark" id="streetHome" href="\/builders\/city"[^>]*>\{\{productName\}\}</);
|
|
491
|
-
|
|
491
|
+
assert.doesNotMatch(html, /streetMode/, 'no light/dark switch: the hall is dark only (task 1005192, ADR 0402)');
|
|
492
|
+
for (const id of ['hudNeeds', 'hudMoving', 'hudShare', 'streetSigns', 'streetPins', 'streetDoors', 'streetCard']) assert.match(html, new RegExp(`id="${id}"`), id);
|
|
492
493
|
const scripts = [...html.matchAll(/<script src="\/builders\/([a-z-]+)\.js/g)].map((m) => m[1]);
|
|
493
494
|
assert.deepEqual(scripts.filter((s) => !/^street-(deck|office)$/.test(s)), ['dom-utils', 'shell-game', 'shell-trail', 'craft-mode', 'shell', 'hall-dialog', 'palette', 'gov-living', 'gov-feeds', 'city-draw', 'street-draw', 'street-card', 'street-inside', 'street-depth', 'street-desk', 'street-amend', 'street'], 'the hall shell (task 1004544), the living model and its one reader (task 1004685: city-draw.js counts with it, so it loads first), the shared kit, then the card (task 1004528), the interiors (task 1004526), the living street\'s exterior (street-depth.js), the floor desk (task 1004558) and the drafted amendment (task 1004530); the deck rides beside them');
|
|
494
495
|
assert.equal(City.STREET_HREF, '/builders/street', 'the city\'s door is this page');
|
|
@@ -552,7 +553,7 @@ function fakeDom() {
|
|
|
552
553
|
};
|
|
553
554
|
const focused = { el: null };
|
|
554
555
|
const html = mk('html');
|
|
555
|
-
const ids = ['
|
|
556
|
+
const ids = ['streetWindow', 'streetSizer', 'streetBoard', 'streetSvg', 'streetFlat', 'streetBlds', 'streetCrates', 'streetCols', 'streetGroups', 'streetDoors', 'streetPins', 'streetSigns', 'streetEvening', 'streetEveningLine', 'hudNeeds', 'hudMoving', 'hudShare', 'hudDot', 'streetHome', 'streetCard', 'streetCardWhere', 'streetCardWhen', 'streetCardTitle', 'streetCardBody', 'streetCardOpen', 'streetCardClose', 'streetToast'];
|
|
556
557
|
for (const id of ids) mk('x').setAttribute('id', id);
|
|
557
558
|
const document = { documentElement: html, getElementById: (id) => byId.get(id) || null, createElement: (t) => mk(t), createElementNS: (_ns, t) => mk(t) };
|
|
558
559
|
return { document, byId, html, focused };
|
|
@@ -701,32 +702,18 @@ test('the header counts wear the pins\' colours, so they double as the legend',
|
|
|
701
702
|
assert.ok(size('urgent') > size('needs') && size('needs') > size('moving') && size('moving') > size('settled'), 'urgent > needs > moving > settled');
|
|
702
703
|
});
|
|
703
704
|
|
|
704
|
-
test('the
|
|
705
|
-
const p = await bootPage({ feeds: FEEDS([]), stored: 'light', osDark:
|
|
706
|
-
assert.equal(p.html.attrs['data-theme'], '
|
|
707
|
-
assert.equal(p.
|
|
708
|
-
assert.equal(p.
|
|
709
|
-
const
|
|
710
|
-
|
|
711
|
-
assert.
|
|
712
|
-
assert.equal(p.html.attrs['data-theme'], 'dark');
|
|
713
|
-
assert.notEqual(JSON.stringify(p.byId.get('streetBlds').children[0].children[0].attrs), before, 'the street redraws in the new mode');
|
|
714
|
-
});
|
|
715
|
-
|
|
716
|
-
test('in the hall, the street and the top bar\'s theme switch never disagree (task 1004544)', async () => {
|
|
717
|
-
let shellPaints = 0;
|
|
718
|
-
const p = await bootPage({ feeds: FEEDS([]), stored: 'light', osDark: true, shell: { applyTheme() { shellPaints++; } } });
|
|
719
|
-
assert.equal(p.observers.length, 1, 'the page watches html[data-theme]');
|
|
720
|
-
p.byId.get('streetMode').fire('click');
|
|
721
|
-
assert.equal(shellPaints, 1, 'this switch repaints the shell\'s');
|
|
722
|
-
const before = JSON.stringify(p.byId.get('streetBlds').children[0].children[0].attrs);
|
|
723
|
-
p.store.set('hall-theme', 'light'); p.html.setAttribute('data-theme', 'light'); // the shell's switch
|
|
724
|
-
p.observers[0].fn();
|
|
725
|
-
assert.equal(p.byId.get('streetModeWord').textContent, 'dark', 'this switch follows the shell\'s');
|
|
726
|
-
assert.notEqual(JSON.stringify(p.byId.get('streetBlds').children[0].children[0].attrs), before, 'and the street redraws in that mode');
|
|
727
|
-
assert.equal(shellPaints, 1, 'following the shell does not echo back to it');
|
|
705
|
+
test('the street is dark only: no stored choice and no OS setting reaches it (task 1005192, ADR 0402)', async () => {
|
|
706
|
+
const p = await bootPage({ feeds: FEEDS([]), stored: 'light', osDark: false, shell: { applyTheme() { throw new Error('the street no longer drives the shell'); } } });
|
|
707
|
+
assert.equal(p.html.attrs['data-theme'], 'dark', 'a stored light choice and a light OS are both ignored');
|
|
708
|
+
assert.equal(p.store.get('hall-theme'), 'light', 'and the old preference is never written');
|
|
709
|
+
assert.equal(p.observers.length, 0, 'nothing flips the theme, so nothing watches it');
|
|
710
|
+
const q = await bootPage({ feeds: FEEDS([]), stored: null, osDark: true });
|
|
711
|
+
assert.equal(JSON.stringify(q.byId.get('streetBlds').children[0].children[0].attrs), JSON.stringify(p.byId.get('streetBlds').children[0].children[0].attrs), 'one drawing whatever was stored');
|
|
712
|
+
assert.doesNotMatch(read('street.js'), /hall-theme|prefers-color-scheme/, 'street.js reads no theme preference');
|
|
728
713
|
});
|
|
729
714
|
|
|
715
|
+
|
|
716
|
+
|
|
730
717
|
test('arriving through a door marks that building, and a failed docket says so', async () => {
|
|
731
718
|
const p = await bootPage({ feeds: FEEDS([]), hash: '#building=treasury' });
|
|
732
719
|
const here = p.byId.get('streetDoors').children.filter((a) => a.attrs['aria-current'] === 'location');
|
|
@@ -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
|
});
|
|
@@ -63,7 +63,7 @@ async function measureBar(width, scheme) {
|
|
|
63
63
|
const ctx = await context(width, scheme);
|
|
64
64
|
const page = await ctx.newPage();
|
|
65
65
|
await page.goto(ORIGIN + '/work');
|
|
66
|
-
await page.waitForSelector('#app-topbar .topbar-side #
|
|
66
|
+
await page.waitForSelector('#app-topbar .topbar-side #zoom-toggle');
|
|
67
67
|
return page.evaluate(() => {
|
|
68
68
|
const side = document.querySelector('#app-topbar .topbar-side');
|
|
69
69
|
if (!document.getElementById('upd-pill')) {
|
|
@@ -86,9 +86,11 @@ for (const scheme of ['light', 'dark']) {
|
|
|
86
86
|
test(`at 320px the top bar fits, every button inside it and at least 24px (${scheme})`, { skip }, async () => {
|
|
87
87
|
const m = await measureBar(320, scheme);
|
|
88
88
|
const ids = m.buttons.map((b) => b.id);
|
|
89
|
-
for (const want of ['upd-pill', 'topbar-jump', 'zoom-toggle', '
|
|
89
|
+
for (const want of ['upd-pill', 'topbar-jump', 'zoom-toggle', 'fullscreen-toggle']) {
|
|
90
90
|
assert.ok(ids.includes(want), `${want} is on the bar (got ${ids.join(', ')})`);
|
|
91
91
|
}
|
|
92
|
+
// the hall is dark only (task 1005192, ADR 0402): the bar offers no light/dark switch
|
|
93
|
+
assert.ok(!ids.includes('theme-toggle'), 'no theme switch on the bar');
|
|
92
94
|
assert.ok(m.scrollWidth <= 320, `the page is ${m.scrollWidth}px wide at 320 (the top bar spills)`);
|
|
93
95
|
for (const b of m.buttons) {
|
|
94
96
|
assert.ok(b.left >= 0 && b.right <= 320, `${b.id} sits at ${Math.round(b.left)}..${Math.round(b.right)}, outside 0..320`);
|