Phase 7-8
This commit is contained in:
parent
a196b8f9a8
commit
0148f48204
25 changed files with 2473 additions and 47 deletions
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)
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue