{"ns":"typing","title":"Tempo Typing","version":"3.4","summary":"Tempo Typing is a minimalist, keyboard-first typing test. People type against time, a word count, a quote or custom text, see an honest scoring breakdown, and — when signed in — keep a history, personal bests, a streak and a place on weekly and all-time leaderboards protected by server-side anti-cheat.","problem":"A typing score is only worth sharing if it cannot be faked and is computed consistently; every figure on the result screen and on a leaderboard must follow from what was actually typed.","personas":[{"name":"Practising typist","need":"A distraction-free test with accurate wpm, raw and accuracy, and control over difficulty and confidence rules."},{"name":"Competitor","need":"Personal bests and leaderboards that only contain legitimate results."},{"name":"Guest","need":"Try the test without an account and keep results on the device."}],"goals":["Scoring follows one formula everywhere: the UI, the saved result and the API agree.","Difficulty, confidence and paste rules are enforced as documented.","Only valid, long-enough, non-zen, non-failed results are saved and ranked."],"features":[{"id":"ENG","title":"Typing engine and scoring","summary":"How keystrokes become a result.","reqs":[{"id":"FR-ENG-01","weight":"Must","text":"A test on custom text ends on the last correct letter (no trailing space needed) and reports a full breakdown.","accept":["Typing \"The quick brown fox jumps over the lazy dog\" exactly ends the test as the final \"g\" is typed","Result shows acc 100%, characters 35/0/0/0 (correct/incorrect/extra/missed), test type \"custom english\", and wpm equal to raw","A guest sees \"Saved on this device. Sign in to keep your history.\""]},{"id":"FR-ENG-02","weight":"Must","text":"Wrong and extra letters are counted in separate buckets, mark the word as wrong, and lower accuracy; net wpm counts only correct letters and correct spaces, so with errors wpm is lower than raw.","accept":["Typing \"Thx quickk brown fox jumps over the lazy dog\" gives characters 34/1/1/0 and acc 91% (title \"90.91% (40 correct / 44 keypresses)\")","wpm = (correct letters + correct spaces) / 5 per minute; raw = (all typed letters + spaces) / 5 per minute; acc = correct keypresses / total keypresses"]},{"id":"FR-ENG-03","weight":"Must","text":"Pasting into the test is blocked: clipboard text types nothing and does not start the timer.","accept":["Toast \"Pasting is disabled\"; no letters are coloured"]},{"id":"FR-ENG-04","weight":"Should","text":"With confidence mode off, Backspace on an empty word re-opens the previous word only if it was incorrect.","accept":["After \"Thx\", space, Backspace twice and \"e\" the first word reads correct","A correct previous word cannot be re-opened","The wrong keypress still counts against accuracy (below 100%)"]},{"id":"FR-ENG-05","weight":"Could","text":"Clicking away blurs the words and shows \"Click here or press any key to focus\"; pressing a key refocuses and types it."},{"id":"FR-ENG-06","weight":"Could","text":"\"Repeat test\" replays the identical word list and labels the type \"words 10 english (repeated)\"."},{"id":"FR-ENG-07","weight":"Could","text":"\"Practice missed words\" builds a test from only the words typed wrong; its result is not saved.","accept":["After a run with errors in \"The\" and \"quick\" the practice test is exactly \"The quick\"","Save line \"Practice tests are not saved\""]}]},{"id":"DIF","title":"Difficulty and confidence","summary":"Rules that make a test stricter.","reqs":[{"id":"FR-DIF-01","weight":"Must","text":"Expert difficulty fails the test the moment an incorrect word is submitted with space; failed tests are not saved.","accept":["Typing \"Thx\" does not fail yet; pressing space shows \"test failed\" and \"Expert: an incorrect word was submitted\"","Save line \"Failed tests are not saved\""]},{"id":"FR-DIF-02","weight":"Should","text":"Master difficulty fails on the first incorrect key, without waiting for space.","accept":["Typing \"Tx\" shows \"test failed\" and \"Master: an incorrect key was pressed\""]},{"id":"FR-DIF-03","weight":"Should","text":"Confidence \"on\" makes a space final: after it, even a wrong word cannot be revisited, while Backspace still works inside the current word.","accept":["A \"confidence on\" badge is shown; \"Thx\", space, Backspace changes nothing","\"qa\" then Backspace leaves \"q\""]},{"id":"FR-DIF-04","weight":"Must","text":"Confidence \"max\" disables every Backspace, including Ctrl+Backspace.","accept":["A \"confidence max\" badge is shown; after \"Thx\", Backspace and Ctrl+Backspace leave \"Thx\" with the x still incorrect"]}]},{"id":"MOD","title":"Modes and configuration","summary":"Time, words, quote, zen and custom tests.","reqs":[{"id":"FR-MOD-01","weight":"Must","text":"A time test counts the timer down and ends on its own at exactly the chosen duration, adding words as the user types.","accept":["Time 15 shows the timer 15 → 1, then a result with time 15s and test type \"time 15 english\""]},{"id":"FR-MOD-02","weight":"Should","text":"Zen mode has no target text: Shift+Enter finishes it, accuracy shows \"-\", there is no \"practice missed words\", and the result is never saved.","accept":["Hint \"start typing — shift + enter to finish\"","Save line \"Zen results are not saved\""]},{"id":"FR-MOD-03","weight":"Should","text":"Quote mode in a language without quotes shows a notice and uses English quotes; the result shows the quote source.","accept":["Toast \"No quotes available for spanish — showing english quotes\"","Test type \"quote short english\" and a \"source\" stat"]},{"id":"FR-MOD-04","weight":"Could","text":"A custom time accepts whole numbers 1–3600 only.","accept":["0 and 3601 show \"Enter a whole number between 1 and 3600\"","45 closes the dialog and the button reads \"45\""]},{"id":"FR-MOD-05","weight":"Could","text":"Custom text cannot be empty and its limit is validated; text cycles up to the word limit.","accept":["Empty text: \"Text cannot be empty\"","Word limit 0: \"Word limit must be 1–1000 words\"","\"a b c\" with word limit 7 shows \"a b c a b c a\""]}]},{"id":"CMD","title":"Command line, themes and settings","summary":"Keyboard control and personalisation.","reqs":[{"id":"FR-CMD-01","weight":"Should","text":"Esc opens a fuzzy-search \"Command line\"; highlighting a theme previews it, Esc reverts and Enter applies and saves it.","accept":["\"thmemb\" highlights \"Theme: ember\" and previews its colours","Esc restores the previous colours; \"theme ember\" + Enter keeps ember and settings shows it selected"]},{"id":"FR-CMD-02","weight":"Could","text":"With quick restart set to \"esc\", Esc restarts the test with fresh words and Ctrl+Shift+P opens the command line.","accept":["Hints read \"esc restart test\" and \"ctrl + shift + p command line\""]},{"id":"FR-CMD-03","weight":"Could","text":"\"reset settings\" asks \"Are you sure?\" with \"yes, reset\" and \"cancel\" before restoring defaults (theme carbon, caret |)."},{"id":"FR-CMD-04","weight":"Should","text":"Signing in applies the account’s saved settings over the local defaults.","accept":["A fresh tab shows the carbon theme (dark grey, yellow accent); after sign-in the seeded account’s lagoon theme (teal) is applied and highlighted in settings"]}]},{"id":"AUT","title":"Accounts and sign-in","summary":"Sign-in with email or username.","reqs":[{"id":"FR-AUT-01","weight":"Must","text":"Email or username plus password signs in; wrong credentials and empty fields show distinct errors.","accept":["Wrong password: an error alert and the sign-in page stays","Empty fields: \"Enter your email or username and password\"","Correct credentials open the account page"]},{"id":"FR-AUT-02","weight":"Could","text":"Signed-out results are kept on the device and listed on the guest account page, where \"clear local history\" empties them.","accept":["Heading \"guest\" and one row in \"local result history\"; after clearing \"No results yet.\""]}]},{"id":"ACC","title":"Account profile and history","summary":"Stats, personal bests and result history.","reqs":[{"id":"FR-ACC-01","weight":"Must","text":"The profile header reflects the seeded history: join date, tests started, tests completed, time typing and current streak.","accept":["Seeded account: \"joined 14 Feb 2026\", tests started 41, tests completed 12, time typing 00:07:12, current streak 3 days"]},{"id":"FR-ACC-02","weight":"Must","text":"Personal best tables show the best result per exact length; untouched lengths show \"-\".","accept":["time: 15 s 93 (98%), 30 s 88, 60 s 84, 120 s 76; tooltip on 15 s starts \"92.80 wpm · 94.40 raw · 98.31% acc\"","words: 10 → 89, 25 → 91, 50 → 83, 100 → \"-\""]},{"id":"FR-ACC-03","weight":"Should","text":"History lists results newest first, ten at a time, with \"load more\" and a mode filter; personal-best results carry a ♛ mark.","accept":["\"10 of 12\"; first row words 10 at 89.19 wpm; \"load more\" shows 12 rows, the last being custom at 63.82 wpm","Filter \"quote\": one row, 77.14 wpm \"quote medium\"","Filter \"words\": three rows (89.19, 90.70, 83.13) each with ♛"]},{"id":"FR-ACC-04","weight":"Must","text":"A signed-in, completed time test is saved and the first result for a configuration is announced as a new personal best.","accept":["A new account’s first time 15 test shows \"♛ New personal best!\"","Account page then shows tests completed 1 and the 15-second PB equal to that wpm"]},{"id":"FR-ACC-05","weight":"Should","text":"A test shorter than 15 seconds and with fewer than 10 words is not saved.","accept":["Nine words typed in under 15 seconds: \"Test too short — result not saved\"; history total stays 12"]}]},{"id":"LDB","title":"Leaderboards","summary":"All-time and weekly rankings for time 15 and time 60.","reqs":[{"id":"FR-LDB-01","weight":"Should","text":"All-time boards rank each account’s best english result for time 15 and time 60; accounts barred from leaderboards are excluded.","accept":["Time 15: top entry 148.20 wpm; the signed-in account is #8 with 92.80 (\"Your rank: #8\"), above an 88.04 entry","A barred account’s 152 wpm result is not listed","Time 60: top entry 126.30; the signed-in account #5 with 83.80"]},{"id":"FR-LDB-02","weight":"Must","text":"Weekly boards only include results from the last 7 days.","accept":["Time 15 weekly: top entry 139.65; the 21-day-old 148.20 and the other all-time leader are gone; signed-in account #5 (92.80)","Time 60 weekly: top entry 121.75; signed-in account #3 with 77.20 because its 83.80 best is 9 days old"]}]},{"id":"API","title":"Results and settings API","summary":"Server-side re-scoring and validation.","reqs":[{"id":"FR-API-01","weight":"Must","text":"POST /api/typing/results re-scores every submission from its breakdown and rejects values that do not follow, a duration that does not match the time mode, zen results and any wpm above 350.","accept":["The documented example body returns 201 with result.wpm 134.4","wpm altered to 180: 400 \"Result failed anti-cheat validation\"","testDuration 30 for time 15: 400 \"Test duration does not match the selected time\"","mode \"zen\": 400 \"Zen results are never saved\"","GET /api/typing/leaderboards/30: 404 \"Leaderboards exist for time 15 and time 60 only\""]},{"id":"FR-API-02","weight":"Could","text":"PUT /api/typing/settings validates keys and values and stores accepted ones.","accept":["{\"theme\":\"neon\"}: 400 Invalid value for \"theme\"","{\"fontSize\":3}: 400 Unknown setting \"fontSize\"","{\"theme\":\"ember\"} is returned by the next GET"]}]}],"nfr":[{"id":"NFR-01","weight":"Must","text":"Security: results cannot be forged — saved wpm, raw and accuracy are always recomputed on the server and paste into the test is blocked."},{"id":"NFR-02","weight":"Could","text":"Accessibility: the whole test, restart and command line are operable from the keyboard alone."}],"glossary":[{"term":"wpm","meaning":"Net words per minute: (correct letters + correct spaces) / 5 per minute."},{"term":"raw","meaning":"Words per minute counting every typed letter and space, right or wrong."},{"term":"PB","meaning":"Personal best, tracked per exact configuration (mode, length, language, modifiers)."},{"term":"Confidence mode","meaning":"Setting that limits correcting earlier input: off, on (space is final) or max (no backspace)."}],"outOfScope":["Sign-up and password reset","Sound output quality","Exact wpm of human-typed runs"]}