clone

a daily shikaku puzzle for phones: one seeded puzzle per date, solved by dragging rectangles

main
rect.run / tests / lib / storage / local_progress_test.ts
local_progress_test.ts 7.9K
1import { assertEquals } from "@std/assert";23import {4 archiveProgress,5 buildHistoryKey,6 buildProgressKey,7 getAchievedStackIndex,8 getDailyStreak,9 getMostRecentStackIndex,10 getPlayableStackIndex,11 getSelectedStackIndex,12 getVisibleStackRange,13 loadPuzzleProgress,14 savePuzzleProgress,15} from "@/lib/storage/local_progress.ts";16import type { PuzzleProgress } from "@/lib/shikaku/types.ts";1718class MemoryStorage implements Storage {19 #data = new Map<string, string>();2021 get length(): number {22 return this.#data.size;23 }2425 clear(): void {26 this.#data.clear();27 }2829 getItem(key: string): string | null {30 return this.#data.get(key) ?? null;31 }3233 key(index: number): string | null {34 return [...this.#data.keys()][index] ?? null;35 }3637 removeItem(key: string): void {38 this.#data.delete(key);39 }4041 setItem(key: string, value: string): void {42 this.#data.set(key, value);43 }44}4546function createProgress(): PuzzleProgress {47 return {48 date: "2026-04-16",49 puzzleId: "daily-2026-04-16-0",50 startedAt: "2026-04-16T10:00:00.000Z",51 updatedAt: "2026-04-16T10:03:00.000Z",52 status: "in_progress",53 elapsedMs: 180000,54 mistakes: 0,55 selections: [56 {57 clueId: 0,58 cells: ["0,0", "0,1", "1,0", "1,1"],59 value: 4,60 },61 ],62 notes: {},63 lastViewedAt: "2026-04-16T10:03:00.000Z",64 clientVersion: 1,65 };66}6768function createProgressFor(69 puzzleId: string,70 status: PuzzleProgress["status"],71 updatedAt: string,72): PuzzleProgress {73 return {74 ...createProgress(),75 date: puzzleId.split("-").slice(1, 4).join("-"),76 puzzleId,77 status,78 updatedAt,79 lastViewedAt: updatedAt,80 };81}8283Deno.test("buildProgressKey and buildHistoryKey use versioned storage keys", () => {84 assertEquals(85 buildProgressKey("daily-2026-04-16-0"),86 "shikaku:v1:progress:daily-2026-04-16-0",87 );88 assertEquals(buildHistoryKey(), "shikaku:v1:history");89});9091Deno.test("savePuzzleProgress and loadPuzzleProgress roundtrip progress", () => {92 const storage = new MemoryStorage();93 const progress = createProgress();9495 savePuzzleProgress(storage, progress);9697 assertEquals(loadPuzzleProgress(storage, progress.puzzleId), progress);98});99100Deno.test("loadPuzzleProgress ignores malformed json", () => {101 const storage = new MemoryStorage();102 storage.setItem(buildProgressKey("daily-2026-04-16-0"), "{not-json");103104 assertEquals(loadPuzzleProgress(storage, "daily-2026-04-16-0"), null);105});106107Deno.test("archiveProgress appends history entry and removes saved progress", () => {108 const storage = new MemoryStorage();109 const progress = createProgress();110 savePuzzleProgress(storage, progress);111112 archiveProgress(storage, {113 date: progress.date,114 puzzleId: progress.puzzleId,115 status: "expired_unsolved",116 mistakes: progress.mistakes,117 });118119 assertEquals(loadPuzzleProgress(storage, progress.puzzleId), null);120 assertEquals(121 storage.getItem(buildHistoryKey()),122 JSON.stringify([123 {124 date: progress.date,125 puzzleId: progress.puzzleId,126 status: "expired_unsolved",127 mistakes: progress.mistakes,128 },129 ]),130 );131});132133Deno.test("getAchievedStackIndex returns the highest solved index for a day", () => {134 const storage = new MemoryStorage();135136 savePuzzleProgress(storage, {137 ...createProgress(),138 puzzleId: "daily-2026-04-16-0",139 status: "solved",140 });141 savePuzzleProgress(storage, {142 ...createProgress(),143 puzzleId: "daily-2026-04-16-1",144 status: "solved",145 });146 savePuzzleProgress(storage, {147 ...createProgress(),148 puzzleId: "daily-2026-04-16-2",149 status: "in_progress",150 });151152 assertEquals(getAchievedStackIndex(storage, "2026-04-16"), 1);153 assertEquals(getAchievedStackIndex(storage, "2026-04-17"), 0);154});155156Deno.test("getPlayableStackIndex starts at zero and unlocks the next stack only after a solve", () => {157 const storage = new MemoryStorage();158159 assertEquals(getPlayableStackIndex(storage, "2026-04-16"), 0);160161 savePuzzleProgress(storage, {162 ...createProgress(),163 puzzleId: "daily-2026-04-16-0",164 status: "in_progress",165 });166 assertEquals(getPlayableStackIndex(storage, "2026-04-16"), 0);167168 savePuzzleProgress(storage, {169 ...createProgress(),170 puzzleId: "daily-2026-04-16-0",171 status: "solved",172 });173 assertEquals(getPlayableStackIndex(storage, "2026-04-16"), 1);174175 savePuzzleProgress(storage, {176 ...createProgress(),177 puzzleId: "daily-2026-04-16-1",178 status: "in_progress",179 });180 assertEquals(getPlayableStackIndex(storage, "2026-04-16"), 1);181});182183Deno.test("getPlayableStackIndex ignores non-contiguous solved entries", () => {184 const storage = new MemoryStorage();185186 savePuzzleProgress(storage, {187 ...createProgress(),188 puzzleId: "daily-2026-04-16-1",189 status: "solved",190 });191192 assertEquals(getPlayableStackIndex(storage, "2026-04-16"), 0);193});194195Deno.test("getMostRecentStackIndex returns the last touched stack for a date", () => {196 const storage = new MemoryStorage();197198 savePuzzleProgress(199 storage,200 createProgressFor(201 "daily-2026-04-16-0",202 "solved",203 "2026-04-16T10:03:00.000Z",204 ),205 );206 savePuzzleProgress(207 storage,208 createProgressFor(209 "daily-2026-04-16-2",210 "in_progress",211 "2026-04-16T10:07:00.000Z",212 ),213 );214215 assertEquals(getMostRecentStackIndex(storage, "2026-04-16"), 2);216 assertEquals(getMostRecentStackIndex(storage, "2026-04-17"), 0);217});218219Deno.test("getVisibleStackRange exposes the highest touched stack for date navigation", () => {220 const storage = new MemoryStorage();221222 savePuzzleProgress(223 storage,224 createProgressFor(225 "daily-2026-04-16-0",226 "solved",227 "2026-04-16T10:03:00.000Z",228 ),229 );230 savePuzzleProgress(231 storage,232 createProgressFor(233 "daily-2026-04-16-3",234 "in_progress",235 "2026-04-16T10:07:00.000Z",236 ),237 );238239 assertEquals(getVisibleStackRange(storage, "2026-04-16"), { min: 0, max: 3 });240 assertEquals(getVisibleStackRange(storage, "2026-04-17"), { min: 0, max: 0 });241});242243Deno.test("getSelectedStackIndex returns the top stack for a date", () => {244 const storage = new MemoryStorage();245246 savePuzzleProgress(247 storage,248 createProgressFor(249 "daily-2026-04-16-0",250 "solved",251 "2026-04-16T10:03:00.000Z",252 ),253 );254 savePuzzleProgress(255 storage,256 createProgressFor(257 "daily-2026-04-16-2",258 "in_progress",259 "2026-04-16T10:07:00.000Z",260 ),261 );262263 assertEquals(getSelectedStackIndex(storage, "2026-04-16"), 2);264 assertEquals(getSelectedStackIndex(storage, "2026-04-17"), 0);265});266267Deno.test("getSelectedStackIndex prefers the highest visible stack over a lower recent one", () => {268 const storage = new MemoryStorage();269270 savePuzzleProgress(271 storage,272 createProgressFor(273 "daily-2026-04-16-1",274 "in_progress",275 "2026-04-16T10:09:00.000Z",276 ),277 );278 savePuzzleProgress(279 storage,280 createProgressFor(281 "daily-2026-04-16-2",282 "solved",283 "2026-04-16T10:05:00.000Z",284 ),285 );286287 assertEquals(getSelectedStackIndex(storage, "2026-04-16"), 2);288});289290Deno.test("getDailyStreak counts consecutive solved days ending today", () => {291 const storage = new MemoryStorage();292 savePuzzleProgress(293 storage,294 createProgressFor("daily-2026-04-18-0", "solved", "t"),295 );296 savePuzzleProgress(297 storage,298 createProgressFor("daily-2026-04-17-1", "solved", "t"),299 );300 savePuzzleProgress(301 storage,302 createProgressFor("daily-2026-04-16-0", "in_progress", "t"),303 );304305 assertEquals(getDailyStreak(storage, "2026-04-18"), 2);306});307308Deno.test("getDailyStreak survives an unsolved today", () => {309 const storage = new MemoryStorage();310 savePuzzleProgress(311 storage,312 createProgressFor("daily-2026-04-17-0", "solved", "t"),313 );314 savePuzzleProgress(315 storage,316 createProgressFor("daily-2026-04-16-0", "solved", "t"),317 );318319 assertEquals(getDailyStreak(storage, "2026-04-18"), 2);320 assertEquals(getDailyStreak(storage, "2026-04-20"), 0);321});