@overcrow/mcp 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +90 -2
- package/dist/audit/index.js +103 -0
- package/dist/audit/lexer.js +417 -0
- package/dist/audit/project.js +200 -0
- package/dist/audit/rules.js +661 -0
- package/dist/bootstrap/download.js +229 -0
- package/dist/bootstrap/install.js +280 -0
- package/dist/bootstrap/pin.js +85 -0
- package/dist/bootstrap/pin.json +119 -0
- package/dist/bootstrap/zip.js +258 -0
- package/dist/cli.js +200 -0
- package/dist/confine.js +244 -0
- package/dist/content/docs/en/cli.md +353 -0
- package/dist/content/docs/en/dev-channel.md +130 -0
- package/dist/content/docs/en/elements.md +469 -0
- package/dist/content/docs/en/forms.md +328 -0
- package/dist/content/docs/en/guide.md +253 -0
- package/dist/content/docs/en/index.md +85 -0
- package/dist/content/docs/en/limits.md +241 -0
- package/dist/content/docs/en/logic.md +446 -0
- package/dist/content/docs/en/manifest.md +381 -0
- package/dist/content/docs/en/mcp.md +165 -0
- package/dist/content/docs/en/package.md +131 -0
- package/dist/content/docs/en/publishing.md +162 -0
- package/dist/content/docs/en/reference.md +55 -0
- package/dist/content/docs/en/result-shapes.md +332 -0
- package/dist/content/docs/en/sdk.md +295 -0
- package/dist/content/docs/en/security.md +115 -0
- package/dist/content/docs/en/service-reference.md +344 -0
- package/dist/content/docs/en/services.md +517 -0
- package/dist/content/docs/en/style-properties.md +270 -0
- package/dist/content/docs/en/style.md +266 -0
- package/dist/content/docs/en/testing.md +400 -0
- package/dist/content/docs/en/view.md +338 -0
- package/dist/content/docs/en/widgets.md +76 -0
- package/dist/content/docs/fr/cli.md +367 -0
- package/dist/content/docs/fr/dev-channel.md +140 -0
- package/dist/content/docs/fr/elements.md +475 -0
- package/dist/content/docs/fr/forms.md +341 -0
- package/dist/content/docs/fr/guide.md +264 -0
- package/dist/content/docs/fr/index.md +90 -0
- package/dist/content/docs/fr/limits.md +244 -0
- package/dist/content/docs/fr/logic.md +461 -0
- package/dist/content/docs/fr/manifest.md +392 -0
- package/dist/content/docs/fr/mcp.md +173 -0
- package/dist/content/docs/fr/package.md +139 -0
- package/dist/content/docs/fr/publishing.md +172 -0
- package/dist/content/docs/fr/reference.md +59 -0
- package/dist/content/docs/fr/result-shapes.md +333 -0
- package/dist/content/docs/fr/sdk.md +301 -0
- package/dist/content/docs/fr/security.md +129 -0
- package/dist/content/docs/fr/service-reference.md +346 -0
- package/dist/content/docs/fr/services.md +540 -0
- package/dist/content/docs/fr/style-properties.md +272 -0
- package/dist/content/docs/fr/style.md +281 -0
- package/dist/content/docs/fr/testing.md +417 -0
- package/dist/content/docs/fr/view.md +352 -0
- package/dist/content/docs/fr/widgets.md +81 -0
- package/dist/content/examples/clock/LICENSE +21 -0
- package/dist/content/examples/clock/README.md +75 -0
- package/dist/content/examples/clock/listing.json +19 -0
- package/dist/content/examples/clock/locales/en.json +4 -0
- package/dist/content/examples/clock/locales/fr.json +4 -0
- package/dist/content/examples/clock/logic.ts +85 -0
- package/dist/content/examples/clock/manifest.json +44 -0
- package/dist/content/examples/clock/package.json +16 -0
- package/dist/content/examples/clock/style.ocss +26 -0
- package/dist/content/examples/clock/tests/hidden.scenario.json +68 -0
- package/dist/content/examples/clock/tests/load-logic.mjs +22 -0
- package/dist/content/examples/clock/tests/logic.test.mjs +89 -0
- package/dist/content/examples/clock/tests/options.scenario.json +108 -0
- package/dist/content/examples/clock/tests/preview.scenario.json +25 -0
- package/dist/content/examples/clock/tests/render.scenario.json +94 -0
- package/dist/content/examples/clock/tests/summer-time.scenario.json +74 -0
- package/dist/content/examples/clock/tests/ticks.scenario.json +120 -0
- package/dist/content/examples/clock/tsconfig.json +16 -0
- package/dist/content/examples/clock/view.ocml +8 -0
- package/dist/content/examples/countdown/LICENSE +20 -0
- package/dist/content/examples/countdown/listing.json +10 -0
- package/dist/content/examples/countdown/locales/en.json +15 -0
- package/dist/content/examples/countdown/locales/fr.json +15 -0
- package/dist/content/examples/countdown/logic.ts +179 -0
- package/dist/content/examples/countdown/manifest.json +34 -0
- package/dist/content/examples/countdown/package.json +15 -0
- package/dist/content/examples/countdown/style.ocss +94 -0
- package/dist/content/examples/countdown/tsconfig.json +16 -0
- package/dist/content/examples/countdown/view.ocml +42 -0
- package/dist/content/examples/fps/LICENSE +21 -0
- package/dist/content/examples/fps/README.md +64 -0
- package/dist/content/examples/fps/listing.json +19 -0
- package/dist/content/examples/fps/locales/en.json +11 -0
- package/dist/content/examples/fps/locales/fr.json +11 -0
- package/dist/content/examples/fps/logic.ts +64 -0
- package/dist/content/examples/fps/manifest.json +30 -0
- package/dist/content/examples/fps/package.json +16 -0
- package/dist/content/examples/fps/style.ocss +31 -0
- package/dist/content/examples/fps/tests/label.scenario.json +63 -0
- package/dist/content/examples/fps/tests/load-logic.mjs +22 -0
- package/dist/content/examples/fps/tests/logic.test.mjs +60 -0
- package/dist/content/examples/fps/tests/preview.scenario.json +29 -0
- package/dist/content/examples/fps/tests/refused.scenario.json +20 -0
- package/dist/content/examples/fps/tests/render.scenario.json +96 -0
- package/dist/content/examples/fps/tests/stale.scenario.json +91 -0
- package/dist/content/examples/fps/tests/unavailable.scenario.json +111 -0
- package/dist/content/examples/fps/tsconfig.json +16 -0
- package/dist/content/examples/fps/view.ocml +11 -0
- package/dist/content/examples/media/LICENSE +21 -0
- package/dist/content/examples/media/README.md +147 -0
- package/dist/content/examples/media/listing.json +19 -0
- package/dist/content/examples/media/locales/en.json +11 -0
- package/dist/content/examples/media/locales/fr.json +11 -0
- package/dist/content/examples/media/logic.ts +191 -0
- package/dist/content/examples/media/manifest.json +35 -0
- package/dist/content/examples/media/package.json +16 -0
- package/dist/content/examples/media/style.ocss +148 -0
- package/dist/content/examples/media/tests/covers.scenario.json +144 -0
- package/dist/content/examples/media/tests/gesture.scenario.json +173 -0
- package/dist/content/examples/media/tests/load-logic.mjs +22 -0
- package/dist/content/examples/media/tests/logic.test.mjs +176 -0
- package/dist/content/examples/media/tests/marquee.scenario.json +127 -0
- package/dist/content/examples/media/tests/preview.scenario.json +52 -0
- package/dist/content/examples/media/tests/read-only.scenario.json +53 -0
- package/dist/content/examples/media/tests/refused.scenario.json +22 -0
- package/dist/content/examples/media/tests/refused.test.mjs +31 -0
- package/dist/content/examples/media/tests/render.scenario.json +136 -0
- package/dist/content/examples/media/tests/states.scenario.json +257 -0
- package/dist/content/examples/media/tests/unavailable.scenario.json +120 -0
- package/dist/content/examples/media/tsconfig.json +16 -0
- package/dist/content/examples/media/view.ocml +41 -0
- package/dist/content/examples/notes/LICENSE +21 -0
- package/dist/content/examples/notes/README.md +219 -0
- package/dist/content/examples/notes/listing.json +19 -0
- package/dist/content/examples/notes/locales/en.json +25 -0
- package/dist/content/examples/notes/locales/fr.json +25 -0
- package/dist/content/examples/notes/logic.ts +799 -0
- package/dist/content/examples/notes/manifest.json +40 -0
- package/dist/content/examples/notes/package.json +16 -0
- package/dist/content/examples/notes/style.ocss +469 -0
- package/dist/content/examples/notes/tests/checks.scenario.json +237 -0
- package/dist/content/examples/notes/tests/create.scenario.json +260 -0
- package/dist/content/examples/notes/tests/delete.scenario.json +342 -0
- package/dist/content/examples/notes/tests/drafts.scenario.json +714 -0
- package/dist/content/examples/notes/tests/editor.scenario.json +232 -0
- package/dist/content/examples/notes/tests/frame.scenario.json +93 -0
- package/dist/content/examples/notes/tests/interactive.scenario.json +344 -0
- package/dist/content/examples/notes/tests/limits.scenario.json +1351 -0
- package/dist/content/examples/notes/tests/load-logic.mjs +22 -0
- package/dist/content/examples/notes/tests/logic.test.mjs +514 -0
- package/dist/content/examples/notes/tests/preview.scenario.json +99 -0
- package/dist/content/examples/notes/tests/read-only.scenario.json +105 -0
- package/dist/content/examples/notes/tests/refused.scenario.json +33 -0
- package/dist/content/examples/notes/tests/save.scenario.json +378 -0
- package/dist/content/examples/notes/tests/states.scenario.json +640 -0
- package/dist/content/examples/notes/tests/typing.scenario.json +562 -0
- package/dist/content/examples/notes/tests/unavailable.scenario.json +246 -0
- package/dist/content/examples/notes/tsconfig.json +16 -0
- package/dist/content/examples/notes/view.ocml +154 -0
- package/dist/content/examples/performance/LICENSE +21 -0
- package/dist/content/examples/performance/README.md +134 -0
- package/dist/content/examples/performance/listing.json +19 -0
- package/dist/content/examples/performance/locales/en.json +20 -0
- package/dist/content/examples/performance/locales/fr.json +20 -0
- package/dist/content/examples/performance/logic.ts +210 -0
- package/dist/content/examples/performance/manifest.json +83 -0
- package/dist/content/examples/performance/package.json +16 -0
- package/dist/content/examples/performance/style.ocss +75 -0
- package/dist/content/examples/performance/tests/fahrenheit.scenario.json +110 -0
- package/dist/content/examples/performance/tests/horizontal.scenario.json +96 -0
- package/dist/content/examples/performance/tests/load-logic.mjs +22 -0
- package/dist/content/examples/performance/tests/logic.test.mjs +226 -0
- package/dist/content/examples/performance/tests/menu.scenario.json +170 -0
- package/dist/content/examples/performance/tests/no-fps-source.scenario.json +47 -0
- package/dist/content/examples/performance/tests/preview.scenario.json +45 -0
- package/dist/content/examples/performance/tests/refused.scenario.json +20 -0
- package/dist/content/examples/performance/tests/refused.test.mjs +19 -0
- package/dist/content/examples/performance/tests/region.scenario.json +89 -0
- package/dist/content/examples/performance/tests/render.scenario.json +112 -0
- package/dist/content/examples/performance/tests/sources.scenario.json +151 -0
- package/dist/content/examples/performance/tests/stale.scenario.json +111 -0
- package/dist/content/examples/performance/tests/telemetry-only.scenario.json +44 -0
- package/dist/content/examples/performance/tests/thresholds.scenario.json +147 -0
- package/dist/content/examples/performance/tests/waiting.scenario.json +105 -0
- package/dist/content/examples/performance/tsconfig.json +16 -0
- package/dist/content/examples/performance/view.ocml +18 -0
- package/dist/content/examples/playervox-journal/LICENSE +21 -0
- package/dist/content/examples/playervox-journal/README.md +150 -0
- package/dist/content/examples/playervox-journal/listing.json +19 -0
- package/dist/content/examples/playervox-journal/locales/en.json +25 -0
- package/dist/content/examples/playervox-journal/locales/fr.json +25 -0
- package/dist/content/examples/playervox-journal/logic.ts +556 -0
- package/dist/content/examples/playervox-journal/manifest.json +19 -0
- package/dist/content/examples/playervox-journal/package.json +16 -0
- package/dist/content/examples/playervox-journal/style.ocss +142 -0
- package/dist/content/examples/playervox-journal/tests/changes.scenario.json +482 -0
- package/dist/content/examples/playervox-journal/tests/delete.scenario.json +432 -0
- package/dist/content/examples/playervox-journal/tests/frame.scenario.json +169 -0
- package/dist/content/examples/playervox-journal/tests/load-logic.mjs +22 -0
- package/dist/content/examples/playervox-journal/tests/logic.test.mjs +349 -0
- package/dist/content/examples/playervox-journal/tests/paging.scenario.json +428 -0
- package/dist/content/examples/playervox-journal/tests/preview.scenario.json +184 -0
- package/dist/content/examples/playervox-journal/tests/read-only.scenario.json +157 -0
- package/dist/content/examples/playervox-journal/tests/refused.scenario.json +39 -0
- package/dist/content/examples/playervox-journal/tests/scale.scenario.json +286 -0
- package/dist/content/examples/playervox-journal/tests/signed-out.scenario.json +134 -0
- package/dist/content/examples/playervox-journal/tests/states.scenario.json +741 -0
- package/dist/content/examples/playervox-journal/tests/unavailable.scenario.json +183 -0
- package/dist/content/examples/playervox-journal/tsconfig.json +16 -0
- package/dist/content/examples/playervox-journal/view.ocml +52 -0
- package/dist/content/examples/playervox-rating/LICENSE +21 -0
- package/dist/content/examples/playervox-rating/README.md +166 -0
- package/dist/content/examples/playervox-rating/listing.json +19 -0
- package/dist/content/examples/playervox-rating/locales/en.json +35 -0
- package/dist/content/examples/playervox-rating/locales/fr.json +35 -0
- package/dist/content/examples/playervox-rating/logic.ts +581 -0
- package/dist/content/examples/playervox-rating/manifest.json +19 -0
- package/dist/content/examples/playervox-rating/package.json +16 -0
- package/dist/content/examples/playervox-rating/style.ocss +333 -0
- package/dist/content/examples/playervox-rating/tests/account.scenario.json +125 -0
- package/dist/content/examples/playervox-rating/tests/bounds.scenario.json +73 -0
- package/dist/content/examples/playervox-rating/tests/draft.scenario.json +199 -0
- package/dist/content/examples/playervox-rating/tests/form.scenario.json +142 -0
- package/dist/content/examples/playervox-rating/tests/load-logic.mjs +22 -0
- package/dist/content/examples/playervox-rating/tests/logic.test.mjs +346 -0
- package/dist/content/examples/playervox-rating/tests/offline.scenario.json +142 -0
- package/dist/content/examples/playervox-rating/tests/preview.scenario.json +44 -0
- package/dist/content/examples/playervox-rating/tests/publish-keyboard.scenario.json +221 -0
- package/dist/content/examples/playervox-rating/tests/publish-pointer.scenario.json +314 -0
- package/dist/content/examples/playervox-rating/tests/read-only.scenario.json +70 -0
- package/dist/content/examples/playervox-rating/tests/refused.scenario.json +45 -0
- package/dist/content/examples/playervox-rating/tests/states.scenario.json +362 -0
- package/dist/content/examples/playervox-rating/tsconfig.json +16 -0
- package/dist/content/examples/playervox-rating/view.ocml +62 -0
- package/dist/content/examples/playervox-reviews/LICENSE +21 -0
- package/dist/content/examples/playervox-reviews/README.md +194 -0
- package/dist/content/examples/playervox-reviews/listing.json +19 -0
- package/dist/content/examples/playervox-reviews/locales/en.json +32 -0
- package/dist/content/examples/playervox-reviews/locales/fr.json +32 -0
- package/dist/content/examples/playervox-reviews/logic.ts +822 -0
- package/dist/content/examples/playervox-reviews/manifest.json +30 -0
- package/dist/content/examples/playervox-reviews/package.json +16 -0
- package/dist/content/examples/playervox-reviews/style.ocss +260 -0
- package/dist/content/examples/playervox-reviews/tests/account.scenario.json +249 -0
- package/dist/content/examples/playervox-reviews/tests/bounds.scenario.json +336 -0
- package/dist/content/examples/playervox-reviews/tests/changes.scenario.json +515 -0
- package/dist/content/examples/playervox-reviews/tests/filter.scenario.json +400 -0
- package/dist/content/examples/playervox-reviews/tests/frame.scenario.json +185 -0
- package/dist/content/examples/playervox-reviews/tests/keyboard.scenario.json +188 -0
- package/dist/content/examples/playervox-reviews/tests/load-logic.mjs +22 -0
- package/dist/content/examples/playervox-reviews/tests/logic.test.mjs +559 -0
- package/dist/content/examples/playervox-reviews/tests/offline-start.scenario.json +152 -0
- package/dist/content/examples/playervox-reviews/tests/offline.scenario.json +311 -0
- package/dist/content/examples/playervox-reviews/tests/paging.scenario.json +497 -0
- package/dist/content/examples/playervox-reviews/tests/preview.scenario.json +286 -0
- package/dist/content/examples/playervox-reviews/tests/reading.scenario.json +591 -0
- package/dist/content/examples/playervox-reviews/tests/refused.scenario.json +54 -0
- package/dist/content/examples/playervox-reviews/tests/scale.scenario.json +303 -0
- package/dist/content/examples/playervox-reviews/tests/states.scenario.json +700 -0
- package/dist/content/examples/playervox-reviews/tests/unavailable.scenario.json +192 -0
- package/dist/content/examples/playervox-reviews/tsconfig.json +16 -0
- package/dist/content/examples/playervox-reviews/view.ocml +110 -0
- package/dist/content/examples/playervox-score/LICENSE +21 -0
- package/dist/content/examples/playervox-score/README.md +131 -0
- package/dist/content/examples/playervox-score/listing.json +19 -0
- package/dist/content/examples/playervox-score/locales/en.json +23 -0
- package/dist/content/examples/playervox-score/locales/fr.json +23 -0
- package/dist/content/examples/playervox-score/logic.ts +430 -0
- package/dist/content/examples/playervox-score/manifest.json +47 -0
- package/dist/content/examples/playervox-score/package.json +16 -0
- package/dist/content/examples/playervox-score/style.ocss +247 -0
- package/dist/content/examples/playervox-score/tests/grades.scenario.json +279 -0
- package/dist/content/examples/playervox-score/tests/load-logic.mjs +22 -0
- package/dist/content/examples/playervox-score/tests/logic.test.mjs +229 -0
- package/dist/content/examples/playervox-score/tests/options.scenario.json +302 -0
- package/dist/content/examples/playervox-score/tests/preview.scenario.json +68 -0
- package/dist/content/examples/playervox-score/tests/pulse.scenario.json +157 -0
- package/dist/content/examples/playervox-score/tests/refused.scenario.json +21 -0
- package/dist/content/examples/playervox-score/tests/signed-out.scenario.json +46 -0
- package/dist/content/examples/playervox-score/tests/states.scenario.json +450 -0
- package/dist/content/examples/playervox-score/tests/unavailable.scenario.json +84 -0
- package/dist/content/examples/playervox-score/tsconfig.json +16 -0
- package/dist/content/examples/playervox-score/view.ocml +51 -0
- package/dist/content/examples/session/LICENSE +21 -0
- package/dist/content/examples/session/README.md +57 -0
- package/dist/content/examples/session/listing.json +19 -0
- package/dist/content/examples/session/locales/en.json +3 -0
- package/dist/content/examples/session/locales/fr.json +3 -0
- package/dist/content/examples/session/logic.ts +26 -0
- package/dist/content/examples/session/manifest.json +19 -0
- package/dist/content/examples/session/package.json +16 -0
- package/dist/content/examples/session/style.ocss +16 -0
- package/dist/content/examples/session/tests/hidden.scenario.json +45 -0
- package/dist/content/examples/session/tests/load-logic.mjs +22 -0
- package/dist/content/examples/session/tests/logic.test.mjs +37 -0
- package/dist/content/examples/session/tests/preview.scenario.json +28 -0
- package/dist/content/examples/session/tests/refused.scenario.json +20 -0
- package/dist/content/examples/session/tests/refused.test.mjs +16 -0
- package/dist/content/examples/session/tests/render.scenario.json +95 -0
- package/dist/content/examples/session/tests/ticks.scenario.json +139 -0
- package/dist/content/examples/session/tests/unknown.scenario.json +37 -0
- package/dist/content/examples/session/tsconfig.json +16 -0
- package/dist/content/examples/session/view.ocml +11 -0
- package/dist/content/examples/stopwatch/LICENSE +21 -0
- package/dist/content/examples/stopwatch/README.md +99 -0
- package/dist/content/examples/stopwatch/listing.json +19 -0
- package/dist/content/examples/stopwatch/locales/en.json +8 -0
- package/dist/content/examples/stopwatch/locales/fr.json +8 -0
- package/dist/content/examples/stopwatch/logic.ts +93 -0
- package/dist/content/examples/stopwatch/manifest.json +19 -0
- package/dist/content/examples/stopwatch/package.json +16 -0
- package/dist/content/examples/stopwatch/style.ocss +54 -0
- package/dist/content/examples/stopwatch/tests/errors.scenario.json +121 -0
- package/dist/content/examples/stopwatch/tests/gesture.scenario.json +154 -0
- package/dist/content/examples/stopwatch/tests/hidden.scenario.json +57 -0
- package/dist/content/examples/stopwatch/tests/hundredths.scenario.json +173 -0
- package/dist/content/examples/stopwatch/tests/load-logic.mjs +22 -0
- package/dist/content/examples/stopwatch/tests/logic.test.mjs +99 -0
- package/dist/content/examples/stopwatch/tests/preview.scenario.json +36 -0
- package/dist/content/examples/stopwatch/tests/read-only.scenario.json +42 -0
- package/dist/content/examples/stopwatch/tests/read-only.test.mjs +18 -0
- package/dist/content/examples/stopwatch/tests/refused.scenario.json +22 -0
- package/dist/content/examples/stopwatch/tests/refused.test.mjs +18 -0
- package/dist/content/examples/stopwatch/tests/render.scenario.json +105 -0
- package/dist/content/examples/stopwatch/tests/states.scenario.json +246 -0
- package/dist/content/examples/stopwatch/tests/unknown.scenario.json +40 -0
- package/dist/content/examples/stopwatch/tsconfig.json +16 -0
- package/dist/content/examples/stopwatch/view.ocml +29 -0
- package/dist/content/examples/twitch-chat/LICENSE +21 -0
- package/dist/content/examples/twitch-chat/README.md +283 -0
- package/dist/content/examples/twitch-chat/listing.json +19 -0
- package/dist/content/examples/twitch-chat/locales/en.json +52 -0
- package/dist/content/examples/twitch-chat/locales/fr.json +52 -0
- package/dist/content/examples/twitch-chat/logic.ts +887 -0
- package/dist/content/examples/twitch-chat/manifest.json +33 -0
- package/dist/content/examples/twitch-chat/package.json +16 -0
- package/dist/content/examples/twitch-chat/style.ocss +544 -0
- package/dist/content/examples/twitch-chat/tests/account.scenario.json +325 -0
- package/dist/content/examples/twitch-chat/tests/burst.scenario.json +1866 -0
- package/dist/content/examples/twitch-chat/tests/channel-refused.scenario.json +372 -0
- package/dist/content/examples/twitch-chat/tests/channel.scenario.json +480 -0
- package/dist/content/examples/twitch-chat/tests/draft.scenario.json +279 -0
- package/dist/content/examples/twitch-chat/tests/fade.scenario.json +269 -0
- package/dist/content/examples/twitch-chat/tests/failures.scenario.json +420 -0
- package/dist/content/examples/twitch-chat/tests/favorites.scenario.json +376 -0
- package/dist/content/examples/twitch-chat/tests/frame.scenario.json +152 -0
- package/dist/content/examples/twitch-chat/tests/lifetime.scenario.json +186 -0
- package/dist/content/examples/twitch-chat/tests/load-logic.mjs +22 -0
- package/dist/content/examples/twitch-chat/tests/logic.test.mjs +1053 -0
- package/dist/content/examples/twitch-chat/tests/passive.scenario.json +536 -0
- package/dist/content/examples/twitch-chat/tests/preview.scenario.json +147 -0
- package/dist/content/examples/twitch-chat/tests/read-only.scenario.json +202 -0
- package/dist/content/examples/twitch-chat/tests/read-only.test.mjs +63 -0
- package/dist/content/examples/twitch-chat/tests/refused.scenario.json +65 -0
- package/dist/content/examples/twitch-chat/tests/refused.test.mjs +43 -0
- package/dist/content/examples/twitch-chat/tests/render.scenario.json +347 -0
- package/dist/content/examples/twitch-chat/tests/reply.scenario.json +384 -0
- package/dist/content/examples/twitch-chat/tests/send-refused.scenario.json +383 -0
- package/dist/content/examples/twitch-chat/tests/send.scenario.json +300 -0
- package/dist/content/examples/twitch-chat/tests/states.scenario.json +455 -0
- package/dist/content/examples/twitch-chat/tests/unavailable.scenario.json +256 -0
- package/dist/content/examples/twitch-chat/tests/unread.scenario.json +997 -0
- package/dist/content/examples/twitch-chat/tsconfig.json +16 -0
- package/dist/content/examples/twitch-chat/view.ocml +141 -0
- package/dist/content/examples/warframe-market/LICENSE +21 -0
- package/dist/content/examples/warframe-market/README.md +176 -0
- package/dist/content/examples/warframe-market/listing.json +19 -0
- package/dist/content/examples/warframe-market/locales/en.json +44 -0
- package/dist/content/examples/warframe-market/locales/fr.json +44 -0
- package/dist/content/examples/warframe-market/logic.ts +1265 -0
- package/dist/content/examples/warframe-market/manifest.json +92 -0
- package/dist/content/examples/warframe-market/package.json +16 -0
- package/dist/content/examples/warframe-market/style.ocss +422 -0
- package/dist/content/examples/warframe-market/tests/bounds.scenario.json +60 -0
- package/dist/content/examples/warframe-market/tests/cached.scenario.json +70 -0
- package/dist/content/examples/warframe-market/tests/detail.scenario.json +251 -0
- package/dist/content/examples/warframe-market/tests/errors.scenario.json +105 -0
- package/dist/content/examples/warframe-market/tests/fixtures.json +295 -0
- package/dist/content/examples/warframe-market/tests/load-logic.mjs +22 -0
- package/dist/content/examples/warframe-market/tests/logic.test.mjs +505 -0
- package/dist/content/examples/warframe-market/tests/no-clipboard.scenario.json +104 -0
- package/dist/content/examples/warframe-market/tests/passive.scenario.json +87 -0
- package/dist/content/examples/warframe-market/tests/preview.scenario.json +146 -0
- package/dist/content/examples/warframe-market/tests/refresh.scenario.json +56 -0
- package/dist/content/examples/warframe-market/tests/refused.scenario.json +74 -0
- package/dist/content/examples/warframe-market/tests/render.scenario.json +210 -0
- package/dist/content/examples/warframe-market/tests/search.scenario.json +189 -0
- package/dist/content/examples/warframe-market/tests/unavailable.scenario.json +54 -0
- package/dist/content/examples/warframe-market/tsconfig.json +16 -0
- package/dist/content/examples/warframe-market/view.ocml +116 -0
- package/dist/content/examples/weather/LICENSE +20 -0
- package/dist/content/examples/weather/listing.json +10 -0
- package/dist/content/examples/weather/locales/en.json +9 -0
- package/dist/content/examples/weather/locales/fr.json +9 -0
- package/dist/content/examples/weather/logic.ts +62 -0
- package/dist/content/examples/weather/manifest.json +51 -0
- package/dist/content/examples/weather/package.json +15 -0
- package/dist/content/examples/weather/style.ocss +29 -0
- package/dist/content/examples/weather/tsconfig.json +16 -0
- package/dist/content/examples/weather/view.ocml +12 -0
- package/dist/content/index.json +3601 -0
- package/dist/content/reference/errors.json +1712 -0
- package/dist/content/reference/limits.json +1154 -0
- package/dist/content/reference/manifest.md +223 -0
- package/dist/content/reference/permissions.json +413 -0
- package/dist/content/templates/blank/locales/en.json +3 -0
- package/dist/content/templates/blank/locales/fr.json +3 -0
- package/dist/content/templates/blank/logic.ts +7 -0
- package/dist/content/templates/blank/manifest.json +16 -0
- package/dist/content/templates/blank/style.ocss +10 -0
- package/dist/content/templates/blank/tests/example.scenario.json +10 -0
- package/dist/content/templates/blank/view.ocml +6 -0
- package/dist/content/templates/chart/locales/en.json +3 -0
- package/dist/content/templates/chart/locales/fr.json +3 -0
- package/dist/content/templates/chart/logic.ts +27 -0
- package/dist/content/templates/chart/manifest.json +16 -0
- package/dist/content/templates/chart/style.ocss +27 -0
- package/dist/content/templates/chart/tests/example.scenario.json +10 -0
- package/dist/content/templates/chart/view.ocml +10 -0
- package/dist/content/templates/counter/locales/en.json +4 -0
- package/dist/content/templates/counter/locales/fr.json +4 -0
- package/dist/content/templates/counter/logic.ts +20 -0
- package/dist/content/templates/counter/manifest.json +16 -0
- package/dist/content/templates/counter/style.ocss +26 -0
- package/dist/content/templates/counter/tests/example.scenario.json +11 -0
- package/dist/content/templates/counter/view.ocml +13 -0
- package/dist/content/templates/list/locales/en.json +8 -0
- package/dist/content/templates/list/locales/fr.json +8 -0
- package/dist/content/templates/list/logic.ts +35 -0
- package/dist/content/templates/list/manifest.json +16 -0
- package/dist/content/templates/list/style.ocss +42 -0
- package/dist/content/templates/list/tests/example.scenario.json +10 -0
- package/dist/content/templates/list/view.ocml +17 -0
- package/dist/content/templates/shared/LICENSE +20 -0
- package/dist/content/templates/shared/package.json +14 -0
- package/dist/content/templates/shared/tsconfig.json +16 -0
- package/dist/content.js +68 -0
- package/dist/env.js +156 -0
- package/dist/fingerprint.js +32 -0
- package/dist/ids.js +27 -0
- package/dist/index.js +82 -0
- package/dist/knowledge.js +222 -0
- package/dist/npm.js +149 -0
- package/dist/prompts.js +47 -0
- package/dist/resources.js +82 -0
- package/dist/run.js +120 -0
- package/dist/server.js +43 -0
- package/dist/session.js +205 -0
- package/dist/sources.js +218 -0
- package/dist/text.js +80 -0
- package/dist/texts.js +44 -0
- package/dist/tools/audit.js +105 -0
- package/dist/tools/build.js +453 -0
- package/dist/tools/common.js +122 -0
- package/dist/tools/docs.js +196 -0
- package/dist/tools/preview.js +87 -0
- package/dist/tools/project.js +178 -0
- package/dist/tools/setup.js +146 -0
- package/dist/tools/submission.js +369 -0
- package/dist/version.js +3 -0
- package/npm-shrinkwrap.json +402 -0
- package/package.json +52 -4
|
@@ -0,0 +1,1053 @@
|
|
|
1
|
+
// Unit tests of the Twitch chat widget's logic on @overcrow/sdk/testing.
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { test } from "node:test";
|
|
5
|
+
import { installRuntime } from "@overcrow/sdk/testing";
|
|
6
|
+
|
|
7
|
+
import { loadLogic } from "./load-logic.mjs";
|
|
8
|
+
|
|
9
|
+
const read = (path) => readFileSync(new URL(`../${path}`, import.meta.url), "utf8");
|
|
10
|
+
const messages = (locale) => JSON.parse(read(`locales/${locale}.json`));
|
|
11
|
+
|
|
12
|
+
const START = Date.UTC(2026, 0, 1, 12);
|
|
13
|
+
const vm = installRuntime({
|
|
14
|
+
now: START,
|
|
15
|
+
host: {
|
|
16
|
+
grants: ["twitch.chat.read", "twitch.chat.compose"],
|
|
17
|
+
mode: "interactive",
|
|
18
|
+
messages: messages("en"),
|
|
19
|
+
options: { "passive-lifetime": 30 },
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
const logic = await loadLogic();
|
|
23
|
+
const { state } = await import("@overcrow/sdk");
|
|
24
|
+
|
|
25
|
+
const SERVICE = "twitch.chat.subscribe";
|
|
26
|
+
const flush = () => new Promise((resolve) => setImmediate(resolve));
|
|
27
|
+
const live = () => vm.subscriptions.filter((subscription) => !subscription.cancelled);
|
|
28
|
+
|
|
29
|
+
/** A chat message as the host delivers it, received now unless `changes` say otherwise. */
|
|
30
|
+
const message = (id, text = `text of ${id}`, changes = {}) => ({
|
|
31
|
+
id,
|
|
32
|
+
author: "Juniper",
|
|
33
|
+
color: "#ff7f50",
|
|
34
|
+
badges: [],
|
|
35
|
+
fragments: [{ text }],
|
|
36
|
+
reply: null,
|
|
37
|
+
deleted: false,
|
|
38
|
+
receivedAt: vm.now,
|
|
39
|
+
...changes,
|
|
40
|
+
});
|
|
41
|
+
/** A message of 16 runs, the most the host delivers. */
|
|
42
|
+
const longest = (id, changes = {}) =>
|
|
43
|
+
message(id, "", {
|
|
44
|
+
fragments: Array.from({ length: 16 }, (_, index) =>
|
|
45
|
+
index % 2 === 0 ? { text: `run ${index} ` } : { emote: `asset:${index}`, alt: "ocGem" },
|
|
46
|
+
),
|
|
47
|
+
...changes,
|
|
48
|
+
});
|
|
49
|
+
const many = (count, from = 1, make = message) =>
|
|
50
|
+
Array.from({ length: count }, (_, index) => make(`m${from + index}`));
|
|
51
|
+
|
|
52
|
+
let generation = 0;
|
|
53
|
+
/** A value of the subscription: a delta of the current generation. */
|
|
54
|
+
const chat = (changes = {}) => ({
|
|
55
|
+
account: "connected",
|
|
56
|
+
channel: "juniper_plays",
|
|
57
|
+
joinState: "joined",
|
|
58
|
+
failure: null,
|
|
59
|
+
favorites: [],
|
|
60
|
+
canSend: true,
|
|
61
|
+
generation,
|
|
62
|
+
reset: false,
|
|
63
|
+
messages: [],
|
|
64
|
+
removed: [],
|
|
65
|
+
skipped: 0,
|
|
66
|
+
...changes,
|
|
67
|
+
});
|
|
68
|
+
const delta = (changes) => vm.push(SERVICE, chat(changes));
|
|
69
|
+
const ids = (rows = state.rows) => rows.map((row) => (row.gap > 0 ? `gap:${row.gap}` : row.id));
|
|
70
|
+
|
|
71
|
+
/** A new generation in Interactive mode: an empty joined chat, nothing in flight. */
|
|
72
|
+
function fresh(changes = {}) {
|
|
73
|
+
vm.setHost({ mode: "interactive", options: { "passive-lifetime": 30 } });
|
|
74
|
+
generation += 1;
|
|
75
|
+
delta({ reset: true, ...changes });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
test("it subscribes once and waits for the host's first answer", () => {
|
|
79
|
+
assert.deepEqual(
|
|
80
|
+
vm.subscriptions.map(({ service }) => service),
|
|
81
|
+
[SERVICE],
|
|
82
|
+
);
|
|
83
|
+
assert.equal(vm.calls.length, 0);
|
|
84
|
+
assert.equal(logic.phase(state), "waiting");
|
|
85
|
+
assert.equal(logic.heading(state), "Twitch");
|
|
86
|
+
assert.equal(logic.statusKey(state), "inactive");
|
|
87
|
+
assert.equal(logic.dotClass(state), "dot");
|
|
88
|
+
assert.equal(logic.selecting(state), false);
|
|
89
|
+
assert.equal(logic.composing(state), false);
|
|
90
|
+
assert.equal(vm.timers.length, 0, "nothing ticks");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("a channel login is trimmed, without its #, lowercased, 1 to 25 ASCII letters, digits and _", () => {
|
|
94
|
+
assert.equal(logic.normalizeChannel("juniper_plays"), "juniper_plays");
|
|
95
|
+
assert.equal(logic.normalizeChannel(" Juniper_Plays \n"), "juniper_plays");
|
|
96
|
+
assert.equal(logic.normalizeChannel("#River_Otter"), "river_otter");
|
|
97
|
+
assert.equal(logic.normalizeChannel(" #MOSS42 "), "moss42");
|
|
98
|
+
assert.equal(logic.normalizeChannel("a"), "a", "one character is enough");
|
|
99
|
+
assert.equal(logic.normalizeChannel("a".repeat(25)), "a".repeat(25));
|
|
100
|
+
assert.equal(logic.normalizeChannel(`#${"B".repeat(25)}`), "b".repeat(25), "the # is not counted");
|
|
101
|
+
for (const invalid of [
|
|
102
|
+
"",
|
|
103
|
+
" ",
|
|
104
|
+
"#",
|
|
105
|
+
"##moss",
|
|
106
|
+
"# moss",
|
|
107
|
+
"a".repeat(26),
|
|
108
|
+
"juniper plays",
|
|
109
|
+
"juniper-plays",
|
|
110
|
+
"juniper.plays",
|
|
111
|
+
"juniper/plays",
|
|
112
|
+
"@juniper",
|
|
113
|
+
"junipér",
|
|
114
|
+
"ジュニパー",
|
|
115
|
+
"moss\u0000",
|
|
116
|
+
]) {
|
|
117
|
+
assert.equal(logic.normalizeChannel(invalid), null, JSON.stringify(invalid));
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("a delta appends new messages and replaces a known one in place", () => {
|
|
122
|
+
const first = logic.applyDelta([], chat({ messages: many(3) }), vm.now);
|
|
123
|
+
assert.deepEqual(ids(first.rows), ["m1", "m2", "m3"]);
|
|
124
|
+
assert.equal(first.appended, 3);
|
|
125
|
+
const edited = message("m2", "edited", { author: "Moss", color: null });
|
|
126
|
+
const second = logic.applyDelta(first.rows, chat({ messages: [edited, message("m4")] }), vm.now);
|
|
127
|
+
assert.deepEqual(ids(second.rows), ["m1", "m2", "m3", "m4"], "m2 keeps its place");
|
|
128
|
+
assert.equal(second.appended, 1, "a replacement is not a new message");
|
|
129
|
+
assert.equal(second.rows[1].author, "Moss: ");
|
|
130
|
+
assert.equal(second.rows[1].color, null);
|
|
131
|
+
assert.deepEqual(second.rows[1].parts, [{ key: "0", text: "edited", emote: "" }]);
|
|
132
|
+
assert.equal(second.rows[1].key, first.rows[1].key, "the same row of the list");
|
|
133
|
+
assert.deepEqual(ids(first.rows), ["m1", "m2", "m3"], "the previous rows are not changed");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("removed IDs and deleted messages leave the history", () => {
|
|
137
|
+
const { rows } = logic.applyDelta([], chat({ messages: many(4) }), vm.now);
|
|
138
|
+
const removed = logic.applyDelta(rows, chat({ removed: ["m2", "unknown"] }), vm.now);
|
|
139
|
+
assert.deepEqual(ids(removed.rows), ["m1", "m3", "m4"]);
|
|
140
|
+
assert.equal(removed.appended, 0);
|
|
141
|
+
const deleted = logic.applyDelta(
|
|
142
|
+
removed.rows,
|
|
143
|
+
chat({ messages: [message("m3", "", { deleted: true }), message("m9", "", { deleted: true }), message("m5")] }),
|
|
144
|
+
vm.now,
|
|
145
|
+
);
|
|
146
|
+
assert.deepEqual(ids(deleted.rows), ["m1", "m4", "m5"], "a deleted message is never shown");
|
|
147
|
+
assert.equal(deleted.appended, 1);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("a reset replaces the whole history", () => {
|
|
151
|
+
const { rows } = logic.applyDelta([], chat({ messages: many(4) }), vm.now);
|
|
152
|
+
const reset = logic.applyDelta(rows, chat({ reset: true, messages: many(2, 7), skipped: 5 }), vm.now);
|
|
153
|
+
assert.deepEqual(ids(reset.rows), ["m7", "m8"], "and has no mark of messages left out");
|
|
154
|
+
assert.equal(reset.appended, 2);
|
|
155
|
+
assert.deepEqual(ids(logic.applyDelta(rows, chat({ reset: true }), vm.now).rows), []);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("messages the host left out are marked by one row, before those it delivered", () => {
|
|
159
|
+
const { rows } = logic.applyDelta([], chat({ messages: many(2) }), vm.now);
|
|
160
|
+
const sampled = logic.applyDelta(rows, chat({ skipped: 3, messages: [message("m3")] }), vm.now);
|
|
161
|
+
assert.deepEqual(ids(sampled.rows), ["m1", "m2", "gap:3", "m3"]);
|
|
162
|
+
assert.equal(sampled.appended, 4, "what arrived, shown or not");
|
|
163
|
+
const gap = sampled.rows[2];
|
|
164
|
+
assert.equal(gap.id, "");
|
|
165
|
+
assert.equal(gap.nodes, 1);
|
|
166
|
+
assert.equal(gap.cls, "gap");
|
|
167
|
+
assert.equal(logic.gapText(gap.gap), "3 messages not shown");
|
|
168
|
+
assert.equal(logic.gapText(1), "1 message not shown");
|
|
169
|
+
|
|
170
|
+
// Two gaps in a row are one.
|
|
171
|
+
const quiet = logic.applyDelta(sampled.rows, chat({ skipped: 2 }), vm.now);
|
|
172
|
+
assert.deepEqual(ids(quiet.rows), ["m1", "m2", "gap:3", "m3", "gap:2"]);
|
|
173
|
+
const merged = logic.applyDelta(quiet.rows, chat({ skipped: 4, messages: [message("m4")] }), vm.now);
|
|
174
|
+
assert.deepEqual(ids(merged.rows), ["m1", "m2", "gap:3", "m3", "gap:6", "m4"]);
|
|
175
|
+
assert.equal(merged.rows[4].key, quiet.rows[4].key, "the same row, with a larger count");
|
|
176
|
+
assert.equal(merged.appended, 5);
|
|
177
|
+
assert.notEqual(merged.rows[2].key, merged.rows[4].key, "each gap has its own key");
|
|
178
|
+
|
|
179
|
+
// A removal between two gaps makes them neighbours, not one row: the
|
|
180
|
+
// next delta adds to the last.
|
|
181
|
+
const emptied = logic.applyDelta(merged.rows, chat({ removed: ["m4"], skipped: 1 }), vm.now);
|
|
182
|
+
assert.deepEqual(ids(emptied.rows), ["m1", "m2", "gap:3", "m3", "gap:7"]);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("a gap never opens the history", () => {
|
|
186
|
+
const alone = logic.applyDelta([], chat({ skipped: 9 }), vm.now);
|
|
187
|
+
assert.deepEqual(ids(alone.rows), []);
|
|
188
|
+
assert.equal(alone.appended, 9);
|
|
189
|
+
const leading = logic.applyDelta([], chat({ skipped: 9, messages: many(2) }), vm.now);
|
|
190
|
+
assert.deepEqual(ids(leading.rows), ["m1", "m2"]);
|
|
191
|
+
// The message before a gap was removed.
|
|
192
|
+
const { rows } = logic.applyDelta([], chat({ messages: [message("m1")] }), vm.now);
|
|
193
|
+
const after = logic.applyDelta(rows, chat({ skipped: 2, messages: [message("m2")] }), vm.now);
|
|
194
|
+
assert.deepEqual(ids(logic.applyDelta(after.rows, chat({ removed: ["m1"] }), vm.now).rows), ["m2"]);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("the history keeps the last 200 messages", () => {
|
|
198
|
+
assert.equal(logic.HISTORY_MAX, 200);
|
|
199
|
+
const { rows } = logic.applyDelta([], chat({ reset: true, messages: many(205) }), vm.now);
|
|
200
|
+
assert.equal(rows.length, 200);
|
|
201
|
+
assert.equal(rows[0].id, "m6");
|
|
202
|
+
assert.equal(rows.at(-1).id, "m205");
|
|
203
|
+
// One more: the oldest leaves. Gaps are not counted as messages.
|
|
204
|
+
const next = logic.applyDelta(rows, chat({ skipped: 2, messages: [message("m206")] }), vm.now);
|
|
205
|
+
assert.equal(next.rows.filter((row) => row.gap === 0).length, 200);
|
|
206
|
+
assert.equal(next.rows[0].id, "m7");
|
|
207
|
+
assert.deepEqual(ids(next.rows.slice(-3)), ["m205", "gap:2", "m206"]);
|
|
208
|
+
// A replacement drops nothing.
|
|
209
|
+
const replaced = logic.applyDelta(next.rows, chat({ messages: [message("m100", "edited")] }), vm.now);
|
|
210
|
+
assert.equal(replaced.rows[0].id, "m7");
|
|
211
|
+
assert.equal(replaced.rows.length, next.rows.length);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("the history stays within its scene node budget, the oldest rows leaving first", () => {
|
|
215
|
+
assert.equal(logic.messageRow(message("m1")).nodes, 5, "the row, its text, the author, the icon, one run");
|
|
216
|
+
assert.equal(logic.messageRow(longest("m1")).nodes, 20);
|
|
217
|
+
const replied = longest("m1", { reply: { author: "Moss", text: "first\nsecond" } });
|
|
218
|
+
assert.equal(logic.messageRow(replied).nodes, 21);
|
|
219
|
+
assert.equal(logic.messageRow(replied).context, "Moss: first second", "the context is one line");
|
|
220
|
+
assert.equal(logic.messageRow(replied).cls, "row replied");
|
|
221
|
+
|
|
222
|
+
const nodes = (rows) => rows.reduce((sum, row) => sum + row.nodes, 0);
|
|
223
|
+
const full = logic.applyDelta([], chat({ reset: true, messages: many(200, 1, longest) }), vm.now).rows;
|
|
224
|
+
assert.equal(full.length, Math.floor(logic.ROW_NODE_BUDGET / 20));
|
|
225
|
+
assert.equal(full.length, 180);
|
|
226
|
+
assert.ok(nodes(full) <= logic.ROW_NODE_BUDGET);
|
|
227
|
+
assert.equal(full.at(-1).id, "m200", "the newest stay");
|
|
228
|
+
assert.equal(full[0].id, "m21");
|
|
229
|
+
|
|
230
|
+
const next = logic.applyDelta(full, chat({ messages: [replied, longest("m201")] }), vm.now).rows;
|
|
231
|
+
assert.ok(nodes(next) <= logic.ROW_NODE_BUDGET);
|
|
232
|
+
assert.equal(next.at(-1).id, "m201");
|
|
233
|
+
// 200 ordinary messages fit whole.
|
|
234
|
+
const plain = logic.applyDelta([], chat({ reset: true, messages: many(200) }), vm.now).rows;
|
|
235
|
+
assert.equal(plain.length, 200);
|
|
236
|
+
assert.ok(nodes(plain) <= logic.ROW_NODE_BUDGET);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("a row carries the author, each run, the emotes with their names and the reply tooltip", () => {
|
|
240
|
+
const row = logic.messageRow(
|
|
241
|
+
message("m1", "", {
|
|
242
|
+
author: "River_Otter",
|
|
243
|
+
fragments: [{ text: "hello " }, { emote: "asset:7", alt: "ocSmile" }, { text: " there" }],
|
|
244
|
+
}),
|
|
245
|
+
);
|
|
246
|
+
assert.equal(row.key, "m:m1");
|
|
247
|
+
assert.equal(row.author, "River_Otter: ");
|
|
248
|
+
assert.deepEqual(row.parts, [
|
|
249
|
+
{ key: "0", text: "hello ", emote: "" },
|
|
250
|
+
{ key: "1", text: "ocSmile", emote: "asset:7" },
|
|
251
|
+
{ key: "2", text: " there", emote: "" },
|
|
252
|
+
]);
|
|
253
|
+
assert.equal(row.tip, "Reply to River_Otter");
|
|
254
|
+
assert.equal(row.context, "");
|
|
255
|
+
assert.equal(row.cls, "row");
|
|
256
|
+
const long = logic.messageRow(message("m2", "", { author: "é".repeat(200) }));
|
|
257
|
+
assert.ok(new TextEncoder().encode(long.tip).length <= 256, "a tooltip within the label bound");
|
|
258
|
+
assert.equal(logic.cutBytes("héllo", 2), "h");
|
|
259
|
+
assert.equal(logic.oneLine(" a \r\n b\n\nc "), "a b c");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("the first value shows the chat: heading, status, history and composer", () => {
|
|
263
|
+
fresh({ messages: many(3), favorites: ["juniper_plays"] });
|
|
264
|
+
assert.equal(logic.phase(state), "chat");
|
|
265
|
+
assert.equal(logic.heading(state), "#juniper_plays");
|
|
266
|
+
assert.equal(logic.statusKey(state), "connected");
|
|
267
|
+
assert.deepEqual(ids(state.shown), ["m1", "m2", "m3"]);
|
|
268
|
+
assert.equal(state.shown, state.rows, "Interactive mode shows the history itself");
|
|
269
|
+
assert.equal(logic.composing(state), true);
|
|
270
|
+
assert.equal(logic.composerClass(state), "composer");
|
|
271
|
+
assert.equal(logic.composerHint(state), "send-hint");
|
|
272
|
+
assert.equal(logic.emptyKey(state), "no-messages");
|
|
273
|
+
assert.equal(logic.isFavorite(state), true);
|
|
274
|
+
assert.equal(logic.starClass(state), "star on");
|
|
275
|
+
assert.equal(logic.favoriteLabel(state), "Unfavorite #juniper_plays");
|
|
276
|
+
assert.equal(logic.favoriteTip(state), "favorite-remove");
|
|
277
|
+
assert.deepEqual(logic.composers(state), [generation], "the form's key is the generation");
|
|
278
|
+
assert.equal(vm.timers.length, 0, "Interactive mode runs no timer");
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test("the status says each state of the connection and each failure", () => {
|
|
282
|
+
const shown = (changes) => {
|
|
283
|
+
fresh(changes);
|
|
284
|
+
return [
|
|
285
|
+
logic.statusKey(state),
|
|
286
|
+
logic.dotClass(state),
|
|
287
|
+
logic.failureKey(state),
|
|
288
|
+
vm.host.messages[logic.statusKey(state)],
|
|
289
|
+
];
|
|
290
|
+
};
|
|
291
|
+
assert.deepEqual(shown({ joinState: "idle", canSend: false }), ["disconnected", "dot", "", "DISCONNECTED"]);
|
|
292
|
+
assert.deepEqual(shown({ joinState: "connecting", canSend: false }), ["connecting", "dot warn", "", "CONNECTING"]);
|
|
293
|
+
assert.equal(logic.composerHint(state), "waiting-hint");
|
|
294
|
+
assert.deepEqual(shown({ joinState: "joined" }), ["connected", "dot ok", "", "CONNECTED"]);
|
|
295
|
+
assert.deepEqual(shown({ joinState: "reconnecting", canSend: false }), [
|
|
296
|
+
"reconnecting",
|
|
297
|
+
"dot warn",
|
|
298
|
+
"",
|
|
299
|
+
"RECONNECTING",
|
|
300
|
+
]);
|
|
301
|
+
const failed = (failure) => shown({ joinState: "failed", failure, canSend: false });
|
|
302
|
+
assert.deepEqual(failed("channel_unavailable"), [
|
|
303
|
+
"channel-unavailable",
|
|
304
|
+
"dot failed",
|
|
305
|
+
"channel-unavailable",
|
|
306
|
+
"CHANNEL UNAVAILABLE",
|
|
307
|
+
]);
|
|
308
|
+
assert.deepEqual(failed("connection"), ["connection-error", "dot failed", "connection-error", "CONNECTION ERROR"]);
|
|
309
|
+
assert.deepEqual(failed("provider"), ["twitch-error", "dot failed", "twitch-error", "TWITCH ERROR"]);
|
|
310
|
+
assert.deepEqual(failed("limit"), ["limit", "dot failed", "limit", "Another widget is using Twitch chat"]);
|
|
311
|
+
assert.deepEqual(failed(null), ["twitch-error", "dot failed", "twitch-error", "TWITCH ERROR"]);
|
|
312
|
+
|
|
313
|
+
// No channel: the selector in Interactive mode, a sentence in Passive mode.
|
|
314
|
+
assert.deepEqual(shown({ channel: null, joinState: "idle", canSend: false }), [
|
|
315
|
+
"disconnected",
|
|
316
|
+
"dot",
|
|
317
|
+
"",
|
|
318
|
+
"DISCONNECTED",
|
|
319
|
+
]);
|
|
320
|
+
assert.equal(logic.phase(state), "no-channel");
|
|
321
|
+
assert.equal(logic.heading(state), "Twitch");
|
|
322
|
+
assert.equal(logic.selecting(state), true);
|
|
323
|
+
assert.equal(logic.composing(state), false);
|
|
324
|
+
vm.setHost({ mode: "passive" });
|
|
325
|
+
assert.equal(logic.selecting(state), false);
|
|
326
|
+
|
|
327
|
+
// Under the host's account panel the widget says the account's state.
|
|
328
|
+
for (const [account, phase] of [
|
|
329
|
+
["signed_out", "signed-out"],
|
|
330
|
+
["expired", "signed-out"],
|
|
331
|
+
["pending", "pending"],
|
|
332
|
+
]) {
|
|
333
|
+
assert.deepEqual(shown({ account, channel: null, joinState: "idle", canSend: false }), [
|
|
334
|
+
"inactive",
|
|
335
|
+
"dot",
|
|
336
|
+
"",
|
|
337
|
+
"INACTIVE",
|
|
338
|
+
]);
|
|
339
|
+
assert.equal(logic.phase(state), phase, account);
|
|
340
|
+
assert.equal(logic.heading(state), "Twitch");
|
|
341
|
+
assert.equal(logic.selecting(state), false, "no selector without the account");
|
|
342
|
+
assert.equal(logic.composing(state), false);
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test("new messages are counted only while the user scrolled away", () => {
|
|
347
|
+
fresh({ messages: many(3) });
|
|
348
|
+
delta({ messages: [message("m4")] });
|
|
349
|
+
assert.equal(state.unread, 0, "following the end");
|
|
350
|
+
assert.equal(state.following, true);
|
|
351
|
+
|
|
352
|
+
logic.stuck(false);
|
|
353
|
+
assert.equal(state.following, false);
|
|
354
|
+
delta({ messages: [message("m5"), message("m6")] });
|
|
355
|
+
assert.equal(state.unread, 2);
|
|
356
|
+
assert.equal(logic.unreadText(state.unread), "2 new messages");
|
|
357
|
+
delta({ messages: [message("m5", "edited")] });
|
|
358
|
+
assert.equal(state.unread, 2, "a replacement is not a new message");
|
|
359
|
+
delta({ removed: ["m1"] });
|
|
360
|
+
assert.equal(state.unread, 2, "nor a removal");
|
|
361
|
+
delta({ skipped: 4, messages: [message("m7")] });
|
|
362
|
+
assert.equal(state.unread, 7, "messages left out are new too");
|
|
363
|
+
|
|
364
|
+
logic.stuck(true);
|
|
365
|
+
assert.equal(state.unread, 0, "back at the end");
|
|
366
|
+
assert.equal(state.following, true);
|
|
367
|
+
|
|
368
|
+
logic.stuck(false);
|
|
369
|
+
delta({ messages: [message("m8")] });
|
|
370
|
+
assert.equal(logic.unreadText(state.unread), "1 new message");
|
|
371
|
+
logic.latest();
|
|
372
|
+
assert.equal(state.unread, 0, "the button returns to the latest");
|
|
373
|
+
assert.equal(state.following, true);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test("the unread count is capped, also when the history is full, and a reset clears it", () => {
|
|
377
|
+
fresh({ messages: many(200) });
|
|
378
|
+
logic.stuck(false);
|
|
379
|
+
delta({ messages: many(5, 201) });
|
|
380
|
+
assert.equal(state.rows.length, 200, "the ring is full");
|
|
381
|
+
assert.equal(state.unread, 5, "and the new messages still count");
|
|
382
|
+
delta({ skipped: 500, messages: many(5, 206) });
|
|
383
|
+
assert.equal(state.unread, logic.HISTORY_MAX);
|
|
384
|
+
delta({ reset: true, messages: many(3) });
|
|
385
|
+
assert.equal(state.unread, 0);
|
|
386
|
+
assert.equal(state.following, true, "a reset shows the end");
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test("another channel or account clears the history, the reply, the draft and the unread count", () => {
|
|
390
|
+
fresh({ messages: many(3) });
|
|
391
|
+
logic.replyTo("m2");
|
|
392
|
+
logic.drafted("half a sentence");
|
|
393
|
+
logic.stuck(false);
|
|
394
|
+
delta({ messages: [message("m4")] });
|
|
395
|
+
logic.keyed("Enter");
|
|
396
|
+
logic.toggleSelector();
|
|
397
|
+
logic.channelTyped("river");
|
|
398
|
+
assert.deepEqual(
|
|
399
|
+
[state.reply?.id, state.draft, state.unread, state.sending, state.selecting, state.channelDraft],
|
|
400
|
+
["m2", "half a sentence", 1, true, true, "river"],
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
// The host says so with a new generation, here without `reset`.
|
|
404
|
+
generation += 1;
|
|
405
|
+
delta({ channel: "river_otter", messages: [message("n1")] });
|
|
406
|
+
assert.deepEqual(ids(), ["n1"], "nothing of the previous channel");
|
|
407
|
+
assert.equal(logic.heading(state), "#river_otter");
|
|
408
|
+
assert.equal(state.reply, null);
|
|
409
|
+
assert.equal(state.draft, "");
|
|
410
|
+
assert.equal(state.unread, 0);
|
|
411
|
+
assert.equal(state.following, true);
|
|
412
|
+
assert.equal(state.sending, false);
|
|
413
|
+
assert.equal(state.sendError, "");
|
|
414
|
+
assert.equal(state.notice, "");
|
|
415
|
+
assert.equal(state.selecting, false);
|
|
416
|
+
assert.equal(state.channelDraft, "");
|
|
417
|
+
assert.deepEqual(logic.composers(state), [generation], "a new form, with an empty field");
|
|
418
|
+
assert.equal(vm.timers.length, 0, "the wait for the send's answer is over");
|
|
419
|
+
|
|
420
|
+
// The same generation keeps them.
|
|
421
|
+
logic.replyTo("n1");
|
|
422
|
+
logic.drafted("kept");
|
|
423
|
+
delta({ messages: [message("n2")] });
|
|
424
|
+
assert.equal(state.reply?.id, "n1");
|
|
425
|
+
assert.equal(state.draft, "kept");
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
test("the reply target follows its message and goes with it", () => {
|
|
429
|
+
fresh({ messages: [message("m1", "hi", { author: "River_Otter" }), message("m2")] });
|
|
430
|
+
logic.replyTo("unknown");
|
|
431
|
+
assert.equal(state.reply, null);
|
|
432
|
+
logic.replyTo("m1");
|
|
433
|
+
assert.deepEqual(state.reply, { id: "m1", author: "River_Otter" });
|
|
434
|
+
assert.equal(logic.replyingTo(state), "Replying to River_Otter");
|
|
435
|
+
assert.equal(logic.replyTarget(state), "m1");
|
|
436
|
+
delta({ messages: [message("m3")] });
|
|
437
|
+
assert.equal(logic.replyTarget(state), "m1", "kept while its message is in the history");
|
|
438
|
+
delta({ removed: ["m1"] });
|
|
439
|
+
assert.equal(state.reply, null, "removed by a moderator");
|
|
440
|
+
assert.equal(logic.replyTarget(state), null);
|
|
441
|
+
assert.equal(logic.replyingTo(state), "");
|
|
442
|
+
|
|
443
|
+
logic.replyTo("m2");
|
|
444
|
+
delta({ messages: [message("m2", "", { deleted: true })] });
|
|
445
|
+
assert.equal(state.reply, null, "deleted");
|
|
446
|
+
|
|
447
|
+
logic.replyTo("m3");
|
|
448
|
+
delta({ reset: true, messages: [message("m4")] });
|
|
449
|
+
assert.equal(state.reply, null, "not in the new list");
|
|
450
|
+
|
|
451
|
+
logic.replyTo("m4");
|
|
452
|
+
delta({ messages: many(200, 5) });
|
|
453
|
+
assert.equal(state.reply, null, "too old for the history");
|
|
454
|
+
|
|
455
|
+
logic.replyTo("m204");
|
|
456
|
+
logic.cancelReply();
|
|
457
|
+
assert.equal(state.reply, null);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
test("the fade starts at two thirds of the lifetime and ends with it", () => {
|
|
461
|
+
assert.equal(logic.FADE_LEVELS, 32);
|
|
462
|
+
assert.equal(logic.fadeLevel(0, 30_000), 0);
|
|
463
|
+
assert.equal(logic.fadeLevel(20_000, 30_000), 0, "opaque until two thirds");
|
|
464
|
+
assert.equal(logic.fadeLevel(20_001, 30_000), 1);
|
|
465
|
+
assert.equal(logic.fadeLevel(22_500, 30_000), 8);
|
|
466
|
+
assert.equal(logic.fadeLevel(25_000, 30_000), 16, "half-way, half opaque");
|
|
467
|
+
assert.equal(logic.fadeLevel(29_999, 30_000), 32);
|
|
468
|
+
assert.equal(logic.fadeLevel(30_000, 30_000), 32);
|
|
469
|
+
assert.equal(logic.fadeLevel(90_000, 30_000), 32, "never beyond the last level");
|
|
470
|
+
assert.equal(logic.fadeLevel(-5_000, 30_000), 0, "a message of the future is opaque");
|
|
471
|
+
assert.equal(logic.fadeLevel(4_000, 6_000), 0);
|
|
472
|
+
assert.equal(logic.fadeLevel(5_000, 6_000), 16);
|
|
473
|
+
let previous = 0;
|
|
474
|
+
for (let age = 0; age <= 120_000; age += 250) {
|
|
475
|
+
const level = logic.fadeLevel(age, 120_000);
|
|
476
|
+
assert.ok(level >= previous && level - previous <= 1, `one level at a time at ${age} ms`);
|
|
477
|
+
previous = level;
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
test("the fade's step is one opacity level, never under the host's shortest timer", () => {
|
|
482
|
+
assert.equal(logic.FADE_STEP_MIN_MS, 100);
|
|
483
|
+
assert.equal(logic.fadeStepMs(5_000), 100);
|
|
484
|
+
assert.equal(logic.fadeStepMs(9_000), 100);
|
|
485
|
+
assert.equal(logic.fadeStepMs(10_000), 104);
|
|
486
|
+
assert.equal(logic.fadeStepMs(30_000), 313);
|
|
487
|
+
assert.equal(logic.fadeStepMs(60_000), 625);
|
|
488
|
+
assert.equal(logic.fadeStepMs(120_000), 1250);
|
|
489
|
+
// A transition would draw a frame at every refresh while a message fades.
|
|
490
|
+
const style = read("style.ocss");
|
|
491
|
+
assert.ok(!/^\s*transition\s*:/m.test(style), "no transition in the style");
|
|
492
|
+
for (let level = 1; level <= logic.FADE_LEVELS; level += 1) {
|
|
493
|
+
const opacity = 1 - level / logic.FADE_LEVELS;
|
|
494
|
+
assert.ok(style.includes(`.fade-${level} {\n opacity: ${opacity};`), `.fade-${level}`);
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
test("Passive mode shows the last 12 messages younger than the lifetime", () => {
|
|
499
|
+
assert.equal(logic.PASSIVE_MAX, 12);
|
|
500
|
+
const now = vm.now;
|
|
501
|
+
const aged = (id, age) => logic.messageRow(message(id, id, { receivedAt: now - age }));
|
|
502
|
+
const rows = [
|
|
503
|
+
...Array.from({ length: 6 }, (_, index) => aged(`old${index}`, 12_000)),
|
|
504
|
+
logic.gapRow(4, now - 11_000),
|
|
505
|
+
...Array.from({ length: 11 }, (_, index) => aged(`new${index}`, 1_000)),
|
|
506
|
+
];
|
|
507
|
+
const shown = logic.passiveRows(rows, now, 30_000);
|
|
508
|
+
assert.equal(shown.length, 12);
|
|
509
|
+
assert.deepEqual(
|
|
510
|
+
ids(shown),
|
|
511
|
+
["old5", ...Array.from({ length: 11 }, (_, index) => `new${index}`)],
|
|
512
|
+
"the last twelve, without the mark of messages left out",
|
|
513
|
+
);
|
|
514
|
+
assert.ok(shown.every((row, index) => row === rows.filter((each) => each.gap === 0).slice(-12)[index]));
|
|
515
|
+
|
|
516
|
+
// Expired messages leave; they are still counted among the last twelve.
|
|
517
|
+
const mixed = [aged("a", 31_000), aged("b", 30_000), aged("c", 29_999), aged("d", 5_000)];
|
|
518
|
+
assert.deepEqual(ids(logic.passiveRows(mixed, now, 30_000)), ["c", "d"]);
|
|
519
|
+
const stale = [aged("recent", 1_000), ...Array.from({ length: 12 }, (_, index) => aged(`s${index}`, 40_000))];
|
|
520
|
+
assert.deepEqual(ids(logic.passiveRows(stale, now, 30_000)), [], "an older message does not come back");
|
|
521
|
+
assert.deepEqual(ids(logic.passiveRows([], now, 30_000)), []);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
test("a Passive row is opaque until two thirds, then has the opacity level of its age", () => {
|
|
525
|
+
const now = vm.now;
|
|
526
|
+
const cls = (age, lifetime = 30_000) => {
|
|
527
|
+
const row = logic.messageRow(message("m1", "hi", { receivedAt: now - age }));
|
|
528
|
+
return logic.passiveRows([row], now, lifetime)[0]?.cls;
|
|
529
|
+
};
|
|
530
|
+
assert.equal(cls(0), "row");
|
|
531
|
+
assert.equal(cls(20_000), "row", "two thirds: still its own class");
|
|
532
|
+
assert.equal(cls(20_001), "row fade-1");
|
|
533
|
+
assert.equal(cls(25_000), "row fade-16");
|
|
534
|
+
assert.equal(cls(29_999), "row fade-32", "transparent when it leaves");
|
|
535
|
+
assert.equal(cls(30_000), undefined);
|
|
536
|
+
assert.equal(cls(3_000, 5_000), "row");
|
|
537
|
+
assert.equal(cls(4_000, 5_000), "row fade-13");
|
|
538
|
+
assert.equal(cls(100_000, 120_000), "row fade-16");
|
|
539
|
+
const replied = logic.messageRow(
|
|
540
|
+
message("m1", "hi", { receivedAt: now - 25_000, reply: { author: "Moss", text: "hey" } }),
|
|
541
|
+
);
|
|
542
|
+
assert.equal(logic.passiveRows([replied], now, 30_000)[0].cls, "row replied fade-16");
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
test("the next change of a Passive row: the start of the first fade, then one step", () => {
|
|
546
|
+
const now = vm.now;
|
|
547
|
+
const aged = (age) => logic.messageRow(message("m", "hi", { receivedAt: now - age }));
|
|
548
|
+
assert.equal(logic.nextFadeMs([], now, 30_000), null, "nothing shows: nothing to wait for");
|
|
549
|
+
assert.equal(logic.nextFadeMs([aged(1_000)], now, 30_000), 19_000);
|
|
550
|
+
assert.equal(logic.nextFadeMs([aged(1_000), aged(9_000)], now, 30_000), 11_000, "the oldest decides");
|
|
551
|
+
assert.equal(logic.nextFadeMs([aged(19_600)], now, 30_000), 400);
|
|
552
|
+
assert.equal(logic.nextFadeMs([aged(19_900)], now, 30_000), 313, "never less than a step");
|
|
553
|
+
assert.equal(logic.nextFadeMs([aged(25_000), aged(0)], now, 30_000), 313, "one step while a message fades");
|
|
554
|
+
assert.equal(logic.nextFadeMs([aged(4_900)], now, 5_000), 100, "never under the host's shortest timer");
|
|
555
|
+
assert.equal(logic.nextFadeMs([aged(0)], now, 120_000), 80_000);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
test("the lifetime is the menu row's, within 5 to 120 s", () => {
|
|
559
|
+
const lifetime = (value) => {
|
|
560
|
+
vm.setHost({ options: value === undefined ? {} : { "passive-lifetime": value } });
|
|
561
|
+
return logic.lifetimeMs();
|
|
562
|
+
};
|
|
563
|
+
assert.equal(lifetime(undefined), 30_000);
|
|
564
|
+
assert.equal(lifetime(5), 5_000);
|
|
565
|
+
assert.equal(lifetime(120), 120_000);
|
|
566
|
+
assert.equal(lifetime(1), 5_000);
|
|
567
|
+
assert.equal(lifetime(900), 120_000);
|
|
568
|
+
assert.equal(lifetime("60"), 30_000, "not a number");
|
|
569
|
+
assert.equal(lifetime(Number.NaN), 30_000);
|
|
570
|
+
vm.setHost({ options: { "passive-lifetime": 30 } });
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
test("one timer drives the fade and stops once nothing shows", () => {
|
|
574
|
+
fresh({
|
|
575
|
+
messages: [
|
|
576
|
+
message("m1", "older", { receivedAt: vm.now - 9_000 }),
|
|
577
|
+
message("m2", "newer", { receivedAt: vm.now - 1_000 }),
|
|
578
|
+
],
|
|
579
|
+
});
|
|
580
|
+
assert.equal(vm.timers.length, 0);
|
|
581
|
+
vm.setHost({ mode: "passive" });
|
|
582
|
+
assert.deepEqual(ids(state.shown), ["m1", "m2"]);
|
|
583
|
+
assert.deepEqual(
|
|
584
|
+
state.shown.map((row) => row.cls),
|
|
585
|
+
["row", "row"],
|
|
586
|
+
);
|
|
587
|
+
assert.equal(logic.composerClass(state), "composer hidden");
|
|
588
|
+
assert.equal(logic.emptyKey(state), "no-recent");
|
|
589
|
+
assert.equal(vm.timers.length, 1, "one timer");
|
|
590
|
+
assert.equal(vm.timers[0].repeat, false);
|
|
591
|
+
assert.equal(vm.timers[0].dueAt - vm.now, 11_000, "asleep until the first fade starts");
|
|
592
|
+
|
|
593
|
+
assert.equal(vm.advance(10_999), 0);
|
|
594
|
+
assert.equal(vm.advance(1), 1);
|
|
595
|
+
assert.equal(state.shown[0].cls, "row", "m1 is 20 s old");
|
|
596
|
+
assert.equal(vm.advance(313), 1, "one tick per step");
|
|
597
|
+
assert.deepEqual(
|
|
598
|
+
state.shown.map((row) => row.cls),
|
|
599
|
+
["row fade-1", "row"],
|
|
600
|
+
);
|
|
601
|
+
assert.equal(vm.timers.length, 1);
|
|
602
|
+
assert.equal(vm.advance(4_800), 15, "one render per opacity level");
|
|
603
|
+
assert.deepEqual(
|
|
604
|
+
state.shown.map((row) => row.cls),
|
|
605
|
+
["row fade-16", "row"],
|
|
606
|
+
"m1 half-way",
|
|
607
|
+
);
|
|
608
|
+
vm.advance(5_200);
|
|
609
|
+
assert.deepEqual(ids(state.shown), ["m2"], "m1 expired at 30 s");
|
|
610
|
+
assert.equal(state.rows.length, 2, "the history keeps it for Interactive mode");
|
|
611
|
+
assert.equal(vm.timers.length, 1, "m2 still shows");
|
|
612
|
+
|
|
613
|
+
// A message arriving meanwhile shows at once, with the same single timer.
|
|
614
|
+
delta({ messages: [message("m3", "late")] });
|
|
615
|
+
assert.deepEqual(ids(state.shown), ["m2", "m3"]);
|
|
616
|
+
assert.equal(vm.timers.length, 1);
|
|
617
|
+
|
|
618
|
+
// The last step lands within one step after the lifetime; the row is
|
|
619
|
+
// transparent meanwhile.
|
|
620
|
+
const ticks = vm.advance(30_000 + logic.fadeStepMs(30_000));
|
|
621
|
+
assert.deepEqual(ids(state.shown), [], "everything expired");
|
|
622
|
+
assert.ok(ticks > 0 && ticks <= 2 * logic.FADE_LEVELS + 6, `${ticks} ticks for two fades`);
|
|
623
|
+
assert.equal(vm.timers.length, 0, "nothing ticks any more");
|
|
624
|
+
assert.equal(vm.advance(600_000), 0);
|
|
625
|
+
|
|
626
|
+
// Interactive mode shows the whole history again, without a timer.
|
|
627
|
+
vm.setHost({ mode: "interactive" });
|
|
628
|
+
assert.deepEqual(ids(state.shown), ["m1", "m2", "m3"]);
|
|
629
|
+
assert.ok(state.shown.every((row) => row.cls === "row"));
|
|
630
|
+
assert.equal(vm.timers.length, 0);
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
test("the fade follows the lifetime row and the mode, and runs nothing in Interactive mode", () => {
|
|
634
|
+
fresh({ messages: [message("m1", "hi", { receivedAt: vm.now - 8_000 })] });
|
|
635
|
+
vm.setHost({ mode: "passive" });
|
|
636
|
+
assert.equal(state.shown[0].cls, "row");
|
|
637
|
+
vm.setHost({ options: { "passive-lifetime": 10 } });
|
|
638
|
+
assert.equal(state.shown[0].cls, "row fade-13", "8 s of a 10 s lifetime");
|
|
639
|
+
assert.equal(vm.timers.length, 1);
|
|
640
|
+
assert.equal(vm.timers[0].dueAt - vm.now, 104);
|
|
641
|
+
vm.setHost({ options: { "passive-lifetime": 5 } });
|
|
642
|
+
assert.deepEqual(ids(state.shown), [], "older than the new lifetime");
|
|
643
|
+
assert.equal(vm.timers.length, 0);
|
|
644
|
+
vm.setHost({ options: { "passive-lifetime": 120 } });
|
|
645
|
+
assert.equal(state.shown[0].cls, "row");
|
|
646
|
+
assert.equal(vm.timers.length, 1);
|
|
647
|
+
vm.setHost({ mode: "interactive" });
|
|
648
|
+
assert.equal(vm.timers.length, 0);
|
|
649
|
+
// Shown again after a pause, the rows are aged at once.
|
|
650
|
+
vm.setHost({ mode: "passive", options: { "passive-lifetime": 30 } });
|
|
651
|
+
vm.setHost({ visible: false });
|
|
652
|
+
vm.advance(15_000);
|
|
653
|
+
vm.setHost({ visible: true });
|
|
654
|
+
assert.equal(state.shown[0].cls, "row fade-10", "23 s old");
|
|
655
|
+
vm.advance(60_000);
|
|
656
|
+
assert.equal(vm.timers.length, 0);
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
test("the row tooltips follow the interface language", () => {
|
|
660
|
+
fresh({ messages: [message("m1", "hi", { author: "Moss" })] });
|
|
661
|
+
assert.equal(state.rows[0].tip, "Reply to Moss");
|
|
662
|
+
vm.setHost({ locale: "fr", messages: messages("fr") });
|
|
663
|
+
assert.equal(state.rows[0].tip, "Répondre à Moss");
|
|
664
|
+
assert.equal(state.shown[0].tip, "Répondre à Moss");
|
|
665
|
+
assert.equal(logic.unreadText(1), "1 nouveau message");
|
|
666
|
+
assert.equal(logic.unreadText(3), "3 nouveaux messages");
|
|
667
|
+
assert.equal(logic.gapText(1), "1 message non affiché");
|
|
668
|
+
assert.equal(logic.gapText(12), "12 messages non affichés");
|
|
669
|
+
vm.setHost({ locale: "en", messages: messages("en") });
|
|
670
|
+
assert.equal(state.rows[0].tip, "Reply to Moss");
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
test("a message is sent by Enter or the button, one at a time, and the draft goes once accepted", () => {
|
|
674
|
+
fresh({ messages: many(2) });
|
|
675
|
+
assert.equal(logic.canSubmit(state), false, "nothing typed");
|
|
676
|
+
logic.drafted(" ");
|
|
677
|
+
assert.equal(logic.canSubmit(state), false, "only spaces");
|
|
678
|
+
logic.drafted("good evening");
|
|
679
|
+
assert.equal(logic.canSubmit(state), true);
|
|
680
|
+
assert.equal(logic.sendMark(state), "");
|
|
681
|
+
|
|
682
|
+
logic.keyed("a");
|
|
683
|
+
assert.equal(state.sending, false, "only Enter sends");
|
|
684
|
+
logic.keyed("Enter");
|
|
685
|
+
assert.equal(state.sending, true);
|
|
686
|
+
assert.equal(logic.sendMark(state), "sending");
|
|
687
|
+
assert.equal(logic.sendMarkClass(state), "pending");
|
|
688
|
+
assert.equal(logic.canSubmit(state), false, "one send at a time");
|
|
689
|
+
assert.equal(vm.timers.length, 1, "the wait for OverCrow's answer");
|
|
690
|
+
const due = vm.timers[0].dueAt;
|
|
691
|
+
vm.advance(1_000);
|
|
692
|
+
logic.sendPressed();
|
|
693
|
+
logic.keyed("Enter");
|
|
694
|
+
assert.equal(vm.timers.length, 1);
|
|
695
|
+
assert.equal(vm.timers[0].dueAt, due, "a second gesture changes nothing");
|
|
696
|
+
assert.equal(vm.calls.length, 0, "the logic sends nothing itself: the form is OverCrow's");
|
|
697
|
+
assert.deepEqual(ids(), ["m1", "m2"], "no local echo in the history");
|
|
698
|
+
|
|
699
|
+
logic.submitted("accepted", undefined);
|
|
700
|
+
assert.equal(state.sending, false);
|
|
701
|
+
assert.equal(state.draft, "", "OverCrow emptied its field");
|
|
702
|
+
assert.equal(state.sendError, "");
|
|
703
|
+
assert.equal(logic.sendMark(state), "");
|
|
704
|
+
assert.equal(vm.timers.length, 0, "no timer left");
|
|
705
|
+
|
|
706
|
+
// The button, with a reply target: both go with the accepted message.
|
|
707
|
+
logic.replyTo("m1");
|
|
708
|
+
logic.drafted("and you?");
|
|
709
|
+
logic.sendPressed();
|
|
710
|
+
assert.equal(state.sending, true);
|
|
711
|
+
assert.equal(logic.replyTarget(state), "m1", "the target stays on the form while it is sent");
|
|
712
|
+
logic.submitted("accepted", undefined);
|
|
713
|
+
assert.equal(state.reply, null);
|
|
714
|
+
assert.equal(state.draft, "");
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
test("a refused message keeps its draft and its reply target, and says why", () => {
|
|
718
|
+
const refusal = (outcome, code) => {
|
|
719
|
+
fresh({ messages: many(2) });
|
|
720
|
+
logic.replyTo("m2");
|
|
721
|
+
logic.drafted("a question");
|
|
722
|
+
logic.sendPressed();
|
|
723
|
+
logic.submitted(outcome, code === undefined ? undefined : { code });
|
|
724
|
+
assert.equal(state.sending, false, code);
|
|
725
|
+
assert.equal(state.draft, "a question", "the draft is kept");
|
|
726
|
+
assert.equal(state.reply?.id, "m2", "and the reply target");
|
|
727
|
+
assert.equal(logic.sendMark(state), "not-sent");
|
|
728
|
+
assert.equal(logic.sendMarkClass(state), "unsent");
|
|
729
|
+
assert.equal(logic.canSubmit(state), true, "it can be sent again");
|
|
730
|
+
assert.equal(vm.timers.length, 0);
|
|
731
|
+
return vm.host.messages[state.sendError];
|
|
732
|
+
};
|
|
733
|
+
assert.equal(refusal("rejected", "busy"), "Too many messages. Wait a moment, then try again.");
|
|
734
|
+
assert.equal(refusal("rejected", "permission_denied"), "This chat does not accept your messages right now.");
|
|
735
|
+
for (const code of ["unavailable", "not_connected", "stale_context", "invalid_request", "timeout", "gesture_required"]) {
|
|
736
|
+
assert.equal(refusal("rejected", code), "Message was not accepted. Try again.", code);
|
|
737
|
+
}
|
|
738
|
+
assert.equal(refusal("rejected", undefined), "Message was not accepted. Try again.");
|
|
739
|
+
assert.equal(refusal("cancelled", undefined), "Message was not sent. Try again.");
|
|
740
|
+
|
|
741
|
+
// The next send clears the message, and an accepted one the draft.
|
|
742
|
+
logic.keyed("Enter");
|
|
743
|
+
assert.equal(state.sendError, "");
|
|
744
|
+
assert.equal(logic.sendMark(state), "sending");
|
|
745
|
+
logic.submitted("accepted", undefined);
|
|
746
|
+
assert.equal(state.draft, "");
|
|
747
|
+
assert.equal(logic.sendMark(state), "");
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
test("Enter on an empty field says nothing", () => {
|
|
751
|
+
fresh();
|
|
752
|
+
logic.keyed("Enter");
|
|
753
|
+
assert.equal(state.sending, false);
|
|
754
|
+
assert.equal(vm.timers.length, 0);
|
|
755
|
+
// OverCrow refuses the empty message; nothing was awaited.
|
|
756
|
+
logic.submitted("rejected", { code: "invalid_request" });
|
|
757
|
+
assert.equal(state.sendError, "");
|
|
758
|
+
assert.equal(logic.sendMark(state), "");
|
|
759
|
+
logic.drafted(" ");
|
|
760
|
+
logic.keyed("Enter");
|
|
761
|
+
logic.submitted("rejected", { code: "invalid_request" });
|
|
762
|
+
assert.equal(state.sendError, "", "nor on spaces");
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
test("nothing is sent while the chat cannot take a message", () => {
|
|
766
|
+
fresh({ joinState: "connecting", canSend: false });
|
|
767
|
+
logic.drafted("too early");
|
|
768
|
+
assert.equal(logic.canSubmit(state), false);
|
|
769
|
+
logic.keyed("Enter");
|
|
770
|
+
logic.sendPressed();
|
|
771
|
+
assert.equal(state.sending, false);
|
|
772
|
+
assert.equal(vm.timers.length, 0);
|
|
773
|
+
delta({ joinState: "joined", canSend: true });
|
|
774
|
+
assert.equal(logic.canSubmit(state), true, "the draft is still there once joined");
|
|
775
|
+
vm.setHost({ mode: "passive" });
|
|
776
|
+
assert.equal(logic.canSubmit(state), false, "never in Passive mode");
|
|
777
|
+
logic.keyed("Enter");
|
|
778
|
+
assert.equal(state.sending, false);
|
|
779
|
+
vm.setHost({ mode: "interactive" });
|
|
780
|
+
assert.equal(state.draft, "too early", "the draft outlives Passive mode");
|
|
781
|
+
assert.equal(logic.canSubmit(state), true);
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
test("should OverCrow give no answer, the composer comes back after 40 s", () => {
|
|
785
|
+
fresh();
|
|
786
|
+
logic.drafted("anyone?");
|
|
787
|
+
logic.sendPressed();
|
|
788
|
+
vm.advance(logic.ANSWER_MS - 1);
|
|
789
|
+
assert.equal(state.sending, true);
|
|
790
|
+
vm.advance(1);
|
|
791
|
+
assert.equal(state.sending, false);
|
|
792
|
+
assert.equal(state.sendError, "error-not-accepted");
|
|
793
|
+
assert.equal(state.draft, "anyone?");
|
|
794
|
+
assert.equal(logic.canSubmit(state), true);
|
|
795
|
+
assert.equal(vm.timers.length, 0);
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
test("joining a channel calls the host with the normalized login", async () => {
|
|
799
|
+
fresh({ channel: null, joinState: "idle", canSend: false, favorites: ["moss_and_fern"] });
|
|
800
|
+
const calls = vm.calls.length;
|
|
801
|
+
assert.equal(logic.canJoin(state), false);
|
|
802
|
+
logic.join();
|
|
803
|
+
logic.channelTyped("not a channel");
|
|
804
|
+
assert.equal(logic.canJoin(state), false);
|
|
805
|
+
logic.join();
|
|
806
|
+
assert.equal(vm.calls.length, calls, "an invalid name calls nothing");
|
|
807
|
+
|
|
808
|
+
logic.channelTyped(" #Juniper_Plays ");
|
|
809
|
+
assert.equal(logic.canJoin(state), true);
|
|
810
|
+
logic.join();
|
|
811
|
+
assert.equal(vm.calls.length, calls + 1);
|
|
812
|
+
assert.equal(vm.lastCall().service, "twitch.chat.join");
|
|
813
|
+
assert.deepEqual(vm.lastCall().params, { channel: "juniper_plays" });
|
|
814
|
+
vm.lastCall().resolve(null);
|
|
815
|
+
await flush();
|
|
816
|
+
assert.equal(
|
|
817
|
+
state.channelDraft,
|
|
818
|
+
" #Juniper_Plays ",
|
|
819
|
+
"the field keeps its text, so the logic keeps it too",
|
|
820
|
+
);
|
|
821
|
+
assert.equal(logic.canJoin(state), true);
|
|
822
|
+
assert.equal(state.notice, "");
|
|
823
|
+
assert.equal(state.channel, null, "the channel is the host's to announce");
|
|
824
|
+
// The host publishes the channel: the selector leaves, and the next one
|
|
825
|
+
// is a new, empty field.
|
|
826
|
+
const selector = state.selector;
|
|
827
|
+
vm.push(SERVICE, chat({ generation: 2, channel: "juniper_plays" }));
|
|
828
|
+
assert.equal(logic.selecting(state), false);
|
|
829
|
+
assert.equal(state.channelDraft, "");
|
|
830
|
+
assert.notEqual(state.selector, selector);
|
|
831
|
+
assert.deepEqual(logic.selectors(state), [state.selector]);
|
|
832
|
+
|
|
833
|
+
logic.joinFavorite("moss_and_fern");
|
|
834
|
+
assert.deepEqual(vm.lastCall("twitch.chat.join").params, { channel: "moss_and_fern" });
|
|
835
|
+
vm.lastCall().resolve(null);
|
|
836
|
+
await flush();
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
test("changing channel keeps the chat until the host joined another", async () => {
|
|
840
|
+
fresh({ messages: many(2) });
|
|
841
|
+
logic.toggleSelector();
|
|
842
|
+
assert.equal(logic.selecting(state), true);
|
|
843
|
+
assert.equal(logic.phase(state), "chat", "the chat stays under the selector");
|
|
844
|
+
logic.channelTyped("river_otter");
|
|
845
|
+
logic.toggleSelector();
|
|
846
|
+
assert.equal(logic.selecting(state), false);
|
|
847
|
+
assert.equal(state.channelDraft, "", "closing the selector forgets what was typed");
|
|
848
|
+
logic.toggleSelector();
|
|
849
|
+
vm.setHost({ mode: "passive" });
|
|
850
|
+
assert.equal(logic.selecting(state), false, "no selector in Passive mode");
|
|
851
|
+
vm.setHost({ mode: "interactive" });
|
|
852
|
+
logic.channelTyped("river_otter");
|
|
853
|
+
logic.join();
|
|
854
|
+
vm.lastCall("twitch.chat.join").resolve(null);
|
|
855
|
+
await flush();
|
|
856
|
+
assert.equal(state.selecting, false, "the selector closes once the host took the channel");
|
|
857
|
+
assert.deepEqual(ids(), ["m1", "m2"]);
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
test("each refusal of a channel or favorite change has its message", async () => {
|
|
861
|
+
const refused = async (act, service, code) => {
|
|
862
|
+
fresh({ favorites: ["moss_and_fern"] });
|
|
863
|
+
logic.toggleSelector();
|
|
864
|
+
logic.channelTyped("river_otter");
|
|
865
|
+
act();
|
|
866
|
+
assert.equal(vm.lastCall().service, service);
|
|
867
|
+
vm.lastCall().reject(code);
|
|
868
|
+
await flush();
|
|
869
|
+
return state.notice === "" ? "" : vm.host.messages[state.notice];
|
|
870
|
+
};
|
|
871
|
+
for (const [act, service] of [
|
|
872
|
+
[logic.join, "twitch.chat.join"],
|
|
873
|
+
[() => logic.joinFavorite("moss_and_fern"), "twitch.chat.join"],
|
|
874
|
+
[logic.toggleFavorite, "twitch.chat.favorite"],
|
|
875
|
+
[logic.leave, "twitch.chat.leave"],
|
|
876
|
+
]) {
|
|
877
|
+
assert.equal(await refused(act, service, "busy"), "Twitch is busy. Try again.", service);
|
|
878
|
+
assert.equal(await refused(act, service, "unavailable"), "Could not save Twitch widget settings.", service);
|
|
879
|
+
assert.equal(await refused(act, service, "quota_exceeded"), "Favorite channel limit reached.", service);
|
|
880
|
+
assert.equal(await refused(act, service, "timeout"), "Could not save Twitch widget settings.", service);
|
|
881
|
+
// The account or the channel changed meanwhile: the state says so.
|
|
882
|
+
assert.equal(await refused(act, service, "not_connected"), "", service);
|
|
883
|
+
assert.equal(await refused(act, service, "stale_context"), "", service);
|
|
884
|
+
}
|
|
885
|
+
assert.equal(logic.noticeKey(null), "error-settings");
|
|
886
|
+
|
|
887
|
+
// A refused join keeps the selector and what was typed; the next
|
|
888
|
+
// gesture clears the message.
|
|
889
|
+
await refused(logic.join, "twitch.chat.join", "busy");
|
|
890
|
+
assert.equal(state.selecting, true);
|
|
891
|
+
assert.equal(state.channelDraft, "river_otter");
|
|
892
|
+
logic.join();
|
|
893
|
+
assert.equal(state.notice, "");
|
|
894
|
+
vm.lastCall().resolve(null);
|
|
895
|
+
await flush();
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
test("the star adds the channel to the favorites or removes it, up to the host's limit", async () => {
|
|
899
|
+
fresh();
|
|
900
|
+
assert.equal(logic.isFavorite(state), false);
|
|
901
|
+
assert.equal(logic.starClass(state), "star");
|
|
902
|
+
assert.equal(logic.favoriteLabel(state), "Favorite #juniper_plays");
|
|
903
|
+
assert.equal(logic.favoriteTip(state), "favorite-add");
|
|
904
|
+
logic.toggleFavorite();
|
|
905
|
+
assert.equal(vm.lastCall().service, "twitch.chat.favorite");
|
|
906
|
+
assert.deepEqual(vm.lastCall().params, { channel: "juniper_plays", favorite: true });
|
|
907
|
+
vm.lastCall().resolve(null);
|
|
908
|
+
await flush();
|
|
909
|
+
assert.equal(logic.isFavorite(state), false, "until the host says so");
|
|
910
|
+
|
|
911
|
+
delta({ favorites: ["moss_and_fern", "juniper_plays"] });
|
|
912
|
+
assert.equal(logic.isFavorite(state), true);
|
|
913
|
+
logic.toggleFavorite();
|
|
914
|
+
assert.deepEqual(vm.lastCall().params, { channel: "juniper_plays", favorite: false });
|
|
915
|
+
vm.lastCall().resolve(null);
|
|
916
|
+
await flush();
|
|
917
|
+
|
|
918
|
+
// Twenty favorites: no room for another, but one of them can leave.
|
|
919
|
+
const twenty = Array.from({ length: 20 }, (_, index) => `channel_${index}`);
|
|
920
|
+
delta({ favorites: twenty });
|
|
921
|
+
assert.equal(logic.favoriteFull(state), true);
|
|
922
|
+
assert.equal(logic.favoriteTip(state), "favorite-limit");
|
|
923
|
+
const calls = vm.calls.length;
|
|
924
|
+
logic.toggleFavorite();
|
|
925
|
+
assert.equal(vm.calls.length, calls, "nothing is asked");
|
|
926
|
+
delta({ favorites: [...twenty.slice(1), "juniper_plays"] });
|
|
927
|
+
assert.equal(logic.favoriteFull(state), false);
|
|
928
|
+
logic.toggleFavorite();
|
|
929
|
+
assert.deepEqual(vm.lastCall().params, { channel: "juniper_plays", favorite: false });
|
|
930
|
+
vm.lastCall().resolve(null);
|
|
931
|
+
await flush();
|
|
932
|
+
|
|
933
|
+
// Without a channel there is nothing to mark.
|
|
934
|
+
fresh({ channel: null, joinState: "idle", canSend: false });
|
|
935
|
+
const before = vm.calls.length;
|
|
936
|
+
logic.toggleFavorite();
|
|
937
|
+
assert.equal(vm.calls.length, before);
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
test("leaving asks the host to forget the channel", async () => {
|
|
941
|
+
fresh({ messages: many(2) });
|
|
942
|
+
logic.leave();
|
|
943
|
+
assert.equal(vm.lastCall().service, "twitch.chat.leave");
|
|
944
|
+
assert.deepEqual(vm.lastCall().params, {});
|
|
945
|
+
vm.lastCall().resolve(null);
|
|
946
|
+
await flush();
|
|
947
|
+
assert.equal(state.notice, "");
|
|
948
|
+
assert.equal(state.channel, "juniper_plays", "until the host says so");
|
|
949
|
+
generation += 1;
|
|
950
|
+
delta({ channel: null, joinState: "idle", canSend: false, reset: true });
|
|
951
|
+
assert.equal(logic.phase(state), "no-channel");
|
|
952
|
+
assert.deepEqual(ids(), []);
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
test("a failed source says so and the widget asks again 5 s later", () => {
|
|
956
|
+
fresh({ messages: many(2) });
|
|
957
|
+
logic.drafted("kept");
|
|
958
|
+
assert.equal(vm.fail(SERVICE, "unavailable"), 1);
|
|
959
|
+
assert.equal(logic.phase(state), "unavailable");
|
|
960
|
+
assert.equal(logic.heading(state), "Twitch");
|
|
961
|
+
assert.equal(logic.statusKey(state), "inactive");
|
|
962
|
+
assert.equal(logic.composing(state), false);
|
|
963
|
+
assert.equal(logic.selecting(state), false);
|
|
964
|
+
assert.equal(live().length, 0);
|
|
965
|
+
assert.equal(vm.timers.length, 1);
|
|
966
|
+
vm.advance(logic.RETRY_MS - 1);
|
|
967
|
+
assert.equal(live().length, 0);
|
|
968
|
+
vm.advance(1);
|
|
969
|
+
assert.equal(live().length, 1, "one new subscription");
|
|
970
|
+
assert.equal(logic.phase(state), "unavailable", "until the host answers");
|
|
971
|
+
assert.equal(vm.timers.length, 0);
|
|
972
|
+
delta({ reset: true, messages: many(3) });
|
|
973
|
+
assert.equal(logic.phase(state), "chat");
|
|
974
|
+
assert.deepEqual(ids(), ["m1", "m2", "m3"]);
|
|
975
|
+
assert.equal(state.draft, "kept", "the same generation: the form and its draft stayed");
|
|
976
|
+
assert.equal(logic.composing(state), true);
|
|
977
|
+
|
|
978
|
+
// It fails again: one retry at a time.
|
|
979
|
+
vm.fail(SERVICE, "unavailable");
|
|
980
|
+
assert.equal(vm.timers.length, 1);
|
|
981
|
+
vm.advance(logic.RETRY_MS);
|
|
982
|
+
assert.equal(live().length, 1);
|
|
983
|
+
delta({ reset: true });
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
/** Every message key the view and the logic can ask for. */
|
|
987
|
+
function usedKeys() {
|
|
988
|
+
const keys = new Set();
|
|
989
|
+
const literal = /\bt\(\s*"([^"]+)"/g;
|
|
990
|
+
for (const source of [read("view.ocml"), read("logic.ts")]) {
|
|
991
|
+
for (const match of source.matchAll(literal)) {
|
|
992
|
+
keys.add(match[1]);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
// Keys the logic computes: every status, tooltip, hint, mark and error.
|
|
996
|
+
const view = (changes) => ({ ...state, ...changes });
|
|
997
|
+
for (const joinState of ["idle", "connecting", "joined", "reconnecting", "failed"]) {
|
|
998
|
+
for (const failure of [null, "channel_unavailable", "connection", "provider", "limit"]) {
|
|
999
|
+
keys.add(logic.statusKey(view({ joinState, failure })));
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
keys.add(logic.statusKey(view({ account: "signed_out" })));
|
|
1003
|
+
const twenty = Array.from({ length: 20 }, (_, index) => `channel_${index}`);
|
|
1004
|
+
for (const favorites of [[], ["juniper_plays"], twenty]) {
|
|
1005
|
+
keys.add(logic.favoriteTip(view({ channel: "juniper_plays", favorites })));
|
|
1006
|
+
}
|
|
1007
|
+
for (const interactive of [true, false]) {
|
|
1008
|
+
keys.add(logic.emptyKey(view({ interactive })));
|
|
1009
|
+
}
|
|
1010
|
+
for (const canSend of [true, false]) {
|
|
1011
|
+
keys.add(logic.composerHint(view({ canSend })));
|
|
1012
|
+
}
|
|
1013
|
+
keys.add(logic.sendMark(view({ sending: true })));
|
|
1014
|
+
keys.add(logic.sendMark(view({ sending: false, sendError: "error-not-accepted" })));
|
|
1015
|
+
const codes = [
|
|
1016
|
+
null,
|
|
1017
|
+
"invalid_request",
|
|
1018
|
+
"permission_denied",
|
|
1019
|
+
"gesture_required",
|
|
1020
|
+
"not_connected",
|
|
1021
|
+
"stale_context",
|
|
1022
|
+
"unavailable",
|
|
1023
|
+
"busy",
|
|
1024
|
+
"cancelled",
|
|
1025
|
+
"quota_exceeded",
|
|
1026
|
+
"unsupported",
|
|
1027
|
+
"timeout",
|
|
1028
|
+
];
|
|
1029
|
+
for (const code of codes) {
|
|
1030
|
+
keys.add(logic.sendErrorKey(code));
|
|
1031
|
+
keys.add(logic.noticeKey(code));
|
|
1032
|
+
}
|
|
1033
|
+
keys.delete("");
|
|
1034
|
+
return [...keys].sort();
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
test("every message the view or the logic uses exists in English and French", () => {
|
|
1038
|
+
fresh();
|
|
1039
|
+
const [en, fr] = [messages("en"), messages("fr")];
|
|
1040
|
+
assert.deepEqual(Object.keys(en).sort(), Object.keys(fr).sort(), "the same keys in both files");
|
|
1041
|
+
const used = usedKeys();
|
|
1042
|
+
assert.ok(used.length >= 45, `${used.length} keys found`);
|
|
1043
|
+
for (const key of used) {
|
|
1044
|
+
assert.equal(typeof en[key], "string", `en: ${key}`);
|
|
1045
|
+
assert.equal(typeof fr[key], "string", `fr: ${key}`);
|
|
1046
|
+
assert.ok(en[key].trim() !== "" && fr[key].trim() !== "", key);
|
|
1047
|
+
}
|
|
1048
|
+
// A message with a placeholder has the same one in both languages.
|
|
1049
|
+
const placeholders = (text) => [...text.matchAll(/\{(\w+)\}/g)].map((match) => match[1]).sort();
|
|
1050
|
+
for (const key of Object.keys(en)) {
|
|
1051
|
+
assert.deepEqual(placeholders(en[key]), placeholders(fr[key]), key);
|
|
1052
|
+
}
|
|
1053
|
+
});
|