E2E coverage audit
What the browser automation in e2e/ walks through, set against every
production route and data-testid hook, and what to do about the gaps.
Refreshed 2026-10-22 and re-counted at the merge of main on 2026-10-23 (the
first snapshot was PR #117, 2026-10-17, hand-written: git show c955cd1e:docs/e2e-audit.md). How the suite works:
Testing locally.
The numbers below the line are generated: python3 scripts/e2e-audit.py --write refreshes them in place. Re-run it before acting on a count.
Findings
- 33 % of the keyless tests never run in CI. The exact-spec job in
.github/workflows/smoke.ymlpasses a fixed list of 18 files (specs: specs/exact.spec.ts …); the smoke job passes the folderspecs/smoke. A spec file not on the list runs only when someone runsscripts/e2e.shby hand. Missing today, 15 files and 79 tests, none of them needing a key:ai-off,avatar-colour,course-report,dev-payments,editor-errors,home,hosted-ai,lessons,profile-autosave,prompt-limits,review-ai-changes,review-and-revise,revise,share,subscription. That is most of the course editor (lessons, revise, review), the home page, sharing, profile autosave and the paid plans.createjoined the list in #422,docsandcourse-copysince, one file at a time: new spec files keep landing outside it.CLAUDE.mdsays “the exact and smoke e2e specs run on every PR”, which reads as all of them. - The learner’s paths are covered, in CI. The smoke specs play every
mode, question type, Sprint exercise kind and game on the test course,
right and wrong, as a fresh account, and check
/api/progressafter; Spanish lesson 1 and French get the same in each mode. - Nothing with a model in the loop runs unattended. The 14 Jev journeys,
2 judged specs, 4 personas (
scripts/e2e-explore.sh) and the course playthroughs needTYPESAFE_API_KEYand no workflow has aschedule.plagiarism.ymlalready reads that secret. - Routes. Of 137 production routes, 3 are reached by no flow:
POST /login/code/resend,POST /login/code/cancel,POST /profile/two-factor/code. The script lists nine more that are reached in ways a grep cannot see:GET /theme.cssandGET /courses/themes.css(stylesheet links the pages load;custom-themes.spec.tschecks the first),POST /profile/google/confirm(a hidden form submitted byprofile-delete-google, ingoogle-signin.spec.ts),GET /profile/export/download(the link in the export e-mail, insmoke/export.spec.ts),/courses/shared/{token}(from the invitation e-mail, inshare.spec.ts, local only),POST /api/courses/{id}/lessons(the editor’s autosave, the URL built by concatenation instatic/courses.js),/pictures/{*path}(a course’s SVG pictures),/play/conjugation/play(drill-setup-start),/auth/google/callback(the dev Google stand-in redirects there). - Testids. 69 % of the hooks are referenced. The learner-facing gaps
are the home page’s submenu links (
home-play-*,home-ai-*,home-guide-next), the settings of Numbers by ear, Listening and Date Dash (rate, range, replay, reveal), the tutor’s provider picker and no-key state, and the Matching leaderboard. Most of the rest are-formwrappers, dialogs’ close buttons and the dev tools.
Flows by area
The per-test list is below the line. In short:
| Area | Specs (CI unless marked) |
|---|---|
| Sign-up, sign-in, e-mail, 2FA | smoke/verify-email, google-signin, mail, two-factor, authenticator, terms, i18n |
| Learning, every mode and game | smoke/happy-paths, smoke/complete-test-course, smoke/test-course-games, smoke/wrong-answers, smoke/wrong-answers-games, smoke/start-level, smoke/french, smoke/daily-goal, smoke/motivator*, modals, lesson-tables |
| Home page | home (local only) |
| Profile | profile-autosave, avatar-colour, ai-off (all local only); custom-themes, profile-nav; smoke/privacy, smoke/export (export, delete) |
| Plans and payments | subscription, dev-payments, hosted-ai (all local only) |
| Generating a course | exact, pathways, dev-ai, create; prompt-limits (local only) |
| Editing a course | editor-autosave, forms, course-copy; lessons, editor-errors, course-report, revise, review-ai-changes, review-and-revise (local only) |
| Sharing | share (local only) |
| Docs, self-hosting, layout | smoke/self-host, smoke/phone-width, docs |
| Jev, with a key | journeys (14 goals from e2e/journeys.ts), judged (2), explore (a persona from e2e/personas/), record (drafts a spec) |
Journeys (e2e/journeys.ts): open a lesson, find the generate form, start
Sprint on a phone (impatient), find “ser vs estar” (classroom), visit every
nav section, share a course, add a lesson with AI, leave an AI lesson out in
the review, edit a lesson as a form, add a lesson to a unit by hand, name a
new course, turn off AI features, start at A2, change the level.
Personas (e2e/personas/): accurate (reads everything, Study mode,
patience 20), classroom (wants one topic now, 10), impatient (a
teenager on a phone in Sprint, 6), testing-bot (sees only markup, 12).
Recommendations
- Run every keyless spec in CI. Pass
specs(the whole folder) to the exact-spec job instead of a list; the Jev specs already skip themselves without a key (--grep-invert @jevstays). Expect some of the 79 to fail at first: they have not run since they were written. Fix or delete each, then the list can never go stale again. - Make the docs say what CI runs. One line in
CLAUDE.mdanddocs/testing.md: CI runs every file undere2e/specs/except the Jev ones (after 1), or, until then, only the files listed insmoke.yml. - Cover the three unreached routes with exact specs: resend and
cancel on
/login/code, and the profile’s two-factor code change. - Spec the learner surfaces with no testid referenced: home submenu links, game settings (rate, range, replay), tutor without a key, the Matching leaderboard.
- Schedule the model-driven checks: a nightly workflow that runs
scripts/e2e-jev.shand one persona whenTYPESAFE_API_KEYis set, reporting, not gating.
Generated numbers
| What | Count |
|---|---|
| Spec files | 91 (71 run in CI) |
| Tests | 387: 287 run in CI, 89 keyless but local only, 11 need a Jev key |
Production routes (src/routes/mod.rs, no /dev) | 182: 103 named by an e2e file, 66 more reached through a page, 13 not reached |
data-testid hooks (templates, static JS) | 1630, 1284 referenced (78 %) |
| Jev journeys / personas | 14 / 4 |
Spec files
| File | Tests | CI | Needs a key |
|---|---|---|---|
specs/accessibility.spec.ts | 7 | yes | |
specs/ai-off.spec.ts | 2 | no | |
specs/authenticator.spec.ts | 4 | yes | |
specs/avatar-colour.spec.ts | 3 | no | |
specs/consent.spec.ts | 2 | yes | |
specs/content-check.spec.ts | 3 | yes | |
specs/content-reports.spec.ts | 1 | yes | |
specs/content-review.spec.ts | 2 | yes | |
specs/course-card-stats.spec.ts | 2 | yes | |
specs/course-copy.spec.ts | 5 | yes | |
specs/course-report.spec.ts | 3 | no | |
specs/course-table.spec.ts | 6 | yes | |
specs/course-translate.spec.ts | 2 | yes | |
specs/create.spec.ts | 11 | yes | |
specs/custom-themes.spec.ts | 6 | yes | |
specs/dev-ai.spec.ts | 11 | yes | |
specs/dev-payments.spec.ts | 1 | no | |
specs/docs.spec.ts | 8 | yes | |
specs/editor-autosave.spec.ts | 5 | yes | |
specs/editor-errors.spec.ts | 4 | no | |
specs/editor-fixes.spec.ts | 3 | no | |
specs/email-owner.spec.ts | 2 | yes | |
specs/exact.spec.ts | 29 | yes | |
specs/explore.spec.ts | 3 | no | yes |
specs/forms.spec.ts | 4 | yes | |
specs/google-signin.spec.ts | 11 | yes | |
specs/home.spec.ts | 4 | no | |
specs/hosted-ai.spec.ts | 6 | no | |
specs/i18n.spec.ts | 2 | yes | |
specs/journeys.spec.ts | 2 | no | yes |
specs/judged.spec.ts | 3 | no | yes |
specs/lesson-conversations.spec.ts | 1 | yes | |
specs/lesson-duplicate.spec.ts | 1 | yes | |
specs/lesson-editor.spec.ts | 10 | yes | |
specs/lesson-header.spec.ts | 1 | yes | |
specs/lesson-tables.spec.ts | 2 | yes | |
specs/lessons.spec.ts | 17 | no | |
specs/mail.spec.ts | 7 | yes | |
specs/mistakes.spec.ts | 1 | yes | |
specs/modals.spec.ts | 5 | yes | |
specs/moderation.spec.ts | 4 | yes | |
specs/owner-resets.spec.ts | 3 | yes | |
specs/pathways.spec.ts | 7 | yes | |
specs/privacy-requests.spec.ts | 2 | yes | |
specs/profile-autosave.spec.ts | 8 | no | |
specs/profile-nav.spec.ts | 1 | yes | |
specs/prompt-limits.spec.ts | 5 | no | |
specs/record.spec.ts | 3 | no | yes |
specs/release.spec.ts | 2 | yes | |
specs/review-ai-changes.spec.ts | 14 | no | |
specs/review-and-revise.spec.ts | 4 | no | |
specs/revise.spec.ts | 10 | no | |
specs/share-edges.spec.ts | 9 | yes | |
specs/share-sign-in.spec.ts | 3 | yes | |
specs/share.spec.ts | 2 | no | |
specs/smoke/complete-test-course.spec.ts | 3 | yes | |
specs/smoke/daily-goal.spec.ts | 1 | yes | |
specs/smoke/drill-first-set.spec.ts | 2 | yes | |
specs/smoke/export.spec.ts | 2 | yes | |
specs/smoke/french.spec.ts | 6 | yes | |
specs/smoke/happy-paths.spec.ts | 10 | yes | |
specs/smoke/mini.spec.ts | 6 | yes | |
specs/smoke/motivator-reward.spec.ts | 1 | yes | |
specs/smoke/motivator-skc.spec.ts | 1 | yes | |
specs/smoke/motivator-social.spec.ts | 1 | yes | |
specs/smoke/motivator-xp.spec.ts | 1 | yes | |
specs/smoke/motivator.spec.ts | 1 | yes | |
specs/smoke/password-reset.spec.ts | 2 | yes | |
specs/smoke/phone-width.spec.ts | 2 | yes | |
specs/smoke/placement.spec.ts | 2 | yes | |
specs/smoke/practice-empty-check.spec.ts | 1 | yes | |
specs/smoke/practice-keys.spec.ts | 2 | yes | |
specs/smoke/practice-nothing-yet.spec.ts | 2 | yes | |
specs/smoke/practice-result-order.spec.ts | 1 | yes | |
specs/smoke/practice-summary.spec.ts | 2 | yes | |
specs/smoke/privacy.spec.ts | 3 | yes | |
specs/smoke/self-host.spec.ts | 3 | yes | |
specs/smoke/skc-shop.spec.ts | 2 | yes | |
specs/smoke/sprint-check-error.spec.ts | 2 | yes | |
specs/smoke/sprint-ladder.spec.ts | 2 | yes | |
specs/smoke/sprint-sort.spec.ts | 2 | yes | |
specs/smoke/start-level.spec.ts | 4 | yes | |
specs/smoke/test-course-games.spec.ts | 7 | yes | |
specs/smoke/verify-email.spec.ts | 3 | yes | |
specs/smoke/wrong-answers-games.spec.ts | 5 | yes | |
specs/smoke/wrong-answers.spec.ts | 7 | yes | |
specs/subscription.spec.ts | 3 | no | |
specs/term-flag.spec.ts | 3 | yes | |
specs/terms.spec.ts | 5 | yes | |
specs/two-factor.spec.ts | 4 | yes | |
specs/word-list.spec.ts | 3 | yes |
Routes reached only through a page
No e2e file names them; a spec drives the form, link (its testid) or script that calls them.
POST /api/practice/finish:static/quiz.jsPOST /api/listening/check:static/listening.jsPOST /api/listening/finish:static/listening.jsPOST /courses/{id}/file/delete:course-edit-file-delete-formPOST /courses/{id}/cancel:course-ai-modal-cancel-form,course-edit-cancel-form,courses-cancelPOST /courses/{id}/undo:course-undo-formPOST /courses/{id}/redo:course-undo-formPOST /courses/{id}/report:content-report-formPOST /courses/{id}/share/remove:course-share-remove-formGET /cookies:footer-cookiesGET POST /legal/copyright:copyright-form,terms-copyright-linkGET POST /terms/accept:terms-accept-checkPOST /terms/dismiss:terms-notice-dismissGET POST /password/change:password-change-formPOST /api/sprint/check:static/sprint.jsPOST /api/sprint/finish:static/sprint.jsPOST /api/guide/step:static/guide.jsPOST /api/guide/restart:static/guide.jsPOST /api/games/matching/start:static/matching.jsPOST /api/games/matching/finish:static/matching.jsPOST /api/games/conjugation/deal:static/conjugation.jsPOST /api/games/conjugation/check:static/conjugation.jsPOST /api/games/conjugation/finish:static/conjugation.jsPOST /api/games/numbers/finish:static/numbers.jsPOST /api/games/dates/finish:static/dates.jsPOST /api/vocabulary/review:static/vocab.jsGET POST /login/code:login-code-inputPOST /login/code/resend:login-code-cancelGET /login/code/status:login-codePOST /login/code/cancel:login-code-cancelPOST /waitlist:waitlist-form{{ suffix }}POST /logout:account-deletion-logout,nav-logout-form,password-change-logout,terms-gate-logoutPOST /account/deletion/cancel:account-deletion-cancelPOST /profile/password:profile-password-formPOST /profile/sessions/end-others:profile-sign-out-others-formPOST /profile/google/link:profile-google-link-formPOST /profile/google/unlink:profile-google-unlink-formPOST /profile/api-keys/remove:profile-api-key-remove-formPOST /profile/ai:static/profile.js,consent-ai-formPOST /profile/own-keys:static/profile.js,profile-own-keys-formPOST /profile/export:profile-export-formGET POST /profile/export/download:profile-export-download-formPOST /profile/delete:profile-delete-formPOST /profile/verify-email/resend:verify-email-resendPOST /profile/phone/send:phone-locked-dialogPOST /profile/phone/verify:phone-verify-formPOST /profile/two-factor/totp/confirm:two-factor-totp-codePOST /superuser/release/invite:release-invitePOST /superuser/invites:superuser-invite-formPOST /superuser/invites/revoke:superuser-invite-revokePOST /superuser/terms-announce:superuser-terms-formPOST /superuser/reports/forward:superuser-report-forwardPOST /superuser/reports/resolve:superuser-report-resolvePOST /superuser/courses/put-back:superuser-put-back-formPOST /superuser/courses/override:superuser-override-formPOST /superuser/courses/clear-override:superuser-clear-override-formPOST /superuser/courses/delete:superuser-course-delete-formPOST /superuser/courses/email-owner:superuser-email-owner-formPOST /superuser/unsuspend:superuser-account-unsuspendPOST /superuser/privacy/export:superuser-privacy-export-formPOST /superuser/privacy/forget:superuser-privacy-forget-formPOST /superuser/unblock-ip:superuser-unblockPOST /api/lesson-check:static/lesson-check.jsPOST /billing/cancel:cancel-formPOST /billing/checkout:plans-formPOST /billing/portal:profile-billing-portal-form
Routes no e2e flow reaches
Not named by an e2e file, and no driven form, link or script calls them.
GET /courses/themes.cssPOST /api/courses/{id}/preview-answerGET /pictures/{*path}GET /theme.cssGET /badges/{file}GET /waitlist/confirmGET /account/deletionGET /auth/google/callbackPOST /profile/google/confirm: called fromtemplates/profile.htmlPOST /profile/two-factor/code: called fromtemplates/profile.htmlPOST /superuser/release/switch: called fromtemplates/superuser_release.htmlPOST /superuser/release/invite-next: called fromtemplates/superuser_release.htmlGET /manifest.webmanifest: called fromtemplates/base.html,templates/sprint.html,templates/sprint_play.html,templates/tutor.html
Testids per template
| File | Hooks | Referenced | Not referenced |
|---|---|---|---|
templates/dev_ai.html | 42 | 14 | dev-ai-base, dev-ai-clear, dev-ai-clear-inject, dev-ai-detail, dev-ai-fixture, dev-ai-fixture-edited, dev-ai-fixture-open, dev-ai-fixtures, dev-ai-fixtures-info, dev-ai-fixtures-info-dialog, dev-ai-hold-info, dev-ai-hold-info-dialog (+16) |
templates/home.html | 51 | 25 | home-ai-api-keys, home-ai-lessons, home-ai-open, home-ai-recommended, home-guide-next, home-guide-open, home-play-dates, home-play-drill, home-play-flashcards, home-play-matching, home-play-numbers, home-play-open (+14) |
templates/superuser_release.html | 24 | 6 | release-back, release-counts, release-empty, release-error, release-inbox, release-info, release-info-dialog, release-invite-next, release-invite-next-form, release-invite-next-n, release-list-card, release-next (+6) |
templates/dev_payments_state.html | 21 | 4 | dev-payments-cancel-at-end, dev-payments-customer, dev-payments-customers, dev-payments-mark-paid, dev-payments-pay-next, dev-payments-payment, dev-payments-payment-status, dev-payments-payments, dev-payments-refund, dev-payments-resume, dev-payments-session, dev-payments-session-open (+5) |
templates/numbers.html | 23 | 7 | numbers-again, numbers-card, numbers-form, numbers-max, numbers-min, numbers-novoice, numbers-range, numbers-rate, numbers-replay, numbers-reveal, numbers-settings, numbers-setup (+4) |
templates/dev_box.html | 26 | 12 | dev-box-info, dev-box-info-dialog, dev-box-off, dev-inbox-clear, dev-inbox-filter, dev-inbox-off, dev-inbox-to, dev-mail-providers, dev-mail-send-form, dev-phone-clear, dev-phone-off, dev-phone-on (+2) |
templates/profile.html | 185 | 172 | email-no-mail-close, email-no-mail-dialog, email-unverified-close, two-factor-confirm-code, two-factor-confirm-form, two-factor-confirm-google, two-factor-confirm-google-done, two-factor-confirm-method, two-factor-confirm-send, two-factor-confirm-sent, two-factor-confirm-with-google, two-factor-totp-error (+1) |
templates/listening_play.html | 19 | 6 | listen, listen-empty, listen-form, listen-novoice, listen-rate, listen-replay, listen-reveal, listen-setup, listen-slow, listen-status, listen-stop, listen-unsupported (+1) |
templates/dev_payments.html | 11 | 0 | dev-payments-api-base, dev-payments-clear, dev-payments-info, dev-payments-info-dialog, dev-payments-log, dev-payments-mode, dev-payments-notice, dev-payments-off, dev-payments-reset, dev-payments-state, dev-payments-webhook-url |
templates/dev_inbox_message.html | 11 | 3 | dev-inbox-back, dev-inbox-headers, dev-inbox-link, dev-inbox-message, dev-inbox-message-archive, dev-inbox-message-delete, dev-inbox-raw, dev-inbox-text |
templates/dev_jobs.html | 10 | 2 | dev-jobs-attempts, dev-jobs-empty, dev-jobs-error, dev-jobs-queue-fail, dev-jobs-row, dev-jobs-run-now, dev-jobs-state, dev-jobs-table |
templates/dev_mail_message.html | 12 | 4 | dev-mail-back, dev-mail-detail, dev-mail-error, dev-mail-headers, dev-mail-html, dev-mail-message-archive, dev-mail-message-delete, dev-mail-text |
templates/dev_text_message.html | 15 | 7 | dev-text-back, dev-text-compose-error, dev-text-compose-form, dev-text-error, dev-text-headers, dev-text-thread, {{ prefix() }}-message-archive, {{ prefix() }}-message-delete |
templates/lesson.html | 40 | 32 | lesson-check-form, lesson-head, lesson-mastery, lesson-pass, lesson-practise, lesson-prerequisite, lesson-read-form, lesson-test |
templates/dates.html | 16 | 9 | dates-again, dates-card, dates-ear, dates-hud, dates-replay, dates-score, dates-setup |
templates/dev_payments_entry.html | 8 | 1 | dev-payments-back, dev-payments-entry-detail, dev-payments-entry-request, dev-payments-entry-resend, dev-payments-entry-response, dev-payments-entry-result, dev-payments-entry-what |
templates/conjugation_setup.html | 13 | 7 | drill-setup-count, drill-setup-empty, drill-setup-form, drill-setup-hide, drill-setup-locked-lesson, drill-setup-locked-list |
templates/matching.html | 11 | 5 | matching-again, matching-board-area, matching-leaderboard, matching-leaderboard-row, matching-status, matching-timer |
templates/dev_ai_held.html | 9 | 4 | dev-ai-answer-error, dev-ai-answer-form, dev-ai-held-empty, dev-ai-reply, dev-ai-request-state |
templates/dev_ai_call.html | 8 | 3 | dev-ai-back, dev-ai-call-detail, dev-ai-call-headers, dev-ai-call-purpose, dev-ai-call-result |
templates/lookup.html | 10 | 5 | lookup-back, lookup-collected, lookup-hits, lookup-more, lookup-term |
templates/register.html | 11 | 6 | register-dev, register-dev-login, register-form, register-invite, register-owner-note |
templates/reset.html | 13 | 8 | reset-again, reset-ask-form, reset-choose-form, reset-error, reset-login |
templates/tutor.html | 12 | 7 | tutor-add-key, tutor-exit, tutor-form, tutor-provider-form, tutor-score |
templates/dev_authenticator.html | 5 | 1 | dev-authenticator-add-form, dev-authenticator-info, dev-authenticator-info-dialog, dev-authenticator-list |
templates/dev_payments_checkout.html | 10 | 6 | dev-checkout-closed, dev-checkout-email, dev-checkout-form, dev-checkout-product |
templates/dev_payments_log.html | 5 | 1 | dev-payments-empty, dev-payments-entry-open, dev-payments-entry-status, dev-payments-resend |
templates/dev_payments_portal.html | 8 | 4 | dev-portal-cancel, dev-portal-cancel-now, dev-portal-empty, dev-portal-update-card |
templates/docs.html | 9 | 5 | docs, docs-llms, docs-nav, docs-page-link |
templates/lessons.html | 9 | 5 | lessons-bar, lessons-item-practise, lessons-list, lessons-shown |
templates/listening.html | 4 | 0 | listening-lesson, listening-lesson-mastery, listening-lesson-play, listening-mixed |
templates/login.html | 16 | 12 | login-dev, login-dev-forget-google-form, login-dev-login, login-privacy-link |
static/tutor.js | 10 | 6 | tutor-buy-credits, tutor-out-of-ai, tutor-own-key, tutor-upgrade |
static/devbox.js | 17 | 14 | dev-jobs, dev-text-message, login-code-device |
templates/dev_ai_fixture.html | 7 | 4 | dev-ai-fixture-form, dev-ai-fixture-key, dev-ai-fixture-save |
templates/conjugation.html | 12 | 9 | drill-again, drill-again-misses, drill-form |
templates/generate_form.html | 22 | 19 | generate-instructions, generate-kind, generate-depth |
templates/google_confirm.html | 6 | 3 | google-confirm, google-confirm-cancel, google-confirm-form |
templates/guide.html | 6 | 3 | guide-continue, guide-stages, guide-three-star |
templates/landing.html | 4 | 1 | landing-course, landing-privacy, landing-terms |
templates/register_closed.html | 3 | 0 | register-closed, register-closed-login, register-closed-message |
templates/terms.html | 5 | 2 | terms-privacy-link, terms-raw, terms-version |
templates/verify_email.html | 7 | 4 | verify-email, verify-email-continue, verify-email-resend-form |
templates/dev_ai_rows.html | 5 | 3 | dev-ai-call-outcome, dev-ai-empty |
templates/games.html | 3 | 1 | games-list, games-play |
templates/lang_picker.html | 3 | 1 | lang-picker-form, lang-picker-submit |
templates/login_code.html | 9 | 7 | login-code-form, login-code-notice |
templates/login_code_device_rows.html | 5 | 3 | login-code-device-code, login-code-device-empty |
templates/map.html | 5 | 3 | map-lesson-link, map-practice |
templates/matching_pick.html | 2 | 0 | matching-board, matching-board-play |
templates/terms_accept.html | 6 | 4 | terms-accept-error, terms-accept-form |
templates/terms_notice.html | 5 | 3 | terms-notice-change, terms-notice-form |
templates/dev_authenticator_rows.html | 7 | 6 | dev-authenticator-account |
templates/error.html | 5 | 4 | error-message |
templates/goal_message.html | 1 | 0 | goal-message |
templates/history.html | 4 | 3 | history-games-scroll |
static/lessons.js | 3 | 2 | lessons-to-top |
static/login_code.js | 2 | 1 | login-code-resend |
templates/login_code_device.html | 2 | 1 | login-code-device-off |
templates/mistakes.html | 9 | 8 | mistakes-scroll |
templates/vocabulary.html | 4 | 3 | vocabulary-study-all |
templates/vocabulary_lesson.html | 4 | 3 | vocabulary-study |
templates/account_deletion.html | 6 | 6 | |
templates/ai_form.html | 12 | 12 | |
templates/ai_out.html | 4 | 4 | |
static/autosave.js | 3 | 3 | |
templates/base.html | 17 | 17 | |
static/profile.js | 37 | 37 | |
templates/content_report_dialog.html | 7 | 7 | |
templates/course_shared_dot.html | 4 | 4 | |
templates/copyright.html | 13 | 13 | |
static/courses.js | 25 | 25 | |
templates/ai_job_modal.html | 14 | 14 | |
templates/course_edit.html | 21 | 21 | |
templates/course_file.html | 6 | 6 | |
templates/lessons_modes.html | 1 | 1 | |
static/lesson-editor.js | 3 | 3 | |
templates/lessons_panel.html | 58 | 58 | |
templates/lessons_row.html | 7 | 7 | |
static/word-list.js | 10 | 10 | |
templates/undo_revision.html | 5 | 5 | |
templates/course_report.html | 7 | 7 | |
templates/course_review_modal.html | 27 | 27 | |
templates/course_share_modal.html | 13 | 13 | |
templates/courses.html | 41 | 41 | |
templates/course_copy.html | 2 | 2 | |
templates/course_settings.html | 19 | 19 | |
static/course-settings.js | 8 | 8 | |
static/placement.js | 4 | 4 | |
templates/course_wizard.html | 3 | 3 | |
templates/course_stat.html | 8 | 8 | |
templates/course_translate.html | 11 | 11 | |
templates/dev_box_kinds.html | 5 | 5 | |
static/conjugation.js | 3 | 3 | |
templates/flag_matches.html | 6 | 6 | |
templates/vocab_study.html | 8 | 8 | |
static/lesson-check.js | 1 | 1 | |
templates/lesson_pager.html | 3 | 3 | |
templates/masked_word.html | 1 | 1 | |
templates/nav_play.html | 12 | 12 | |
templates/nav_study.html | 11 | 11 | |
templates/pager.html | 4 | 4 | |
templates/password_change.html | 9 | 9 | |
templates/placement_test.html | 10 | 10 | |
templates/proxy_signin.html | 4 | 4 | |
templates/quiz.html | 11 | 11 | |
static/quiz.js | 2 | 2 | |
templates/register_sent.html | 2 | 2 | |
static/say.js | 1 | 1 | |
templates/self_host.html | 15 | 15 | |
templates/shop.html | 10 | 10 | |
templates/sprint.html | 24 | 24 | |
static/sprint.js | 5 | 5 | |
templates/sprint_play.html | 13 | 13 | |
templates/stage.html | 12 | 12 | |
static/guide.js | 1 | 1 | |
templates/superuser.html | 86 | 86 | |
templates/superuser_courses.html | 78 | 78 | |
templates/superuser_privacy.html | 19 | 19 | |
templates/waitlist.html | 10 | 10 | |
templates/dev_box_rows.html | 5 | 5 | |
templates/dev_google.html | 2 | 2 | |
templates/pick_tabs.html | 2 | 2 | |
templates/terms_check.html | 5 | 5 | |
templates/ai_switch.html | 4 | 4 |
Every test, by file
Titles as Playwright lists them (from the source: run npm ci in e2e/ for loops expanded).
specs/accessibility.spec.ts(7, CI): pages fit a phone at 200 % text size (#342) · the root font size follows the browser’s text size (#342) · the system’s reduce-motion setting stops animations and transitions (#342) · the first Tab reaches a skip link that jumps to the content (#342) · /sprint has one page heading (#342) · status pills and links in text have 4.5:1 contrast (#342) · inline code keeps its size with the % root font size (#342)specs/ai-off.spec.ts(2, local only): the sign-up box hides every AI feature, and the profile turns them back on · the profile’s switch is a negative, hides the AI features at once, and asks first when a key would be deletedspecs/authenticator.spec.ts(4, CI): a pasted otpauth link is added, shows the RFC 6238 code with a countdown, copies it, and is removed (#291) · the next code shows by itself when the period runs out, and a link’s settings are honoured · a scan link opens the form filled in, a setup key works too, and a bad key says why · when the list goes empty, an empty add box offers the example account again (#291)specs/avatar-colour.spec.ts(3, local only): a colour is picked in the course’s settings and the corner wears it in the course · a theme picked in the settings adds scenes of its world, offered only while it is picked · a theme brings its own colours for the same slotspecs/consent.spec.ts(2, CI): the consent card shows each consent, and Do Not Show Me AI Features hides them and shows them again across a reload · with a key on file the card’s AI switch asks first, and the key line goesspecs/content-check.spec.ts(3, CI): a course is scanned before it is shared; a high score the AI review passes blocks nothing · the nightly check waits in the job table for the next midnight (#1038)specs/content-reports.spec.ts(1, CI): a shared course is reported, a copyright notice is sent, and the superuser forwards and resolves themspecs/content-review.spec.ts(2, CI): a harmful course is flagged with a short report; an override note is followed until new harm comesspecs/course-card-stats.spec.ts(2, CI): a course card shows passed and mastered as a bar and chips; a chip opens its stat in a modal and its name fits a phone (#684) · a chip’s modal fetches what is next as it opens: the next lesson to pass and each unit, the lessons to practise, and opens the course at one (#900)specs/course-copy.spec.ts(5, CI): the copy icon in Spanish’s settings opens Copy of Spanish in the editor, with every file, and lists it · copies count against the account’s course bytes: past the limit the copy is refused with a notice · no copy or translate icon in the settings of a course shared with you; copying or translating it, or another user’s private course, is a 404 · no copy icon while a course is being generated, nor after it stopped; a POST says why · at phone width the copy icon sits beside the X, inside the dialog and clear of its titlespecs/course-report.spec.ts(3, local only): the report shows which features each lesson uses · the report fits a phone · the report follows a savespecs/course-table.spec.ts(6, CI): a flagged course: search, the reason, override with a note, clear the override · more than five matches show as a count that opens them all · take down and put back from the course’s dialog · delete a course after a warning: gone from the table and the owner’s list · suspend a course’s owner from its dialog: their session endsspecs/course-translate.spec.ts(2, CI): Copy and translate to Español makes a translated copy; the original stays as it was · the dialog does not offer the language a course is already explained inspecs/create.spec.ts(11, CI): create, close the dialog, name the course and its lessons, then generate into it · editing a lesson’s content takes Generate away · a refused generate form comes back open with what was typed · the old pages lead to the new flow · the dialog asks which provider only when two or more keys are saved · the progress modal can’t be dismissed while it runs; stopped, the course waits on the list with Resume · a generation that fails before its outline: Retry on the card, the dialog filled in, and a retry writes the same course · a stopped course can be deleted from its card · a stopped course’s red ! does not widen the course list on a phone (#675) · a stopped course’s red ! tip stays on a phone screen while it shows (#864) · the old status page leads to the editorspecs/custom-themes.spec.ts(6, CI): wear a theme you made in a course, see it everywhere in it, then delete it · a theme you made is invisible to everyone else · a made theme is checked like a theme file, within the account’s limits · the course settings with your themes fit a phone · a course wears a theme of its own; outside a course the app wears Default (#565, #577) · a themed example’s mark reads right for a theme title that starts with The (#952)specs/dev-ai.spec.ts(11, CI): a course generated through the dev AI provider shows in /dev/ai; limits, a timeout and an edited fixture reach the learner · the wire API refuses unknown keys and answers the OpenAI format; the platform key has its own profile · Manual replies: a request waits, folded at the bottom of /dev/ai, for the answer typed there; an ERROR: answer is the provider’s error · leaving the tutor while its first turn waits on the model does not lock the tutor (#659) · a learner who leaves the tutor mid-turn twice has one call with the model, not two (#848) · coming back while the tutor is still answering, Cancel lets go of the old reply and starts again (#890) · a slow reply gets a Cancel after ten seconds, and the learner can carry on (#890) · in another language, coming back while the tutor is still answering also offers Cancel (#890) · coming back to the tutor in the same tab after a failed first turn starts again (#932) · each profile’s keys are copy buttons in their provider’s coloursspecs/dev-payments.spec.ts(1, local only): subscribe through the fake checkout, fail a renewal, cancel; a tampered webhook signature is refusedspecs/docs.spec.ts(8, CI): the docs index is reachable logged out, with user and developer pages apart · every user page is served as HTML and as raw Markdown · /llms.txt lists the course format and its schema, and both are served · no docs page scrolls sideways at phone width · a long bare link in docs prose wraps at phone width · every in-app link on every docs page opens, and its #heading exists · a docs page’s heading is on the first screen, the page list after it · on a desktop the page list stays beside the pagespecs/editor-autosave.spec.ts(5, CI): typing in a lesson’s JSON saves 3 s after the last keystroke, or when the drawer loses focus · a hidden tab sends what waits at once · what does not validate turns the circle red, says why, and is not stored but stays on the page · a save the server fails is tried again, and a page behind another tab is told to reload · an AI job locks the editor: what was typed is saved first, then nothing is saved until it endsspecs/editor-errors.spec.ts(4, local only): a parse error, an unknown prerequisite and a duplicate id each say what, where and how to fix it · a problem in another lesson opens that lesson’s drawer · the file page words a parse error as the editor does · Undo takes back changes that cannot be saved, after saving what can (#288)specs/editor-fixes.spec.ts(3, local only): the file page’s path box checks the path and logs no console error (#273) · on a phone, Course settings and + Add unit do not overlap (#274) · closing a rename box by clicking elsewhere leaves focus where the click went (#275)specs/email-owner.spec.ts(2, CI): e-mail a flagged course’s owner from its dialog, with an editspecs/exact.spec.ts(29, CI): generate a course from the fixtures and open it · revise a generated course with feedback, then undo it · the lesson table: move, rename, delete, add, edit JSON, each saved as it is made, then undo each page’s edits · a course someone makes stays private to them · a failed generation explains itself in the dialog; its card keeps the raw detail behind a red ! · a key the provider does not know is refused by the provider, not by the app · the course list fills a desktop row and stacks on a phone (#428) · filter the lessons list, and say when nothing matches · look a word up from the lessons filter: its meaning and the lesson that teaches it (#348) · lessons show one unit at a time, with a tab per unit and page links for a long unit (#837) · the verb drill setup shows one set at a time, and its locked lessons a page at a time (#837) · the verb drill draws the person slots with profile pictures, the learner’s own for yo (#879) · pass a lesson’s reading check · a lesson’s tappable words look tappable and take the keyboard (#403) · tapping a word shows its meaning in a bubble that fits the screen · finish a Sprint mode session · Sprint home fits a phone in every app language (#670) · on a phone the History table scrolls in its box, not the page (#424) · on a phone the Vocabulary term table scrolls in its box, not the page (#933) · on a phone the Study and Play mode bars take two rows and scroll sideways (#423) · the modes’ old paths redirect, keeping the query string · the mode bars link to Study’s Lessons, not to the /study redirect · Free has no Skrongle AI: no usage card, no own-keys pill, the tutor offers only the saved keys · the dev toggle switches the plan on the profile · the profile has no Dev section for anyone; the seeded keys answer generate and the tutor · the profile’s e-mail field: a Verified check, and an (i) dialog with the name others see · Start asks for a course’s settings before it opens, then keeps them behind its gear; the password changes in a dialog · a page that does not exist is a 404 inside the normal layout, with ways back (#397) · the site installs as an app: every layout links a manifest that opens at the home page (#345)specs/explore.spec.ts(3, needs a key): explore ${replayPath ? “replay” : who}specs/forms.spec.ts(4, CI): Forms mode: add a word and a question in the lesson editor, see them as JSON, and find them in the file as saved · the JSON switch: a drawer reopened after a flip follows it, and quick flips end where they point · a new lesson is a whole form before it is saved · Forms mode edits the course settings too, and fits a phonespecs/google-signin.spec.ts(11, CI): a new learner signs up with Google, confirms the Terms, and signs in again · a learner connects a Google account with the password, nobody else can take it, and unlinks it (#360) · Google with a password account’s address: refused while unconfirmed, then joins that account once confirmed · with two-factor sign-in on, Google joins the account only once the code is entered (#677) · registering with a password for a Google-only address makes nothing and mails its owner · a Google-only account confirms with Google before its deletion is scheduled (#376, #970) · signing out everywhere else ends the learner’s other sessions (#375) · the ${error} button: the sign-in fails and comes back to the login page with the reason · the register page has the same buttons, each leading to its message · the buttons fit a phone screen · the timeout button: a Google that never answers fails the sign-in after the app’s own timeoutspecs/home.spec.ts(4, local only): the home page has a map row and the Guide as a mode of its own · each mode’s explanation sits behind its (i) · the home page fits a phone · the map row and the Guide card are plain linksspecs/hosted-ai.spec.ts(6, local only): a Plus learner with no key tutors and generates on Skrongle AI; the month’s bar goes down · the platform key’s limit reaches the learner as a plain sentence · a plan with no Skrongle AI (Free) is not offered it, and the tutor card says why · Basic, the CA$3 plan, has no Skrongle AI either: the tutor and the generator want an own key (#931) · credit packs are not sold without Stripe: the checkout answers 400 · only a superuser sees everyone’s Skrongle AI spend today; a learner sees their own barspecs/i18n.spec.ts(2, CI): a visitor picks Spanish on the register page, the account keeps it, the profile changes it back · the browser’s language is used before any pick, and an unknown one falls back to Englishspecs/journeys.spec.ts(2, needs a key): journey ${journey.id}specs/judged.spec.ts(3, needs a key): a lesson page teaches a topic and ends with a check · a failed generation explains itself in plain wordsspecs/lesson-conversations.spec.ts(1, CI): a lesson with a conversation shows both speakers’ linesspecs/lesson-duplicate.spec.ts(1, CI): duplicate a lesson: the copy sits below the original, with new ids, and the course still loadsspecs/lesson-editor.spec.ts(10, CI): the outline lists every section of the lesson, opens its items, and the preview knows which modes show them · a multiple-choice question: the right answer is picked, a new option grows from the dashed box, the type switch reshapes the form, and it all lands in the file · checks appear under the field as you type, in the outline, and from the server · Try it: the preview grades a question with the course’s own checker, in Study and in Sprint · Sprint exercises: buckets and items, the wrong word by a click, tiles and distractors, played in the preview · words, sentences, conversations and tables have their own forms · items are added and deleted from the outline, the Guide’s steps are ordered by hand, and the lesson’s prerequisites are picked · on a phone the three columns are tabs, and nothing overflows · the gear’s Language features switch hides what only a language course fills, and is kept in the course settings · the gear’s Levels switch holds the Placement test switch, which adds or takes out placement.json under the course settings (#1106)specs/lesson-header.spec.ts(1, CI): on a phone the lesson text starts under 420 px down a 390x844 screen: one main button, the rest under More ways, the details behind an (i) (#999)specs/lesson-tables.spec.ts(2, CI): the test course’s lessons with tables fit a phone · Spanish paradigm tables scroll in their box, not the pagespecs/lessons.spec.ts(17, local only): the server refuses a table it cannot use, and changes nothing · the last lesson cannot be deleted, and Escape keeps a lesson’s name · a stopped run keeps its Resume, and has no editor until it is finished · while an AI lesson is being written, every way to change the course is refused · three saves at once leave a course that loads · a folded unit hides its lessons and still takes new ones · Add unit: named in place, saved in the settings, it takes lessons, an empty one goes at once and one with lessons asks first (#232) · course settings are the first row: pinned, kept, and their units reshape the table as they are typed · add-lesson refusals: short brief, and a reply that cannot be used leaves the course alone · Use AI: Only add appends a lesson each time; Allow overall changes can replace the whole course · a hand-made course can be changed overall too · the download is every file by path, with a format version · Delete this course is a red button that warns first · without JavaScript the table still lists the lessons, and Edit opens one · the lesson table fits a phone · a course with progress warns before a delete, and the table is for the owner’s own courses only · a lesson page links the lessons before and after it in course order, below the lesson only, and neither past the ends (#402, #999)specs/mail.spec.ts(7, CI): a new account’s confirmation e-mail opens in the dev inbox, and its link confirms the address · the outbox’s test message arrives in the inbox and opens from the outbox · the dev mailbox is a mail client’s table: bold until opened, archive strikes out and sinks, delete removes, all without a reload (#223) · a superuser resets another account’s e-mail verification, and the new link confirms it again · only a superuser sees the Superuser page · the Text section works like the e-mail one, styled like a messaging app (#225) · the dev mailbox needs no sign-in: signed out and as a learner, every section and its JSON openspecs/mistakes.spec.ts(1, CI): a wrong quiz answer shows on the mistakes list from the course home, and a right one clears it (#338)specs/modals.spec.ts(5, CI): a Sprint mode stop’s sheet closes on a tap outside it and on Escape (#233) · profile dialogs close on a click outside; one with something typed in its form stays (#226) · the Generate dialog closes on a click outside until a wish is typed; a running job’s box stays (#226) · the /dev/jobs (i) opens its dialog; OK and a click outside close it (#872) · without JavaScript a server-opened dialog opens in view at ${viewport.width}px (#1057)specs/moderation.spec.ts(4, CI): a course taken down shows its owner why, and comes back when put back · a suspended account is signed out and can’t sign in until the suspension is lifted · a superuser deletes an account after a warning; the address can register again · a superuser blocks an address range and removes the block; others get 404specs/owner-resets.spec.ts(3, CI): a superuser sets a temporary password; the learner signs in with it and must choose a new one · a superuser turns off another account’s two-factor; others get 404 · reloading after a temporary password is set does not set anotherspecs/pathways.spec.ts(7, CI): switch course: Spanish, then a generated course, then back · generate from notes and uploaded files, with no wish · cancel a slow generation, then resume it to a ready course · resume a run that stopped on a lesson: the resumed job keeps what was written · saving into a stopped run keeps it resumable, and Resume keeps the edit (#81) · the editor opened between the outline and lesson 1 still follows the job to its end · remove an API key: the profile says no key, and generating no longer offers itspecs/privacy-requests.spec.ts(2, CI): a superuser sends an account its data; the link goes to that account’s address · a superuser forgets an address with no account, not one with an accountspecs/profile-autosave.spec.ts(8, local only): the green checks come a moment after the save, fade after 5 s, and come back with the next edit · names save a second after typing stops, in one request, and the heading follows · a field that needs attention turns red at once, is not sent, and leaving asks first · the server refusing a field marks it red with the server’s reason · without a connection the circle turns red, leaving asks, and it saves once back online · the phone number wears a +1 and punctuates the digits as they are typed (#496) · the profile has no picture and no theme: both are a course’s settings · API keys: folded away, a pasted key saves at once, a typed one 3 s after typing stopsspecs/profile-nav.spec.ts(1, CI): the profile nav marks the section in viewspecs/prompt-limits.spec.ts(5, local only): the Generate dialog counts the wish and the notes, red outside 20 to the maximum · the server refuses Generate prompts outside the limits, in the same words · Use AI counts its text, red outside 20 to the maximum, and the server agrees · a Generate form that is too short is refused in place, calmly · short notes are refused in place too, and the notes box is the one markedspecs/record.spec.ts(3, needs a key): record ${id}specs/release.spec.ts(2, CI): a person joins the waitlist, confirms, is invited and signs upspecs/review-ai-changes.spec.ts(14, local only): a generated course reviews as new lessons, listed like the lesson table · Only add shows the new lesson as one column; what was there keeps no red · Use AI runs on the course page: the modal comes up in place, holds the page, then the review opens · a refused Use AI request comes back on the same page, text kept · a revision shows red and green side by side, and later hand edits do not change it · leaving out a line and a lesson: Accept writes only the rest, and the review remembers · editing a line in place as text: it turns blue and stays in, Undo/Redo step through edits, Accept writes them · after a lesson-table edit, leaving out part of a lesson still goes in (#280) · after Undo of the AI run the review says so and offers no Accept (#281) · the review labels lines in plain words, not JSON keys (#282) · Don’t show this automatically next time: the review waits for its button · Review changes is a button next to Submit · a course nobody used AI on has no review button · without JavaScript the review is a page whose boxes Accept sendsspecs/review-and-revise.spec.ts(4, local only): while a revision runs the modal covers the locked workspace; closing the review shows the new lessons · a lesson edited by hand survives a revision that keeps it · undo puts back the files from before the revision and drops later hand edits · a hand edit that breaks the course rules does not block a revision of a course that loads againspecs/revise.spec.ts(10, local only): revise a course twice in a row, and undo goes back one step · undo and redo icons right after a revision, from the course page the review closes onto (#231) · Undo and Redo: rows the step adds fade in, rows it takes away fade out, the rest stay still; none under reduced motion (#251) · a generated course is changed with Only add or Allow overall changes, not generated again · feedback that is too short is refused on the form and kept · a plan that removes a lesson drops it, keeps the others’ progress, and undo brings it back · a rewritten lesson keeps its progress · a plan the AI cannot give leaves the course alone and offers neither undo nor resume · while a revision runs the course still opens but its files are locked; cancel releases it · two revisions asked for at the same moment start only one jobspecs/share-edges.spec.ts(9, CI): a used link turns a second account away; the invited address, confirmed later, has the course with or without the link · unknown, odd and padded tokens: a 404 for the link, the usual sign-in pages · removed and invited again: a new link, the old one dead, the learner’s progress back · an old link after the share was removed says it no longer works · every mode opens on a shared course with the learner’s own progress, kept when the owner edits the lesson · the share removed mid-session: the next page goes to the course list, the API answers 400 · the share removed mid-Sprint: the page goes to the course list instead of ‘try again’ · the owner deletes the course, or the whole account: the course leaves the learner’s list and session · the invitation is in the owner’s language; the learner’s pages stay in theirsspecs/share-sign-in.spec.ts(3, CI): a share link survives the two-factor code step, and cancelling it keeps the link · a share link survives Google sign-up’s confirm step · a share link survives a Google sign-in with two-factor onspecs/share.spec.ts(2, local only): an invited address registers from the link and has the course read only, until the owner removes the share · a shared course’s circle tip wraps inside a phone screen while it shows, even with a long owner address (#954)specs/smoke/complete-test-course.spec.ts(3, CI): K: read and practise every lesson · D: every lesson’s session, every exercise kind, the review mix and lightning · G: every Guide stage, in orderspecs/smoke/daily-goal.spec.ts(1, CI): every mode fills one daily goal and resumes where the learner left offspecs/smoke/drill-first-set.spec.ts(2, CI): the drill opens on the first set with unlocked verbs, not Present · Start drill works without JavaScriptspecs/smoke/export.spec.ts(2, CI): a learner asks for their data, gets the link by e-mail and downloads the JSON oncespecs/smoke/french.spec.ts(6, CI): the course is on /courses, offers its four levels, and starts at greetings · Study: read greetings, pass its check, practise it; numbers’ numeric questions grade · Sprint: play greetings’ session right at phone width · Play: play the first stage · Realm: the theme’s French material comes first and the course’s French praise is used · games: the verb drill (être, avoir) and Date Dashspecs/smoke/happy-paths.spec.ts(10, CI): 1. a new learner sees lesson 1 as next · 1b. a stage read in Study but never played says Start in the Guide, not Try again (#417) · 1c. a Guide stage played to the end under 70 % says Try again, not Continue past its last step (#417) · 2. Study: read lesson 1, pass its check, practise it: passed everywhere · 3. Sprint: play lesson 1’s session right: passed everywhere · 4. Play: play stage 1: passed everywhere · 5. a lesson passed in one mode is passed in the other two · 6. progress survives logging out and back in · 7. a lesson whose every answer was right is not listed as needing practice (#681) · 8. a Guide stage’s quick checks mix their options, so the right one is not always first (#962)specs/smoke/mini.spec.ts(6, CI): a learner registers, signs in again and passes a lesson with nothing configured · there is no dev mailbox and no dev phone · the server says it is a Skrongle mini · the profile has no Subscription, no phone field and no red ! on the e-mail · sharing shows the link once, and a reload goes back to the dialog without inviting againspecs/smoke/motivator-reward.spec.ts(1, CI): every mode’s end screen shows the worn motivator’s reward linespecs/smoke/motivator-skc.spec.ts(1, CI): Skrongle Cash draws the hero and reward line, and keeps counting while another motivator is wornspecs/smoke/motivator-social.spec.ts(1, CI): Social compares the learner with the course’s other learners, anonymouslyspecs/smoke/motivator-xp.spec.ts(1, CI): XP counts what was earned under any motivator and levels the learner upspecs/smoke/motivator.spec.ts(1, CI): the motivator worn draws the course home, and switching keeps progressspecs/smoke/password-reset.spec.ts(2, CI): registering a taken address looks the same and mails its owner · a forgotten password is reset from the login page through the mailed linkspecs/smoke/phone-width.spec.ts(2, CI): the main pages and a lesson in each mode fit a 390 px phone · the course editor fits a 390 px phonespecs/smoke/placement.spec.ts(2, CI): the placement test suggests the first unit with a miss, shows a report card, and the course starts there · the marking is the server’s: answers come back with each unit’s tally and no question carries its answerspecs/smoke/practice-empty-check.spec.ts(1, CI): Check with no answer asks for one inline, on a choice and on a typed questionspecs/smoke/practice-keys.spec.ts(2, CI): number keys pick an option and Enter checks, then advances · digits typed into a fill-in answer stay in the text boxspecs/smoke/practice-nothing-yet.spec.ts(2, CI): a fresh learner’s practice shows Read next and no counter · a round with questions still shows the counterspecs/smoke/practice-result-order.spec.ts(1, CI): the result box sits above Next, and Hint goes once the answer is gradedspecs/smoke/practice-summary.spec.ts(2, CI): the mastery change is shown as (+N), and left out when nothing changed · a round too short to count says what it needs the answers forspecs/smoke/privacy.spec.ts(3, CI): the privacy policy is served to visitors · deleting the account signs out and waits 14 days; signing in can cancel it (#970) · the AI forms say which provider gets the notesspecs/smoke/self-host.spec.ts(3, CI): the self-hosting guide is linked from the docs and readable on a phone · the guide is only linked from the docs, not the landing page or the footer · the self-hosting FAQ is linked from the technical guide and points at the diagnosticsspecs/smoke/skc-shop.spec.ts(2, CI): the shop sells a badge once, keeps it across motivators and draws it in the worn theme · without JavaScript a refused buy says whyspecs/smoke/sprint-check-error.spec.ts(2, CI): a failing check shows one error line, replaced on retry, cleared on success · a typed answer stays editable after a failed check (issue #672)specs/smoke/sprint-ladder.spec.ts(2, CI): fast taps all land on the rungs, a filled rung gives its form back, all right grades right · one form on a wrong rung grades wrong, shows the key, and History agrees with its ✗specs/smoke/sprint-sort.spec.ts(2, CI): a wrong bucket shakes the word and keeps it; the sort checks itself, wrong, and History says why · a clean sort, by the keyboard: Enter on the right bucket drops the word, and the sort checks itself rightspecs/smoke/start-level.spec.ts(4, CI): start Spanish at A2: the A1 lessons are skipped and never offered · change level mid-course (#333): progress stays, every mode moves to the chosen level · the test course offers each of its units as a level · a course without levels never asks for onespecs/smoke/test-course-games.spec.ts(7, CI): every game on /play is played here · K Listening: a lesson’s round, then a mixed round · Vocabulary: collect words in Sprint mode, then the vocabulary page and the flashcards · Word Match: the lesson board and the all-lessons board · the paradigm drill, Numbers by ear, Date Dash (each drill) · Tutor: a tutored lesson over the fixtures, one tapped and one typed answer · Theme: the worn theme’s material comes first, and nothing is missing without itspecs/smoke/verify-email.spec.ts(3, CI): a new learner confirms their e-mail address from the link · sending the e-mail again replaces the first linkspecs/smoke/wrong-answers-games.spec.ts(5, CI): Guide: a stage of wrong quick checks isn’t passed and the next stays locked; a replay passes it · Word Match: wrong tries are counted and keep the lesson from passing · the games: a wrong answer is marked wrong and History counts it · flashcards: a card marked not known comes back in the missed deck · Tutor: a wrong answer gets a wrong verdictspecs/smoke/wrong-answers.spec.ts(7, CI): K: every question type, answered wrong, is marked wrong and doesn’t pass · K: one lesson answered wrong reads as "1 lesson needs practice" on the course home and map · K: the 70 % line, 3 of 5 doesn’t pass and 4 of 5 does · near misses are wrong, and the answers they nearly are stay right · a reading check with one wrong answer fails, the lesson stays unread, and a retry passes · D: every exercise kind, answered wrong, is marked wrong, and no lesson passes on wrong answers · D: the session’s score is the answers given, on the screen, in History and in progressspecs/subscription.spec.ts(3, local only): Plus: bought through Checkout, shown with its renewal date, the AI bar and the own-keys pill; cancelled, it lasts the month · Basic: no Skrongle AI card or pill, an Upgrade to Plus button that opens the Billing Portal, and the chooser marks Basic as the current plan · dev login ?plan= takes basic, plus and the old pro (now Plus) through the dev Checkout; anything else names the plansspecs/term-flag.spec.ts(3, CI): a course with a listed word can’t start or be shared until the word is gone · more than five matches show as a count that opens the list, ten to a pagespecs/terms.spec.ts(5, CI): the terms page is public and says when it was last updated · registration needs the terms checkbox, and records the acceptance · a material change: the notice cannot be dismissed and blocks the API until accepted · a minor change: the notice shows what changed and Continue closes it for good · the profile’s read link shows the terms in a dialog and closes back to the profile (#152)specs/two-factor.spec.ts(4, CI): a phone number is verified with a texted code, and text-message sign-in then works (#292) · e-mail sign-in codes are not offered until the address is verified; then the code comes by e-mail and a wrong one is refused (#292) · an authenticator app is added through the dev authenticator and its codes sign in, each once (#292) · a number that replied STOP gets its sign-in code by e-mail, and START plus a new code turns texts back on (#969)specs/word-list.spec.ts(3, CI): a pasted word list previews, flags what it can’t read, and becomes a lesson that opens with the words · a paste past the cap is refused in words, not with a body-limit error · the button follows the Language features switch