diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd31904..ecde0b8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,6 +33,9 @@ jobs: - name: TypeScript run: npm run typecheck + - name: Vitest(テスト+カバレッジ) + run: npm run test:coverage + bundle: name: Metro bundle smoke test runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index d914c32..083caab 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ # dependencies node_modules/ +# testing +coverage/ + # Expo .expo/ dist/ diff --git a/README.md b/README.md index 5426e52..8ae9534 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ - **2種類の手帳** - **プロフィール帳式** — 最初からかわいい飾り枠つき。工程フローや自由帳ページを作れる - **ノート式** — 白紙のキャンバスに、テキスト・写真・シールを自由に配置してコラージュ -- **工程フロー** — 工程をチェックリストで並べる。完了するとシールが弾ける演出。**if分岐**(はい/いいえ)で条件分けもできる +- **工程フロー** — 工程をチェックリストで並べる。完了するとシールが弾ける演出。**if分岐**(はい/いいえ)で条件分けもでき、分岐の中にさらにif分岐を**入れ子**にして深い条件分けも作れる - **フリーコラージュ** — テキストカード・写真・シールをドラッグ/拡大/回転で自由配置。カード同士を**つなぎ線**で結べる - **シール** — 星・花・ハート・リボン・きらめきの5種類。好きな大きさ・角度で貼れる - **飾り枠** — シンプル/オーロラ滲み/星屑ちらし/リボン留め diff --git a/app.json b/app.json index 1f663c7..68b212b 100644 --- a/app.json +++ b/app.json @@ -2,7 +2,7 @@ "expo": { "name": "かわいくデコれる手帳", "slug": "kawaii-deconote", - "version": "0.1.0", + "version": "0.2.0", "orientation": "portrait", "icon": "./assets/icon.png", "scheme": "kawaiideconote", @@ -15,7 +15,7 @@ }, "android": { "package": "io.github.pppzet.kawaiideconote", - "versionCode": 1, + "versionCode": 2, "adaptiveIcon": { "foregroundImage": "./assets/adaptive-icon.png", "backgroundColor": "#F3E9F7" diff --git a/package-lock.json b/package-lock.json index af74e40..538a334 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "kawaii-deconote", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "kawaii-deconote", - "version": "0.1.0", + "version": "0.2.0", "dependencies": { "@expo-google-fonts/m-plus-rounded-1c": "^0.4.1", "@expo-google-fonts/yomogi": "^0.4.1", @@ -39,10 +39,12 @@ }, "devDependencies": { "@types/react": "~19.1.0", + "@vitest/coverage-v8": "^4.1.10", "oxlint": "^1.73.0", "prettier": "^3.9.5", "sharp": "^0.35.3", - "typescript": "~5.9.2" + "typescript": "~5.9.2", + "vitest": "^4.1.10" } }, "node_modules/@0no-co/graphql.web": { @@ -1536,6 +1538,16 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@egjs/hammerjs": { "version": "2.0.17", "resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz", @@ -1548,6 +1560,18 @@ "node": ">=0.8.0" } }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, "node_modules/@emnapi/runtime": { "version": "1.11.2", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", @@ -1559,6 +1583,17 @@ "tslib": "^2.4.0" } }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@expo-google-fonts/m-plus-rounded-1c": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/@expo-google-fonts/m-plus-rounded-1c/-/m-plus-rounded-1c-0.4.4.tgz", @@ -2763,6 +2798,35 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@oxlint/binding-android-arm-eabi": { "version": "1.74.0", "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.74.0.tgz", @@ -3649,6 +3713,281 @@ "nanoid": "^3.3.11" } }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, "node_modules/@sinclair/typebox": { "version": "0.27.12", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.12.tgz", @@ -3673,6 +4012,24 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -3714,6 +4071,31 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -3822,6 +4204,150 @@ "@urql/core": "^5.0.0" } }, + "node_modules/@vitest/coverage-v8": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.10.tgz", + "integrity": "sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.10", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.10", + "vitest": "4.1.10" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@xmldom/xmldom": { "version": "0.8.13", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", @@ -3986,6 +4512,35 @@ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "license": "MIT" }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.5.tgz", + "integrity": "sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/async-limiter": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", @@ -4425,6 +4980,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -5027,6 +5592,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "dev": true, + "license": "MIT" + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -5067,6 +5639,16 @@ "node": ">=4" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -5082,7 +5664,17 @@ "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", "license": "MIT", "engines": { - "node": ">=6" + "node": ">=6" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" } }, "node_modules/expo": { @@ -6218,6 +6810,13 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC" }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/html2canvas": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", @@ -6455,6 +7054,35 @@ "node": ">=8" } }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/jest-environment-node": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", @@ -7118,6 +7746,57 @@ "yallist": "^3.0.2" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz", + "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -7681,6 +8360,20 @@ "node": ">=0.10.0" } }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -8027,6 +8720,13 @@ "node": "20 || >=22" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -8937,6 +9637,40 @@ "node": "*" } }, + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -9206,6 +9940,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -9329,6 +10070,13 @@ "node": ">=8" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/stackframe": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", @@ -9365,6 +10113,13 @@ "node": ">= 0.6" } }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, "node_modules/stream-buffers": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-2.2.0.tgz", @@ -9656,6 +10411,23 @@ "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -9701,6 +10473,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -10178,6 +10960,229 @@ } } }, + "node_modules/vite": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vite/node_modules/postcss": { + "version": "8.5.22", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz", + "integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/vitest": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/vlq": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/vlq/-/vlq-1.0.1.tgz", @@ -10252,6 +11257,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wonka": { "version": "6.3.6", "resolved": "https://registry.npmjs.org/wonka/-/wonka-6.3.6.tgz", diff --git a/package.json b/package.json index 543751f..7b8ccb3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "kawaii-deconote", - "version": "0.1.0", + "version": "0.2.0", "main": "expo-router/entry", "scripts": { "start": "expo start", @@ -8,6 +8,9 @@ "ios": "expo run:ios", "typecheck": "tsc --noEmit", "typecheck:watch": "tsc --noEmit --watch", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", "lint": "oxlint --format agent", "lint:fix": "oxlint --fix", "lint:strict": "oxlint --deny-warnings", @@ -47,10 +50,12 @@ }, "devDependencies": { "@types/react": "~19.1.0", + "@vitest/coverage-v8": "^4.1.10", "oxlint": "^1.73.0", "prettier": "^3.9.5", "sharp": "^0.35.3", - "typescript": "~5.9.2" + "typescript": "~5.9.2", + "vitest": "^4.1.10" }, "private": true } diff --git a/src/components/flowchart/IfCard.tsx b/src/components/flowchart/IfCard.tsx index fad2885..0534aa1 100644 --- a/src/components/flowchart/IfCard.tsx +++ b/src/components/flowchart/IfCard.tsx @@ -1,17 +1,38 @@ import { Pressable, StyleSheet, Text, TextInput, View } from "react-native"; import { colors, fonts, radii } from "@/lib/theme"; import { useNotebooks } from "@/state/notebooks"; -import type { BranchStep, IfStep } from "@/lib/types"; - -type BranchKey = "yes" | "no"; +import type { BranchKey, IfStep, NormalStep } from "@/lib/types"; +// 最上位(cb.steps 直下)の if 分岐カード。上下移動・削除ボタン付き。 export function IfCard({ step, index, count }: { step: IfStep; index: number; count: number }) { + const deleteStep = useNotebooks((s) => s.deleteStep); + return deleteStep(step.id)} move={{ index, count }} />; +} + +// if 分岐カードの共通描画(トップレベルでも分岐の中=ネストでも使う再帰対応版)。 +function IfCardNode({ + step, + depth, + onDelete, + move, +}: { + step: IfStep; + depth: number; + onDelete: () => void; + move?: { index: number; count: number }; +}) { const setStepText = useNotebooks((s) => s.setStepText); const moveStep = useNotebooks((s) => s.moveStep); - const deleteStep = useNotebooks((s) => s.deleteStep); return ( - + = 2 && styles.cardNest2, + ]} + > 🔀もし… - moveStep(index, -1)} disabled={index === 0} hitSlop={4}> - - - moveStep(index, 1)} disabled={index === count - 1} hitSlop={4}> - - - deleteStep(step.id)} hitSlop={4}> + {move ? ( + <> + moveStep(move.index, -1)} disabled={move.index === 0} hitSlop={4}> + + + moveStep(move.index, 1)} disabled={move.index === move.count - 1} hitSlop={4}> + + + + ) : null} + - - + + ); } -function BranchColumn({ step, branchKey }: { step: IfStep; branchKey: BranchKey }) { +function BranchColumn({ step, branchKey, depth }: { step: IfStep; branchKey: BranchKey; depth: number }) { const setBranchLabel = useNotebooks((s) => s.setBranchLabel); const addBranchStep = useNotebooks((s) => s.addBranchStep); + const addBranchIfStep = useNotebooks((s) => s.addBranchIfStep); return ( @@ -55,39 +81,58 @@ function BranchColumn({ step, branchKey }: { step: IfStep; branchKey: BranchKey maxLength={12} style={styles.branchLabel} /> - {step.branches[branchKey].map((b) => ( - - ))} - addBranchStep(step.id, branchKey)}> - - + {/* 分岐の中身は「工程カード」か「入れ子 if 分岐カード」のどちらか */} + {step.branches[branchKey].map((item) => + item.type === "if" ? ( + + ) : ( + + ), + )} + + addBranchStep(step.id, branchKey)}> + +工程 + + addBranchIfStep(step.id, branchKey)}> + +🔀if + + ); } -function BranchStepRow({ stepId, branchKey, branch }: { stepId: string; branchKey: BranchKey; branch: BranchStep }) { - const setBranchStepText = useNotebooks((s) => s.setBranchStepText); - const toggleBranchStep = useNotebooks((s) => s.toggleBranchStep); - const deleteBranchStep = useNotebooks((s) => s.deleteBranchStep); +function NestedIfItem({ step, depth }: { step: IfStep; depth: number }) { + const deleteStep = useNotebooks((s) => s.deleteStep); + return ( + + deleteStep(step.id)} /> + + ); +} + +function BranchStepRow({ branch }: { branch: NormalStep }) { + const setStepText = useNotebooks((s) => s.setStepText); + const toggleStep = useNotebooks((s) => s.toggleStep); + const deleteStep = useNotebooks((s) => s.deleteStep); return ( toggleBranchStep(stepId, branchKey, branch.id)} + onPress={() => toggleStep(branch.id)} hitSlop={4} > {branch.done ? "✓" : ""} setBranchStepText(stepId, branchKey, branch.id, t)} + onChangeText={(t) => setStepText(branch.id, t)} multiline placeholder="工程を入力…" placeholderTextColor="rgba(90,77,112,0.4)" style={[styles.branchText, branch.done && styles.branchTextDone]} /> - deleteBranchStep(stepId, branchKey, branch.id)} hitSlop={4}> + deleteStep(branch.id)} hitSlop={4}> @@ -97,8 +142,6 @@ function BranchStepRow({ stepId, branchKey, branch }: { stepId: string; branchKe const styles = StyleSheet.create({ card: { width: "100%", - maxWidth: 420, - alignSelf: "center", backgroundColor: "#fff8ef", borderWidth: 1.5, borderColor: "#d9a441", @@ -107,6 +150,21 @@ const styles = StyleSheet.create({ padding: 12, boxShadow: "0 3px 10px rgba(90,70,110,0.1)", }, + cardTop: { + maxWidth: 420, + alignSelf: "center", + }, + cardNested: { + padding: 10, + }, + cardNest1: { + backgroundColor: "#faf5ff", + borderColor: "#c9a0d9", + }, + cardNest2: { + backgroundColor: "#f2f8ff", + borderColor: "#a3c9e0", + }, head: { flexDirection: "row", alignItems: "flex-start", @@ -138,12 +196,12 @@ const styles = StyleSheet.create({ iconHidden: { opacity: 0, }, + // スマホ幅では「はい/いいえ」を縦積みにして、ネストが深くても横幅で潰れないようにする。 branches: { - flexDirection: "row", gap: 10, }, branch: { - flex: 1, + width: "100%", backgroundColor: "rgba(255,255,255,0.6)", borderRadius: 12, padding: 8, @@ -156,6 +214,9 @@ const styles = StyleSheet.create({ textAlign: "center", padding: 2, }, + nestedIfWrap: { + marginVertical: 2, + }, branchStep: { flexDirection: "row", alignItems: "flex-start", @@ -204,8 +265,13 @@ const styles = StyleSheet.create({ color: "#b5a8c4", paddingTop: 2, }, + branchAddRow: { + flexDirection: "row", + gap: 6, + justifyContent: "center", + flexWrap: "wrap", + }, branchAdd: { - alignSelf: "center", borderWidth: 1, borderColor: "rgba(155,130,180,0.4)", borderStyle: "dashed", diff --git a/src/lib/format.test.ts b/src/lib/format.test.ts new file mode 100644 index 0000000..2016b39 --- /dev/null +++ b/src/lib/format.test.ts @@ -0,0 +1,83 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { clamp, degrees, fileStamp, pad2, randomOf, todayStamp } from "./format"; + +describe("pad2", () => { + it("2桁未満はゼロ埋めする", () => { + expect(pad2(0)).toBe("00"); + expect(pad2(5)).toBe("05"); + expect(pad2(9)).toBe("09"); + }); + + it("2桁以上はそのまま文字列化する", () => { + expect(pad2(10)).toBe("10"); + expect(pad2(42)).toBe("42"); + expect(pad2(100)).toBe("100"); + }); +}); + +describe("todayStamp / fileStamp", () => { + afterEach(() => { + vi.useRealTimers(); + }); + + it("todayStamp は YYYY-MM-DD 形式(月日ゼロ埋め)", () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date(2026, 6, 5, 9, 3)); // 2026-07-05 09:03 + expect(todayStamp()).toBe("2026-07-05"); + }); + + it("fileStamp は YYYYMMDD-HHMM 形式(時分ゼロ埋め)", () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date(2026, 11, 31, 0, 7)); // 2026-12-31 00:07 + expect(fileStamp()).toBe("20261231-0007"); + }); +}); + +describe("randomOf", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("空配列では undefined を返す", () => { + expect(randomOf([])).toBeUndefined(); + }); + + it("Math.random に応じた要素を返す", () => { + const arr = ["a", "b", "c", "d"] as const; + vi.spyOn(Math, "random").mockReturnValue(0); + expect(randomOf(arr)).toBe("a"); + vi.spyOn(Math, "random").mockReturnValue(0.99); + expect(randomOf(arr)).toBe("d"); + }); + + it("返す値は必ず配列の要素", () => { + const arr = [10, 20, 30]; + for (let i = 0; i < 20; i++) { + expect(arr).toContain(randomOf(arr)); + } + }); +}); + +describe("clamp", () => { + it("範囲内はそのまま", () => { + expect(clamp(5, 0, 10)).toBe(5); + }); + + it("下限・上限で丸める", () => { + expect(clamp(-3, 0, 10)).toBe(0); + expect(clamp(99, 0, 10)).toBe(10); + }); + + it("境界値はそのまま", () => { + expect(clamp(0, 0, 10)).toBe(0); + expect(clamp(10, 0, 10)).toBe(10); + }); +}); + +describe("degrees", () => { + it("ラジアンを度に変換する", () => { + expect(degrees(Math.PI)).toBeCloseTo(180); + expect(degrees(Math.PI / 2)).toBeCloseTo(90); + expect(degrees(0)).toBe(0); + }); +}); diff --git a/src/lib/model.test.ts b/src/lib/model.test.ts new file mode 100644 index 0000000..6f17879 --- /dev/null +++ b/src/lib/model.test.ts @@ -0,0 +1,638 @@ +import { describe, expect, it } from "vitest"; +import { + appendToBranch, + findStepInTree, + newId, + newIfStep, + newNotebook, + newPage, + newStep, + normalizeNotebook, + normalizePage, + normalizeState, + notebookDisplayName, + pageDisplayTitle, + removeStepFromTree, + seedUid, + updateStepInTree, +} from "./model"; +import type { IfStep, NormalStep, Step } from "./types"; + +// --- テスト用の工程ツリー・ビルダー --- +const plain = (id: string, text = "", done = false): NormalStep => ({ id, type: "step", text, done }); +const ifStep = (id: string, yes: Step[] = [], no: Step[] = []): IfStep => ({ + id, + type: "if", + text: "", + done: false, + labels: { yes: "はい", no: "いいえ" }, + branches: { yes, no }, +}); + +const idNum = (id: string): number => parseInt(id.replace(/^id(\d+)_.*/, "$1"), 10); + +describe("factories", () => { + it("newStep は空の工程を作る", () => { + const s = newStep(); + expect(s.type).toBe("step"); + expect(s.text).toBe(""); + expect(s.done).toBe(false); + expect(s.id).toBeTruthy(); + }); + + it("newIfStep は空の分岐と既定ラベルを持つ", () => { + const s = newIfStep(); + expect(s.type).toBe("if"); + expect(s.labels).toEqual({ yes: "はい", no: "いいえ" }); + expect(s.branches).toEqual({ yes: [], no: [] }); + }); + + it("newPage は既定値を持つフローチャートページ", () => { + const p = newPage(); + expect(p.type).toBe("flowchart"); + expect(p.frame).toBe("aurora"); + expect(p.steps).toEqual([]); + expect(p.stickers).toEqual([]); + expect(p.ruleStyle).toBe("lines"); + }); + + it("newNotebook は1ページを持ち activePageId が一致する", () => { + const nb = newNotebook("notestyle", "ノート", "#A8E6CF"); + expect(nb.pages).toHaveLength(1); + expect(nb.activePageId).toBe(nb.pages[0]?.id); + expect(nb.type).toBe("notestyle"); + expect(nb.name).toBe("ノート"); + }); + + it("newId は毎回ユニーク", () => { + const ids = new Set(Array.from({ length: 50 }, () => newId())); + expect(ids.size).toBe(50); + }); +}); + +describe("seedUid", () => { + it("ネストした工程まで走査して最大番号の次から採番する", () => { + seedUid({ + activeNotebookId: null, + notebooks: [ + { + id: "id10_a", + name: "", + type: "profile", + color: "#C9B6E4", + activePageId: "id20_b", + pages: [ + { + ...newPage(), + id: "id20_b", + steps: [ifStep("id30_c", [plain("id900_deep")], [])], + }, + ], + }, + ], + }); + // ネストの id900 を拾えていれば、次の採番は 901。 + expect(idNum(newId())).toBe(901); + }); +}); + +describe("updateStepInTree", () => { + it("トップレベルの工程を更新する", () => { + const tree: Step[] = [plain("a", "old"), plain("b")]; + const out = updateStepInTree(tree, "a", (s) => ({ ...s, text: "new" })); + expect((out[0] as NormalStep).text).toBe("new"); + expect((out[1] as NormalStep).text).toBe(""); + }); + + it("分岐の中(ネスト)の工程を更新する", () => { + const tree: Step[] = [ifStep("if1", [plain("y1", "old")], [plain("n1")])]; + const out = updateStepInTree(tree, "y1", (s) => ({ ...s, text: "changed" })); + const branchYes = (out[0] as IfStep).branches.yes[0] as NormalStep; + expect(branchYes.text).toBe("changed"); + }); + + it("2段以上深いネストも更新する", () => { + const tree: Step[] = [ifStep("if1", [ifStep("if2", [plain("deep", "old")])])]; + const out = updateStepInTree(tree, "deep", (s) => ({ ...s, text: "deepnew" })); + const deep = ((out[0] as IfStep).branches.yes[0] as IfStep).branches.yes[0] as NormalStep; + expect(deep.text).toBe("deepnew"); + }); + + it("元の配列・オブジェクトを破壊しない", () => { + const original = plain("a", "keep"); + const tree: Step[] = [original]; + const out = updateStepInTree(tree, "a", (s) => ({ ...s, text: "new" })); + expect(original.text).toBe("keep"); + expect(out).not.toBe(tree); + }); + + it("該当 id が無ければ内容は変わらない", () => { + const tree: Step[] = [plain("a", "x")]; + const out = updateStepInTree(tree, "zzz", (s) => ({ ...s, text: "!" })); + expect((out[0] as NormalStep).text).toBe("x"); + }); +}); + +describe("removeStepFromTree", () => { + it("トップレベルの工程を削除する", () => { + const tree: Step[] = [plain("a"), plain("b"), plain("c")]; + const out = removeStepFromTree(tree, "b"); + expect(out.map((s) => s.id)).toEqual(["a", "c"]); + }); + + it("分岐の中の工程を削除し、兄弟は残す", () => { + const tree: Step[] = [ifStep("if1", [plain("y1"), plain("y2")], [plain("n1")])]; + const out = removeStepFromTree(tree, "y1"); + const top = out[0] as IfStep; + expect(top.branches.yes.map((s) => s.id)).toEqual(["y2"]); + expect(top.branches.no.map((s) => s.id)).toEqual(["n1"]); + }); + + it("ネストした if カードを丸ごと削除する(子ごと消える)", () => { + const tree: Step[] = [ifStep("if1", [ifStep("if2", [plain("deep")])], [])]; + const out = removeStepFromTree(tree, "if2"); + expect((out[0] as IfStep).branches.yes).toHaveLength(0); + expect(findStepInTree(out, "deep")).toBeUndefined(); + }); +}); + +describe("findStepInTree", () => { + const tree: Step[] = [plain("a"), ifStep("if1", [plain("y1"), ifStep("if2", [plain("deep")])], [plain("n1")])]; + + it("トップレベル・ネスト・深いネストいずれも見つける", () => { + expect(findStepInTree(tree, "a")?.id).toBe("a"); + expect(findStepInTree(tree, "y1")?.id).toBe("y1"); + expect(findStepInTree(tree, "n1")?.id).toBe("n1"); + expect(findStepInTree(tree, "deep")?.id).toBe("deep"); + }); + + it("見つからなければ undefined", () => { + expect(findStepInTree(tree, "missing")).toBeUndefined(); + }); +}); + +describe("appendToBranch", () => { + it("トップの if の yes / no に追加する", () => { + const tree: Step[] = [ifStep("if1")]; + const withYes = appendToBranch(tree, "if1", "yes", plain("y1")); + expect((withYes[0] as IfStep).branches.yes.map((s) => s.id)).toEqual(["y1"]); + const withNo = appendToBranch(withYes, "if1", "no", newIfStep()); + expect((withNo[0] as IfStep).branches.no[0]?.type).toBe("if"); + }); + + it("ネストした if の分岐にも追加できる", () => { + const tree: Step[] = [ifStep("if1", [ifStep("if2")], [])]; + const out = appendToBranch(tree, "if2", "yes", plain("newDeep")); + const nested = (out[0] as IfStep).branches.yes[0] as IfStep; + expect(nested.branches.yes.map((s) => s.id)).toEqual(["newDeep"]); + }); + + it("存在しない id には何もしない", () => { + const tree: Step[] = [ifStep("if1")]; + const out = appendToBranch(tree, "zzz", "yes", plain("x")); + expect((out[0] as IfStep).branches.yes).toHaveLength(0); + }); + + it("対象が if でなければ(工程カードの id)追加しない", () => { + const tree: Step[] = [plain("a")]; + const out = appendToBranch(tree, "a", "yes", plain("x")); + expect(out[0]).toEqual(plain("a")); + }); +}); + +describe("normalizeState(ネスト対応の取り込み)", () => { + it("旧フォーマットの分岐工程(type なし)を type:'step' に変換する", () => { + const page = normalizePage({ + type: "flowchart", + steps: [ + { + id: "if1", + type: "if", + text: "cond", + branches: { yes: [{ id: "b1", text: "x", done: true }], no: [] }, + }, + ], + }); + const top = page.steps[0] as IfStep; + expect(top.type).toBe("if"); + expect(top.branches.yes[0]).toEqual({ id: "b1", type: "step", text: "x", done: true }); + // ラベル未指定は既定値 + expect(top.labels).toEqual({ yes: "はい", no: "いいえ" }); + }); + + it("分岐に入れ子になった if を再帰的に正規化する", () => { + const page = normalizePage({ + steps: [ + { + id: "if1", + type: "if", + branches: { + yes: [{ id: "if2", type: "if", branches: { yes: [{ id: "deep", text: "d", done: false }], no: [] } }], + no: [], + }, + }, + ], + }); + const nested = (page.steps[0] as IfStep).branches.yes[0] as IfStep; + expect(nested.type).toBe("if"); + const deep = nested.branches.yes[0] as NormalStep; + expect(deep).toEqual({ id: "deep", type: "step", text: "d", done: false }); + }); + + it("ネストを含む文書をまるごと往復できる", () => { + const state = normalizeState({ + activeNotebookId: "nb1", + notebooks: [ + { + id: "nb1", + name: "テスト", + type: "profile", + color: "#C9B6E4", + activePageId: "pg1", + pages: [ + { + id: "pg1", + type: "flowchart", + title: "t", + steps: [ + { + id: "if1", + type: "if", + text: "cond", + labels: { yes: "Y", no: "N" }, + branches: { + yes: [ + { id: "s1", text: "plain", done: false }, + { + id: "if2", + type: "if", + text: "nested", + branches: { yes: [{ id: "s2", text: "deep", done: true }], no: [] }, + }, + ], + no: [], + }, + }, + ], + }, + ], + }, + ], + }); + expect(state.activeNotebookId).toBe("nb1"); + const top = state.notebooks[0]?.pages[0]?.steps[0] as IfStep; + expect(top.labels).toEqual({ yes: "Y", no: "N" }); + expect((top.branches.yes[0] as NormalStep).type).toBe("step"); + const nested = top.branches.yes[1] as IfStep; + expect(nested.type).toBe("if"); + expect(nested.text).toBe("nested"); + expect(nested.branches.yes[0]).toEqual({ id: "s2", type: "step", text: "deep", done: true }); + }); + + it("ネストも含めて重複 id を解消する", () => { + const state = normalizeState({ + notebooks: [ + { + id: "nb1", + type: "profile", + activePageId: "pg1", + pages: [ + { + id: "pg1", + steps: [ + { id: "dup", type: "step", text: "top" }, + { id: "if1", type: "if", branches: { yes: [{ id: "dup", text: "branch" }], no: [] } }, + ], + }, + ], + }, + ], + }); + const steps = state.notebooks[0]?.pages[0]?.steps ?? []; + const branchId = (steps[1] as IfStep).branches.yes[0]?.id; + expect(steps[0]?.id).toBe("dup"); + expect(branchId).toBeTruthy(); + expect(branchId).not.toBe("dup"); + }); + + it("無効な activeNotebookId は null になる", () => { + const state = normalizeState({ + activeNotebookId: "does-not-exist", + notebooks: [{ id: "nb1", type: "profile", activePageId: "pg1", pages: [{ id: "pg1", steps: [] }] }], + }); + expect(state.activeNotebookId).toBeNull(); + }); +}); + +describe("normalizeNotebook", () => { + it("非オブジェクトは null", () => { + expect(normalizeNotebook(null)).toBeNull(); + expect(normalizeNotebook(42)).toBeNull(); + }); + + it("有効なページが無ければ null", () => { + expect(normalizeNotebook({ id: "nb1", pages: [] })).toBeNull(); + }); + + it("旧 rollbahn タイプは notestyle に移行する", () => { + const nb = normalizeNotebook({ + id: "nb1", + type: "rollbahn", + activePageId: "pg1", + pages: [{ id: "pg1", steps: [] }], + }); + expect(nb?.type).toBe("notestyle"); + }); + + it("notestyle では note をシェイプへ移し note を空にする", () => { + const nb = normalizeNotebook({ + id: "nb1", + type: "notestyle", + activePageId: "pg1", + pages: [{ id: "pg1", type: "notebook", note: "メモ本文", shapes: [] }], + }); + const page = nb?.pages[0]; + expect(page?.note).toBe(""); + expect(page?.shapes.some((s) => s.text === "メモ本文")).toBe(true); + }); + + it("activePageId が無効なら先頭ページに合わせる", () => { + const nb = normalizeNotebook({ + id: "nb1", + type: "profile", + activePageId: "nope", + pages: [ + { id: "pg1", steps: [] }, + { id: "pg2", steps: [] }, + ], + }); + expect(nb?.activePageId).toBe("pg1"); + }); +}); + +describe("normalizePage", () => { + it("空オブジェクトから既定値を埋める", () => { + const p = normalizePage({}); + expect(p.type).toBe("flowchart"); + expect(p.frame).toBe("aurora"); + expect(p.ruleStyle).toBe("lines"); + expect(p.paperColor).toBe("#FBF7F2"); + expect(p.steps).toEqual([]); + }); + + it("存在しないシェイプを指す矢印は除去し、両端がそろう矢印は残す", () => { + const p = normalizePage({ + shapes: [ + { id: "sh1", text: "A", x: 0, y: 0, w: 150, rot: 0 }, + { id: "sh2", text: "B", x: 0, y: 0, w: 150, rot: 0 }, + ], + arrows: [ + { id: "ar1", from: "sh1", to: "sh2", manual: false }, + { id: "ar2", from: "sh1", to: "ghost", manual: false }, + ], + }); + expect(p.arrows.map((a) => a.id)).toEqual(["ar1"]); + }); + + it("不正な dataUrl の写真は除去する", () => { + const p = normalizePage({ + photos: [ + { id: "ph1", x: 0, y: 0, w: 140, rot: 0, dataUrl: "data:image/png;base64,AAAA" }, + { id: "ph2", x: 0, y: 0, w: 140, rot: 0, dataUrl: "not-an-image" }, + ], + }); + expect(p.photos.map((ph) => ph.id)).toEqual(["ph1"]); + }); +}); + +describe("正規化の細部(クランプ・フォールバック・矢印)", () => { + it("ステッカーは種別・座標・サイズをクランプ/既定化する", () => { + const p = normalizePage({ + stickers: [ + { id: "st1", type: "bogus", x: -5, y: 99999, size: 999, rot: 12 }, + { id: "st2", type: "heart", x: 10, y: 20, size: 44, rot: 0 }, + ], + }); + expect(p.stickers[0]?.type).toBe("star"); + expect(p.stickers[0]?.x).toBe(0); + expect(p.stickers[0]?.y).toBe(20000); + expect(p.stickers[0]?.size).toBe(160); + expect(p.stickers[1]?.type).toBe("heart"); + }); + + it("シェイプ幅はクランプし、数値でない座標は 0 にする", () => { + const p = normalizePage({ + shapes: [ + { id: "sh1", text: "wide", x: "oops", y: 5, w: 9999, rot: 0 }, + { id: "sh2", text: "narrow", x: 0, y: 0, w: 1, rot: 0 }, + ], + }); + expect(p.shapes[0]?.w).toBe(420); + expect(p.shapes[0]?.x).toBe(0); + expect(p.shapes[1]?.w).toBe(80); + }); + + it("写真の dataUrl は image フィールドからも拾う", () => { + const p = normalizePage({ + photos: [{ id: "ph1", x: 0, y: 0, w: 140, rot: 0, image: "data:image/jpeg;base64,BBBB" }], + }); + expect(p.photos).toHaveLength(1); + expect(p.photos[0]?.dataUrl).toBe("data:image/jpeg;base64,BBBB"); + }); + + it("frame / ruleStyle / type が不正なら既定値に落とす", () => { + const p = normalizePage({ type: "bogus", frame: "bogus", ruleStyle: "bogus", paperColor: "red" }); + expect(p.type).toBe("flowchart"); + expect(p.frame).toBe("aurora"); + expect(p.ruleStyle).toBe("lines"); + expect(p.paperColor).toBe("#FBF7F2"); + }); + + it("手動矢印は範囲内なら保持し、範囲外なら自動に戻す", () => { + const p = normalizePage({ + shapes: [ + { id: "sh1", x: 0, y: 0, w: 150, rot: 0 }, + { id: "sh2", x: 0, y: 0, w: 150, rot: 0 }, + ], + arrows: [ + { id: "a-ok", from: "sh1", to: "sh2", manual: true, mx: 5, my: 6, length: 10, angle: 1.2 }, + { id: "a-len0", from: "sh1", to: "sh2", manual: true, mx: 5, my: 6, length: 0, angle: 0 }, + { id: "a-mx", from: "sh1", to: "sh2", manual: true, mx: 999999, my: 6, length: 10, angle: 0 }, + { id: "a-my", from: "sh1", to: "sh2", manual: true, mx: 6, my: 999999, length: 10, angle: 0 }, + ], + }); + const byId = Object.fromEntries(p.arrows.map((a) => [a.id, a])); + expect(byId["a-ok"]).toMatchObject({ manual: true, mx: 5, my: 6, length: 10, angle: 1.2 }); + expect(byId["a-len0"]).toMatchObject({ manual: false, mx: 0, my: 0, length: 0 }); + expect(byId["a-mx"]?.manual).toBe(false); + expect(byId["a-my"]?.manual).toBe(false); + }); + + it("from / to が欠けた矢印は捨てる", () => { + const p = normalizePage({ + shapes: [{ id: "sh2", x: 0, y: 0, w: 150, rot: 0 }], + arrows: [{ id: "nofrom", to: "sh2", manual: false }], + }); + expect(p.arrows).toHaveLength(0); + }); + + it("不正な色は notebook の既定色に落とす", () => { + const bad = normalizeNotebook({ + id: "nb1", + type: "profile", + color: "red", + activePageId: "pg1", + pages: [{ id: "pg1", steps: [] }], + }); + expect(bad?.color).toBe("#C9B6E4"); + const good = normalizeNotebook({ + id: "nb2", + type: "profile", + color: "#A8E6CF", + activePageId: "pg1", + pages: [{ id: "pg1", steps: [] }], + }); + expect(good?.color).toBe("#A8E6CF"); + }); + + it("写真 id がシェイプや他の写真と衝突したら振り直す", () => { + const state = normalizeState({ + notebooks: [ + { + id: "nb1", + type: "profile", + activePageId: "pg1", + pages: [ + { + id: "pg1", + shapes: [{ id: "shared", x: 0, y: 0, w: 150, rot: 0 }], + photos: [ + { id: "shared", x: 0, y: 0, w: 140, rot: 0, dataUrl: "data:image/png;base64,AAAA" }, + { id: "dup", x: 0, y: 0, w: 140, rot: 0, dataUrl: "data:image/png;base64,BBBB" }, + { id: "dup", x: 0, y: 0, w: 140, rot: 0, dataUrl: "data:image/png;base64,CCCC" }, + ], + }, + ], + }, + ], + }); + const photos = state.notebooks[0]?.pages[0]?.photos ?? []; + const ids = photos.map((ph) => ph.id); + expect(photos).toHaveLength(3); + expect(new Set(ids).size).toBe(3); + expect(ids).not.toContain("shared"); + }); +}); + +describe("id 採番と全要素の走査", () => { + const genId = /^id\d+_/; + + it("id が無い工程・ステッカー・シェイプ・写真には新しい id を振る", () => { + const p = normalizePage({ + steps: [ + { type: "step", text: "no-id-step" }, + { type: "if", text: "no-id-if", branches: { yes: [{ text: "no-id-branch" }], no: [] } }, + ], + stickers: [{ type: "star", x: 0, y: 0, size: 44, rot: 0 }], + shapes: [{ text: "s", x: 0, y: 0, w: 150, rot: 0 }], + photos: [{ x: 0, y: 0, w: 140, rot: 0, dataUrl: "data:image/png;base64,AAAA" }], + }); + const top = p.steps[1] as IfStep; + expect(p.steps[0]?.id).toMatch(genId); + expect(top.id).toMatch(genId); + expect(top.branches.yes[0]?.id).toMatch(genId); + expect(p.stickers[0]?.id).toMatch(genId); + expect(p.shapes[0]?.id).toMatch(genId); + expect(p.photos[0]?.id).toMatch(genId); + }); + + it("id が無い矢印にも新しい id を振る", () => { + const p = normalizePage({ + shapes: [ + { id: "sh1", x: 0, y: 0, w: 150, rot: 0 }, + { id: "sh2", x: 0, y: 0, w: 150, rot: 0 }, + ], + arrows: [{ from: "sh1", to: "sh2", manual: false }], + }); + expect(p.arrows[0]?.id).toMatch(genId); + }); + + it("id が無い notebook にも新しい id を振る", () => { + const nb = normalizeNotebook({ type: "profile", activePageId: "pg1", pages: [{ id: "pg1", steps: [] }] }); + expect(nb?.id).toMatch(genId); + }); + + it("seedUid はステッカー・矢印・両分岐まで走査し、空 id は無視する", () => { + seedUid({ + activeNotebookId: null, + notebooks: [ + { + id: "id10_a", + name: "", + type: "profile", + color: "#C9B6E4", + activePageId: "id20_b", + pages: [ + { + ...newPage(), + id: "id20_b", + steps: [ifStep("id30_c", [plain("id40_y")], [plain("id2000_n"), ifStep("id50_z", [plain("id60_d")])])], + stickers: [{ id: "id70_s", type: "star", x: 0, y: 0, rot: 0, size: 44 }], + shapes: [{ id: "", text: "", x: 0, y: 0, w: 150, rot: 0 }], + photos: [{ id: "id80_p", x: 0, y: 0, w: 140, rot: 0, dataUrl: "data:image/png;base64,AAAA" }], + arrows: [ + { id: "id90_a", from: "id70_s", to: "id70_s", manual: false, mx: 0, my: 0, length: 0, angle: 0 }, + ], + }, + ], + }, + ], + }); + // no 分岐にある id2000 が最大なので、次の採番は 2001。 + expect(idNum(newId())).toBe(2001); + }); + + it("normalizeState はステッカー・矢印・no 分岐まで dedupe を通す", () => { + const state = normalizeState({ + notebooks: [ + { + id: "nb1", + type: "profile", + activePageId: "pg1", + pages: [ + { + id: "pg1", + steps: [ + { id: "if1", type: "if", branches: { yes: [{ id: "y1", text: "y" }], no: [{ id: "n1", text: "n" }] } }, + ], + stickers: [{ id: "st1", type: "star", x: 0, y: 0, rot: 0, size: 44 }], + shapes: [ + { id: "sh1", x: 0, y: 0, w: 150, rot: 0 }, + { id: "sh2", x: 0, y: 0, w: 150, rot: 0 }, + ], + arrows: [{ id: "ar1", from: "sh1", to: "sh2", manual: false }], + }, + ], + }, + ], + }); + const pg = state.notebooks[0]?.pages[0]; + const steps = pg?.steps ?? []; + expect(pg?.stickers).toHaveLength(1); + expect(pg?.arrows).toHaveLength(1); + expect((steps[0] as IfStep).branches.no.map((s) => s.id)).toEqual(["n1"]); + }); +}); + +describe("表示名ヘルパー", () => { + it("notebookDisplayName", () => { + expect(notebookDisplayName({ name: "マイ帳", type: "profile" })).toBe("マイ帳"); + expect(notebookDisplayName({ name: "", type: "notestyle" })).toBe("無題のノート"); + expect(notebookDisplayName({ name: "", type: "profile" })).toBe("無題のプロフィール帳"); + }); + + it("pageDisplayTitle", () => { + expect(pageDisplayTitle({ title: "1ページ目" })).toBe("1ページ目"); + expect(pageDisplayTitle({ title: "" })).toBe("無題のページ"); + }); +}); diff --git a/src/lib/model.ts b/src/lib/model.ts index bef1530..c1be45b 100644 --- a/src/lib/model.ts +++ b/src/lib/model.ts @@ -1,7 +1,7 @@ import type { AppState, Arrow, - BranchStep, + BranchKey, Frame, IfStep, NormalStep, @@ -45,17 +45,18 @@ export function seedUid(state: AppState): void { const n = digits ? parseInt(digits, 10) : 0; if (Number.isFinite(n)) max = Math.max(max, n); }; + const scanStep = (st: Step): void => { + scan(st.id); + if (st.type === "if") { + for (const b of st.branches.yes) scanStep(b); + for (const b of st.branches.no) scanStep(b); + } + }; for (const nb of state.notebooks) { scan(nb.id); for (const pg of nb.pages) { scan(pg.id); - for (const st of pg.steps) { - scan(st.id); - if (st.type === "if") { - for (const b of st.branches.yes) scan(b.id); - for (const b of st.branches.no) scan(b.id); - } - } + for (const st of pg.steps) scanStep(st); for (const s of pg.stickers) scan(s.id); for (const s of pg.shapes) scan(s.id); for (const p of pg.photos) scan(p.id); @@ -103,8 +104,62 @@ export function newIfStep(): IfStep { }; } -export function newBranchStep(): BranchStep { - return { id: newId(), text: "", done: false }; +// --- 工程ツリー(ネスト対応)を不変更新するための再帰ヘルパー --- + +// id が一致する工程を木のどこにあっても探して update を適用し、新しい配列を返す。 +export function updateStepInTree(steps: Step[], id: string, update: (s: Step) => Step): Step[] { + return steps.map((s) => { + if (s.id === id) return update(s); + if (s.type === "if") { + return { + ...s, + branches: { + yes: updateStepInTree(s.branches.yes, id, update), + no: updateStepInTree(s.branches.no, id, update), + }, + }; + } + return s; + }); +} + +// id が一致する工程を木のどこにあっても取り除いて、新しい配列を返す。 +export function removeStepFromTree(steps: Step[], id: string): Step[] { + const result: Step[] = []; + for (const s of steps) { + if (s.id === id) continue; + if (s.type === "if") { + result.push({ + ...s, + branches: { + yes: removeStepFromTree(s.branches.yes, id), + no: removeStepFromTree(s.branches.no, id), + }, + }); + } else { + result.push(s); + } + } + return result; +} + +// id が一致する工程を木のどこにあっても探して返す(見つからなければ undefined)。 +export function findStepInTree(steps: Step[], id: string): Step | undefined { + for (const s of steps) { + if (s.id === id) return s; + if (s.type === "if") { + const found = findStepInTree(s.branches.yes, id) ?? findStepInTree(s.branches.no, id); + if (found) return found; + } + } + return undefined; +} + +// ifId の if 分岐(key 側)の末尾に child を追加した新しい配列を返す。 +export function appendToBranch(steps: Step[], ifId: string, key: BranchKey, child: Step): Step[] { + return updateStepInTree(steps, ifId, (s) => + s.type === "if" ? { ...s, branches: { ...s.branches, [key]: [...s.branches[key], child] } } : s, + ); } function rec(v: unknown): Record { @@ -138,11 +193,6 @@ function hexColor(v: unknown, fallback: string): string { return typeof v === "string" && HEX_COLOR.test(v) ? v : fallback; } -function normalizeBranchStep(raw: unknown): BranchStep { - const r = rec(raw); - return { id: str(r.id) || newId(), text: str(r.text), done: bool(r.done) }; -} - function normalizeStep(raw: unknown): Step { const r = rec(raw); if (r.type === "if") { @@ -154,9 +204,11 @@ function normalizeStep(raw: unknown): Step { text: str(r.text), done: bool(r.done), labels: { yes: str(labels.yes, "はい"), no: str(labels.no, "いいえ") }, + // 分岐の中身も工程として再帰的に正規化する。 + // 旧データ(type なしの分岐工程 {id,text,done})は type:"step" の工程に変換される。 branches: { - yes: list(branches.yes).map(normalizeBranchStep), - no: list(branches.no).map(normalizeBranchStep), + yes: list(branches.yes).map(normalizeStep), + no: list(branches.no).map(normalizeStep), }, }; } @@ -254,11 +306,10 @@ export function normalizeNotebook(raw: unknown): Notebook | null { if (!isPlainObject(raw)) return null; const r = raw; const pages = list(r.pages).filter(isPlainObject).map(normalizePage); - if (pages.length === 0) return null; - const rawType = r.type === "rollbahn" ? "notestyle" : r.type; - const type = oneOf(rawType, ["profile", "notestyle"], "profile"); const firstPage = pages[0]; if (!firstPage) return null; + const rawType = r.type === "rollbahn" ? "notestyle" : r.type; + const type = oneOf(rawType, ["profile", "notestyle"], "profile"); let activePageId = str(r.activePageId); if (!pages.some((p) => p.id === activePageId)) activePageId = firstPage.id; @@ -286,6 +337,14 @@ function uniqueId(seen: Set, obj: { id: string }): void { seen.add(obj.id); } +function dedupeStep(st: Step, seen: Set): void { + uniqueId(seen, st); + if (st.type === "if") { + for (const b of st.branches.yes) dedupeStep(b, seen); + for (const b of st.branches.no) dedupeStep(b, seen); + } +} + function dedupeIds(notebooks: Notebook[]): void { const seenNb = new Set(); const seenPhoto = new Set(); @@ -295,13 +354,7 @@ function dedupeIds(notebooks: Notebook[]): void { for (const pg of nb.pages) { uniqueId(seenPg, pg); const stepIds = new Set(); - for (const st of pg.steps) { - uniqueId(stepIds, st); - if (st.type === "if") { - for (const b of st.branches.yes) uniqueId(stepIds, b); - for (const b of st.branches.no) uniqueId(stepIds, b); - } - } + for (const st of pg.steps) dedupeStep(st, stepIds); const drawableIds = new Set(); for (const s of pg.shapes) uniqueId(drawableIds, s); for (const p of pg.photos) { diff --git a/src/lib/types.ts b/src/lib/types.ts index 816e2be..629d58e 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -10,12 +10,6 @@ export type RuleStyle = (typeof RULE_STYLES)[number]; export type NotebookType = "profile" | "notestyle"; export type PageType = "flowchart" | "notebook"; -export interface BranchStep { - id: string; - text: string; - done: boolean; -} - export interface NormalStep { id: string; type: "step"; @@ -29,11 +23,14 @@ export interface IfStep { text: string; done: boolean; labels: { yes: string; no: string }; - branches: { yes: BranchStep[]; no: BranchStep[] }; + // 分岐の中身は「工程カード」だけでなく、入れ子の if 分岐カードも置ける(再帰)。 + branches: { yes: Step[]; no: Step[] }; } export type Step = NormalStep | IfStep; +export type BranchKey = "yes" | "no"; + export interface Sticker { id: string; type: StickerType; diff --git a/src/state/notebooks.ts b/src/state/notebooks.ts index 5820922..41920a5 100644 --- a/src/state/notebooks.ts +++ b/src/state/notebooks.ts @@ -1,9 +1,19 @@ import { create } from "zustand"; import { diagnoseStorage, emptyState, loadState, saveState } from "@/lib/store"; -import { newBranchStep, newId, newIfStep, newNotebook, newPage, newStep } from "@/lib/model"; +import { + appendToBranch, + newId, + newIfStep, + newNotebook, + newPage, + newStep, + removeStepFromTree, + updateStepInTree, +} from "@/lib/model"; import { PHOTO_DEFAULT_WIDTH, SHAPE_DEFAULT_WIDTH, STICKER_DEFAULT_SIZE } from "@/lib/types"; import type { AppState, + BranchKey, Frame, Notebook, NotebookType, @@ -14,8 +24,6 @@ import type { StickerType, } from "@/lib/types"; -type BranchKey = "yes" | "no"; - interface NotebooksState { ready: boolean; doc: AppState; @@ -55,9 +63,7 @@ interface NotebooksState { setBranchLabel: (stepId: string, key: BranchKey, label: string) => void; addBranchStep: (stepId: string, key: BranchKey) => void; - setBranchStepText: (stepId: string, key: BranchKey, branchId: string, text: string) => void; - toggleBranchStep: (stepId: string, key: BranchKey, branchId: string) => void; - deleteBranchStep: (stepId: string, key: BranchKey, branchId: string) => void; + addBranchIfStep: (stepId: string, key: BranchKey) => void; addSticker: (type: StickerType, x: number, y: number) => void; updateSticker: (id: string, patch: Partial>) => void; @@ -362,7 +368,7 @@ export const useNotebooks = create()((set, get) => { commit( mapActivePage((pg) => ({ ...pg, - steps: pg.steps.map((s) => (s.id === stepId ? { ...s, text } : s)), + steps: updateStepInTree(pg.steps, stepId, (s) => ({ ...s, text })), })), { immediate: false }, ), @@ -372,8 +378,8 @@ export const useNotebooks = create()((set, get) => { commit( mapActivePage((pg) => ({ ...pg, - steps: pg.steps.map((s) => { - if (s.id !== stepId || s.type !== "step") return s; + steps: updateStepInTree(pg.steps, stepId, (s) => { + if (s.type !== "step") return s; nowDone = !s.done; return { ...s, done: nowDone }; }), @@ -397,76 +403,24 @@ export const useNotebooks = create()((set, get) => { }), ), - deleteStep: (stepId) => commit(mapActivePage((pg) => ({ ...pg, steps: pg.steps.filter((s) => s.id !== stepId) }))), + deleteStep: (stepId) => commit(mapActivePage((pg) => ({ ...pg, steps: removeStepFromTree(pg.steps, stepId) }))), setBranchLabel: (stepId, key, label) => commit( mapActivePage((pg) => ({ ...pg, - steps: pg.steps.map((s) => - s.id === stepId && s.type === "if" ? { ...s, labels: { ...s.labels, [key]: label } } : s, + steps: updateStepInTree(pg.steps, stepId, (s) => + s.type === "if" ? { ...s, labels: { ...s.labels, [key]: label } } : s, ), })), { immediate: false }, ), addBranchStep: (stepId, key) => - commit( - mapActivePage((pg) => ({ - ...pg, - steps: pg.steps.map((s) => - s.id === stepId && s.type === "if" - ? { ...s, branches: { ...s.branches, [key]: [...s.branches[key], newBranchStep()] } } - : s, - ), - })), - ), - - setBranchStepText: (stepId, key, branchId, text) => - commit( - mapActivePage((pg) => ({ - ...pg, - steps: pg.steps.map((s) => { - if (s.id !== stepId || s.type !== "if") return s; - return { - ...s, - branches: { - ...s.branches, - [key]: s.branches[key].map((b) => (b.id === branchId ? { ...b, text } : b)), - }, - }; - }), - })), - { immediate: false }, - ), + commit(mapActivePage((pg) => ({ ...pg, steps: appendToBranch(pg.steps, stepId, key, newStep()) }))), - toggleBranchStep: (stepId, key, branchId) => - commit( - mapActivePage((pg) => ({ - ...pg, - steps: pg.steps.map((s) => { - if (s.id !== stepId || s.type !== "if") return s; - return { - ...s, - branches: { - ...s.branches, - [key]: s.branches[key].map((b) => (b.id === branchId ? { ...b, done: !b.done } : b)), - }, - }; - }), - })), - ), - - deleteBranchStep: (stepId, key, branchId) => - commit( - mapActivePage((pg) => ({ - ...pg, - steps: pg.steps.map((s) => { - if (s.id !== stepId || s.type !== "if") return s; - return { ...s, branches: { ...s.branches, [key]: s.branches[key].filter((b) => b.id !== branchId) } }; - }), - })), - ), + addBranchIfStep: (stepId, key) => + commit(mapActivePage((pg) => ({ ...pg, steps: appendToBranch(pg.steps, stepId, key, newIfStep()) }))), addSticker: (type, x, y) => commit( diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..e5f7251 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,26 @@ +import { fileURLToPath } from "node:url"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + resolve: { + alias: { + "@": fileURLToPath(new URL("./src", import.meta.url)), + }, + }, + test: { + environment: "node", + include: ["src/**/*.test.ts"], + coverage: { + provider: "v8", + reporter: ["text", "text-summary", "html"], + // 純粋ロジック(RN/Expo に依存しない部分)だけをカバレッジ対象にする。 + include: ["src/lib/model.ts", "src/lib/format.ts"], + thresholds: { + lines: 100, + functions: 100, + branches: 100, + statements: 100, + }, + }, + }, +});