Phase 7-8
This commit is contained in:
parent
a196b8f9a8
commit
0148f48204
25 changed files with 2473 additions and 47 deletions
|
|
@ -9,7 +9,9 @@ export function useTheme() {
|
|||
|
||||
function apply(next: Theme) {
|
||||
theme.value = next
|
||||
if (import.meta.client) {
|
||||
// ใช้ typeof window แทน import.meta.client — เทียบเท่ากันใน Nuxt (SSR ไม่มี window)
|
||||
// และรันได้ใน vitest ตรงๆ โดยไม่ต้องพึ่งการ transform เฉพาะของ Nuxt
|
||||
if (typeof window !== 'undefined') {
|
||||
document.documentElement.classList.toggle('dark', next === 'dark')
|
||||
// ข้อยกเว้นเดียวที่ localStorage อนุญาต: theme preference ไม่ใช่ credential (กติกา #4)
|
||||
localStorage.setItem(STORAGE_KEY, next)
|
||||
|
|
@ -18,7 +20,7 @@ export function useTheme() {
|
|||
|
||||
/** Sync state กับที่ inline script ตั้งไว้ตอนโหลดหน้า (client เท่านั้น) */
|
||||
function initTheme() {
|
||||
if (!import.meta.client) return
|
||||
if (typeof window === 'undefined') return
|
||||
const stored = localStorage.getItem(STORAGE_KEY)
|
||||
if (stored === 'light' || stored === 'dark') {
|
||||
theme.value = stored
|
||||
|
|
|
|||
446
web/package-lock.json
generated
446
web/package-lock.json
generated
|
|
@ -11,6 +11,10 @@
|
|||
"nuxt": "^4.5.2",
|
||||
"vue": "^3.5.41",
|
||||
"vue-router": "^5.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"happy-dom": "^20.11.6",
|
||||
"vitest": "^4.1.11"
|
||||
}
|
||||
},
|
||||
"node_modules/@alloc/quick-lru": {
|
||||
|
|
@ -2828,6 +2832,31 @@
|
|||
"integrity": "sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==",
|
||||
"license": "CC0-1.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/@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",
|
||||
|
|
@ -2840,12 +2869,39 @@
|
|||
"integrity": "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "26.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz",
|
||||
"integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~8.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/resolve": {
|
||||
"version": "1.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
||||
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/whatwg-mimetype": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz",
|
||||
"integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@unhead/bundler": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@unhead/bundler/-/bundler-3.3.2.tgz",
|
||||
|
|
@ -2987,6 +3043,149 @@
|
|||
"vue": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz",
|
||||
"integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.1.0",
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/spy": "4.1.11",
|
||||
"@vitest/utils": "4.1.11",
|
||||
"chai": "^6.2.2",
|
||||
"tinyrainbow": "^3.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz",
|
||||
"integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/spy": "4.1.11",
|
||||
"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/mocker/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/@vitest/mocker/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/@vitest/pretty-format": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz",
|
||||
"integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tinyrainbow": "^3.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/runner": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz",
|
||||
"integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/utils": "4.1.11",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/snapshot": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz",
|
||||
"integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "4.1.11",
|
||||
"@vitest/utils": "4.1.11",
|
||||
"magic-string": "^0.30.21",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/snapshot/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/@vitest/spy": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz",
|
||||
"integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/utils": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz",
|
||||
"integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "4.1.11",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"tinyrainbow": "^3.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue-macros/common": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@vue-macros/common/-/common-3.1.4.tgz",
|
||||
|
|
@ -3505,6 +3704,16 @@
|
|||
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
|
||||
"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-kit": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.2.0.tgz",
|
||||
|
|
@ -3862,6 +4071,19 @@
|
|||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/buffer-image-size": {
|
||||
"version": "0.6.4",
|
||||
"resolved": "https://registry.npmjs.org/buffer-image-size/-/buffer-image-size-0.6.4.tgz",
|
||||
"integrity": "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bundle-name": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz",
|
||||
|
|
@ -3905,16 +4127,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/cac": {
|
||||
"version": "6.7.14",
|
||||
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
||||
"integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
|
||||
"extraneous": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cache-content-type": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz",
|
||||
|
|
@ -4017,6 +4229,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",
|
||||
|
|
@ -4137,16 +4359,6 @@
|
|||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "15.0.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz",
|
||||
"integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==",
|
||||
"extraneous": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/commondir": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
|
||||
|
|
@ -5046,6 +5258,16 @@
|
|||
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
||||
}
|
||||
},
|
||||
"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/exsolve": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.1.tgz",
|
||||
|
|
@ -5499,6 +5721,25 @@
|
|||
"uncrypto": "^0.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/happy-dom": {
|
||||
"version": "20.11.6",
|
||||
"resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.11.6.tgz",
|
||||
"integrity": "sha512-Hldbg8AdAa5a5oDcZpjqnGitp7JB0hqWmfv/8qr+kft4vzSD8BHsbdRfzYvL/0QcbKcURC/yyoygbeDQarPvYg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": ">=20.0.0",
|
||||
"@types/whatwg-mimetype": "^3.0.2",
|
||||
"@types/ws": "^8.18.1",
|
||||
"buffer-image-size": "^0.6.4",
|
||||
"entities": "^7.0.1",
|
||||
"whatwg-mimetype": "^3.0.0",
|
||||
"ws": "^8.21.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
|
|
@ -9158,6 +9399,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": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
|
|
@ -9277,6 +9525,13 @@
|
|||
"node": ">=20.16.0"
|
||||
}
|
||||
},
|
||||
"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/standard-as-callback": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
|
||||
|
|
@ -9869,6 +10124,13 @@
|
|||
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
||||
"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/tinyclip": {
|
||||
"version": "0.1.15",
|
||||
"resolved": "https://registry.npmjs.org/tinyclip/-/tinyclip-0.1.15.tgz",
|
||||
|
|
@ -9903,6 +10165,16 @@
|
|||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyrainbow": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz",
|
||||
"integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
|
|
@ -10075,6 +10347,13 @@
|
|||
"node": ">=22.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
|
||||
"integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unenv": {
|
||||
"version": "2.0.0-rc.24",
|
||||
"resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz",
|
||||
|
|
@ -10777,6 +11056,106 @@
|
|||
"@types/estree": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz",
|
||||
"integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/expect": "4.1.11",
|
||||
"@vitest/mocker": "4.1.11",
|
||||
"@vitest/pretty-format": "4.1.11",
|
||||
"@vitest/runner": "4.1.11",
|
||||
"@vitest/snapshot": "4.1.11",
|
||||
"@vitest/spy": "4.1.11",
|
||||
"@vitest/utils": "4.1.11",
|
||||
"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.11",
|
||||
"@vitest/browser-preview": "4.1.11",
|
||||
"@vitest/browser-webdriverio": "4.1.11",
|
||||
"@vitest/coverage-istanbul": "4.1.11",
|
||||
"@vitest/coverage-v8": "4.1.11",
|
||||
"@vitest/ui": "4.1.11",
|
||||
"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/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/vue": {
|
||||
"version": "3.5.41",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.41.tgz",
|
||||
|
|
@ -10953,6 +11332,16 @@
|
|||
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/whatwg-mimetype": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
|
||||
"integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
|
|
@ -10978,6 +11367,23 @@
|
|||
"node": "^20.17.0 || >=22.9.0"
|
||||
}
|
||||
},
|
||||
"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/wrap-ansi": {
|
||||
"version": "9.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
|
||||
|
|
|
|||
|
|
@ -7,12 +7,17 @@
|
|||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare"
|
||||
"postinstall": "nuxt prepare",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||
"nuxt": "^4.5.2",
|
||||
"vue": "^3.5.41",
|
||||
"vue-router": "^5.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"happy-dom": "^20.11.6",
|
||||
"vitest": "^4.1.11"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
84
web/test/fieldValidation.spec.ts
Normal file
84
web/test/fieldValidation.spec.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { validateFieldValues, type FieldDefLike } from '../app/utils/fieldValidation'
|
||||
|
||||
/** ชุดนิยามครบทุก type ที่ระบบรองรับ (ตาม TemplateField: text/number/date) */
|
||||
const defs: FieldDefLike[] = [
|
||||
{ key: 'employee_name', label: 'ชื่อพนักงาน', type: 'text', isRequired: true },
|
||||
{ key: 'position', label: 'ตำแหน่ง', type: 'text', isRequired: false },
|
||||
{ key: 'salary', label: 'เงินเดือน', type: 'number', isRequired: false },
|
||||
{ key: 'start_date', label: 'วันเริ่มงาน', type: 'date', isRequired: false },
|
||||
]
|
||||
|
||||
describe('validateFieldValues — dynamic form ของหน้าสร้างเอกสาร (FR-10)', () => {
|
||||
it('ค่าครบและถูก type ทุก field → ไม่มี issue', () => {
|
||||
const issues = validateFieldValues(defs, {
|
||||
employee_name: 'สมหญิง รักงาน',
|
||||
position: 'วิศวกรซอฟต์แวร์',
|
||||
salary: '75000',
|
||||
start_date: '2026-08-01',
|
||||
})
|
||||
expect(issues).toEqual([])
|
||||
})
|
||||
|
||||
describe('required', () => {
|
||||
it('required ขาด (ไม่มี key) → issue ชี้ field นั้น', () => {
|
||||
const issues = validateFieldValues(defs, { salary: '1' })
|
||||
expect(issues).toHaveLength(1)
|
||||
expect(issues[0].key).toBe('employee_name')
|
||||
expect(issues[0].message).toContain('ชื่อพนักงาน')
|
||||
})
|
||||
|
||||
it('required ว่างเปล่า / มีแต่วรรค → issue (ค่าที่เป็นช่องว่างนับเป็นว่าง)', () => {
|
||||
expect(validateFieldValues(defs, { employee_name: '' })[0].key).toBe('employee_name')
|
||||
expect(validateFieldValues(defs, { employee_name: ' ' })[0].key).toBe('employee_name')
|
||||
})
|
||||
|
||||
it('ไม่ required และว่าง → ผ่าน (ไม่บังคับ type ด้วย)', () => {
|
||||
const issues = validateFieldValues(defs, {
|
||||
employee_name: 'มีชื่อ',
|
||||
position: '',
|
||||
salary: '',
|
||||
start_date: '',
|
||||
})
|
||||
expect(issues).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('type = number (ตรง NumberStyles.Number ของ backend)', () => {
|
||||
it.each(['1', '-5', '+3.14', '1,234.56', '0'])('ค่า %s ผ่าน', (value) => {
|
||||
expect(validateFieldValues(defs, { employee_name: 'x', salary: value })).toEqual([])
|
||||
})
|
||||
|
||||
it.each(['abc', '1.2.3', '12.', '.5', '1e5', '--2'])('ค่า %s เป็น issue', (value) => {
|
||||
const issues = validateFieldValues(defs, { employee_name: 'x', salary: value })
|
||||
expect(issues).toHaveLength(1)
|
||||
expect(issues[0].message).toContain('ตัวเลข')
|
||||
})
|
||||
})
|
||||
|
||||
describe('type = date', () => {
|
||||
it.each(['2026-08-01', '2026-08-01T09:00:00Z', '2026-12-31'])('ค่า %s ผ่าน', (value) => {
|
||||
expect(validateFieldValues(defs, { employee_name: 'x', start_date: value })).toEqual([])
|
||||
})
|
||||
|
||||
it.each(['not-a-date', '31/02/2026 ผิดรูป', 'สิงหาคม'])('ค่า %s เป็น issue', (value) => {
|
||||
const issues = validateFieldValues(defs, { employee_name: 'x', start_date: value })
|
||||
expect(issues).toHaveLength(1)
|
||||
expect(issues[0].message).toContain('วันที่')
|
||||
})
|
||||
})
|
||||
|
||||
it('ผิดหลาย field พร้อมกัน → issue เรียงตามลำดับ defs และ issue ละ field เดียว', () => {
|
||||
const issues = validateFieldValues(defs, {
|
||||
employee_name: 'มีชื่อแล้ว', // required ผ่าน เหลือดูเฉพาะ type ของ salary/start_date
|
||||
salary: 'abc',
|
||||
start_date: 'วันนี้',
|
||||
})
|
||||
expect(issues.map((i) => i.key)).toEqual(['salary', 'start_date'])
|
||||
})
|
||||
|
||||
it('type อื่นที่ไม่รู้จัก (เช่น จาก backend อนาคต) → ไม่ validate เพิ่ม', () => {
|
||||
const futureDefs: FieldDefLike[] = [{ key: 'x', label: 'อะไรใหม่', type: 'email', isRequired: false }]
|
||||
expect(validateFieldValues(futureDefs, { x: 'ไม่ใช่ email' })).toEqual([])
|
||||
})
|
||||
})
|
||||
27
web/test/setup.ts
Normal file
27
web/test/setup.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* Stub Nuxt auto-imports ที่ composable ใช้ — เบากว่าการลาก @nuxt/test-utils ทั้งชุดเข้ามา
|
||||
* - useState: ตาม semantics ของ Nuxt คือ state แชร์กันตาม key (สร้างครั้งแรก ครั้งถัดไปได้ตัวเดิม)
|
||||
* - computed: ของ vue ตรงๆ
|
||||
* เคลียร์ state ทุก test เพื่อไม่ให้ theme ของ test ก่อนหน้ารั่วมา
|
||||
*/
|
||||
import { beforeEach } from 'vitest'
|
||||
import { computed, ref, type Ref } from 'vue'
|
||||
|
||||
const sharedState = new Map<string, Ref<unknown>>()
|
||||
|
||||
;(globalThis as Record<string, unknown>).useState = <T>(key: string, init: () => T): Ref<T> => {
|
||||
let entry = sharedState.get(key)
|
||||
if (!entry) {
|
||||
entry = ref(init()) as Ref<unknown>
|
||||
sharedState.set(key, entry)
|
||||
}
|
||||
return entry as Ref<T>
|
||||
}
|
||||
|
||||
;(globalThis as Record<string, unknown>).computed = computed
|
||||
|
||||
beforeEach(() => {
|
||||
sharedState.clear()
|
||||
document.documentElement.classList.remove('dark')
|
||||
localStorage.clear()
|
||||
})
|
||||
77
web/test/snapshot.spec.ts
Normal file
77
web/test/snapshot.spec.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { normalizeSnapshot } from '../app/utils/snapshot'
|
||||
|
||||
describe('normalizeSnapshot — แปลง snapshot 2 รูปแบบของ backend ให้เป็น list สำหรับ UI (FR-12)', () => {
|
||||
it('รูปแบบเอกสารผูก template: { key: { value, missing } }', () => {
|
||||
const result = normalizeSnapshot({
|
||||
employee_name: { value: 'สมหญิง รักงาน', missing: false },
|
||||
position: { value: 'วิศวกรเดิม', missing: true },
|
||||
})
|
||||
|
||||
expect(result).toEqual([
|
||||
{ key: 'employee_name', value: 'สมหญิง รักงาน', missing: false },
|
||||
{ key: 'position', value: 'วิศวกรเดิม', missing: true },
|
||||
])
|
||||
})
|
||||
|
||||
it('รูปแบบเอกสารไม่ผูก template: map ดิบ { key: value } → missing=false ทั้งหมด', () => {
|
||||
const result = normalizeSnapshot({ any_tag: 'ค่าอะไรก็ได้' })
|
||||
|
||||
expect(result).toEqual([{ key: 'any_tag', value: 'ค่าอะไรก็ได้', missing: false }])
|
||||
})
|
||||
|
||||
it('value ไม่ใช่ string → แปลงเป็น string; null/undefined → ""', () => {
|
||||
const result = normalizeSnapshot({
|
||||
number_value: 123,
|
||||
bool_value: true,
|
||||
null_value: null,
|
||||
undefined_value: undefined,
|
||||
object_value: { value: null, missing: false },
|
||||
})
|
||||
|
||||
expect(result).toEqual([
|
||||
{ key: 'number_value', value: '123', missing: false },
|
||||
{ key: 'bool_value', value: 'true', missing: false },
|
||||
{ key: 'null_value', value: '', missing: false },
|
||||
{ key: 'undefined_value', value: '', missing: false },
|
||||
{ key: 'object_value', value: '', missing: false },
|
||||
])
|
||||
})
|
||||
|
||||
it('fields เป็น null/undefined/ค่าไม่ใช่ object → list ว่าง (version ไม่มี snapshot)', () => {
|
||||
expect(normalizeSnapshot(null)).toEqual([])
|
||||
expect(normalizeSnapshot(undefined)).toEqual([])
|
||||
expect(normalizeSnapshot('string')).toEqual([])
|
||||
expect(normalizeSnapshot(42)).toEqual([])
|
||||
})
|
||||
|
||||
it('orderKeys กำหนดลำดับตาม field defs — key นอก defs ต่อท้ายตามตัวอักษร', () => {
|
||||
const result = normalizeSnapshot(
|
||||
{ orphan: '1', salary: '2', employee_name: '3', zzz: '4', position: '5' },
|
||||
['employee_name', 'position', 'salary'],
|
||||
)
|
||||
|
||||
expect(result.map((e) => e.key)).toEqual([
|
||||
'employee_name',
|
||||
'position',
|
||||
'salary',
|
||||
'orphan',
|
||||
'zzz', // orphan สองตัวเรียงตาม localeCompare
|
||||
])
|
||||
})
|
||||
|
||||
it('orderKeys ว่าง/ไม่ส่ง → คงลำดับเดิมของ object', () => {
|
||||
const fields = { b: 'x', a: 'y' }
|
||||
|
||||
expect(normalizeSnapshot(fields, []).map((e) => e.key)).toEqual(['b', 'a'])
|
||||
expect(normalizeSnapshot(fields).map((e) => e.key)).toEqual(['b', 'a'])
|
||||
})
|
||||
|
||||
it('key ซ้ำใน object (ผ่าน JSON เข้ามา) — ตัวหลังชนะตามพฤติกรรม Object.entries', () => {
|
||||
// JSON.parse ทำให้ key ซ้ำเหลือตัวเดียวอยู่แล้ว — assert กัน regression เรื่อง Map dedupe
|
||||
const parsed = JSON.parse('{"k":"first","k":"second"}')
|
||||
const result = normalizeSnapshot(parsed)
|
||||
|
||||
expect(result).toEqual([{ key: 'k', value: 'second', missing: false }])
|
||||
})
|
||||
})
|
||||
84
web/test/useTheme.spec.ts
Normal file
84
web/test/useTheme.spec.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useTheme } from '../app/composables/useTheme'
|
||||
|
||||
/** กำหนด prefers-color-scheme ของระบบ — initTheme ใช้เป็นค่า default เมื่อยังไม่มี preference */
|
||||
function mockPrefersDark(matches: boolean) {
|
||||
vi.stubGlobal('matchMedia', vi.fn().mockReturnValue({ matches }))
|
||||
}
|
||||
|
||||
describe('useTheme — light/dark toggle + persist + system default (Phase 6.1)', () => {
|
||||
beforeEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('state เริ่มที่ light และ toggle สลับไป-กลับ พร้อม class บน <html>', () => {
|
||||
mockPrefersDark(false)
|
||||
const { theme, isDark, toggle } = useTheme()
|
||||
|
||||
expect(theme.value).toBe('light')
|
||||
expect(isDark.value).toBe(false)
|
||||
|
||||
toggle()
|
||||
expect(theme.value).toBe('dark')
|
||||
expect(isDark.value).toBe(true)
|
||||
expect(document.documentElement.classList.contains('dark')).toBe(true)
|
||||
|
||||
toggle()
|
||||
expect(theme.value).toBe('light')
|
||||
expect(document.documentElement.classList.contains('dark')).toBe(false)
|
||||
})
|
||||
|
||||
it('toggle เขียน preference ลง localStorage (ข้อยกเว้นเดียวที่อนุญาต — ไม่ใช่ credential)', () => {
|
||||
mockPrefersDark(false)
|
||||
const { toggle } = useTheme()
|
||||
|
||||
toggle()
|
||||
expect(localStorage.getItem('theme-preference')).toBe('dark')
|
||||
|
||||
toggle()
|
||||
expect(localStorage.getItem('theme-preference')).toBe('light')
|
||||
})
|
||||
|
||||
it('initTheme ใช้ค่าที่เคยบันทึกไว้ (แม้ระบบเป็นอีกค่า)', () => {
|
||||
mockPrefersDark(true)
|
||||
localStorage.setItem('theme-preference', 'light')
|
||||
const { theme, initTheme } = useTheme()
|
||||
|
||||
initTheme()
|
||||
|
||||
expect(theme.value).toBe('light') // stored ชนะ system
|
||||
expect(document.documentElement.classList.contains('dark')).toBe(false)
|
||||
})
|
||||
|
||||
it('initTheme ไม่มีค่าเก็บไว้ → ตาม prefers-color-scheme ของระบบ', () => {
|
||||
mockPrefersDark(true)
|
||||
const { theme, isDark, initTheme } = useTheme()
|
||||
|
||||
initTheme()
|
||||
|
||||
expect(theme.value).toBe('dark')
|
||||
expect(isDark.value).toBe(true)
|
||||
expect(document.documentElement.classList.contains('dark')).toBe(true)
|
||||
})
|
||||
|
||||
it('ค่าใน localStorage ที่ไม่ใช่ light/dark → ตกไปใช้ค่าระบบแทน (กันค่าเสีย)', () => {
|
||||
mockPrefersDark(false)
|
||||
localStorage.setItem('theme-preference', 'neon')
|
||||
const { theme, initTheme } = useTheme()
|
||||
|
||||
initTheme()
|
||||
|
||||
expect(theme.value).toBe('light') // ค่าเสียถูกเมิน ใช้ system default
|
||||
})
|
||||
|
||||
it('state แชร์กันตาม key (semantics ของ useState) — สอง instance เห็นค่าเดียวกัน', () => {
|
||||
mockPrefersDark(false)
|
||||
const first = useTheme()
|
||||
const second = useTheme()
|
||||
|
||||
first.toggle()
|
||||
|
||||
expect(second.theme.value).toBe('dark')
|
||||
expect(second.isDark.value).toBe(true)
|
||||
})
|
||||
})
|
||||
12
web/vitest.config.ts
Normal file
12
web/vitest.config.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
// Unit test สำหรับ pure functions (fieldValidation/snapshot) + useTheme (ต้องมี DOM)
|
||||
// ไม่ดึง @nuxt/test-utils — stub เฉพาะ auto-imports ที่ใช้จริง (test/setup.ts) ให้เบาและอธิบายตัวเอง
|
||||
// (useTheme แยก client/server ด้วย typeof window จึงไม่ต้อง replace import.meta.client)
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: 'happy-dom',
|
||||
setupFiles: ['./test/setup.ts'],
|
||||
include: ['test/**/*.spec.ts'],
|
||||
},
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue