@hybridlabor-api/aos 4.6.6 → 4.7.1
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/.agents/agents.md +9 -3
- package/.agents/nodes.json +7 -2
- package/.agents/state.schema.json +26 -1
- package/.agents/vendor-manifest.json +56 -0
- package/.agents/workflows/startcycle-graph.md +2 -2
- package/.claude/agents/architect.md +7 -2
- package/.claude/agents/security-reviewer.md +1 -1
- package/.claude/agents/techlead.md +2 -0
- package/.claude/hooks/memb-inject.mjs +257 -135
- package/.claude/hooks/trail-relay.mjs +18 -0
- package/.claude/settings.json +70 -0
- package/.claude/workflows/startcycle-dispatch.mjs +203 -17
- package/.opencode/agents/architect.md +7 -2
- package/.opencode/agents/techlead.md +2 -0
- package/.opencode/plugins/bdb-aos.js +75 -0
- package/THIRD_PARTY_NOTICES.md +57 -1
- package/bin/aos-doctor.mjs +366 -0
- package/bin/aos-store.mjs +167 -0
- package/bin/aos-uninstall.mjs +55 -1
- package/docs/AUDIT_BRIEF_ECC_STORE.md +122 -0
- package/docs/PLAN_AOS_STORE_PHASE1.md +190 -0
- package/docs/handover/agenttrail-for-ao.md +58 -0
- package/docs/handover/deja-for-ao.md +51 -0
- package/installer.js +182 -38
- package/lib/aos-archify-contract.mjs +277 -0
- package/lib/ecc-store-index.json +1 -0
- package/mcp_config.json +8 -0
- package/mcps/mcsc/README.md +24 -0
- package/mcps/mcsc/package-lock.json +1234 -0
- package/mcps/mcsc/package.json +14 -0
- package/mcps/mcsc/packages/core/bin/mcsc.js +162 -0
- package/mcps/mcsc/packages/core/rulebook.default.yaml +49 -0
- package/mcps/mcsc/packages/core/schema/rulebook.schema.json +56 -0
- package/mcps/mcsc/packages/core/src/adapters/agy.js +107 -0
- package/mcps/mcsc/packages/core/src/adapters/codex.js +179 -0
- package/mcps/mcsc/packages/core/src/adapters/native.js +14 -0
- package/mcps/mcsc/packages/core/src/adapters/opencode.js +183 -0
- package/mcps/mcsc/packages/core/src/adapters/types.js +21 -0
- package/mcps/mcsc/packages/core/src/capabilities/buildOfferTable.js +34 -0
- package/mcps/mcsc/packages/core/src/capabilities/registry.js +99 -0
- package/mcps/mcsc/packages/core/src/capabilities/types.js +24 -0
- package/mcps/mcsc/packages/core/src/detect/cache.js +47 -0
- package/mcps/mcsc/packages/core/src/detect/inventory.js +73 -0
- package/mcps/mcsc/packages/core/src/detect/probes.js +158 -0
- package/mcps/mcsc/packages/core/src/detect/types.js +26 -0
- package/mcps/mcsc/packages/core/src/rulebook/load.js +53 -0
- package/mcps/mcsc/packages/core/src/rulebook/resolve.js +227 -0
- package/mcps/mcsc/packages/core/src/rulebook/rulebook.types.js +89 -0
- package/mcps/mcsc/packages/core/src/trail.js +17 -0
- package/mcps/mcsc/packages/mcp/server.js +302 -0
- package/mcps/mcsc/server.js +2 -0
- package/package.json +8 -2
- package/scripts/build-ecc-store-index.mjs +147 -0
- package/skills/basic/bdbmediastorm/SKILL.md +1 -1
- package/skills/basic/godmode-ui-ux/SKILL.md +2 -2
- package/skills/basic/godmode-ui-ux/references/anti-slop-rules.md +44 -40
- package/skills/basic/godmode-ui-ux/references/taste-archetypes.md +49 -0
- package/skills/basic/startcycle/SKILL.md +7 -0
- package/skills/basic/startcycle-graph/SKILL.md +25 -0
- package/skills/basic/startcycle-graph-user/SKILL.md +3 -0
- package/skills/basic/teamwork-preview/SKILL.md +2 -0
- package/skills/bdbrainstorm/SKILL.md +1 -0
- package/skills/global_config/agenttrail/LICENSE +21 -0
- package/skills/global_config/agenttrail/SKILL.md +48 -0
- package/skills/global_config/agenttrail/bin/agenttrail.mjs +1113 -0
- package/skills/global_config/agenttrail/public/index.html +662 -0
- package/skills/global_config/aos-project-init/SKILL.md +25 -5
- package/skills/global_config/aos-setup/SKILL.md +34 -1
- package/skills/global_config/aos-setup/scripts/aos-doctor.mjs +115 -1
- package/skills/global_config/archify/LICENSE +22 -0
- package/skills/global_config/archify/SKILL.md +140 -0
- package/skills/global_config/archify/THIRD_PARTY_NOTICES.md +69 -0
- package/skills/global_config/archify/assets/JetBrainsMono-OFL.txt +93 -0
- package/skills/global_config/archify/assets/template.html +14937 -0
- package/skills/global_config/archify/bin/archify.mjs +2139 -0
- package/skills/global_config/archify/bin/open-artifact.mjs +86 -0
- package/skills/global_config/archify/bin/preview.mjs +653 -0
- package/skills/global_config/archify/bin/visual-check.mjs +829 -0
- package/skills/global_config/archify/brand-marks/README.md +31 -0
- package/skills/global_config/archify/brand-marks/catalog.json +131 -0
- package/skills/global_config/archify/delta/architecture-delta.mjs +1232 -0
- package/skills/global_config/archify/examples/agent-run.lifecycle.json +60 -0
- package/skills/global_config/archify/examples/agent-tool-call.workflow.json +94 -0
- package/skills/global_config/archify/examples/async-job-roundtrip.sequence.json +61 -0
- package/skills/global_config/archify/examples/brand-aware-delivery.architecture.json +47 -0
- package/skills/global_config/archify/examples/cache-miss-request.sequence.json +82 -0
- package/skills/global_config/archify/examples/checkout-platform.base.architecture.json +31 -0
- package/skills/global_config/archify/examples/checkout-platform.head.architecture.json +31 -0
- package/skills/global_config/archify/examples/dataflow-product-analytics.html +15048 -0
- package/skills/global_config/archify/examples/deployment-release.lifecycle.json +49 -0
- package/skills/global_config/archify/examples/event-stream.dataflow.json +57 -0
- package/skills/global_config/archify/examples/incident-response.workflow.json +64 -0
- package/skills/global_config/archify/examples/lifecycle-agent-run.html +14983 -0
- package/skills/global_config/archify/examples/product-analytics.dataflow.json +76 -0
- package/skills/global_config/archify/examples/production-deployment.architecture.json +71 -0
- package/skills/global_config/archify/examples/release-delivery.workflow.json +62 -0
- package/skills/global_config/archify/examples/sequence-cache-miss-request.html +15063 -0
- package/skills/global_config/archify/examples/web-app-rendered.html +15012 -0
- package/skills/global_config/archify/examples/web-app.architecture.json +46 -0
- package/skills/global_config/archify/examples/workflow-agent-tool-call-rendered.html +15054 -0
- package/skills/global_config/archify/migrations/workflow-v2.mjs +279 -0
- package/skills/global_config/archify/package.json +39 -0
- package/skills/global_config/archify/recipes/scenarios.mjs +391 -0
- package/skills/global_config/archify/references/authoring-contract.md +247 -0
- package/skills/global_config/archify/references/brand-marks.md +65 -0
- package/skills/global_config/archify/references/delivery-contract.md +138 -0
- package/skills/global_config/archify/references/viewer-runtime.md +45 -0
- package/skills/global_config/archify/renderers/architecture/grid.mjs +62 -0
- package/skills/global_config/archify/renderers/architecture/render-architecture.mjs +1078 -0
- package/skills/global_config/archify/renderers/dataflow/README.md +104 -0
- package/skills/global_config/archify/renderers/dataflow/render-dataflow.mjs +483 -0
- package/skills/global_config/archify/renderers/lifecycle/README.md +115 -0
- package/skills/global_config/archify/renderers/lifecycle/render-lifecycle.mjs +561 -0
- package/skills/global_config/archify/renderers/sequence/README.md +114 -0
- package/skills/global_config/archify/renderers/sequence/render-sequence.mjs +464 -0
- package/skills/global_config/archify/renderers/shared/brand-marks.mjs +563 -0
- package/skills/global_config/archify/renderers/shared/cli.mjs +218 -0
- package/skills/global_config/archify/renderers/shared/desktop-readability.mjs +26 -0
- package/skills/global_config/archify/renderers/shared/diagnostics.mjs +160 -0
- package/skills/global_config/archify/renderers/shared/engineering-profiles.mjs +157 -0
- package/skills/global_config/archify/renderers/shared/generated-brand-marks.mjs +2003 -0
- package/skills/global_config/archify/renderers/shared/generated-validators.mjs +13 -0
- package/skills/global_config/archify/renderers/shared/geometry.mjs +1423 -0
- package/skills/global_config/archify/renderers/shared/i18n.mjs +595 -0
- package/skills/global_config/archify/renderers/shared/layout-report.mjs +40 -0
- package/skills/global_config/archify/renderers/shared/legend.mjs +217 -0
- package/skills/global_config/archify/renderers/shared/output-path.mjs +340 -0
- package/skills/global_config/archify/renderers/shared/repository-evidence.mjs +239 -0
- package/skills/global_config/archify/renderers/shared/repository-location.mjs +58 -0
- package/skills/global_config/archify/renderers/shared/text-fit.mjs +49 -0
- package/skills/global_config/archify/renderers/shared/utils.mjs +232 -0
- package/skills/global_config/archify/renderers/shared/validator.mjs +86 -0
- package/skills/global_config/archify/renderers/workflow/README.md +223 -0
- package/skills/global_config/archify/renderers/workflow/render-workflow.mjs +35 -0
- package/skills/global_config/archify/renderers/workflow/workflow-compiler.mjs +4400 -0
- package/skills/global_config/archify/renderers/workflow/workflow-migration-geometry.mjs +144 -0
- package/skills/global_config/archify/schemas/README.md +211 -0
- package/skills/global_config/archify/schemas/architecture.schema.json +178 -0
- package/skills/global_config/archify/schemas/common.schema.json +115 -0
- package/skills/global_config/archify/schemas/dataflow.schema.json +243 -0
- package/skills/global_config/archify/schemas/lifecycle.schema.json +266 -0
- package/skills/global_config/archify/schemas/sequence.schema.json +223 -0
- package/skills/global_config/archify/schemas/workflow.schema.json +428 -0
- package/skills/global_config/archify/scripts/check-render-output.mjs +916 -0
- package/skills/global_config/archify/scripts/generate-brand-marks.mjs +141 -0
- package/skills/global_config/archify/scripts/generate-validators.mjs +66 -0
- package/skills/global_config/archify/scripts/render-examples.mjs +26 -0
- package/skills/global_config/archify/skill-release.json +10 -0
- package/skills/global_config/ask-tim/SKILL.md +10 -0
- package/skills/global_config/bdb-security-audit/SKILL.md +109 -0
- package/skills/global_config/bdb-security-audit/references/audit-checklists.md +172 -0
- package/skills/global_config/bdb-security-audit/references/skylos-guide.md +139 -0
- package/skills/global_config/bdb-security-audit/references/xalgorix-bridge.md +110 -0
- package/skills/global_config/bdbdesignpro/SKILL.md +4 -4
- package/skills/global_config/bdbdesignpro/references/bdb-motion-tokens.md +52 -31
- package/skills/global_config/bdbresilience/SKILL.md +10 -0
- package/skills/global_config/brag/SKILL.md +179 -0
- package/skills/global_config/brag/assets/music/README.md +20 -0
- package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-1-by-ende-dot-app.music-cues.json +3846 -0
- package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-1-by-ende-dot-app.music-cues.md +35 -0
- package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-10-by-ende-dot-app.music-cues.json +1826 -0
- package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-10-by-ende-dot-app.music-cues.md +35 -0
- package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-11-by-ende-dot-app.music-cues.json +2266 -0
- package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-11-by-ende-dot-app.music-cues.md +35 -0
- package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-12-by-ende-dot-app.music-cues.json +2806 -0
- package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-12-by-ende-dot-app.music-cues.md +35 -0
- package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-9-by-ende-dot-app.music-cues.json +2796 -0
- package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-9-by-ende-dot-app.music-cues.md +35 -0
- package/skills/global_config/brag/assets/music/happy-beats-business-moves-vol-1-by-ende-dot-app.mp3 +0 -0
- package/skills/global_config/brag/assets/music/happy-beats-business-moves-vol-10-by-ende-dot-app.mp3 +0 -0
- package/skills/global_config/brag/assets/music/happy-beats-business-moves-vol-11-by-ende-dot-app.mp3 +0 -0
- package/skills/global_config/brag/assets/music/happy-beats-business-moves-vol-12-by-ende-dot-app.mp3 +0 -0
- package/skills/global_config/brag/assets/music/happy-beats-business-moves-vol-9-by-ende-dot-app.mp3 +0 -0
- package/skills/global_config/brag/assets/sfx/casino/card-fan-1.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/card-fan-2.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/card-place-1.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/card-place-2.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/card-place-3.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/card-place-4.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/card-shove-1.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/card-shove-2.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/card-shove-3.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/card-shove-4.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/card-shuffle.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/card-slide-1.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/card-slide-2.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/card-slide-3.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/card-slide-4.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/card-slide-5.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/card-slide-6.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/card-slide-7.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/card-slide-8.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/cards-pack-open-1.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/cards-pack-open-2.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/chip-lay-1.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/chip-lay-2.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/chip-lay-3.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/chips-collide-1.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/chips-collide-2.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/chips-collide-3.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/chips-collide-4.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/chips-handle-1.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/chips-handle-2.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/chips-handle-3.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/chips-handle-4.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/chips-handle-6.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/chips-stack-1.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/chips-stack-2.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/chips-stack-3.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/chips-stack-4.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/chips-stack-5.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/chips-stack-6.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/dice-grab-1.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/dice-grab-2.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/dice-shake-1.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/dice-shake-2.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/dice-shake-3.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/dice-throw-1.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/dice-throw-2.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/dice-throw-3.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/die-throw-1.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/die-throw-2.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/die-throw-3.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/die-throw-4.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_carpet_000.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_carpet_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_carpet_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_concrete_000.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_concrete_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_concrete_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_concrete_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_concrete_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_grass_000.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_grass_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_grass_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_grass_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_grass_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_snow_000.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_snow_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_snow_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_snow_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_snow_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_wood_000.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_wood_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_wood_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_wood_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_wood_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactBell_heavy_000.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactBell_heavy_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactBell_heavy_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactGeneric_light_000.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactGeneric_light_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactGeneric_light_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactGeneric_light_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactGeneric_light_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactGlass_heavy_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactGlass_light_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactGlass_light_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactGlass_light_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactGlass_medium_000.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactGlass_medium_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactGlass_medium_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactMetal_heavy_000.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactMetal_heavy_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactMetal_heavy_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactMetal_light_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactMetal_light_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactMetal_medium_000.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactMetal_medium_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactMetal_medium_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactMetal_medium_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactMetal_medium_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactMining_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPlank_medium_000.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPlank_medium_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPlank_medium_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPlank_medium_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPlank_medium_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPlate_heavy_000.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPlate_heavy_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPlate_heavy_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPlate_heavy_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPlate_heavy_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPlate_light_000.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPlate_light_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPlate_light_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPlate_light_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPlate_light_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPlate_medium_000.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPlate_medium_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPlate_medium_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPlate_medium_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPlate_medium_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPunch_heavy_000.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPunch_heavy_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPunch_heavy_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPunch_heavy_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPunch_heavy_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPunch_medium_000.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPunch_medium_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPunch_medium_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPunch_medium_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPunch_medium_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactSoft_heavy_000.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactSoft_heavy_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactSoft_heavy_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactSoft_heavy_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactSoft_heavy_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactSoft_medium_000.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactSoft_medium_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactSoft_medium_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactSoft_medium_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactSoft_medium_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactTin_medium_000.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactTin_medium_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactTin_medium_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactTin_medium_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactTin_medium_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactWood_heavy_000.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactWood_heavy_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactWood_heavy_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactWood_heavy_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactWood_heavy_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactWood_light_000.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactWood_light_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactWood_light_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactWood_light_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactWood_light_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactWood_medium_000.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactWood_medium_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactWood_medium_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactWood_medium_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactWood_medium_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/interface/bong_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/interface/click_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/interface/click_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/interface/click_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/interface/click_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/interface/click_005.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/interface/drop_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/interface/drop_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/interface/drop_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/interface/error_005.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/interface/error_006.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/interface/glitch_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/interface/glitch_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/interface/select_008.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/interface/switch_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/interface/switch_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/interface/switch_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/interface/switch_005.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/interface/switch_006.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/interface/switch_007.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-001.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-002.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-003.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-004.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-005.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-006.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-007.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-008.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-009.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-010.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-011.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-012.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-013.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-014.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-015.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-016.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-017.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-018.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-019.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-020.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-021.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-022.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-023.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-024.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-025.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-026.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-027.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-028.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-029.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-030.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-031.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-032.wav +0 -0
- package/skills/global_config/brag/assets/sfx/sfx-analysis.json +15584 -0
- package/skills/global_config/brag/assets/sfx/sfx-analysis.md +75 -0
- package/skills/global_config/brag/assets/sfx/ui/click1.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/click2.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/click3.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/click4.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/click5.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/mouseclick1.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/rollover1.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/rollover2.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/rollover4.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/rollover5.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch1.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch10.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch11.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch12.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch13.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch14.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch15.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch16.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch17.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch18.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch19.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch2.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch20.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch21.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch22.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch23.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch24.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch25.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch26.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch27.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch28.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch29.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch3.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch30.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch31.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch32.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch33.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch34.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch35.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch36.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch37.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch38.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch4.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch5.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch6.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch7.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch8.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch9.ogg +0 -0
- package/skills/global_config/brag/references/audio.md +307 -0
- package/skills/global_config/brag/references/step-1-inspect.md +133 -0
- package/skills/global_config/brag/references/step-2-plan.md +234 -0
- package/skills/global_config/brag/references/step-3-compose.md +210 -0
- package/skills/global_config/brag/references/step-4-deliver.md +168 -0
- package/skills/global_config/brag/references/tones.md +231 -0
- package/skills/global_config/brag/scripts/analyze_music_cues.py +309 -0
- package/skills/global_config/brag/scripts/pyproject.toml +14 -0
- package/skills/global_config/brag/scripts/uv.lock +819 -0
- package/skills/global_config/deja-memory/SKILL.md +42 -0
- package/skills/global_config/editable-design/LICENSE +202 -0
- package/skills/global_config/editable-design/README.md +34 -0
- package/skills/global_config/editable-design/SKILL.md +630 -0
- package/skills/global_config/editable-design/THIRD_PARTY_NOTICES.md +10 -0
- package/skills/global_config/editable-design/agents/openai.yaml +4 -0
- package/skills/global_config/editable-design/assets/editor/layer-editor.css +262 -0
- package/skills/global_config/editable-design/assets/editor/layer-editor.js +1929 -0
- package/skills/global_config/editable-design/assets/poster-starter/README.md +92 -0
- package/skills/global_config/editable-design/assets/poster-starter/assets/.gitkeep +0 -0
- package/skills/global_config/editable-design/assets/poster-starter/index.html +91 -0
- package/skills/global_config/editable-design/assets/poster-starter/poster.json +6 -0
- package/skills/global_config/editable-design/assets/poster-starter/reference/.gitkeep +1 -0
- package/skills/global_config/editable-design/assets/replay/icons/assets.png +0 -0
- package/skills/global_config/editable-design/assets/replay/icons/html.png +0 -0
- package/skills/global_config/editable-design/assets/replay/icons/input.png +0 -0
- package/skills/global_config/editable-design/assets/replay/icons/layers.png +0 -0
- package/skills/global_config/editable-design/assets/replay/icons/plan.png +0 -0
- package/skills/global_config/editable-design/assets/replay/icons/reference.png +0 -0
- package/skills/global_config/editable-design/assets/replay/icons/review.png +0 -0
- package/skills/global_config/editable-design/assets/replay/index.html +59 -0
- package/skills/global_config/editable-design/assets/replay/replay-motion.js +410 -0
- package/skills/global_config/editable-design/assets/replay/replay.css +380 -0
- package/skills/global_config/editable-design/assets/replay/replay.js +374 -0
- package/skills/global_config/editable-design/font-kit/package-lock.json +115 -0
- package/skills/global_config/editable-design/font-kit/package.json +19 -0
- package/skills/global_config/editable-design/pack.sh +20 -0
- package/skills/global_config/editable-design/references/asset-architecture.md +72 -0
- package/skills/global_config/editable-design/references/editor-pitfalls.md +70 -0
- package/skills/global_config/editable-design/references/editor-runtime.md +131 -0
- package/skills/global_config/editable-design/references/font-system.md +89 -0
- package/skills/global_config/editable-design/references/imagery.md +145 -0
- package/skills/global_config/editable-design/references/layout-typography.md +116 -0
- package/skills/global_config/editable-design/references/replay-contract.md +169 -0
- package/skills/global_config/editable-design/scripts/_browser.mjs +247 -0
- package/skills/global_config/editable-design/scripts/bake.mjs +87 -0
- package/skills/global_config/editable-design/scripts/build-replay.mjs +316 -0
- package/skills/global_config/editable-design/scripts/check-contract.mjs +293 -0
- package/skills/global_config/editable-design/scripts/check-fonts.sh +252 -0
- package/skills/global_config/editable-design/scripts/check-poster.sh +70 -0
- package/skills/global_config/editable-design/scripts/doctor.sh +78 -0
- package/skills/global_config/editable-design/scripts/explode.mjs +115 -0
- package/skills/global_config/editable-design/scripts/font-kit.mjs +187 -0
- package/skills/global_config/editable-design/scripts/import-asset.sh +23 -0
- package/skills/global_config/editable-design/scripts/init-poster.sh +64 -0
- package/skills/global_config/editable-design/scripts/install-font-kit.sh +13 -0
- package/skills/global_config/editable-design/scripts/package-lock.json +334 -0
- package/skills/global_config/editable-design/scripts/package.json +14 -0
- package/skills/global_config/editable-design/scripts/remove-chroma-key.py +201 -0
- package/skills/global_config/editable-design/scripts/render-poster.sh +202 -0
- package/skills/global_config/editable-design/scripts/trace.sh +159 -0
- package/skills/global_config/editable-design/scripts/verify-replay.mjs +97 -0
- package/skills/global_config/editable-design/scripts/verify.mjs +782 -0
- package/skills/global_config/editable-design/scripts/wire-editor.mjs +104 -0
- package/skills/global_config/editable-design/tests/fixtures/small-explode/index.html +86 -0
- package/skills/global_config/mcsc/SKILL.md +60 -0
- package/skills/global_config/plan-canvas/SKILL.md +8 -0
- package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/server.js +31 -6
- package/skills/global_config/plan-canvas/scripts/plan-canvas.js +4 -2
|
@@ -0,0 +1,782 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* verify.mjs —— 编辑器功能验证
|
|
4
|
+
*
|
|
5
|
+
* node verify.mjs <海报 index.html> [--timings]
|
|
6
|
+
*
|
|
7
|
+
* A 类断言(转换类):固化视觉零偏差。
|
|
8
|
+
* B 类断言(编辑类):改完导出、重新加载后 computed style 与几何一致。
|
|
9
|
+
* —— 编辑功能是「故意改变视觉」的,A 类断言对它们不适用。
|
|
10
|
+
* B 类要防的是「改得动但导不出去」,尤其字体静默回落成系统字体。
|
|
11
|
+
*
|
|
12
|
+
* 退出码 0 = 全绿,1 = 有失败。
|
|
13
|
+
*/
|
|
14
|
+
import { writeFileSync, existsSync, unlinkSync } from 'node:fs';
|
|
15
|
+
import { dirname, join, resolve, basename } from 'node:path';
|
|
16
|
+
import { launch, open, probe, diff, pixelDiff, wireTemp } from './_browser.mjs';
|
|
17
|
+
|
|
18
|
+
const file = process.argv[2];
|
|
19
|
+
if (!file || !existsSync(resolve(file))) {
|
|
20
|
+
console.error('用法: node verify.mjs <海报 index.html>');
|
|
21
|
+
process.exit(2);
|
|
22
|
+
}
|
|
23
|
+
const src = resolve(file);
|
|
24
|
+
const dir = dirname(src);
|
|
25
|
+
const layersOutput = join(dir, 'layers.html');
|
|
26
|
+
const showTimings = process.argv.includes('--timings');
|
|
27
|
+
const timingStartedAt = Date.now();
|
|
28
|
+
let timingMarkedAt = timingStartedAt;
|
|
29
|
+
let timingPhase = '启动浏览器';
|
|
30
|
+
const timingRows = [];
|
|
31
|
+
|
|
32
|
+
const markTiming = (nextPhase) => {
|
|
33
|
+
const now = Date.now();
|
|
34
|
+
if (showTimings) timingRows.push({ phase: timingPhase, ms: now - timingMarkedAt });
|
|
35
|
+
timingPhase = nextPhase;
|
|
36
|
+
timingMarkedAt = now;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
let fails = 0;
|
|
40
|
+
let verifiedLayersHTML = '';
|
|
41
|
+
const check = (name, ok, extra = '') => {
|
|
42
|
+
if (!ok) fails++;
|
|
43
|
+
console.log(` ${ok ? '✅' : '❌'} ${name}${extra ? ' ' + extra : ''}`);
|
|
44
|
+
};
|
|
45
|
+
const group = (t) => {
|
|
46
|
+
markTiming(t);
|
|
47
|
+
console.log(`\n【${t}】`);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** 采集每个图层的 computed style 与几何,用于 B 类往返比对。
|
|
51
|
+
* 不依赖编辑器注入的 class,因此导出物上同样可用。 */
|
|
52
|
+
const COLLECT = () => {
|
|
53
|
+
const cv = document.querySelector('[data-canvas-width]') || document.querySelector('.poster-canvas');
|
|
54
|
+
const c = cv.getBoundingClientRect();
|
|
55
|
+
const scale = c.width / (+cv.dataset.canvasWidth || cv.offsetWidth);
|
|
56
|
+
const out = {};
|
|
57
|
+
for (const el of cv.querySelectorAll('[data-layer-id]')) {
|
|
58
|
+
const nodes = [el, ...el.querySelectorAll('*')].filter((n) => !(n instanceof SVGElement));
|
|
59
|
+
const r = el.getBoundingClientRect();
|
|
60
|
+
out[el.dataset.layerId] = {
|
|
61
|
+
left: +((r.left - c.left) / scale).toFixed(1),
|
|
62
|
+
top: +((r.top - c.top) / scale).toFixed(1),
|
|
63
|
+
width: +(r.width / scale).toFixed(1),
|
|
64
|
+
height: +(r.height / scale).toFixed(1),
|
|
65
|
+
fonts: nodes.map((n) => {
|
|
66
|
+
const cs = getComputedStyle(n);
|
|
67
|
+
return `${cs.fontSize}|${cs.fontFamily}`;
|
|
68
|
+
}),
|
|
69
|
+
widths: nodes.map((n) => +(n.getBoundingClientRect().width / scale).toFixed(1)),
|
|
70
|
+
text: el.textContent.replace(/\s+/g, ' ').trim(),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
function compare(a, b) {
|
|
77
|
+
const issues = [];
|
|
78
|
+
for (const id of Object.keys(a)) {
|
|
79
|
+
if (!b[id]) { issues.push(`${id} 在导出物中缺失`); continue; }
|
|
80
|
+
const x = a[id], y = b[id];
|
|
81
|
+
if (Math.abs(x.left - y.left) > 1 || Math.abs(x.top - y.top) > 1) {
|
|
82
|
+
issues.push(`${id} 坐标漂移 ${x.left},${x.top} → ${y.left},${y.top}`);
|
|
83
|
+
}
|
|
84
|
+
if (Math.abs(x.width - y.width) > 1 || Math.abs(x.height - y.height) > 1) {
|
|
85
|
+
issues.push(`${id} 尺寸漂移 ${x.width}×${x.height} → ${y.width}×${y.height}`);
|
|
86
|
+
}
|
|
87
|
+
if (x.text !== y.text) issues.push(`${id} 文本不一致`);
|
|
88
|
+
if (x.fonts.join('#') !== y.fonts.join('#')) {
|
|
89
|
+
const i = x.fonts.findIndex((v, k) => v !== y.fonts[k]);
|
|
90
|
+
issues.push(`${id} 字体/字号不一致: "${x.fonts[i]}" → "${y.fonts[i] ?? '(缺)'}"`);
|
|
91
|
+
}
|
|
92
|
+
// 字体若静默回落成系统字体,computed family 可能不变但渲染宽度会变
|
|
93
|
+
const wBad = x.widths.findIndex((v, k) => Math.abs(v - (y.widths[k] ?? 0)) > 1);
|
|
94
|
+
if (wBad >= 0) issues.push(`${id} 渲染宽度不一致 ${x.widths[wBad]} → ${y.widths[wBad] ?? '(缺)'}(字体可能回落)`);
|
|
95
|
+
}
|
|
96
|
+
for (const id of Object.keys(b)) if (!a[id]) issues.push(`${id} 多出于导出物`);
|
|
97
|
+
return issues;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const wired = wireTemp(src, '_hf_verify.html');
|
|
101
|
+
const browser = await launch();
|
|
102
|
+
try {
|
|
103
|
+
/* ---------- A 类:固化零偏差 ---------- */
|
|
104
|
+
group('A 类 · 固化视觉零偏差');
|
|
105
|
+
const { page: p0 } = await open(browser, src);
|
|
106
|
+
const before = await probe(p0);
|
|
107
|
+
const { page, errors } = await open(browser, wired.file, { waitEditor: true });
|
|
108
|
+
const afterBake = await probe(page);
|
|
109
|
+
const dA = diff(before, afterBake, 1);
|
|
110
|
+
check(`${dA.rows.length} 个图层坐标不变`, dA.ok, `最大偏差 ${dA.worst}px`);
|
|
111
|
+
|
|
112
|
+
// 几何一致不等于视觉一致:上下文样式、背景、伪元素和绘制顺序
|
|
113
|
+
// 发生变化时,图层框仍可能完全不动。所以固化类验收必须再过一道像素门。
|
|
114
|
+
// 编辑器里的画布放在固定、可滚动的舞台中,直接截取超出视口的
|
|
115
|
+
// DOM 可能被父层 overflow 裁掉。先导出一份未修改的 baked HTML,
|
|
116
|
+
// 再在同尺寸视口中与原稿比像素,比对的仍然是 bake 结果。
|
|
117
|
+
const bakedFile = join(dir, '_hf_verify_baked.html');
|
|
118
|
+
writeFileSync(bakedFile, await page.evaluate(() => window.__layerEditor.fullHTML()));
|
|
119
|
+
const canvasSize = await p0.evaluate(() => {
|
|
120
|
+
const cv = document.querySelector('[data-canvas-width]') || document.querySelector('.poster-canvas');
|
|
121
|
+
return {
|
|
122
|
+
width: Math.ceil(+cv.dataset.canvasWidth || cv.offsetWidth),
|
|
123
|
+
height: Math.ceil(+cv.dataset.canvasHeight || cv.offsetHeight),
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
const pixelViewport = {
|
|
127
|
+
width: Math.max(1600, Math.min(8192, canvasSize.width + 20)),
|
|
128
|
+
height: Math.max(1200, Math.min(8192, canvasSize.height + 20)),
|
|
129
|
+
deviceScaleFactor: 1,
|
|
130
|
+
};
|
|
131
|
+
await p0.setViewport(pixelViewport);
|
|
132
|
+
const { page: pBaked, errors: eBaked } = await open(browser, bakedFile, pixelViewport);
|
|
133
|
+
const pixels = await pixelDiff(
|
|
134
|
+
p0, '[data-canvas-width],.poster-canvas',
|
|
135
|
+
pBaked, '[data-canvas-width],.poster-canvas',
|
|
136
|
+
);
|
|
137
|
+
// 少量 mix-blend-mode / 阴影在被提升进新的 stacking context 后会产生
|
|
138
|
+
// 亚像素级合成差异;同时约束“变化像素比例”和“平均通道差”,可容纳
|
|
139
|
+
// 这种不可见噪声,又会拦住丢背景、错层、去旋转等真实视觉漂移。
|
|
140
|
+
const pixelOK = eBaked.length === 0 && pixels.ok
|
|
141
|
+
&& pixels.changedRatio <= 0.005 && pixels.meanAbs <= 0.05;
|
|
142
|
+
check('画布像素级无可见漂移', pixelOK, pixels.reason
|
|
143
|
+
|| `变化 ${(pixels.changedRatio * 100).toFixed(3)}%,平均通道差 ${pixels.meanAbs.toFixed(4)}`);
|
|
144
|
+
await pBaked.close();
|
|
145
|
+
try { unlinkSync(bakedFile); } catch {}
|
|
146
|
+
|
|
147
|
+
const scale = await page.evaluate(() => window.__layerEditor.state.scale);
|
|
148
|
+
const at = (sel) => page.evaluate((s) => {
|
|
149
|
+
const r = document.querySelector(s).getBoundingClientRect();
|
|
150
|
+
return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
|
|
151
|
+
}, sel);
|
|
152
|
+
// puppeteer 的 clickCount:2 只改参数、不真的按两次,detail 恒为 1,
|
|
153
|
+
// 浏览器不会合成 dblclick。必须手动发两轮 down/up。
|
|
154
|
+
const dbl = async (sel) => {
|
|
155
|
+
const { x, y } = await at(sel);
|
|
156
|
+
await page.mouse.move(x, y);
|
|
157
|
+
await page.mouse.down(); await page.mouse.up();
|
|
158
|
+
await page.mouse.down({ clickCount: 2 }); await page.mouse.up({ clickCount: 2 });
|
|
159
|
+
await new Promise((r) => setTimeout(r, 180));
|
|
160
|
+
};
|
|
161
|
+
const layerIds = await page.evaluate(() => window.__layerEditor.layers().map((l) => l.id));
|
|
162
|
+
const groupInfo = await page.evaluate(() => [...window.__layerEditor.state.groups.values()].map((group) => ({
|
|
163
|
+
id: group.id,
|
|
164
|
+
label: group.label,
|
|
165
|
+
members: group.memberIds,
|
|
166
|
+
})));
|
|
167
|
+
const explodeExpectation = await page.evaluate(() => {
|
|
168
|
+
const layers = window.__layerEditor.layers();
|
|
169
|
+
const groups = [...window.__layerEditor.state.groups.values()];
|
|
170
|
+
const grouped = new Set(groups.flatMap((group) => group.memberIds));
|
|
171
|
+
const primary = groups.length + layers.filter((layer) => !grouped.has(layer.id)).length;
|
|
172
|
+
const textSupplements = primary < 5 ? layers.filter(({ id, el }) => {
|
|
173
|
+
if (!grouped.has(id)) return false;
|
|
174
|
+
const role = (el.dataset.explodeRole || 'copy').trim();
|
|
175
|
+
if (['surface', 'image', 'icon', 'decoration'].includes(role)) return false;
|
|
176
|
+
return Boolean((el.innerText || el.textContent || '').replace(/\s+/g, ' ').trim());
|
|
177
|
+
}).length : 0;
|
|
178
|
+
return { primary, textSupplements, units: primary + textSupplements + 2 };
|
|
179
|
+
});
|
|
180
|
+
const expectedExplodedUnits = explodeExpectation.units;
|
|
181
|
+
const first = await page.evaluate(() => {
|
|
182
|
+
const layers = window.__layerEditor.layers();
|
|
183
|
+
return layers.find(({ el }) =>
|
|
184
|
+
el.classList.contains('hf-slot') || el.querySelector('.hf-slot'))?.id
|
|
185
|
+
|| layers[0]?.id;
|
|
186
|
+
});
|
|
187
|
+
const sel1 = `[data-layer-id="${first}"]`;
|
|
188
|
+
const geomOf = (id) => page.evaluate((i) => {
|
|
189
|
+
const el = document.querySelector(`[data-layer-id="${i}"]`);
|
|
190
|
+
return el ? { left: parseFloat(el.style.left), top: parseFloat(el.style.top) } : null;
|
|
191
|
+
}, id);
|
|
192
|
+
const boxOf = (id) => page.evaluate((i) => {
|
|
193
|
+
const el = document.querySelector(`[data-layer-id="${i}"]`);
|
|
194
|
+
if (!el) return null;
|
|
195
|
+
const r = el.getBoundingClientRect();
|
|
196
|
+
const scale = window.__layerEditor.state.scale;
|
|
197
|
+
return {
|
|
198
|
+
left: parseFloat(el.style.left), top: parseFloat(el.style.top),
|
|
199
|
+
width: r.width / scale, height: r.height / scale,
|
|
200
|
+
widthLocked: el.dataset.hfWidthLocked === '1',
|
|
201
|
+
heightLocked: el.dataset.hfHeightLocked === '1',
|
|
202
|
+
};
|
|
203
|
+
}, id);
|
|
204
|
+
|
|
205
|
+
/* ---------- 拖拽 ---------- */
|
|
206
|
+
group('拖拽');
|
|
207
|
+
const g0 = await geomOf(first);
|
|
208
|
+
const pt = await at(sel1);
|
|
209
|
+
await page.mouse.move(pt.x, pt.y);
|
|
210
|
+
await page.mouse.down();
|
|
211
|
+
await page.mouse.move(pt.x + 60, pt.y + 40, { steps: 12 });
|
|
212
|
+
await page.mouse.up();
|
|
213
|
+
const g1 = await geomOf(first);
|
|
214
|
+
const moved = Math.abs(g1.left - g0.left) > 20 && Math.abs(g1.top - g0.top) > 10;
|
|
215
|
+
check('位移按画布缩放换算', moved,
|
|
216
|
+
`屏幕 +60,+40 → 画布 +${(g1.left - g0.left).toFixed(1)},+${(g1.top - g0.top).toFixed(1)}(缩放 ${Math.round(scale * 100)}%,含吸附修正)`);
|
|
217
|
+
check('拖拽未误选文字', await page.evaluate(() => getSelection().toString() === ''));
|
|
218
|
+
await page.evaluate(() => window.__layerEditor.undo());
|
|
219
|
+
const g2 = await geomOf(first);
|
|
220
|
+
check('撤销精确复位', Math.abs(g2.left - g0.left) < 0.5 && Math.abs(g2.top - g0.top) < 0.5);
|
|
221
|
+
const release0 = await geomOf(first);
|
|
222
|
+
await page.evaluate((id) => {
|
|
223
|
+
const el = window.__layerEditor.layers().find((item) => item.id === id).el;
|
|
224
|
+
const rect = el.getBoundingClientRect();
|
|
225
|
+
const init = { bubbles: true, pointerId: 97, pointerType: 'mouse', isPrimary: true };
|
|
226
|
+
el.dispatchEvent(new PointerEvent('pointerdown', {
|
|
227
|
+
...init, button: 0, buttons: 1, clientX: rect.left + 4, clientY: rect.top + 4,
|
|
228
|
+
}));
|
|
229
|
+
el.dispatchEvent(new PointerEvent('pointermove', {
|
|
230
|
+
...init, button: -1, buttons: 0, clientX: rect.left + 90, clientY: rect.top + 60,
|
|
231
|
+
}));
|
|
232
|
+
window.dispatchEvent(new PointerEvent('pointerup', {
|
|
233
|
+
...init, button: 0, buttons: 0, clientX: rect.left + 90, clientY: rect.top + 60,
|
|
234
|
+
}));
|
|
235
|
+
}, first);
|
|
236
|
+
const release1 = await geomOf(first);
|
|
237
|
+
check('鼠标已松开时不会残留拖拽', Math.abs(release1.left - release0.left) < 0.5
|
|
238
|
+
&& Math.abs(release1.top - release0.top) < 0.5);
|
|
239
|
+
const escape0 = await geomOf(first);
|
|
240
|
+
const escapeAt = await at(`[data-layer-id="${first}"]`);
|
|
241
|
+
await page.mouse.move(escapeAt.x, escapeAt.y);
|
|
242
|
+
await page.mouse.down();
|
|
243
|
+
await page.mouse.move(escapeAt.x + 42, escapeAt.y + 24, { steps: 3 });
|
|
244
|
+
const escapeMoved = await geomOf(first);
|
|
245
|
+
await page.keyboard.press('Escape');
|
|
246
|
+
await page.mouse.up();
|
|
247
|
+
const escape1 = await geomOf(first);
|
|
248
|
+
check('Esc 可取消进行中的拖拽', Math.abs(escapeMoved.left - escape0.left) > 5
|
|
249
|
+
&& Math.abs(escape1.left - escape0.left) < 0.5
|
|
250
|
+
&& Math.abs(escape1.top - escape0.top) < 0.5);
|
|
251
|
+
|
|
252
|
+
/* ---------- 宽高缩放 ---------- */
|
|
253
|
+
group('宽高缩放');
|
|
254
|
+
await page.evaluate((id) => {
|
|
255
|
+
const layer = window.__layerEditor.layers().find((item) => item.id === id);
|
|
256
|
+
window.__layerEditor.select(layer.el);
|
|
257
|
+
}, first);
|
|
258
|
+
check('选中后显示八个缩放手柄', await page.evaluate(() =>
|
|
259
|
+
document.querySelectorAll('.hf-resize-handle').length === 8
|
|
260
|
+
&& !document.querySelector('.hf-resize-overlay').hidden));
|
|
261
|
+
check('缩放手柄具有宽松点击热区', await page.evaluate(() =>
|
|
262
|
+
[...document.querySelectorAll('.hf-resize-handle')].every((handle) => {
|
|
263
|
+
const rect = handle.getBoundingClientRect();
|
|
264
|
+
return rect.width >= 28 && rect.height >= 28;
|
|
265
|
+
})));
|
|
266
|
+
const size0 = await boxOf(first);
|
|
267
|
+
const dragHandle = async (selector, dx, dy) => {
|
|
268
|
+
const handle = await page.$(selector);
|
|
269
|
+
const box = await handle?.boundingBox();
|
|
270
|
+
if (!box) return false;
|
|
271
|
+
const x = box.x + box.width / 2;
|
|
272
|
+
const y = box.y + box.height / 2;
|
|
273
|
+
await page.mouse.move(x, y);
|
|
274
|
+
await page.mouse.down();
|
|
275
|
+
await page.mouse.move(x + dx, y + dy, { steps: 10 });
|
|
276
|
+
await page.mouse.up();
|
|
277
|
+
return true;
|
|
278
|
+
};
|
|
279
|
+
const eastReady = await dragHandle('.hf-resize-handle.e', 52, 0);
|
|
280
|
+
const sizeW = await boxOf(first);
|
|
281
|
+
check('左右手柄只改变宽度', eastReady
|
|
282
|
+
&& sizeW.width > size0.width + 20
|
|
283
|
+
&& Math.abs(sizeW.height - size0.height) < 0.5
|
|
284
|
+
&& sizeW.widthLocked && !sizeW.heightLocked,
|
|
285
|
+
`${size0.width.toFixed(1)}×${size0.height.toFixed(1)} → ${sizeW.width.toFixed(1)}×${sizeW.height.toFixed(1)}`);
|
|
286
|
+
const southReady = await dragHandle('.hf-resize-handle.s', 0, 38);
|
|
287
|
+
const sizeWH = await boxOf(first);
|
|
288
|
+
check('上下手柄只改变高度', southReady
|
|
289
|
+
&& sizeWH.height > sizeW.height + 15
|
|
290
|
+
&& Math.abs(sizeWH.width - sizeW.width) < 0.5
|
|
291
|
+
&& sizeWH.widthLocked && sizeWH.heightLocked,
|
|
292
|
+
`${sizeW.width.toFixed(1)}×${sizeW.height.toFixed(1)} → ${sizeWH.width.toFixed(1)}×${sizeWH.height.toFixed(1)}`);
|
|
293
|
+
const resizedSnapshot = await page.evaluate((id) =>
|
|
294
|
+
window.__layerEditor.snapshot().layers.find((layer) => layer.id === id), first);
|
|
295
|
+
check('草稿快照保留手动宽高', resizedSnapshot?.manualWidth === true
|
|
296
|
+
&& resizedSnapshot?.manualHeight === true && !!resizedSnapshot?.height);
|
|
297
|
+
const lockedWidth = sizeWH.width;
|
|
298
|
+
await page.evaluate((id) => {
|
|
299
|
+
const layer = window.__layerEditor.layers().find((item) => item.id === id);
|
|
300
|
+
window.__layerEditor.scaleFont(layer.el, 1.08);
|
|
301
|
+
}, first);
|
|
302
|
+
const sizeAfterFont = await boxOf(first);
|
|
303
|
+
check('手动宽度不被文字或字号自动撑回', Math.abs(sizeAfterFont.width - lockedWidth) < 0.5,
|
|
304
|
+
`${lockedWidth.toFixed(1)} → ${sizeAfterFont.width.toFixed(1)}`);
|
|
305
|
+
await page.evaluate(() => {
|
|
306
|
+
window.__layerEditor.undo();
|
|
307
|
+
window.__layerEditor.undo();
|
|
308
|
+
window.__layerEditor.undo();
|
|
309
|
+
});
|
|
310
|
+
const sizeBack = await boxOf(first);
|
|
311
|
+
check('撤销恢复原始宽高与自动适配状态', Math.abs(sizeBack.width - size0.width) < 0.5
|
|
312
|
+
&& Math.abs(sizeBack.height - size0.height) < 0.5
|
|
313
|
+
&& !sizeBack.widthLocked && !sizeBack.heightLocked,
|
|
314
|
+
`${sizeBack.width.toFixed(1)}×${sizeBack.height.toFixed(1)}`);
|
|
315
|
+
const resizeEscape0 = await boxOf(first);
|
|
316
|
+
const seHandle = await page.$('.hf-resize-handle.se');
|
|
317
|
+
const seBox = await seHandle?.boundingBox();
|
|
318
|
+
if (seBox) {
|
|
319
|
+
const x = seBox.x + seBox.width / 2;
|
|
320
|
+
const y = seBox.y + seBox.height / 2;
|
|
321
|
+
await page.mouse.move(x, y);
|
|
322
|
+
await page.mouse.down();
|
|
323
|
+
await page.mouse.move(x + 44, y + 32, { steps: 3 });
|
|
324
|
+
const resizeEscapeMoved = await boxOf(first);
|
|
325
|
+
await page.keyboard.press('Escape');
|
|
326
|
+
await page.mouse.up();
|
|
327
|
+
const resizeEscape1 = await boxOf(first);
|
|
328
|
+
check('Esc 可取消进行中的缩放', Math.abs(resizeEscapeMoved.width - resizeEscape0.width) > 5
|
|
329
|
+
&& Math.abs(resizeEscape1.width - resizeEscape0.width) < 1
|
|
330
|
+
&& Math.abs(resizeEscape1.height - resizeEscape0.height) < 1);
|
|
331
|
+
} else {
|
|
332
|
+
check('Esc 可取消进行中的缩放', false, '未找到右下角手柄');
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/* ---------- 文本编辑 ---------- */
|
|
336
|
+
group('文本编辑');
|
|
337
|
+
const slotSel = await page.evaluate((id) => {
|
|
338
|
+
const layer = document.querySelector(`[data-layer-id="${id}"]`);
|
|
339
|
+
const slot = layer.classList.contains('hf-slot') ? layer : layer.querySelector('.hf-slot');
|
|
340
|
+
if (!slot) return null;
|
|
341
|
+
slot.setAttribute('data-hf-probe', '1');
|
|
342
|
+
return '[data-hf-probe]';
|
|
343
|
+
}, first);
|
|
344
|
+
if (!slotSel) {
|
|
345
|
+
check('图层内找到文本槽', false, `${first} 无文本槽`);
|
|
346
|
+
} else {
|
|
347
|
+
const origText = await page.evaluate((s) => document.querySelector(s).textContent, slotSel);
|
|
348
|
+
await dbl(slotSel);
|
|
349
|
+
check('双击进入编辑态', await page.evaluate(() => !!document.querySelector('.hf-editing')));
|
|
350
|
+
check('已设为 plaintext-only',
|
|
351
|
+
await page.evaluate(() => document.querySelector('.hf-editing')?.getAttribute('contenteditable') === 'plaintext-only'));
|
|
352
|
+
check('内容已全选', await page.evaluate((t) => getSelection().toString().trim() === t.trim(), origText));
|
|
353
|
+
|
|
354
|
+
const posBefore = await geomOf(first);
|
|
355
|
+
await page.keyboard.press('ArrowRight');
|
|
356
|
+
await page.keyboard.press('ArrowDown');
|
|
357
|
+
const posAfter = await geomOf(first);
|
|
358
|
+
check('编辑态方向键不挪图层', posBefore.left === posAfter.left && posBefore.top === posAfter.top);
|
|
359
|
+
|
|
360
|
+
await page.keyboard.press('Escape');
|
|
361
|
+
await new Promise((r) => setTimeout(r, 120));
|
|
362
|
+
check('Esc 回滚原文', await page.evaluate((s, t) => document.querySelector(s).textContent === t, slotSel, origText));
|
|
363
|
+
|
|
364
|
+
await dbl(slotSel);
|
|
365
|
+
await page.evaluate((s) => {
|
|
366
|
+
const r = document.createRange();
|
|
367
|
+
r.selectNodeContents(document.querySelector(s));
|
|
368
|
+
const g = getSelection(); g.removeAllRanges(); g.addRange(r);
|
|
369
|
+
}, slotSel);
|
|
370
|
+
await page.keyboard.type('验证文本ABC');
|
|
371
|
+
await page.keyboard.press('Enter');
|
|
372
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
373
|
+
check('Enter 提交改动', await page.evaluate((s) => document.querySelector(s).textContent === '验证文本ABC', slotSel));
|
|
374
|
+
await page.evaluate(() => window.__layerEditor.undo());
|
|
375
|
+
check('⌘Z 还原文字', await page.evaluate((s, t) => document.querySelector(s).textContent === t, slotSel, origText));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/* ---------- 多字号结构安全 ---------- */
|
|
379
|
+
const multi = await page.evaluate(() => {
|
|
380
|
+
for (const l of window.__layerEditor.layers()) {
|
|
381
|
+
const slots = [...l.el.querySelectorAll('.hf-slot')];
|
|
382
|
+
if (slots.length >= 2) {
|
|
383
|
+
const sizes = new Set(slots.map((s) => getComputedStyle(s).fontSize));
|
|
384
|
+
if (sizes.size >= 2) {
|
|
385
|
+
slots[0].parentElement.setAttribute('data-hf-multi', '1');
|
|
386
|
+
return { id: l.id, n: slots.length };
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return null;
|
|
391
|
+
});
|
|
392
|
+
if (multi) {
|
|
393
|
+
group(`结构安全 · ${multi.id}(多字号拼排)`);
|
|
394
|
+
const structBefore = await page.evaluate(() => {
|
|
395
|
+
const box = document.querySelector('[data-hf-multi]');
|
|
396
|
+
return [...box.children].map((c) => `${c.tagName}.${(c.className || '').replace(/hf-\S+/g, '').trim()}`).join(',');
|
|
397
|
+
});
|
|
398
|
+
const target = await page.evaluate(() => {
|
|
399
|
+
const box = document.querySelector('[data-hf-multi]');
|
|
400
|
+
const slot = box.querySelector('.hf-slot');
|
|
401
|
+
slot.setAttribute('data-hf-probe2', '1');
|
|
402
|
+
return { fs: getComputedStyle(slot).fontSize, color: getComputedStyle(slot).color };
|
|
403
|
+
});
|
|
404
|
+
await dbl('[data-hf-probe2]');
|
|
405
|
+
await page.keyboard.type('99');
|
|
406
|
+
await page.keyboard.press('Enter');
|
|
407
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
408
|
+
const structAfter = await page.evaluate(() => {
|
|
409
|
+
const box = document.querySelector('[data-hf-multi]');
|
|
410
|
+
const slot = box.querySelector('[data-hf-probe2]');
|
|
411
|
+
return {
|
|
412
|
+
children: [...box.children].map((c) => `${c.tagName}.${(c.className || '').replace(/hf-\S+/g, '').trim()}`).join(','),
|
|
413
|
+
text: slot.textContent,
|
|
414
|
+
fs: getComputedStyle(slot).fontSize,
|
|
415
|
+
color: getComputedStyle(slot).color,
|
|
416
|
+
};
|
|
417
|
+
});
|
|
418
|
+
check('兄弟 span 结构未被破坏', structBefore === structAfter.children, structAfter.children);
|
|
419
|
+
check('文字已改', structAfter.text === '99');
|
|
420
|
+
check('字号未变', structAfter.fs === target.fs, structAfter.fs);
|
|
421
|
+
check('颜色未变', structAfter.color === target.color);
|
|
422
|
+
await page.evaluate(() => window.__layerEditor.undo());
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/* ---------- 字号 ---------- */
|
|
426
|
+
group('字号');
|
|
427
|
+
const fsBefore = await page.evaluate((s) => getComputedStyle(document.querySelector(s)).fontSize, slotSel || sel1);
|
|
428
|
+
await page.evaluate((id) => {
|
|
429
|
+
const l = window.__layerEditor.layers().find((x) => x.id === id);
|
|
430
|
+
window.__layerEditor.select(l.el);
|
|
431
|
+
window.__layerEditor.scaleFont(l.el, 1.08);
|
|
432
|
+
}, first);
|
|
433
|
+
await new Promise((r) => setTimeout(r, 120));
|
|
434
|
+
const fsAfter = await page.evaluate((s) => getComputedStyle(document.querySelector(s)).fontSize, slotSel || sel1);
|
|
435
|
+
check('A+ 放大字号', parseFloat(fsAfter) > parseFloat(fsBefore), `${fsBefore} → ${fsAfter}`);
|
|
436
|
+
await page.evaluate(() => window.__layerEditor.undo());
|
|
437
|
+
const fsBack = await page.evaluate((s) => getComputedStyle(document.querySelector(s)).fontSize, slotSel || sel1);
|
|
438
|
+
check('撤销恢复字号', fsBack === fsBefore, fsBack);
|
|
439
|
+
|
|
440
|
+
/* ---------- 字体 ---------- */
|
|
441
|
+
group('字体');
|
|
442
|
+
const opts = await page.evaluate(() => window.__layerEditor.fontOptions());
|
|
443
|
+
check('从 :root 解析出字体族', opts.length > 0, opts.map((o) => o.label).join(' / ') || '(无)');
|
|
444
|
+
let fontChanged = false;
|
|
445
|
+
if (opts.length > 1) {
|
|
446
|
+
const ffBefore = await page.evaluate((s) => getComputedStyle(document.querySelector(s)).fontFamily, slotSel || sel1);
|
|
447
|
+
// 选一个与当前不同的字体族
|
|
448
|
+
for (const o of opts) {
|
|
449
|
+
await page.evaluate((id, v) => {
|
|
450
|
+
const l = window.__layerEditor.layers().find((x) => x.id === id);
|
|
451
|
+
window.__layerEditor.setFont(l.el, v);
|
|
452
|
+
}, first, o.value);
|
|
453
|
+
await new Promise((r) => setTimeout(r, 120));
|
|
454
|
+
const now = await page.evaluate((s) => getComputedStyle(document.querySelector(s)).fontFamily, slotSel || sel1);
|
|
455
|
+
if (now !== ffBefore) { fontChanged = true; break; }
|
|
456
|
+
await page.evaluate(() => window.__layerEditor.undo());
|
|
457
|
+
}
|
|
458
|
+
check('切换字体生效', fontChanged);
|
|
459
|
+
} else if (opts.length === 1) {
|
|
460
|
+
check('单一字体栈无需切换', true, opts[0].label);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/* ---------- 删除图层 ---------- */
|
|
464
|
+
group('删除图层');
|
|
465
|
+
const victim = layerIds.find((id) => id !== first && id !== 'hero_photo') || layerIds[layerIds.length - 1];
|
|
466
|
+
const othersBefore = await page.evaluate(COLLECT);
|
|
467
|
+
await page.evaluate((id) => {
|
|
468
|
+
const l = window.__layerEditor.layers().find((x) => x.id === id);
|
|
469
|
+
window.__layerEditor.removeLayer(l.el);
|
|
470
|
+
}, victim);
|
|
471
|
+
await new Promise((r) => setTimeout(r, 120));
|
|
472
|
+
check(`${victim} 已从 DOM 移除`, await page.evaluate((id) => !document.querySelector(`[data-layer-id="${id}"]`), victim));
|
|
473
|
+
const othersAfter = await page.evaluate(COLLECT);
|
|
474
|
+
const untouched = Object.keys(othersAfter).every((id) =>
|
|
475
|
+
Math.abs(othersAfter[id].left - othersBefore[id].left) < 0.5
|
|
476
|
+
&& Math.abs(othersAfter[id].top - othersBefore[id].top) < 0.5);
|
|
477
|
+
check('其他图层坐标不受影响', untouched);
|
|
478
|
+
await page.evaluate(() => window.__layerEditor.undo());
|
|
479
|
+
await new Promise((r) => setTimeout(r, 120));
|
|
480
|
+
check('撤销恢复图层', await page.evaluate((id) => !!document.querySelector(`[data-layer-id="${id}"]`), victim));
|
|
481
|
+
const restored = await page.evaluate(COLLECT);
|
|
482
|
+
check('恢复后位置正确', Object.keys(othersBefore).every((id) =>
|
|
483
|
+
restored[id] && Math.abs(restored[id].left - othersBefore[id].left) < 0.5));
|
|
484
|
+
|
|
485
|
+
/* ---------- 拆解视图 ---------- */
|
|
486
|
+
group('拆解视图');
|
|
487
|
+
const composedBeforeSpread = await page.evaluate(COLLECT);
|
|
488
|
+
const composedScreenHeight = await page.evaluate(() =>
|
|
489
|
+
document.querySelector('.hf-canvas').getBoundingClientRect().height);
|
|
490
|
+
const enterMotion = await page.evaluate(async () => {
|
|
491
|
+
void window.__layerEditor.setExploded(true);
|
|
492
|
+
await new Promise((r) => setTimeout(r, 180));
|
|
493
|
+
return {
|
|
494
|
+
running: document.getAnimations().filter((a) => a.playState === 'running').length,
|
|
495
|
+
scanning: document.querySelector('.hf-exploded-scan-beam')?.getAnimations()
|
|
496
|
+
.some((a) => a.playState === 'running') || false,
|
|
497
|
+
labelsVisible: [...document.querySelectorAll('.hf-exploded-label')].some((label) =>
|
|
498
|
+
Number.parseFloat(getComputedStyle(label).opacity) > .05),
|
|
499
|
+
reduced: matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
500
|
+
|| new URLSearchParams(location.search).get('motion') === '0',
|
|
501
|
+
};
|
|
502
|
+
});
|
|
503
|
+
check('拆解前先执行扫描特效', enterMotion.reduced || enterMotion.scanning,
|
|
504
|
+
enterMotion.reduced ? '已遵循 reduced-motion' : '扫描束运行中');
|
|
505
|
+
check('进入时执行分层散开动画', enterMotion.reduced || enterMotion.running > 0,
|
|
506
|
+
enterMotion.reduced ? '已遵循 reduced-motion' : `${enterMotion.running} 个动画运行中`);
|
|
507
|
+
check('扫描与散开期间不放大模块标签', enterMotion.reduced || !enterMotion.labelsVisible,
|
|
508
|
+
enterMotion.reduced ? '已遵循 reduced-motion' : '运动阶段标签保持隐藏');
|
|
509
|
+
await page.waitForFunction(() => !window.__layerEditor.state.transitioning, { timeout: 20000 });
|
|
510
|
+
await new Promise((r) => setTimeout(r, 220));
|
|
511
|
+
const liveSpread = await page.evaluate(() => ({
|
|
512
|
+
active: document.body.classList.contains('hf-exploded-on'),
|
|
513
|
+
board: !!document.querySelector('.hf-exploded-board'),
|
|
514
|
+
scanHeight: document.querySelector('.hf-exploded-scan')?.getBoundingClientRect().height || 0,
|
|
515
|
+
boardHeight: document.querySelector('.hf-exploded-board')?.getBoundingClientRect().height || 0,
|
|
516
|
+
laneCount: window.__layerEditor.state.spread?.laneCount || 0,
|
|
517
|
+
units: document.querySelectorAll('.hf-exploded-board .hf-exploded-item').length,
|
|
518
|
+
layers: document.querySelectorAll('.hf-exploded-board [data-exploded-layer]').length,
|
|
519
|
+
uniqueLayers: new Set([...document.querySelectorAll('.hf-exploded-board [data-exploded-layer]')]
|
|
520
|
+
.map((node) => node.dataset.explodedLayer)).size,
|
|
521
|
+
primaryContent: Number(document.querySelector('.hf-exploded-board')?.dataset.explodedPrimaryCount || 0),
|
|
522
|
+
textSupplements: document.querySelectorAll('.hf-exploded-item[data-exploded-text-supplement]').length,
|
|
523
|
+
bases: document.querySelectorAll('.hf-exploded-base').length,
|
|
524
|
+
overviews: document.querySelectorAll('.hf-exploded-item[data-exploded-overview]').length,
|
|
525
|
+
backgrounds: document.querySelectorAll('.hf-exploded-item[data-exploded-background]').length,
|
|
526
|
+
groups: document.querySelectorAll('.hf-exploded-item[data-exploded-group]').length,
|
|
527
|
+
gallery: (() => {
|
|
528
|
+
const board = document.querySelector('.hf-exploded-board');
|
|
529
|
+
const width = parseFloat(board?.style.width) || 1;
|
|
530
|
+
const height = parseFloat(board?.style.height) || 1;
|
|
531
|
+
const items = [...document.querySelectorAll('.hf-exploded-item')].map((item) => ({
|
|
532
|
+
left: parseFloat(item.style.left) || 0,
|
|
533
|
+
top: parseFloat(item.style.top) || 0,
|
|
534
|
+
width: parseFloat(item.style.width) || 0,
|
|
535
|
+
height: parseFloat(item.style.height) || 0,
|
|
536
|
+
}));
|
|
537
|
+
const minX = Math.min(...items.map((item) => item.left));
|
|
538
|
+
const maxX = Math.max(...items.map((item) => item.left + item.width));
|
|
539
|
+
const special = [...document.querySelectorAll('[data-exploded-overview],[data-exploded-background]')]
|
|
540
|
+
.map((item) => (parseFloat(item.style.height) || 0) / height);
|
|
541
|
+
return {
|
|
542
|
+
span: (maxX - minX) / width,
|
|
543
|
+
centerOccupied: items.some((item) => item.left < width * .58 && item.left + item.width > width * .42),
|
|
544
|
+
specialMin: Math.min(...special), specialMax: Math.max(...special),
|
|
545
|
+
};
|
|
546
|
+
})(),
|
|
547
|
+
labelsVisible: [...document.querySelectorAll('.hf-exploded-label')].some((label) =>
|
|
548
|
+
Number.parseFloat(getComputedStyle(label).opacity) > .05),
|
|
549
|
+
labelsNamed: [...document.querySelectorAll('.hf-exploded-item[data-exploded-group]')].every((item) =>
|
|
550
|
+
Boolean(item.title?.trim())),
|
|
551
|
+
hasDrilldownAPI: typeof window.__layerEditor.openExplodedGroup === 'function',
|
|
552
|
+
level: document.querySelector('.hf-exploded-board')?.dataset.explodedLevel,
|
|
553
|
+
theme: document.querySelector('.hf-exploded-board')?.dataset.explodedTheme,
|
|
554
|
+
expectedTheme: (() => {
|
|
555
|
+
const cs = getComputedStyle(document.querySelector('.hf-canvas'));
|
|
556
|
+
const parse = (raw) => {
|
|
557
|
+
const rgba = String(raw).match(/rgba?\((\d+(?:\.\d+)?)[, ]+(\d+(?:\.\d+)?)[, ]+(\d+(?:\.\d+)?)(?:\s*[,/]\s*(\d*(?:\.\d+)?))?/i);
|
|
558
|
+
if (rgba) return { rgb: rgba.slice(1, 4).map(Number), alpha: rgba[4] === undefined ? 1 : Number(rgba[4]) };
|
|
559
|
+
const hex = String(raw).match(/#([0-9a-f]{6}|[0-9a-f]{3})(?![0-9a-f])/i);
|
|
560
|
+
if (!hex) return null;
|
|
561
|
+
const v = hex[1].length === 3 ? [...hex[1]].map((c) => c + c).join('') : hex[1];
|
|
562
|
+
return { rgb: [0, 2, 4].map((i) => parseInt(v.slice(i, i + 2), 16)), alpha: 1 };
|
|
563
|
+
};
|
|
564
|
+
let color = parse(cs.backgroundColor);
|
|
565
|
+
if (!color || color.alpha <= .25) {
|
|
566
|
+
const tokens = String(cs.backgroundImage).match(/#[0-9a-f]{3,8}|rgba?\([^)]*\)/ig) || [];
|
|
567
|
+
color = tokens.map(parse).find((entry) => entry && entry.alpha > .65) || null;
|
|
568
|
+
}
|
|
569
|
+
const rgb = color?.rgb || [244, 241, 232];
|
|
570
|
+
const linear = rgb.map((n) => {
|
|
571
|
+
const c = n / 255;
|
|
572
|
+
return c <= .03928 ? c / 12.92 : ((c + .055) / 1.055) ** 2.4;
|
|
573
|
+
});
|
|
574
|
+
return linear[0] * .2126 + linear[1] * .7152 + linear[2] * .0722 > .52 ? 'light' : 'dark';
|
|
575
|
+
})(),
|
|
576
|
+
sourceVisible: getComputedStyle(document.querySelector('.hf-stage > .hf-canvas')).display !== 'none',
|
|
577
|
+
}));
|
|
578
|
+
check('可切换到横向拆解画布', liveSpread.active && liveSpread.board && !liveSpread.sourceVisible);
|
|
579
|
+
check('全部内容叶子均归入一级拆解', liveSpread.uniqueLayers === layerIds.length,
|
|
580
|
+
`${liveSpread.uniqueLayers}/${layerIds.length} 个唯一叶子,${liveSpread.layers} 次呈现`);
|
|
581
|
+
check('完整总览与背景均作为普通组件', liveSpread.overviews === 1
|
|
582
|
+
&& liveSpread.backgrounds === 1 && liveSpread.bases === 0,
|
|
583
|
+
`总览 ${liveSpread.overviews}、背景 ${liveSpread.backgrounds}、固定基底 ${liveSpread.bases}`);
|
|
584
|
+
check('组件画廊数量完整', liveSpread.units === expectedExplodedUnits,
|
|
585
|
+
`${liveSpread.units}/${expectedExplodedUnits}`);
|
|
586
|
+
check('少组件时补入分组文字叶子', liveSpread.primaryContent === explodeExpectation.primary
|
|
587
|
+
&& liveSpread.textSupplements === explodeExpectation.textSupplements,
|
|
588
|
+
`一级组件 ${liveSpread.primaryContent},文字补充 ${liveSpread.textSupplements}`);
|
|
589
|
+
check('拆解舞台跟随海报明暗主题', liveSpread.theme === liveSpread.expectedTheme,
|
|
590
|
+
`${liveSpread.theme}/${liveSpread.expectedTheme}`);
|
|
591
|
+
check('一级拆解保持分组兼容', liveSpread.level === '1' && liveSpread.groups === groupInfo.length,
|
|
592
|
+
`${liveSpread.groups}/${groupInfo.length} 个模块`);
|
|
593
|
+
check('拆解视图不提供第二级叶子钻取', !liveSpread.hasDrilldownAPI,
|
|
594
|
+
'仅保留一级内容模块');
|
|
595
|
+
check('静止拆解稿不叠加冗余模块标签', !liveSpread.labelsVisible,
|
|
596
|
+
'名称仅在明确交互时提示');
|
|
597
|
+
check('模块仍保留可发现的语义名称', !groupInfo.length || liveSpread.labelsNamed,
|
|
598
|
+
groupInfo.length ? `${liveSpread.groups} 个模块均有名称` : '无一级模块标签');
|
|
599
|
+
const originRatio = liveSpread.scanHeight / composedScreenHeight;
|
|
600
|
+
check('扫描原点保持完整成稿的可读尺度', originRatio >= 0.68,
|
|
601
|
+
`扫描态为编辑态高度的 ${(originRatio * 100).toFixed(1)}%`);
|
|
602
|
+
check('终态铺满舞台且中央无大块保留区', liveSpread.laneCount >= 2
|
|
603
|
+
&& liveSpread.gallery.centerOccupied && liveSpread.gallery.span >= .72,
|
|
604
|
+
`${liveSpread.laneCount} 列,横向覆盖 ${(liveSpread.gallery.span * 100).toFixed(1)}%`);
|
|
605
|
+
check('总览与背景均为小型组件', liveSpread.gallery.specialMin >= .08
|
|
606
|
+
&& liveSpread.gallery.specialMax <= .38,
|
|
607
|
+
`高度占舞台 ${(liveSpread.gallery.specialMin * 100).toFixed(1)}%–${(liveSpread.gallery.specialMax * 100).toFixed(1)}%`);
|
|
608
|
+
|
|
609
|
+
if (groupInfo.length) {
|
|
610
|
+
await page.hover('.hf-exploded-item[data-exploded-group]');
|
|
611
|
+
await new Promise((r) => setTimeout(r, 220));
|
|
612
|
+
check('悬停模块时才显示名称提示', await page.evaluate(() =>
|
|
613
|
+
Number.parseFloat(getComputedStyle(document.querySelector('.hf-exploded-item[data-exploded-group] .hf-exploded-label')).opacity) > .9));
|
|
614
|
+
await page.mouse.move(0, 0);
|
|
615
|
+
await page.click('.hf-exploded-item[data-exploded-group]');
|
|
616
|
+
await new Promise((r) => setTimeout(r, 120));
|
|
617
|
+
check('点击内容模块不会进入细粒度第二层', await page.evaluate(() =>
|
|
618
|
+
document.querySelector('.hf-exploded-board')?.dataset.explodedLevel === '1'
|
|
619
|
+
&& !document.querySelector('.hf-exploded-nav')));
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const spreadHTML = await page.evaluate(() => window.__layerEditor.explodedHTML());
|
|
623
|
+
verifiedLayersHTML = spreadHTML;
|
|
624
|
+
check('拆解导出物无编辑器依赖', !/layer-editor|class="hf-(bar|panel|modal|toast)/.test(spreadHTML));
|
|
625
|
+
const spreadFile = join(dir, '_hf_verify_layers.html');
|
|
626
|
+
writeFileSync(spreadFile, spreadHTML);
|
|
627
|
+
const { page: pSpread, errors: eSpread } = await open(browser, spreadFile, { width: 1600, height: 1000 });
|
|
628
|
+
await pSpread.waitForSelector('html[data-hf-exploded-ready]', { timeout: 20000 });
|
|
629
|
+
const spreadAudit = await pSpread.evaluate(() => ({
|
|
630
|
+
units: document.querySelectorAll('.hf-exploded-board .hf-exploded-item').length,
|
|
631
|
+
layers: document.querySelectorAll('[data-exploded-layer]').length,
|
|
632
|
+
uniqueLayers: new Set([...document.querySelectorAll('[data-exploded-layer]')]
|
|
633
|
+
.map((node) => node.dataset.explodedLayer)).size,
|
|
634
|
+
primaryContent: Number(document.querySelector('.hf-exploded-board')?.dataset.explodedPrimaryCount || 0),
|
|
635
|
+
textSupplements: document.querySelectorAll('.hf-exploded-item[data-exploded-text-supplement]').length,
|
|
636
|
+
bases: document.querySelectorAll('.hf-exploded-base').length,
|
|
637
|
+
overviews: document.querySelectorAll('[data-exploded-overview]').length,
|
|
638
|
+
backgrounds: document.querySelectorAll('[data-exploded-background]').length,
|
|
639
|
+
groups: document.querySelectorAll('.hf-exploded-item[data-exploded-group]').length,
|
|
640
|
+
level: document.querySelector('.hf-exploded-board')?.dataset.explodedLevel,
|
|
641
|
+
drilldownUI: !!document.querySelector('.hf-exploded-nav'),
|
|
642
|
+
centerOccupied: (() => {
|
|
643
|
+
const board = document.querySelector('.hf-exploded-board');
|
|
644
|
+
const width = parseFloat(board?.style.width) || 1;
|
|
645
|
+
return [...document.querySelectorAll('.hf-exploded-item')].some((item) => {
|
|
646
|
+
const left = parseFloat(item.style.left) || 0;
|
|
647
|
+
const itemWidth = parseFloat(item.style.width) || 0;
|
|
648
|
+
return left < width * .58 && left + itemWidth > width * .42;
|
|
649
|
+
});
|
|
650
|
+
})(),
|
|
651
|
+
replay: !!document.querySelector('.hf-exploded-replay'),
|
|
652
|
+
motion: document.documentElement.dataset.hfMotion,
|
|
653
|
+
overflow: document.documentElement.scrollWidth > innerWidth + 1
|
|
654
|
+
|| document.documentElement.scrollHeight > innerHeight + 1,
|
|
655
|
+
}));
|
|
656
|
+
check('拆解稿可独立渲染无报错', eSpread.length === 0, eSpread.join('; '));
|
|
657
|
+
check('拆解稿自适应且图层齐全', spreadAudit.uniqueLayers === layerIds.length && !spreadAudit.overflow,
|
|
658
|
+
`${spreadAudit.uniqueLayers}/${layerIds.length} 个唯一叶子,${spreadAudit.layers} 次呈现`);
|
|
659
|
+
check('独立拆解稿不保留中央大基底', spreadAudit.bases === 0
|
|
660
|
+
&& spreadAudit.centerOccupied, `${spreadAudit.bases} 个固定基底`);
|
|
661
|
+
check('独立稿含完整总览与背景组件', spreadAudit.overviews === 1
|
|
662
|
+
&& spreadAudit.backgrounds === 1 && spreadAudit.units === expectedExplodedUnits,
|
|
663
|
+
`${spreadAudit.units}/${expectedExplodedUnits} 个组件`);
|
|
664
|
+
check('独立稿保留少组件文字补充', spreadAudit.primaryContent === explodeExpectation.primary
|
|
665
|
+
&& spreadAudit.textSupplements === explodeExpectation.textSupplements,
|
|
666
|
+
`一级组件 ${spreadAudit.primaryContent},文字补充 ${spreadAudit.textSupplements}`);
|
|
667
|
+
check('拆解稿保留一级模块语义', spreadAudit.groups === groupInfo.length,
|
|
668
|
+
`${spreadAudit.groups}/${groupInfo.length} 个模块`);
|
|
669
|
+
check('独立拆解稿只有一级内容层', spreadAudit.level === '1' && !spreadAudit.drilldownUI,
|
|
670
|
+
`level ${spreadAudit.level}`);
|
|
671
|
+
check('拆解稿带自动播放与重播入口', spreadAudit.replay
|
|
672
|
+
&& ['played', 'reduced'].includes(spreadAudit.motion), spreadAudit.motion || '未播放');
|
|
673
|
+
const replayMotion = await pSpread.evaluate(async () => {
|
|
674
|
+
if (document.documentElement.dataset.hfMotion === 'reduced') return { any: true, scan: true };
|
|
675
|
+
document.querySelector('.hf-exploded-replay').click();
|
|
676
|
+
await new Promise((r) => setTimeout(r, 160));
|
|
677
|
+
return {
|
|
678
|
+
any: document.getAnimations().some((a) => a.playState === 'running'),
|
|
679
|
+
scan: document.querySelector('.hf-exploded-scan-beam')?.getAnimations()
|
|
680
|
+
.some((a) => a.playState === 'running') || false,
|
|
681
|
+
};
|
|
682
|
+
});
|
|
683
|
+
check('重播按钮可重新触发扫描与拆解', replayMotion.any && replayMotion.scan);
|
|
684
|
+
await pSpread.close();
|
|
685
|
+
try { unlinkSync(spreadFile); } catch {}
|
|
686
|
+
|
|
687
|
+
const exitMotion = await page.evaluate(async () => {
|
|
688
|
+
void window.__layerEditor.setExploded(false);
|
|
689
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
690
|
+
return {
|
|
691
|
+
running: document.getAnimations().filter((a) => a.playState === 'running').length,
|
|
692
|
+
reduced: matchMedia('(prefers-reduced-motion: reduce)').matches,
|
|
693
|
+
};
|
|
694
|
+
});
|
|
695
|
+
check('返回时执行反向合拢动画', exitMotion.reduced || exitMotion.running > 0,
|
|
696
|
+
exitMotion.reduced ? '已遵循 reduced-motion' : `${exitMotion.running} 个动画运行中`);
|
|
697
|
+
await page.waitForFunction(() => !window.__layerEditor.state.transitioning, { timeout: 20000 });
|
|
698
|
+
const composedAfterSpread = await page.evaluate(COLLECT);
|
|
699
|
+
check('返回编辑后原布局不变', compare(composedBeforeSpread, composedAfterSpread).length === 0);
|
|
700
|
+
|
|
701
|
+
/* ---------- B 类:导出往返一致 ---------- */
|
|
702
|
+
group('B 类 · 导出往返一致');
|
|
703
|
+
const stateBefore = await page.evaluate(COLLECT);
|
|
704
|
+
const html = await page.evaluate(() => window.__layerEditor.fullHTML());
|
|
705
|
+
check('导出物无编辑器残留', !/hf-|layer-editor/.test(html));
|
|
706
|
+
const outFile = join(dir, '_hf_verify_out.html');
|
|
707
|
+
writeFileSync(outFile, html);
|
|
708
|
+
const { page: pOut, errors: eOut } = await open(browser, outFile);
|
|
709
|
+
const stateAfter = await pOut.evaluate(COLLECT);
|
|
710
|
+
const issues = compare(stateBefore, stateAfter);
|
|
711
|
+
check('导出物可独立渲染无报错', eOut.length === 0, eOut.join('; '));
|
|
712
|
+
check('坐标 / 字号 / 字体 / 渲染宽度全部一致', issues.length === 0, issues.slice(0, 4).join(' | '));
|
|
713
|
+
try { unlinkSync(outFile); } catch {}
|
|
714
|
+
|
|
715
|
+
/* ---------- 草稿 ---------- */
|
|
716
|
+
group('草稿(localStorage)');
|
|
717
|
+
await page.evaluate((id) => {
|
|
718
|
+
const l = window.__layerEditor.layers().find((x) => x.id === id);
|
|
719
|
+
l.el.style.left = '777px';
|
|
720
|
+
l.el.style.top = '333px';
|
|
721
|
+
l.el.style.width = `${parseFloat(l.el.style.width) + 33}px`;
|
|
722
|
+
l.el.style.height = `${parseFloat(l.el.style.height) + 17}px`;
|
|
723
|
+
l.el.dataset.hfWidthLocked = '1';
|
|
724
|
+
l.el.dataset.hfHeightLocked = '1';
|
|
725
|
+
window.__layerEditor.state.removed.length = 0;
|
|
726
|
+
}, first);
|
|
727
|
+
await page.evaluate(() => window.__layerEditor.snapshot());
|
|
728
|
+
await page.evaluate((id) => {
|
|
729
|
+
// 走真实路径:模拟一次方向键触发 touch() 的草稿保存
|
|
730
|
+
const l = window.__layerEditor.layers().find((x) => x.id === id);
|
|
731
|
+
window.__layerEditor.select(l.el);
|
|
732
|
+
}, first);
|
|
733
|
+
await page.keyboard.press('ArrowRight');
|
|
734
|
+
await new Promise((r) => setTimeout(r, 800));
|
|
735
|
+
const draftRaw = await page.evaluate(() => {
|
|
736
|
+
const prefix = 'hf-draft:' + location.pathname;
|
|
737
|
+
const key = Object.keys(localStorage).find((candidate) =>
|
|
738
|
+
candidate === prefix || candidate.startsWith(prefix + ':'));
|
|
739
|
+
return key ? localStorage.getItem(key) : null;
|
|
740
|
+
});
|
|
741
|
+
check('草稿已写入 localStorage', !!draftRaw, draftRaw ? `${(draftRaw.length / 1024).toFixed(1)}KB` : '');
|
|
742
|
+
|
|
743
|
+
const expect = await boxOf(first);
|
|
744
|
+
await page.reload({ waitUntil: 'networkidle0' });
|
|
745
|
+
await page.waitForSelector('html[data-hf-ready]', { timeout: 20000 });
|
|
746
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
747
|
+
const afterReload = await boxOf(first);
|
|
748
|
+
check('刷新后自动恢复草稿', afterReload && Math.abs(afterReload.left - expect.left) < 0.5,
|
|
749
|
+
`期望 ${expect.left},${expect.top} 实得 ${afterReload?.left},${afterReload?.top}`);
|
|
750
|
+
check('刷新后恢复手动宽高状态', afterReload
|
|
751
|
+
&& Math.abs(afterReload.width - expect.width) < 0.5
|
|
752
|
+
&& Math.abs(afterReload.height - expect.height) < 0.5
|
|
753
|
+
&& afterReload.widthLocked && afterReload.heightLocked,
|
|
754
|
+
`期望 ${expect.width.toFixed(1)}×${expect.height.toFixed(1)} 实得 ${afterReload?.width.toFixed(1)}×${afterReload?.height.toFixed(1)}`);
|
|
755
|
+
check('草稿提示条可见', await page.evaluate(() => !document.getElementById('hf-draft').hidden));
|
|
756
|
+
|
|
757
|
+
if (!fails && verifiedLayersHTML) {
|
|
758
|
+
try {
|
|
759
|
+
writeFileSync(layersOutput, verifiedLayersHTML);
|
|
760
|
+
check('已生成默认 layers.html', true, layersOutput);
|
|
761
|
+
} catch (error) {
|
|
762
|
+
check('已生成默认 layers.html', false, String(error));
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
if (errors.length) console.log('\n编辑器页面报错:', errors.join('; '));
|
|
767
|
+
} finally {
|
|
768
|
+
wired.cleanup();
|
|
769
|
+
await browser.close();
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
markTiming('完成');
|
|
773
|
+
if (showTimings) {
|
|
774
|
+
console.log('\n【分阶段计时】');
|
|
775
|
+
for (const row of timingRows) {
|
|
776
|
+
console.log(` TIMING\t${row.phase}\t${(row.ms / 1000).toFixed(3)}s`);
|
|
777
|
+
}
|
|
778
|
+
console.log(` TIMING\t总计\t${((Date.now() - timingStartedAt) / 1000).toFixed(3)}s`);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
console.log(fails === 0 ? '\n全部通过 ✅\n' : `\n${fails} 项失败 ❌\n`);
|
|
782
|
+
process.exit(fails ? 1 : 0);
|