diff --git a/.forgejo/workflows/ci-cd.yml b/.forgejo/workflows/ci-cd.yml index 62a125b..4c508ef 100644 --- a/.forgejo/workflows/ci-cd.yml +++ b/.forgejo/workflows/ci-cd.yml @@ -5,7 +5,7 @@ on: push: tags: - 'v[0-9]+.[0-9]+.[0-9]+' - # - 'version-[0-9]+.[0-9]+.[0-9]+' + - 'version-[0-9]+.[0-9]+.[0-9]+' workflow_dispatch: env: diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index f974864..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,155 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -This is an HRMS (Human Resource Management System) check-in/check-out web application built for the Bangkok Metropolitan Administration (BMA). The application allows employees to record their work attendance using geolocation verification and camera features. - -**Tech Stack:** -- Vue 3 with Composition API and TypeScript -- Vite for build tooling -- Quasar Framework for UI components -- Pinia for state management -- Vue Router for routing -- Keycloak for authentication -- ArcGIS API and Google Maps for location features -- PWA capabilities with offline support -- Docker deployment with nginx - -**Development Server:** Runs on port 3008 - -## Common Commands - -```bash -# Development -npm run dev # Start dev server on port 3008 - -# Building -npm run build # Build for production -npm run preview # Preview production build on port 3008 - -# Testing -npm run test:unit # Run unit tests with vitest -npm run test:e2e # Run e2e tests with cypress -npm run test:e2e:dev # Run cypress in dev mode - -# Code Quality -npm run lint # Lint and auto-fix code -npm run format # Format code with prettier -npm run type-check # TypeScript type checking - -# Docker -docker buildx build --platform=linux/amd64 -f docker/Dockerfile . -t hrms-checkin:0.1 -``` - -## Architecture - -### Directory Structure - -``` -src/ -├── api/ # API layer modules (checkin, history, message) -├── components/ # Reusable Vue components -├── composables/ # Vue composables (e.g., usePermissions) -├── interface/ # TypeScript type definitions -│ └── response/ # API response types -├── plugins/ # Vue plugins (auth, http, keycloak) -├── router/ # Vue Router configuration -├── stores/ # Pinia stores for state management -├── style/ # Global styles and Quasar variables -├── utils/ # Utility functions -└── views/ # Page-level Vue components -``` - -### Key Architecture Patterns - -**Authentication Flow:** -- Uses Keycloak for SSO authentication -- Tokens stored in cookies with `BMAHRISCKI_KEYCLOAK_IDENTITY` key -- Auth state managed in `src/plugins/auth.ts` -- Router guards enforce authentication on protected routes -- 401 responses trigger automatic logout via axios interceptor - -**State Management:** -- `stores/mixin.ts` - Global utilities (Thai date formatting, dialogs, loaders, error handling) -- `stores/chekin.ts` - Check-in history and status management -- `stores/privacy.ts` - Privacy consent management -- `stores/positionKeycloak.ts` - User position/role data - -**API Layer:** -- Base axios instance in `src/plugins/http.ts` with automatic token injection -- API endpoints organized by feature in `src/api/` -- Environment-specific API URLs configured in `src/api/index.ts` -- Production API URL set via `VITE_API_URI_CONFIG` env variable - -**Routing:** -- Main layout (`MainView`) wraps authenticated routes -- Protected routes require `meta.Auth: true` -- Auth check runs in router beforeEach guard -- Public routes: `/login`, `/auth`, `/reset-password`, `/history` - -**Geolocation & Maps:** -- ArcGIS JS API for advanced mapping features -- Google Maps integration for location services -- Camera integration for check-in photo verification -- Privacy consent required before accessing camera/location - -**Date/Time Handling:** -- Thai Buddhist calendar (BE) conversion (+543 years) -- Thai month names (full and abbreviated) -- `date-fns-tz` for timezone handling (Asia/Bangkok) -- Fiscal year calculation (October start) - -### Component Conventions - -- Components use Quasar UI components (`q-btn`, `q-dialog`, etc.) -- Thai language throughout the UI -- Custom dialogs use `CustomDialog.vue` component -- Loading states use Quasar's `QSpinnerCube` -- Error dialogs prevent overlap with `activeErrorDialog` tracking - -### Localization - -- Quasar configured with Thai language (`quasar/lang/th`) -- All UI text in Thai -- Date formatting uses `date2Thai()` and `monthYear2Thai()` from mixin store -- Status labels translated: ABSENT→'ขาดราชการ', NORMAL→'ปกติ', LATE→'สาย' - -### Environment Variables - -Required in `.env.production`: -- `VITE_API_URI_CONFIG` - Production API base URL -- `VITE_URL_SSO` - Keycloak SSO logout URL -- `VITE_URL_USER` - User service URL for redirects - -### PWA Configuration - -- Auto-update registration with `vite-plugin-pwa` -- Manifest: "HRMS-Checkin" / "HRMS Checkin" -- Icons: 192x192 and 512x512 PNG -- Service worker registered in `src/registerServiceWorker.ts` - -## Development Notes - -**When adding new features:** -1. Create API functions in `src/api/` -2. Define TypeScript interfaces in `src/interface/response/` -3. Use Pinia stores for complex state, composables for reusable logic -4. Follow Thai localization patterns for user-facing text -5. Ensure privacy consent flow for camera/location features - -**When working with dates:** -- Always use `date2Thai()` or `convertDateToAPI()` from mixin store -- Dates stored in BE format (year + 543) -- API expects `yyyy-MM-dd` or `yyyy-MM-dd HH:mm:ss` format - -**When handling errors:** -- Use `messageError()` from mixin store for consistent error dialogs -- 401 errors trigger automatic logout -- Dialog overlap prevention built into error handling - -**When running tests:** -- Unit tests use Vitest with jsdom environment -- E2E tests use Cypress with baseUrl: `http://localhost:4173` -- Test files follow pattern: `*.cy.{js,ts}` or `*.spec.{js,ts}` diff --git a/docs/test-cases-location.md b/docs/test-cases-location.md deleted file mode 100644 index ed19dcd..0000000 --- a/docs/test-cases-location.md +++ /dev/null @@ -1,860 +0,0 @@ -# Test Cases: Location Features -# HRMS Check-in/Check-out System - -## Document Information - -| Field | Value | -|-------|-------| -| **Document Version** | 1.0.0 | -| **Last Updated** | 2025-03-09 | -| **Project** | HRMS Check-in/Check-out | -| **Module** | Location Features | -| **Author** | QA Team | - ---- - -## 1. Overview (ภาพรวม) - -ระบบ HRMS Check-in/Check-out มีฟีเจอร์ Location ที่สำคัญในการลงเวลาเข้า-ออกงาน ฟีเจอร์ Location ประกอบด้วย: - -### 1.1 Location Features - -| Feature | Description | -|---------|-------------| -| **Location Services** | รับพิกัด GPS จากอุปกรณ์ผ่าน Geolocation API | -| **Location Validation** | ตรวจสอบความถูกต้องของตำแหน่ง (Mock Location Detection) | -| **POI Resolution** | แปลงพิกัดเป็นชื่อสถานที่ (Bangkok GIS / ArcGIS) | -| **Check-in/Check-out** | บันทึกตำแหน่งพร้อมรูปถ่าย | -| **Privacy Consent** | ขอความยินยอมการเข้าถึงตำแหน่ง | - -### 1.2 Related Files - -| File Path | Description | -|-----------|-------------| -| `/src/composables/useLocationValidation.ts` | Location validation logic | -| `/src/views/HomeView.vue` | Check-in/Check-out page | -| `/src/components/AscGISMap.vue` | Map component with location validation | -| `/src/api/api.checkin.ts` | Check-in API endpoints | -| `/src/stores/privacy.ts` | Privacy store for consent management | -| `/src/composables/usePermissions.ts` | Permission checking utilities | - ---- - -## 2. Test Environment (สภาพแวดล้อมการทดสอบ) - -### 2.1 Devices (อุปกรณ์ทดสอบ) - -| Device Type | OS | Browser | Notes | -|-------------|-------|---------|-------| -| Mobile | iOS 17+ | Safari | Test with real GPS | -| Mobile | Android 13+ | Chrome | Test with mock location apps | -| Desktop | macOS 14+ | Chrome 121+ | Test with Developer Tools | -| Desktop | Windows 11 | Edge 121+ | Test with Developer Tools | - -### 2.2 Tools (เครื่องมือทดสอบ) - -| Tool | Purpose | -|------|---------| -| Fake GPS Location (Android) | Mock location testing | -| Location Changer (iOS) | Mock location testing | -| Chrome DevTools | Sensor simulation | -| Xcode Simulator | iOS location simulation | -| Android Studio Emulator | Android location simulation | - -### 2.3 Test Data (ข้อมูลทดสอบ) - -| Data Type | Valid Value | Invalid Value | -|-----------|-------------|---------------| -| Latitude | 13.7563 (Bangkok) | 0, 91, -91 | -| Longitude | 100.5018 (Bangkok) | 0, 181, -181 | -| Accuracy (meters) | 10-50 | 101+ | -| Timestamp | Current time | 60+ seconds old | - ---- - -## 3. Validation Rules (กฎการตรวจสอบ) - -### 3.1 Location Validation Rules - -| Rule | Threshold | Error Message (Thai) | Mock Indicator | -|------|-----------|---------------------|----------------| -| Valid Coordinates | -90 to 90 lat, -180 to 180 lon, not (0,0) | "พิกัดตำแหน่งไม่ถูกต้อง กรุณาลองใหม่" | +3 | -| Fresh Timestamp | <= 60 seconds old | "ข้อมูลตำแหน่งเก่าเกินไป กรุณารับสัญญาณ GPS ใหม่" | +2 | -| GPS Accuracy | <= 100 meters | "ความแม่นยำตำแหน่งต่ำเกินไป กรุณาตรวจสอบการรับสัญญาณ GPS" | +1 | -| Movement Speed | <= 100 m/s (~360 km/h) | "ตรวจพบการเคลื่อนที่ด้วยความเร็วผิดปกติ อาจเป็นการจำลองตำแหน่ง" | +3 | -| Mock Detection | >= 3 indicators | "ตรวจพบว่าตำแหน่ง GPS อาจไม่ถูกต้อง กรุณาปิดแอปจำลองตำแหน่งและลองใหม่" | - | - -### 3.2 Validation Configuration - -```typescript -VALIDATION_CONFIG = { - MAX_TIMESTAMP_AGE_MS: 60_000, // 60 seconds - MAX_ACCURACY_METERS: 100, // 100 meters - MAX_SPEED_MS: 100, // ~360 km/h - POSITION_HISTORY_SIZE: 5, // positions for pattern detection - MOCK_INDICATOR_THRESHOLD: 3, // indicators for mock detection -} -``` - ---- - -## 4. Test Scenarios - -### 4.1 TC-LOC-01: Location Permission - -| Test Case | Description | Priority | -|-----------|-------------|----------| -| TC-LOC-01-01 | อนุญาตให้เข้าถึงตำแหน่ง | High | -| TC-LOC-01-02 | ปฏิเสธการเข้าถึงตำแหน่ง | High | - -### 4.2 TC-LOC-02: Location Acquisition - -| Test Case | Description | Priority | -|-----------|-------------|----------| -| TC-LOC-02-01 | รับพิกัด GPS สำเร็จ (Outdoor) | High | -| TC-LOC-02-02 | รับพิกัด GPS สำเร็จ (Indoor) | Medium | -| TC-LOC-02-03 | รับพิกัด GPS ล้มเหลว (GPS ไม่ทำงาน) | High | -| TC-LOC-02-04 | หมดเวลาขอรับพิกัด (Timeout) | Medium | - -### 4.3 TC-LOC-03: Location Validation - -| Test Case | Description | Priority | -|-----------|-------------|----------| -| TC-LOC-03-01 | พิกัดถูกต้อง (Valid coordinates) | High | -| TC-LOC-03-02 | พิกัดไม่ถูกต้อง (Invalid coordinates - (0,0)) | High | -| TC-LOC-03-03 | พิกัดนอกช่วง (Out of range) | Medium | -| TC-LOC-03-04 | ข้อมูลตำแหน่งเก่าเกินไป (Stale timestamp) | High | -| TC-LOC-03-05 | ความแม่นยำต่ำ (Poor accuracy) | Medium | -| TC-LOC-03-06 | ความเร็วเคลื่อนที่ผิดปกติ (Impossible speed) | High | - -### 4.4 TC-LOC-04: Mock Location Detection - -| Test Case | Description | Priority | -|-----------|-------------|----------| -| TC-LOC-04-01 | ไม่พบ Mock Location (Normal GPS) | High | -| TC-LOC-04-02 | พบ Mock Location - Fake GPS App | High | -| TC-LOC-04-03 | พบ Mock Location - พิกัด (0,0) | High | -| TC-LOC-04-04 | พบ Mock Location - ข้อมูลเก่าเกินไป | High | -| TC-LOC-04-05 | พบ Mock Location - ความเร็วผิดปกติ | High | -| TC-LOC-04-06 | พบ Mock Location - หลาย indicators (Confidence High) | High | - -### 4.5 TC-LOC-05: POI Resolution - -| Test Case | Description | Priority | -|-----------|-------------|----------| -| TC-LOC-05-01 | แปลงพิกัดเป็นชื่อสถานที่สำเร็จ (Bangkok GIS) | High | -| TC-LOC-05-02 | แปลงพิกัดเป็นชื่อสถานที่สำเร็จ (ArcGIS Fallback) | Medium | -| TC-LOC-05-03 | แปลงพิกัดล้มเหลว (ทั้งสอง service down) | Low | - -### 4.6 TC-LOC-06: Check-in with Location - -| Test Case | Description | Priority | -|-----------|-------------|----------| -| TC-LOC-06-01 | ลงเวลาเข้าสำเร็จ - ณ สถานที่ตั้ง (In-place) | High | -| TC-LOC-06-02 | ลงเวลาเข้าสำเร็จ - นอกสถานที่ตั้ง (Off-site) | High | -| TC-LOC-06-03 | ลงเวลาเข้าล้มเหลว - Mock Location detected | High | -| TC-LOC-06-04 | ลงเวลาเข้าล้มเหลว - Location permission denied | High | - -### 4.7 TC-LOC-07: Check-out with Location - -| Test Case | Description | Priority | -|-----------|-------------|----------| -| TC-LOC-07-01 | ลงเวลาออกสำเร็จ | High | -| TC-LOC-07-02 | ลงเวลาออกล้มเหลว - Mock Location detected | High | - -### 4.8 TC-LOC-08: Special Time Entry - -| Test Case | Description | Priority | -|-----------|-------------|----------| -| TC-LOC-08-01 | บันทึกเวลาพิเศษพร้อมตำแหน่งสำเร็จ | Medium | -| TC-LOC-08-02 | บันทึกเวลาพิเศษล้มเหลว - ไม่ระบุตำแหน่ง | Medium | - -### 4.9 TC-LOC-09: Privacy Consent - -| Test Case | Description | Priority | -|-----------|-------------|----------| -| TC-LOC-09-01 | แสดง Privacy Modal ก่อนใช้ Location | High | -| TC-LOC-09-02 | ยอมรับ Privacy Policy | High | -| TC-LOC-09-03 | ปฏิเสธ Privacy Policy | High | - ---- - -## 5. Test Cases Details - -### Test Case Template - -``` -Test Case ID: TC-LOC-XX-XX -Test Case Name: [ชื่อ Test Case] -Description: [คำอธิบาย] -Priority: High/Medium/Low -Pre-conditions: [เงื่อนไขเบื้องต้น] -Test Data: [ข้อมูลทดสอบ] -Test Steps: [ขั้นตอนการทดสอบ] -Expected Result: [ผลลัพธ์ที่คาดหวัง] -Actual Result: [ผลลัพธ์จริง - ว่างไว้กรอก] -Status: [Pass/Fail/Not Run] -Tested By: [ผู้ทดสอบ] -Test Date: [วันที่ทดสอบ] -``` - ---- - -### TC-LOC-01: Location Permission - -#### TC-LOC-01-01: อนุญาตให้เข้าถึงตำแหน่ง - -| Field | Value | -|-------|-------| -| **Test Case ID** | TC-LOC-01-01 | -| **Test Case Name** | อนุญาตให้เข้าถึงตำแหน่ง | -| **Description** | ผู้ใช้อนุญาตให้แอปเข้าถึงตำแหน่ง GPS | -| **Priority** | High | -| **Pre-conditions** | 1. ยอมรับ Privacy Policy แล้ว
2. เปิดแอปครั้งแรก หรือยังไม่เคยอนุญาต Location Permission | -| **Test Data** | - | -| **Test Steps** | 1. เปิดแอป HRMS
2. กดปุ่มขอตำแหน่ง (ถ้าจำเป็น)
3. เลือก "Allow" เมื่อระบบขอ Location Permission | -| **Expected Result** | 1. แสดงแผนที่พร้อมตำแหน่งปัจจุบัน
2. แสดงชื่อสถานที่ใกล้เคียง
3. ปุ่มลงเวลาเข้า/ออกใช้งานได้ | -| **Actual Result** | | -| **Status** | Not Run | -| **Tested By** | | -| **Test Date** | | - -#### TC-LOC-01-02: ปฏิเสธการเข้าถึงตำแหน่ง - -| Field | Value | -|-------|-------| -| **Test Case ID** | TC-LOC-01-02 | -| **Test Case Name** | ปฏิเสธการเข้าถึงตำแหน่ง | -| **Description** | ผู้ใช้ปฏิเสธการเข้าถึงตำแหน่ง GPS | -| **Priority** | High | -| **Pre-conditions** | 1. ยอมรับ Privacy Policy แล้ว
2. เปิดแอปครั้งแรก หรือยังไม่เคยอนุญาต Location Permission | -| **Test Data** | - | -| **Test Steps** | 1. เปิดแอป HRMS
2. กดปุ่มขอตำแหน่ง (ถ้าจำเป็น)
3. เลือก "Deny" เมื่อระบบขอ Location Permission | -| **Expected Result** | 1. แสดงข้อความแจ้งเตือน: "ไม่สามารถระบุตำแหน่งปัจจุบันได้ เนื่องจากคุณปฏิเสธการเข้าถึงตำแหน่ง กรุณาเปิดการเข้าถึงตำแหน่ง"
2. แผนที่ไม่แสดง
3. ปุ่มลงเวลาเข้า/ออกใช้งานไม่ได้ | -| **Actual Result** | | -| **Status** | Not Run | -| **Tested By** | | -| **Test Date** | | - ---- - -### TC-LOC-02: Location Acquisition - -#### TC-LOC-02-01: รับพิกัด GPS สำเร็จ (Outdoor) - -| Field | Value | -|-------|-------| -| **Test Case ID** | TC-LOC-02-01 | -| **Test Case Name** | รับพิกัด GPS สำเร็จ (Outdoor) | -| **Description** | รับพิกัด GPS สำเร็จในพื้นที่เปิดกว้าง | -| **Priority** | High | -| **Pre-conditions** | 1. อนุญาต Location Permission แล้ว
2. ยอมรับ Privacy Policy แล้ว | -| **Test Data** | Location: อาคารรัฐสภา แขวง ถนนนครไชยศรี เขตดุสิต กรุงเทพมหานคร (13.7563, 100.5018) | -| **Test Steps** | 1. เปิดแอป HRMS
2. อยู่ในพื้นที่เปิดกว้าง (Outdoor)
3. รอรับพิกัด GPS | -| **Expected Result** | 1. รับพิกัด GPS สำเร็จ
2. แสดงแผนที่พร้อมตำแหน่ง
3. แสดงชื่อสถานที่ใกล้เคียง
4. locationGranted = true | -| **Actual Result** | | -| **Status** | Not Run | -| **Tested By** | | -| **Test Date** | | - -#### TC-LOC-02-02: รับพิกัด GPS สำเร็จ (Indoor) - -| Field | Value | -|-------|-------| -| **Test Case ID** | TC-LOC-02-02 | -| **Test Case Name** | รับพิกัด GPS สำเร็จ (Indoor) | -| **Description** | รับพิกัด GPS สำเร็จในพื้นที่ปิด | -| **Priority** | Medium | -| **Pre-conditions** | 1. อนุญาต Location Permission แล้ว
2. ยอมรับ Privacy Policy แล้ว | -| **Test Data** | Location: ภายในอาคารสำนักงาน | -| **Test Steps** | 1. เปิดแอป HRMS
2. อยู่ในพื้นที่ปิด (Indoor)
3. รอรับพิกัด GPS | -| **Expected Result** | 1. รับพิกัด GPS สำเร็จ
2. แสดงแผนที่พร้อมตำแหน่ง
3. แสดงชื่อสถานที่ใกล้เคียง
4. อาจมีความแม่นยำต่ำกว่า Outdoor | -| **Actual Result** | | -| **Status** | Not Run | -| **Tested By** | | -| **Test Date** | | - -#### TC-LOC-02-03: รับพิกัด GPS ล้มเหลว (GPS ไม่ทำงาน) - -| Field | Value | -|-------|-------| -| **Test Case ID** | TC-LOC-02-03 | -| **Test Case Name** | รับพิกัด GPS ล้มเหลว (GPS ไม่ทำงาน) | -| **Description** | GPS ไม่ทำงาน หรืออยู่ในพื้นที่ที่ไม่มีสัญญาณ GPS | -| **Priority** | High | -| **Pre-conditions** | 1. อนุญาต Location Permission แล้ว
2. ยอมรับ Privacy Policy แล้ว | -| **Test Data** | - | -| **Test Steps** | 1. เปิดแอป HRMS
2. ปิด GPS ในอุปกรณ์
3. พยายามขอรับพิกัด | -| **Expected Result** | 1. แสดงข้อความแจ้งเตือน: "ไม่สามารถระบุตำแหน่งปัจจุบันได้"
2. แผนที่ไม่แสดง | -| **Actual Result** | | -| **Status** | Not Run | -| **Tested By** | | -| **Test Date** | | - -#### TC-LOC-02-04: หมดเวลาขอรับพิกัด (Timeout) - -| Field | Value | -|-------|-------| -| **Test Case ID** | TC-LOC-02-04 | -| **Test Case Name** | หมดเวลาขอรับพิกัด (Timeout) | -| **Description** | การร้องขอตำแหน่งหมดเวลา | -| **Priority** | Medium | -| **Pre-conditions** | 1. อนุญาต Location Permission แล้ว
2. ยอมรับ Privacy Policy แล้ว | -| **Test Data** | - | -| **Test Steps** | 1. เปิดแอป HRMS
2. จำลองสภาวะที่ GPS ตอบสนองช้า (ใช้ Developer Tools)
3. รอจนหมดเวลา | -| **Expected Result** | 1. แสดงข้อความแจ้งเตือน: "การร้องขอตำแหน่งหมดเวลา กรุณาลองอีกครั้ง" | -| **Actual Result** | | -| **Status** | Not Run | -| **Tested By** | | -| **Test Date** | | - ---- - -### TC-LOC-03: Location Validation - -#### TC-LOC-03-01: พิกัดถูกต้อง (Valid coordinates) - -| Field | Value | -|-------|-------| -| **Test Case ID** | TC-LOC-03-01 | -| **Test Case Name** | พิกัดถูกต้อง (Valid coordinates) | -| **Description** | ตรวจสอบพิกัดที่ถูกต้อง | -| **Priority** | High | -| **Pre-conditions** | 1. อนุญาต Location Permission แล้ว
2. ยอมรับ Privacy Policy แล้ว | -| **Test Data** | Latitude: 13.7563, Longitude: 100.5018, Accuracy: 10m, Timestamp: current | -| **Test Steps** | 1. เปิดแอป HRMS
2. รับพิกัด GPS ที่ถูกต้อง | -| **Expected Result** | 1. validation = { isValid: true, isMockDetected: false }
2. locationGranted = true
3. ไม่มี error/warning | -| **Actual Result** | | -| **Status** | Not Run | -| **Tested By** | | -| **Test Date** | | - -#### TC-LOC-03-02: พิกัดไม่ถูกต้อง (Invalid coordinates - (0,0)) - -| Field | Value | -|-------|-------| -| **Test Case ID** | TC-LOC-03-02 | -| **Test Case Name** | พิกัดไม่ถูกต้อง (Invalid coordinates - (0,0)) | -| **Description** | ตรวจสอบพิกัด (0,0) ซึ่งเป็นค่า default ของ mock location | -| **Priority** | High | -| **Pre-conditions** | 1. อนุญาต Location Permission แล้ว
2. ยอมรับ Privacy Policy แล้ว | -| **Test Data** | Latitude: 0, Longitude: 0 | -| **Test Steps** | 1. เปิดแอป HRMS
2. จำลองพิกัด (0,0) ด้วย Mock Location App | -| **Expected Result** | 1. validation = { isValid: false, isMockDetected: true, confidence: 'high' }
2. แสดงข้อความ: "พิกัดตำแหน่งไม่ถูกต้อง กรุณาลองใหม่"
3. disabledBtn = true | -| **Actual Result** | | -| **Status** | Not Run | -| **Tested By** | | -| **Test Date** | | - -#### TC-LOC-03-03: พิกัดนอกช่วง (Out of range) - -| Field | Value | -|-------|-------| -| **Test Case ID** | TC-LOC-03-03 | -| **Test Case Name** | พิกัดนอกช่วง (Out of range) | -| **Description** | ตรวจสอบพิกัดที่อยู่นอกช่วงที่กำหนด | -| **Priority** | Medium | -| **Pre-conditions** | 1. อนุญาต Location Permission แล้ว
2. ยอมรับ Privacy Policy แล้ว | -| **Test Data** | Latitude: 91, Longitude: 181 (out of range) | -| **Test Steps** | 1. เปิดแอป HRMS
2. จำลองพิกัดนอกช่วงด้วย Developer Tools | -| **Expected Result** | 1. validation = { isValid: false }
2. แสดงข้อความ: "พิกัดตำแหน่งไม่ถูกต้อง กรุณาลองใหม่" | -| **Actual Result** | | -| **Status** | Not Run | -| **Tested By** | | -| **Test Date** | | - -#### TC-LOC-03-04: ข้อมูลตำแหน่งเก่าเกินไป (Stale timestamp) - -| Field | Value | -|-------|-------| -| **Test Case ID** | TC-LOC-03-04 | -| **Test Case Name** | ข้อมูลตำแหน่งเก่าเกินไป (Stale timestamp) | -| **Description** | ตรวจสอบว่าข้อมูลตำแหน่งเก่าเกิน 60 วินาที | -| **Priority** | High | -| **Pre-conditions** | 1. อนุญาต Location Permission แล้ว
2. ยอมรับ Privacy Policy แล้ว | -| **Test Data** | Timestamp: Date.now() - 70,000 ms (70 seconds old) | -| **Test Steps** | 1. เปิดแอป HRMS
2. จำลอง timestamp เก่าเกิน 60 วินาที | -| **Expected Result** | 1. validation.errors = ["ข้อมูลตำแหน่งเก่าเกินไป กรุณารับสัญญาณ GPS ใหม่"]
2. mockIndicators += 2 | -| **Actual Result** | | -| **Status** | Not Run | -| **Tested By** | | -| **Test Date** | | - -#### TC-LOC-03-05: ความแม่นยำต่ำ (Poor accuracy) - -| Field | Value | -|-------|-------| -| **Test Case ID** | TC-LOC-03-05 | -| **Test Case Name** | ความแม่นยำต่ำ (Poor accuracy) | -| **Description** | ตรวจสอบความแม่นยำของ GPS ต่ำกว่า 100 เมตร | -| **Priority** | Medium | -| **Pre-conditions** | 1. อนุญาต Location Permission แล้ว
2. ยอมรับ Privacy Policy แล้ว | -| **Test Data** | Accuracy: 150 meters | -| **Test Steps** | 1. เปิดแอป HRMS
2. จำลองความแม่นยำต่ำด้วย Developer Tools | -| **Expected Result** | 1. validation.warnings = ["ความแม่นยำตำแหน่งต่ำเกินไป กรุณาตรวจสอบการรับสัญญาณ GPS"]
2. mockIndicators += 1 | -| **Actual Result** | | -| **Status** | Not Run | -| **Tested By** | | -| **Test Date** | | - -#### TC-LOC-03-06: ความเร็วเคลื่อนที่ผิดปกติ (Impossible speed) - -| Field | Value | -|-------|-------| -| **Test Case ID** | TC-LOC-03-06 | -| **Test Case Name** | ความเร็วเคลื่อนที่ผิดปกติ (Impossible speed) | -| **Description** | ตรวจสอบความเร็วเคลื่อนที่เกิน 100 m/s (360 km/h) | -| **Priority** | High | -| **Pre-conditions** | 1. อนุญาต Location Permission แล้ว
2. ยอมรับ Privacy Policy แล้ว
3. มีตำแหน่งเดิมใน history | -| **Test Data** | Position 1: (13.7563, 100.5018) at t=0
Position 2: (15.8700, 100.9925) at t=60 (~180 km in 60s = 3000 km/h) | -| **Test Steps** | 1. เปิดแอป HRMS
2. จำลองการเคลื่อนที่ด้วยความเร็วผิดปกติ | -| **Expected Result** | 1. validation.errors = ["ตรวจพบการเคลื่อนที่ด้วยความเร็วผิดปกติ อาจเป็นการจำลองตำแหน่ง"]
2. mockIndicators += 3 | -| **Actual Result** | | -| **Status** | Not Run | -| **Tested By** | | -| **Test Date** | | - ---- - -### TC-LOC-04: Mock Location Detection - -#### TC-LOC-04-01: ไม่พบ Mock Location (Normal GPS) - -| Field | Value | -|-------|-------| -| **Test Case ID** | TC-LOC-04-01 | -| **Test Case Name** | ไม่พบ Mock Location (Normal GPS) | -| **Description** | การใช้งาน GPS ปกติ ไม่พบ mock location | -| **Priority** | High | -| **Pre-conditions** | 1. อนุญาต Location Permission แล้ว
2. ยอมรับ Privacy Policy แล้ว
3. ไม่ได้เปิด Mock Location App | -| **Test Data** | Normal GPS data | -| **Test Steps** | 1. เปิดแอป HRMS
2. ใช้งาน GPS ปกติ | -| **Expected Result** | 1. validation = { isValid: true, isMockDetected: false }
2. locationGranted = true
3. isMockLocationDetected = false
4. ปุ่มลงเวลาเข้า/ออกใช้งานได้ | -| **Actual Result** | | -| **Status** | Not Run | -| **Tested By** | | -| **Test Date** | | - -#### TC-LOC-04-02: พบ Mock Location - Fake GPS App - -| Field | Value | -|-------|-------| -| **Test Case ID** | TC-LOC-04-02 | -| **Test Case Name** | พบ Mock Location - Fake GPS App | -| **Description** | ตรวจพบ mock location จาก Fake GPS App | -| **Priority** | High | -| **Pre-conditions** | 1. อนุญาต Location Permission แล้ว
2. ยอมรับ Privacy Policy แล้ว | -| **Test Data** | Mock location from Fake GPS App | -| **Test Steps** | 1. เปิด Fake GPS Location App
2. ตั้งค่าตำแหน่งปลอม
3. เปิดแอป HRMS
4. ขอรับพิกัด | -| **Expected Result** | 1. ตรวจพบ mock location (indicators >= 3)
2. แสดงข้อความ: "ตรวจพบว่าตำแหน่ง GPS อาจไม่ถูกต้อง กรุณาปิดแอปจำลองตำแหน่งและลองใหม่"
3. isMockLocationDetected = true
4. disabledBtn = true | -| **Actual Result** | | -| **Status** | Not Run | -| **Tested By** | | -| **Test Date** | | - -#### TC-LOC-04-03: พบ Mock Location - พิกัด (0,0) - -| Field | Value | -|-------|-------| -| **Test Case ID** | TC-LOC-04-03 | -| **Test Case Name** | พบ Mock Location - พิกัด (0,0) | -| **Description** | ตรวจพบพิกัด (0,0) ซึ่งเป็นค่า default ของ mock location | -| **Priority** | High | -| **Pre-conditions** | 1. อนุญาต Location Permission แล้ว
2. ยอมรับ Privacy Policy แล้ว | -| **Test Data** | Latitude: 0, Longitude: 0 | -| **Test Steps** | 1. จำลองพิกัด (0,0) ด้วย Mock Location App
2. เปิดแอป HRMS
3. ขอรับพิกัด | -| **Expected Result** | 1. mockIndicators = 3 (จาก invalid coordinates)
2. isMockDetected = true
3. confidence = 'medium'
4. แสดงข้อความแจ้งเตือน | -| **Actual Result** | | -| **Status** | Not Run | -| **Tested By** | | -| **Test Date** | | - -#### TC-LOC-04-04: พบ Mock Location - ข้อมูลเก่าเกินไป - -| Field | Value | -|-------|-------| -| **Test Case ID** | TC-LOC-04-04 | -| **Test Case Name** | พบ Mock Location - ข้อมูลเก่าเกินไป | -| **Description** | ตรวจพบข้อมูลตำแหน่งเก่าเกิน 60 วินาที | -| **Priority** | High | -| **Pre-conditions** | 1. อนุญาต Location Permission แล้ว
2. ยอมรับ Privacy Policy แล้ว | -| **Test Data** | Timestamp: Date.now() - 70,000 ms | -| **Test Steps** | 1. จำลอง timestamp เก่าเกิน 60 วินาที
2. เปิดแอป HRMS
3. ขอรับพิกัด | -| **Expected Result** | 1. mockIndicators = 2 (จาก stale timestamp)
2. แสดง warning: "ข้อมูลตำแหน่งเก่าเกินไป กรุณารับสัญญาณ GPS ใหม่" | -| **Actual Result** | | -| **Status** | Not Run | -| **Tested By** | | -| **Test Date** | | - -#### TC-LOC-04-05: พบ Mock Location - ความเร็วผิดปกติ - -| Field | Value | -|-------|-------| -| **Test Case ID** | TC-LOC-04-05 | -| **Test Case Name** | พบ Mock Location - ความเร็วผิดปกติ | -| **Description** | ตรวจพบความเร็วเคลื่อนที่ผิดปกติ | -| **Priority** | High | -| **Pre-conditions** | 1. อนุญาต Location Permission แล้ว
2. ยอมรับ Privacy Policy แล้ว
3. มีตำแหน่งเดิมใน history | -| **Test Data** | Speed > 100 m/s | -| **Test Steps** | 1. จำลองการเคลื่อนที่ด้วยความเร็ว > 100 m/s
2. เปิดแอป HRMS | -| **Expected Result** | 1. mockIndicators = 3 (จาก impossible speed)
2. isMockDetected = true
3. confidence = 'medium' | -| **Actual Result** | | -| **Status** | Not Run | -| **Tested By** | | -| **Test Date** | | - -#### TC-LOC-04-06: พบ Mock Location - หลาย indicators (Confidence High) - -| Field | Value | -|-------|-------| -| **Test Case ID** | TC-LOC-04-06 | -| **Test Case Name** | พบ Mock Location - หลาย indicators (Confidence High) | -| **Description** | ตรวจพบหลาย mock indicators พร้อมกัน | -| **Priority** | High | -| **Pre-conditions** | 1. อนุญาต Location Permission แล้ว
2. ยอมรับ Privacy Policy แล้ว | -| **Test Data** | Combined: invalid coordinates (0,0) + stale timestamp + impossible speed | -| **Test Steps** | 1. จำลอง multiple mock indicators
2. เปิดแอป HRMS | -| **Expected Result** | 1. mockIndicators >= 5
2. isMockDetected = true
3. confidence = 'high'
4. แสดงข้อความแจ้งเตือนชัดเจน | -| **Actual Result** | | -| **Status** | Not Run | -| **Tested By** | | -| **Test Date** | | - ---- - -### TC-LOC-05: POI Resolution - -#### TC-LOC-05-01: แปลงพิกัดเป็นชื่อสถานที่สำเร็จ (Bangkok GIS) - -| Field | Value | -|-------|-------| -| **Test Case ID** | TC-LOC-05-01 | -| **Test Case Name** | แปลงพิกัดเป็นชื่อสถานที่สำเร็จ (Bangkok GIS) | -| **Description** | แปลงพิกัดเป็นชื่อสถานที่สำเร็จผ่าน Bangkok GIS API | -| **Priority** | High | -| **Pre-conditions** | 1. อนุญาต Location Permission แล้ว
2. ยอมรับ Privacy Policy แล้ว
3. รับพิกัดสำเร็จแล้ว | -| **Test Data** | Latitude: 13.7563, Longitude: 100.5018 (Sanam Suea Pa, Bangkok) | -| **Test Steps** | 1. เปิดแอป HRMS
2. รับพิกัด GPS
3. รอให้ระบบแปลงพิกัดเป็นชื่อสถานที่ | -| **Expected Result** | 1. เรียก Bangkok GIS API: `https://bmagis.bangkok.go.th/...`
2. แสดงชื่อสถานที่ (POI name)
3. poiPlaceName มีค่า | -| **Actual Result** | | -| **Status** | Not Run | -| **Tested By** | | -| **Test Date** | | - -#### TC-LOC-05-02: แปลงพิกัดเป็นชื่อสถานที่สำเร็จ (ArcGIS Fallback) - -| Field | Value | -|-------|-------| -| **Test Case ID** | TC-LOC-05-02 | -| **Test Case Name** | แปลงพิกัดเป็นชื่อสถานที่สำเร็จ (ArcGIS Fallback) | -| **Description** | แปลงพิกัดเป็นชื่อสถานที่สำเร็จผ่าน ArcGIS API (เมื่อ Bangkok GIS fail) | -| **Priority** | Medium | -| **Pre-conditions** | 1. อนุญาต Location Permission แล้ว
2. ยอมรับ Privacy Policy แล้ว
3. Bangkok GIS API ล้มเหลว | -| **Test Data** | Latitude: 13.7563, Longitude: 100.5018 | -| **Test Steps** | 1. เปิดแอป HRMS
2. รับพิกัด GPS
3. จำลอง Bangkok GIS API fail
4. รอ fallback ไป ArcGIS | -| **Expected Result** | 1. เรียก ArcGIS API: `https://geocode.arcgis.com/...`
2. แสดงชื่อสถานที่ (POI name)
3. poiPlaceName มีค่า | -| **Actual Result** | | -| **Status** | Not Run | -| **Tested By** | | -| **Test Date** | | - -#### TC-LOC-05-03: แปลงพิกัดล้มเหลว (ทั้งสอง service down) - -| Field | Value | -|-------|-------| -| **Test Case ID** | TC-LOC-05-03 | -| **Test Case Name** | แปลงพิกัดล้มเหลว (ทั้งสอง service down) | -| **Description** | แปลงพิกัดล้มเหลวเมื่อทั้ง Bangkok GIS และ ArcGIS down | -| **Priority** | Low | -| **Pre-conditions** | 1. อนุญาต Location Permission แล้ว
2. ยอมรับ Privacy Policy แล้ว | -| **Test Data** | - | -| **Test Steps** | 1. เปิดแอป HRMS
2. จำลองทั้งสอง API fail | -| **Expected Result** | 1. แอปยังทำงานต่อไปได้
2. แผนที่แสดงตำแหน่งแต่ไม่มีชื่อสถานที่
3. formLocation.POI = '' | -| **Actual Result** | | -| **Status** | Not Run | -| **Tested By** | | -| **Test Date** | | - ---- - -### TC-LOC-06: Check-in with Location - -#### TC-LOC-06-01: ลงเวลาเข้าสำเร็จ - ณ สถานที่ตั้ง (In-place) - -| Field | Value | -|-------|-------| -| **Test Case ID** | TC-LOC-06-01 | -| **Test Case Name** | ลงเวลาเข้าสำเร็จ - ณ สถานที่ตั้ง (In-place) | -| **Description** | ลงเวลาเข้าสำเร็จเมื่ออยู่ ณ สถานที่ตั้ง | -| **Priority** | High | -| **Pre-conditions** | 1. อนุญาต Location Permission แล้ว
2. ยอมรับ Privacy Policy แล้ว
3. รับพิกัดและชื่อสถานที่สำเร็จ
4. ถ่ายรูปภาพแล้ว
5. statusCheckin = true (เข้างาน) | -| **Test Data** | workplace: 'in-place', locationName: '', POI: 'สนามเสือป่า', img: (รูปถ่าย) | -| **Test Steps** | 1. เลือก "ในสถานที่"
2. กด "ลงเวลาเข้างาน"
3. ยืนยันการลงเวลา | -| **Expected Result** | 1. API call: POST `/leave/check-in`
2. formdata: { lat, lon, POI, isLocation: true, locationName: '', img, remark, checkInId }
3. แสดง modal ลงเวลาเข้างานสำเร็จ
4. statusCheckin = false | -| **Actual Result** | | -| **Status** | Not Run | -| **Tested By** | | -| **Test Date** | | - -#### TC-LOC-06-02: ลงเวลาเข้าสำเร็จ - นอกสถานที่ตั้ง (Off-site) - -| Field | Value | -|-------|-------| -| **Test Case ID** | TC-LOC-06-02 | -| **Test Case Name** | ลงเวลาเข้าสำเร็จ - นอกสถานที่ตั้ง (Off-site) | -| **Description** | ลงเวลาเข้าสำเร็จเมื่ออยู่นอกสถานที่ตั้ง | -| **Priority** | High | -| **Pre-conditions** | 1. อนุญาต Location Permission แล้ว
2. ยอมรับ Privacy Policy แล้ว
3. รับพิกัดและชื่อสถานที่สำเร็จ
4. ถ่ายรูปภาพแล้ว
5. statusCheckin = true (เข้างาน) | -| **Test Data** | workplace: 'off-site', locationName: 'ปฏิบัติงานที่บ้าน (WFH)', POI: 'บ้าน', img: (รูปถ่าย) | -| **Test Steps** | 1. เลือก "นอกสถานที่"
2. เลือก "ปฏิบัติงานที่บ้าน (WFH)"
3. กด "ลงเวลาเข้างาน"
4. ยืนยันการลงเวลา | -| **Expected Result** | 1. API call: POST `/leave/check-in`
2. formdata: { lat, lon, POI, isLocation: false, locationName: 'ปฏิบัติงานที่บ้าน (WFH)', img, remark, checkInId }
3. แสดง modal ลงเวลาเข้างานสำเร็จ | -| **Actual Result** | | -| **Status** | Not Run | -| **Tested By** | | -| **Test Date** | | - -#### TC-LOC-06-03: ลงเวลาเข้าล้มเหลว - Mock Location detected - -| Field | Value | -|-------|-------| -| **Test Case ID** | TC-LOC-06-03 | -| **Test Case Name** | ลงเวลาเข้าล้มเหลว - Mock Location detected | -| **Description** | ลงเวลาเข้าล้มเหลวเมื่อตรวจพบ mock location | -| **Priority** | High | -| **Pre-conditions** | 1. อนุญาต Location Permission แล้ว
2. ยอมรับ Privacy Policy แล้ว
3. เปิด Mock Location App | -| **Test Data** | Mock location enabled | -| **Test Steps** | 1. เปิด Mock Location App
2. ตั้งค่าตำแหน่งปลอง
3. เปิดแอป HRMS
4. พยายามลงเวลาเข้างาน | -| **Expected Result** | 1. แสดงข้อความ: "ตรวจพบว่าตำแหน่ง GPS อาจไม่ถูกต้อง..."
2. isMockLocationDetected = true
3. disabledBtn = true
4. ไม่สามารถลงเวลาเข้างานได้ | -| **Actual Result** | | -| **Status** | Not Run | -| **Tested By** | | -| **Test Date** | | - -#### TC-LOC-06-04: ลงเวลาเข้าล้มเหลว - Location permission denied - -| Field | Value | -|-------|-------| -| **Test Case ID** | TC-LOC-06-04 | -| **Test Case Name** | ลงเวลาเข้าล้มเหลว - Location permission denied | -| **Description** | ลงเวลาเข้าล้มเหลวเมื่อปฏิเสธ Location Permission | -| **Priority** | High | -| **Pre-conditions** | 1. ยอมรับ Privacy Policy แล้ว
2. ปฏิเสธ Location Permission | -| **Test Data** | - | -| **Test Steps** | 1. เปิดแอป HRMS
2. ปฏิเสธ Location Permission
3. พยายามลงเวลาเข้างาน | -| **Expected Result** | 1. locationGranted = false
2. ปุ่มลงเวลาเข้า/ออก disabled
3. ไม่สามารถลงเวลาเข้างานได้ | -| **Actual Result** | | -| **Status** | Not Run | -| **Tested By** | | -| **Test Date** | | - ---- - -### TC-LOC-07: Check-out with Location - -#### TC-LOC-07-01: ลงเวลาออกสำเร็จ - -| Field | Value | -|-------|-------| -| **Test Case ID** | TC-LOC-07-01 | -| **Test Case Name** | ลงเวลาออกสำเร็จ | -| **Description** | ลงเวลาออกสำเร็จพร้อมตำแหน่ง | -| **Priority** | High | -| **Pre-conditions** | 1. อนุญาต Location Permission แล้ว
2. ยอมรับ Privacy Policy แล้ว
3. เคยลงเวลาเข้างานแล้ว
4. statusCheckin = false (ออกงาน) | -| **Test Data** | POI: 'Office', img: (รูปถ่าย) | -| **Test Steps** | 1. เปิดแอป HRMS
2. ถ่ายรูปภาพ
3. กด "ลงเวลาออกงาน" | -| **Expected Result** | 1. API call: POST `/leave/check-in` (with checkInId)
2. แสดง modal ลงเวลาออกงานสำเร็จ
3. statusCheckin = true | -| **Actual Result** | | -| **Status** | Not Run | -| **Tested By** | | -| **Test Date** | | - -#### TC-LOC-07-02: ลงเวลาออกล้มเหลว - Mock Location detected - -| Field | Value | -|-------|-------| -| **Test Case ID** | TC-LOC-07-02 | -| **Test Case Name** | ลงเวลาออกล้มเหลว - Mock Location detected | -| **Description** | ลงเวลาออกล้มเหลวเมื่อตรวจพบ mock location | -| **Priority** | High | -| **Pre-conditions** | 1. อนุญาต Location Permission แล้ว
2. ยอมรับ Privacy Policy แล้ว
3. เคยลงเวลาเข้างานแล้ว
4. เปิด Mock Location App | -| **Test Data** | Mock location enabled | -| **Test Steps** | 1. เปิด Mock Location App
2. พยายามลงเวลาออกงาน | -| **Expected Result** | 1. แสดงข้อความ: "ตรวจพบว่าตำแหน่ง GPS อาจไม่ถูกต้อง..."
2. disabledBtn = true
3. ไม่สามารถลงเวลาออกงานได้ | -| **Actual Result** | | -| **Status** | Not Run | -| **Tested By** | | -| **Test Date** | | - ---- - -### TC-LOC-08: Special Time Entry - -#### TC-LOC-08-01: บันทึกเวลาพิเศษพร้อมตำแหน่งสำเร็จ - -| Field | Value | -|-------|-------| -| **Test Case ID** | TC-LOC-08-01 | -| **Test Case Name** | บันทึกเวลาพิเศษพร้อมตำแหน่งสำเร็จ | -| **Description** | บันทึกเวลาพิเศษพร้อมตำแหน่งสำเร็จ | -| **Priority** | Medium | -| **Pre-conditions** | 1. อนุญาต Location Permission แล้ว
2. ยอมรับ Privacy Policy แล้ว
3. รับพิกัดสำเร็จ | -| **Test Data** | POI: 'Meeting Room', img: (รูปถ่าย) | -| **Test Steps** | 1. เปิดแอป HRMS
2. ไปที่หน้าบันทึกเวลาพิเศษ
3. กรอกข้อมูล
4. บันทึก | -| **Expected Result** | 1. บันทึกเวลาพิเศษสำเร็จ
2. มีข้อมูลตำแหน่ง | -| **Actual Result** | | -| **Status** | Not Run | -| **Tested By** | | -| **Test Date** | | - -#### TC-LOC-08-02: บันทึกเวลาพิเศษล้มเหลว - ไม่ระบุตำแหน่ง - -| Field | Value | -|-------|-------| -| **Test Case ID** | TC-LOC-08-02 | -| **Test Case Name** | บันทึกเวลาพิเศษล้มเหลว - ไม่ระบุตำแหน่ง | -| **Description** | บันทึกเวลาพิเศษล้มเหลวเมื่อไม่ระบุตำแหน่ง | -| **Priority** | Medium | -| **Pre-conditions** | 1. ยอมรับ Privacy Policy แล้ว
2. ปฏิเสธ Location Permission | -| **Test Data** | - | -| **Test Steps** | 1. เปิดแอป HRMS
2. ปฏิเสธ Location Permission
3. พยายามบันทึกเวลาพิเศษ | -| **Expected Result** | 1. แจ้งเตือนให้ระบุตำแหน่ง | -| **Actual Result** | | -| **Status** | Not Run | -| **Tested By** | | -| **Test Date** | | - ---- - -### TC-LOC-09: Privacy Consent - -#### TC-LOC-09-01: แสดง Privacy Modal ก่อนใช้ Location - -| Field | Value | -|-------|-------| -| **Test Case ID** | TC-LOC-09-01 | -| **Test Case Name** | แสดง Privacy Modal ก่อนใช้ Location | -| **Description** | แสดง Privacy Modal ก่อนใช้ Location | -| **Priority** | High | -| **Pre-conditions** | 1. เปิดแอปครั้งแรก
2. ยังไม่ยอมรับ Privacy Policy | -| **Test Data** | - | -| **Test Steps** | 1. เปิดแอป HRMS
2. กดปุ่มที่ต้องการ Location (แผนที่/กล้อง) | -| **Expected Result** | 1. privacyStore.modalPrivacy = true
2. แสดง Privacy Modal
3. ไม่สามารถใช้งาน Location ได้จนกว่าจะยอมรับ | -| **Actual Result** | | -| **Status** | Not Run | -| **Tested By** | | -| **Test Date** | | - -#### TC-LOC-09-02: ยอมรับ Privacy Policy - -| Field | Value | -|-------|-------| -| **Test Case ID** | TC-LOC-09-02 | -| **Test Case Name** | ยอมรับ Privacy Policy | -| **Description** | ยอมรับ Privacy Policy และใช้งาน Location ได้ | -| **Priority** | High | -| **Pre-conditions** | 1. เปิดแอปครั้งแรก
2. Privacy Modal แสดงขึ้นมา | -| **Test Data** | - | -| **Test Steps** | 1. เปิดแอป HRMS
2. แสดง Privacy Modal
3. กด "ยอมรับ" | -| **Expected Result** | 1. privacyStore.isAccepted = true
2. privacyStore.modalPrivacy = false
3. สามารถใช้งาน Location ได้
4. เรียก mapRef.value?.requestLocationPermission() | -| **Actual Result** | | -| **Status** | Not Run | -| **Tested By** | | -| **Test Date** | | - -#### TC-LOC-09-03: ปฏิเสธ Privacy Policy - -| Field | Value | -|-------|-------| -| **Test Case ID** | TC-LOC-09-03 | -| **Test Case Name** | ปฏิเสธ Privacy Policy | -| **Description** | ปฏิเสธ Privacy Policy และไม่สามารถใช้งาน Location | -| **Priority** | High | -| **Pre-conditions** | 1. เปิดแอปครั้งแรก
2. Privacy Modal แสดงขึ้นมา | -| **Test Data** | - | -| **Test Steps** | 1. เปิดแอป HRMS
2. แสดง Privacy Modal
3. กด "ปฏิเสธ" หรือปิด Modal | -| **Expected Result** | 1. privacyStore.isAccepted = false
2. privacyStore.modalPrivacy = false
3. ไม่สามารถใช้งาน Location ได้
4. แจ้งเตือนเมื่อพยายามใช้งาน Location | -| **Actual Result** | | -| **Status** | Not Run | -| **Tested By** | | -| **Test Date** | | - ---- - -## 6. Test Summary - -### 6.1 Test Case Statistics - -| Category | Total | Critical | High | Medium | Low | -|----------|-------|----------|------|--------|-----| -| Location Permission | 2 | - | 2 | - | - | -| Location Acquisition | 4 | - | 2 | 2 | - | -| Location Validation | 6 | - | 4 | 2 | - | -| Mock Location Detection | 6 | - | 6 | - | - | -| POI Resolution | 3 | - | 1 | 1 | 1 | -| Check-in with Location | 4 | - | 4 | - | - | -| Check-out with Location | 2 | - | 2 | - | - | -| Special Time Entry | 2 | - | - | 2 | - | -| Privacy Consent | 3 | - | 3 | - | - | -| **Total** | **32** | - | **24** | **7** | **1** | - -### 6.2 Execution Status - -| Status | Count | Percentage | -|--------|-------|------------| -| Not Run | 32 | 100% | -| Pass | 0 | 0% | -| Fail | 0 | 0% | -| Blocked | 0 | 0% | - ---- - -## 7. References - -### 7.1 API Endpoints - -| Endpoint | Method | Description | -|----------|--------|-------------| -| `/leave/check-in` | POST | Check-in/Check-out with location data | -| `/leave/check-time` | GET | Get check-in/check-out status | -| `/leave/check-status` | GET | Get queue status | -| `/leave/user/checkout-check/{isSeminar}` | GET | Check before check-out | - -### 7.2 Key Functions - -| Function | Location | Description | -|----------|----------|-------------| -| `validateLocation()` | `useLocationValidation.ts` | Main validation function | -| `validateCoordinates()` | `useLocationValidation.ts` | Coordinate validation | -| `validateTimestamp()` | `useLocationValidation.ts` | Timestamp validation | -| `validateAccuracy()` | `useLocationValidation.ts` | Accuracy validation | -| `validateSpeed()` | `useLocationValidation.ts` | Speed validation | -| `haversineDistance()` | `useLocationValidation.ts` | Distance calculation | -| `checkPrivacyAccepted()` | `usePermissions.ts` | Check privacy consent | -| `requestLocationPermission()` | `AscGISMap.vue` | Request location permission | - -### 7.3 Related Files - -- `/src/composables/useLocationValidation.ts` - Location validation logic -- `/src/views/HomeView.vue` - Check-in/Check-out page -- `/src/components/AscGISMap.vue` - Map component with location validation -- `/src/api/api.checkin.ts` - Check-in API endpoints -- `/src/stores/privacy.ts` - Privacy store for consent management -- `/src/composables/usePermissions.ts` - Permission checking utilities - ---- - -## 8. Revision History - -| Version | Date | Author | Changes | -|---------|------|--------|---------| -| 1.0.0 | 2025-03-09 | QA Team | Initial version - 32 test cases for Location features | - ---- - -## 9. Approval - -| Role | Name | Signature | Date | -|------|------|-----------|------| -| QA Lead | | | | -| Developer | | | | -| Product Owner | | | | diff --git a/docs/test-steps-location.md b/docs/test-steps-location.md deleted file mode 100644 index f62a2fa..0000000 --- a/docs/test-steps-location.md +++ /dev/null @@ -1,1026 +0,0 @@ -# Test Steps: Location Features -# HRMS Check-in/Check-out System - -## Document Information - -| Field | Value | -|-------|-------| -| **Document Version** | 1.0.0 | -| **Last Updated** | 2025-03-09 | -| **Project** | HRMS Check-in/Check-out | -| **Module** | Location Features | -| **Author** | QA Team | - ---- - -## Table of Contents - -1. [Overview](#1-overview) -2. [Test Environment Setup](#2-test-environment-setup) -3. [Manual Testing Procedures](#3-manual-testing-procedures) -4. [Mock Location Testing](#4-mock-location-testing) -5. [Developer Tools Testing](#5-developer-tools-testing) -6. [Device-Specific Testing](#6-device-specific-testing) -7. [Test Data & Scenarios](#7-test-data--scenarios) -8. [Troubleshooting](#8-troubleshooting) - ---- - -## 1. Overview - -เอกสารนี้ประกอบด้วยขั้นตอนการทดสอบ (Test Steps) แบบละเอียดสำหรับการทดสอบฟีเจอร์ Location ของระบบ HRMS Check-in/Check-out โดยเน้นการทดสอบแบบ Manual Testing และการจำลองสถานการณ์ต่าง ๆ - -### 1.1 Testing Scope - -| Area | Description | -|------|-------------| -| **Location Permission** | การขอและการจัดการสิทธิ์การเข้าถึงตำแหน่ง | -| **Location Acquisition** | การรับพิกัด GPS จากอุปกรณ์ | -| **Location Validation** | การตรวจสอบความถูกต้องของตำแหน่ง | -| **Mock Detection** | การตรวจจับการจำลองตำแหน่ง | -| **POI Resolution** | การแปลงพิกัดเป็นชื่อสถานที่ | -| **Check-in/Check-out** | การลงเวลาเข้า-ออกงานพร้อมตำแหน่ง | -| **Privacy Consent** | การขอความยินยอมการใช้ข้อมูล | - ---- - -## 2. Test Environment Setup - -### 2.1 Device Preparation - -#### 2.1.1 Android Device Setup - -1. **Enable Developer Options** - - เปิด Settings > About Phone - - แตะที่ Build Number 7 ครั้ง - - กลับมาที่ Settings > Developer Options - -2. **Enable Mock Location (สำหรับการทดสอบ)** - - เปิด Settings > Developer Options - - เลือก "Select mock location app" - - เลือก Fake GPS Location App ที่ติดตั้ง - -3. **Install Fake GPS Apps** - - Fake GPS Location (by Lexa) - [Google Play](https://play.google.com/store/apps/details?id=com.lexa.fakegps) - - GPS Joystick - [Google Play](https://play.google.com/store/apps/details?id=com.theappninjas.android.gpsjoke) - -#### 2.1.2 iOS Device Setup - -1. **Xcode Simulation (ต้องใช้ Mac)** - - เปิด Xcode - - เลือก Device > Debug Location > Custom Location... - - ป้อน Latitude และ Longitude - -2. **Location Changer Apps** - - Location Changer - [App Store](https://apps.apple.com/app/location-changer/id1383869695) - -#### 2.1.3 Desktop Browser Setup - -1. **Chrome DevTools** - - กด F12 หรือ Cmd+Option+I (Mac) / Ctrl+Shift+I (Windows) - - เลือก More tools > Sensors - - ตั้งค่า Location ใน Sensors tab - -2. **Edge DevTools** - - กด F12 - - เลือก More tools > Sensors - - ตั้งค่า Location ใน Sensors tab - -### 2.2 Network Setup - -| Scenario | Setup | -|----------|-------| -| **Normal Network** | WiFi/4G/5G เชื่อมต่อปกติ | -| **Slow Network** | Chrome DevTools > Network > Throttling > Slow 3G | -| **Offline** | Chrome DevTools > Network > Offline | -| **GPS Blocking** | Disable Location Service ในระบบ | - -### 2.3 Test Location Data - -| Location Name | Latitude | Longitude | Notes | -|---------------|----------|-----------|-------| -| Sanam Suea Pa (สนามเสือป่า) | 13.7563 | 100.5018 | Bangkok Government Office | -| Suvarnabhumi Airport | 13.6900 | 100.7501 | Outdoor, high GPS accuracy | -| Central World | 13.7944 | 100.5439 | Indoor shopping mall | -| Null Island | 0 | 0 | Default mock location (invalid) | -| Out of Range | 91 | 181 | Invalid coordinates | -| Chiang Mai | 18.7883 | 98.9853 | Northern Thailand | - ---- - -## 3. Manual Testing Procedures - -### 3.1 TC-LOC-01: Location Permission Testing - -#### Test Case: TC-LOC-01-01 - อนุญาตให้เข้าถึงตำแหน่ง - -**Objective**: ทดสอบการอนุญาตให้เข้าถึงตำแหน่ง - -| Step | Action | Expected Result | Screenshot | -|------|--------|-----------------|------------| -| 1 | เปิดแอป HRMS ครั้งแรก | แสดงหน้าแรกของแอป | [ ] | -| 2 | แสดง Privacy Modal (ถ้ายังไม่ยอมรับ) | แสดง Privacy Policy Modal | [ ] | -| 3 | กด "ยอมรับ" เพื่อยอมรับ Privacy Policy | Modal ปิด, `isAccepted = true` | [ ] | -| 4 | กดปุ่มขอตำแหน่ง (mapRef.requestLocationPermission()) | ระบบขอ Location Permission | [ ] | -| 5 | เลือก "Allow" เมื่อระบบขอ Location Permission | รับสิทธิ์สำเร็จ | [ ] | -| 6 | รอรับพิกัด GPS | แสดง Skeleton loading | [ ] | -| 7 | รับพิกัดสำเร็จ | แสดงแผนที่พร้อมตำแหน่งปัจจุบัน | [ ] | -| 8 | ตรวจสอบชื่อสถานที่ใกล้เคียง | แสดงชื่อสถานที่ (POI) | [ ] | -| 9 | ตรวจสอบสถานะปุ่มลงเวลา | ปุ่มใช้งานได้ (disabled = false) | [ ] | - -**Verification Checklist:** - -- [ ] แผนที่แสดงตำแหน่งปัจจุบันถูกต้อง -- [ ] ชื่อสถานที่ใกล้เคียงแสดงถูกต้อง -- [ ] `locationGranted = true` -- [ ] ปุ่มลงเวลาเข้า/ออกใช้งานได้ -- [ ] ไม่มีข้อความ error แสดงขึ้น - ---- - -#### Test Case: TC-LOC-01-02 - ปฏิเสธการเข้าถึงตำแหน่ง - -**Objective**: ทดสอบการปฏิเสธการเข้าถึงตำแหน่ง - -| Step | Action | Expected Result | Screenshot | -|------|--------|-----------------|------------| -| 1 | เปิดแอป HRMS ครั้งแรก | แสดงหน้าแรกของแอป | [ ] | -| 2 | ยอมรับ Privacy Policy | Modal ปิด | [ ] | -| 3 | กดปุ่มขอตำแหน่ง | ระบบขอ Location Permission | [ ] | -| 4 | เลือก "Deny" เมื่อระบบขอ Location Permission | ปฏิเสธสิทธิ์ | [ ] | -| 5 | ตรวจสอบ error message | แสดง "ไม่สามารถระบุตำแหน่งปัจจุบันได้ เนื่องจากคุณปฏิเสธการเข้าถึงตำแหน่ง กรุณาเปิดการเข้าถึงตำแหน่ง" | [ ] | -| 6 | ตรวจสอบแผนที่ | แผนที่ไม่แสดง (poiPlaceName = '') | [ ] | -| 7 | ตรวจสอบปุ่มลงเวลา | ปุ่มใช้งานไม่ได้ (disabled = true) | [ ] | - -**Verification Checklist:** - -- [ ] แสดง error message ถูกต้อง -- [ ] แผนที่ไม่แสดง -- [ ] `locationGranted = false` -- [ ] ปุ่มลงเวลาเข้า/ออก disabled -- [ ] ไม่สามารถลงเวลาได้ - ---- - -### 3.2 TC-LOC-02: Location Acquisition Testing - -#### Test Case: TC-LOC-02-01 - รับพิกัด GPS สำเร็จ (Outdoor) - -**Objective**: ทดสอบการรับพิกัด GPS สำเร็จในพื้นที่เปิดกว้าง - -| Step | Action | Expected Result | Screenshot | -|------|--------|-----------------|------------| -| 1 | ไปยังพื้นที่เปิดกว้าง (Outdoor) | อยู่กลางแจ้ง มีสัญญาณ GPS ดี | [ ] | -| 2 | เปิดแอป HRMS | แสดงหน้าแรก | [ ] | -| 3 | อนุญาต Location Permission | รับสิทธิ์สำเร็จ | [ ] | -| 4 | กดปุ่มขอตำแหน่ง | เริ่มรับพิกัด GPS | [ ] | -| 5 | รอรับพิกัด (ประมาณ 3-5 วินาที) | แสดง Skeleton loading | [ ] | -| 6 | รับพิกัดสำเร็จ | แสดงแผนที่ | [ ] | -| 7 | ตรวจสอบพิกัด | Latitude และ Longitude ถูกต้อง | [ ] | -| 8 | ตรวจสอบความแม่นยำ | Accuracy <= 100 meters | [ ] | -| 9 | ตรวจสอบ timestamp | Timestamp ภายใน 60 วินาที | [ ] | -| 10 | ตรวจสอบชื่อสถานที่ | แสดงชื่อสถานที่ใกล้เคียง | [ ] | - -**Verification Checklist:** - -- [ ] รับพิกัดสำเร็จ -- [ ] พิกัดอยู่ในช่วงที่ถูกต้อง (lat: -90 to 90, lon: -180 to 180) -- [ ] ไม่ใช่ (0, 0) -- [ ] Accuracy <= 100 meters -- [ ] Timestamp ภายใน 60 วินาที -- [ ] ชื่อสถานที่ใกล้เคียงแสดงถูกต้อง - ---- - -#### Test Case: TC-LOC-02-02 - รับพิกัด GPS สำเร็จ (Indoor) - -**Objective**: ทดสอบการรับพิกัด GPS สำเร็จในพื้นที่ปิด - -| Step | Action | Expected Result | Screenshot | -|------|--------|-----------------|------------| -| 1 | ไปยังพื้นที่ปิด (Indoor) | อยู่ภายในอาคาร | [ ] | -| 2 | เปิดแอป HRMS | แสดงหน้าแรก | [ ] | -| 3 | อนุญาต Location Permission | รับสิทธิ์สำเร็จ | [ ] | -| 4 | กดปุ่มขอตำแหน่ง | เริ่มรับพิกัด GPS | [ ] | -| 5 | รอรับพิกัด (อาจนานกว่าปกติ) | แสดง Skeleton loading | [ ] | -| 6 | รับพิกัดสำเร็จ | แสดงแผนที่ | [ ] | -| 7 | ตรวจสอบพิกัด | Latitude และ Longitude ถูกต้อง | [ ] | -| 8 | ตรวจสอบความแม่นยำ | อาจมีความแม่นยำต่ำกว่า Outdoor | [ ] | - -**Verification Checklist:** - -- [ ] รับพิกัดสำเร็จ -- [ ] อาจมีความแม่นยำต่ำกว่า Outdoor -- [ ] แผนที่แสดงถูกต้อง - ---- - -#### Test Case: TC-LOC-02-03 - รับพิกัด GPS ล้มเหลว (GPS ไม่ทำงาน) - -**Objective**: ทดสอบกรณี GPS ไม่ทำงาน - -| Step | Action | Expected Result | Screenshot | -|------|--------|-----------------|------------| -| 1 | เปิดแอป HRMS | แสดงหน้าแรก | [ ] | -| 2 | อนุญาต Location Permission | รับสิทธิ์สำเร็จ | [ ] | -| 3 | ไปที่ Settings > Location > ปิด GPS | GPS ปิดอยู่ | [ ] | -| 4 | กดปุ่มขอตำแหน่ง | เริ่มรับพิกัด GPS | [ ] | -| 5 | ตรวจสอบ error message | แสดง "ไม่สามารถระบุตำแหน่งปัจจุบันได้" | [ ] | -| 6 | ตรวจสอบแผนที่ | แผนที่ไม่แสดง | [ ] | - -**Verification Checklist:** - -- [ ] แสดง error message ถูกต้อง -- [ ] แผนที่ไม่แสดง -- [ ] ไม่สามารถรับพิกัดได้ - ---- - -### 3.3 TC-LOC-03: Location Validation Testing - -#### Test Case: TC-LOC-03-01 - พิกัดถูกต้อง (Valid coordinates) - -**Objective**: ทดสอบการตรวจสอบพิกัดที่ถูกต้อง - -| Step | Action | Expected Result | Screenshot | -|------|--------|-----------------|------------| -| 1 | เปิดแอป HRMS | แสดงหน้าแรก | [ ] | -| 2 | อนุญาต Location Permission | รับสิทธิ์สำเร็จ | [ ] | -| 3 | รับพิกัด GPS ปกติ | พิกัดถูกต้อง (เช่น 13.7563, 100.5018) | [ ] | -| 4 | เปิด Console และตรวจสอบ validationResult | { isValid: true, isMockDetected: false } | [ ] | -| 5 | ตรวจสอบ locationGranted | locationGranted = true | [ ] | -| 6 | ตรวจสอบปุ่มลงเวลา | ปุ่มใช้งานได้ | [ ] | - -**Verification Checklist:** - -- [ ] `validationResult.isValid = true` -- [ ] `validationResult.isMockDetected = false` -- [ ] `validationResult.errors = []` -- [ ] `locationGranted = true` -- [ ] ไม่มี error/warning message - ---- - -#### Test Case: TC-LOC-03-02 - พิกัดไม่ถูกต้อง (Invalid coordinates - (0,0)) - -**Objective**: ทดสอบการตรวจสอบพิกัด (0,0) - -| Step | Action | Expected Result | Screenshot | -|------|--------|-----------------|------------| -| 1 | เปิด Fake GPS App | เปิดแอป | [ ] | -| 2 | ตั้งค่าพิกัด (0, 0) | Latitude: 0, Longitude: 0 | [ ] | -| 3 | เปิดแอป HRMS | แสดงหน้าแรก | [ ] | -| 4 | กดปุ่มขอตำแหน่ง | เริ่มรับพิกัด GPS | [ ] | -| 5 | ตรวจสอบ Console (validateCoordinates) | validateCoordinates(0, 0) = false | [ ] | -| 6 | ตรวจสอบ validationResult | { isValid: false, isMockDetected: true, confidence: 'high' } | [ ] | -| 7 | ตรวจสอบ error message | "พิกัดตำแหน่งไม่ถูกต้อง กรุณาลองใหม่" | [ ] | -| 8 | ตรวจสอบ disabledBtn | disabledBtn = true | [ ] | - -**Verification Checklist:** - -- [ ] `validateCoordinates(0, 0) = false` -- [ ] `mockIndicators = 3` -- [ ] `isMockDetected = true` -- [ ] `confidence = 'high'` -- [ ] แสดง error message ถูกต้อง -- [ ] ปุ่มลงเวลา disabled - ---- - -#### Test Case: TC-LOC-03-04 - ข้อมูลตำแหน่งเก่าเกินไป (Stale timestamp) - -**Objective**: ทดสอบการตรวจสอบ timestamp เก่าเกิน 60 วินาที - -| Step | Action | Expected Result | Screenshot | -|------|--------|-----------------|------------| -| 1 | เปิด Chrome DevTools | F12 > Sensors | [ ] | -| 2 | เลือก Custom Location | กำหนดพิกัด | [ ] | -| 3 | เปิด Console และเขียน script | จำลอง timestamp เก่า | [ ] | -| 4 | Run script: | | | -| ```javascript | | | -| const stalePosition = { | | | -| coords: { latitude: 13.7563, longitude: 100.5018, accuracy: 10 }, | | | -| timestamp: Date.now() - 70000 // 70 seconds ago | | | -| } | | | -| navigator.geolocation.getCurrentPosition(pos => console.log(pos)) | | | -| ``` | | | -| 5 | รีเฟรชแอป | รับพิกัด | [ ] | -| 6 | ตรวจสอบ validationResult | mockIndicators += 2 | [ ] | -| 7 | ตรวจสอบ error message | "ข้อมูลตำแหน่งเก่าเกินไป กรุณารับสัญญาณ GPS ใหม่" | [ ] | - -**Verification Checklist:** - -- [ ] `validateTimestamp(staleTimestamp) = false` -- [ ] `mockIndicators += 2` -- [ ] แสดง error message ถูกต้อง - ---- - -#### Test Case: TC-LOC-03-05 - ความแม่นยำต่ำ (Poor accuracy) - -**Objective**: ทดสอบการตรวจสอบความแม่นยำต่ำ - -| Step | Action | Expected Result | Screenshot | -|------|--------|-----------------|------------| -| 1 | เปิด Chrome DevTools | F12 > Sensors | [ ] | -| 2 | เลือก Custom Location | กำหนดพิกัด | [ ] | -| 3 | เปิด Console และเขียน script | จำลอง accuracy ต่ำ | [ ] | -| 4 | Run script: | | | -| ```javascript | | | -| const poorAccuracyPosition = { | | | -| coords: { latitude: 13.7563, longitude: 100.5018, accuracy: 150 }, | | | -| timestamp: Date.now() | | | -| } | | | -| ``` | | | -| 5 | รีเฟรชแอป | รับพิกัด | [ ] | -| 6 | ตรวจสอบ validationResult | warnings = ["ความแม่นยำตำแหน่งต่ำเกินไป..."] | [ ] | -| 7 | ตรวจสอบ mockIndicators | mockIndicators += 1 | [ ] | - -**Verification Checklist:** - -- [ ] `validateAccuracy(150) = false` -- [ ] `mockIndicators += 1` -- [ ] แสดง warning message ถูกต้อง - ---- - -#### Test Case: TC-LOC-03-06 - ความเร็วเคลื่อนที่ผิดปกติ (Impossible speed) - -**Objective**: ทดสอบการตรวจสอบความเร็วเคลื่อนที่ผิดปกติ - -| Step | Action | Expected Result | Screenshot | -|------|--------|-----------------|------------| -| 1 | เปิดแอป HRMS ที่ตำแหน่ง A (13.7563, 100.5018) | รับพิกัด A | [ ] | -| 2 | รอสักครู่เพื่อให้บันทึกตำแหน่ง A ลง history | previousPositions มีค่า | [ ] | -| 3 | เปิด Fake GPS App | เปิดแอป | [ ] | -| 4 | ตั้งค่าพิกัด B (15.8700, 100.9925) ที่ห่างกัน ~180 กม. | พิกัด B | [ ] | -| 5 | รีเฟรชแอปทันที (ภายใน 60 วินาที) | รับพิกัด B | [ ] | -| 6 | ตรวจสอบ Console (calculateSpeed) | speed = ~3000 km/h (> 100 m/s) | [ ] | -| 7 | ตรวจสอบ validationResult | mockIndicators += 3 | [ ] | -| 8 | ตรวจสอบ error message | "ตรวจพบการเคลื่อนที่ด้วยความเร็วผิดปกติ..." | [ ] | - -**Verification Checklist:** - -- [ ] `calculateSpeed()` = ความเร็ว > 100 m/s -- [ ] `mockIndicators += 3` -- [ ] แสดง error message ถูกต้อง - ---- - -### 3.4 TC-LOC-04: Mock Location Detection Testing - -#### Test Case: TC-LOC-04-01 - ไม่พบ Mock Location (Normal GPS) - -**Objective**: ทดสอบการใช้งาน GPS ปกติ ไม่พบ mock location - -| Step | Action | Expected Result | Screenshot | -|------|--------|-----------------|------------| -| 1 | ตรวจสอบว่าปิด Fake GPS App | ไม่มี Fake GPS App ทำงาน | [ ] | -| 2 | เปิดแอป HRMS | แสดงหน้าแรก | [ ] | -| 3 | อนุญาต Location Permission | รับสิทธิ์สำเร็จ | [ ] | -| 4 | กดปุ่มขอตำแหน่ง | เริ่มรับพิกัด GPS | [ ] | -| 5 | รับพิกัดสำเร็จ | แสดงแผนที่ | [ ] | -| 6 | ตรวจสอบ Console (validationResult) | { isValid: true, isMockDetected: false } | [ ] | -| 7 | ตรวจสอบ locationGranted | locationGranted = true | [ ] | -| 8 | ตรวจสอบ isMockLocationDetected | isMockLocationDetected = false | [ ] | -| 9 | ตรวจสอบปุ่มลงเวลา | ปุ่มใช้งานได้ | [ ] | - -**Verification Checklist:** - -- [ ] `isValid = true` -- [ ] `isMockDetected = false` -- [ ] `locationGranted = true` -- [ ] `isMockLocationDetected = false` -- [ ] ปุ่มลงเวลาใช้งานได้ - ---- - -#### Test Case: TC-LOC-04-02 - พบ Mock Location - Fake GPS App - -**Objective**: ทดสอบการตรวจจับ mock location จาก Fake GPS App - -| Step | Action | Expected Result | Screenshot | -|------|--------|-----------------|------------| -| 1 | เปิด Fake GPS Location App | เปิดแอป | [ ] | -| 2 | ตั้งค่าตำแหน่งปลอง (เช่น 13.7563, 100.5018) | ตั้งค่าสำเร็จ | [ ] | -| 3 | เปิด Developer Options > Select mock location app | เลือก Fake GPS App | [ ] | -| 4 | เปิดแอป HRMS | แสดงหน้าแรก | [ ] | -| 5 | กดปุ่มขอตำแหน่ง | เริ่มรับพิกัด GPS | [ ] | -| 6 | รับพิกัดจาก Fake GPS | แสดงพิกัดปลอง | [ ] | -| 7 | ตรวจสอบ Console (validationResult) | isMockDetected = true | [ ] | -| 8 | ตรวจสอบ mockIndicators | mockIndicators >= 3 | [ ] | -| 9 | ตรวจสอบ error message | "ตรวจพบว่าตำแหน่ง GPS อาจไม่ถูกต้อง..." | [ ] | -| 10 | ตรวจสอบ isMockLocationDetected | isMockLocationDetected = true | [ ] | -| 11 | ตรวจสอบ disabledBtn | disabledBtn = true | [ ] | -| 12 | พยายามกดปุ่มลงเวลา | กดไม่ได้ | [ ] | - -**Verification Checklist:** - -- [ ] `isMockDetected = true` -- [ ] `mockIndicators >= 3` -- [ ] แสดง error message ถูกต้อง -- [ ] `isMockLocationDetected = true` -- [ ] `disabledBtn = true` -- [ ] ไม่สามารถลงเวลาได้ - ---- - -### 3.5 TC-LOC-05: POI Resolution Testing - -#### Test Case: TC-LOC-05-01 - แปลงพิกัดเป็นชื่อสถานที่สำเร็จ (Bangkok GIS) - -**Objective**: ทดสอบการแปลงพิกัดเป็นชื่อสถานที่สำเร็จผ่าน Bangkok GIS API - -| Step | Action | Expected Result | Screenshot | -|------|--------|-----------------|------------| -| 1 | เปิดแอป HRMS | แสดงหน้าแรก | [ ] | -| 2 | อนุญาต Location Permission | รับสิทธิ์สำเร็จ | [ ] | -| 3 | กดปุ่มขอตำแหน่ง | เริ่มรับพิกัด GPS | [ ] | -| 4 | รับพิกัดสำเร็จ | แสดงแผนที่ | [ ] | -| 5 | เปิด Network Tab (DevTools) | แสดง requests | [ ] | -| 6 | ตรวจสอบ request ไป Bangkok GIS | GET `https://bmagis.bangkok.go.th/...` | [ ] | -| 7 | ตรวจสอบ response | status = 200, มี PlaceName | [ ] | -| 8 | ตรวจสอบ poiPlaceName | poiPlaceName มีค่า | [ ] | -| 9 | ตรวจสอบ formLocation.POI | formLocation.POI มีค่า | [ ] | -| 10 | ตรวจสอบหน้าจอ | แสดงชื่อสถานที่ใกล้เคียง | [ ] | - -**Verification Checklist:** - -- [ ] เรียก Bangkok GIS API สำเร็จ -- [ ] Response มี PlaceName -- [ ] `poiPlaceName` มีค่า -- [ ] แสดงชื่อสถานที่ใกล้เคียงถูกต้อง - ---- - -#### Test Case: TC-LOC-05-02 - แปลงพิกัดเป็นชื่อสถานที่สำเร็จ (ArcGIS Fallback) - -**Objective**: ทดสอบการแปลงพิกัดเป็นชื่อสถานที่สำเร็จผ่าน ArcGIS API (เมื่อ Bangkok GIS fail) - -| Step | Action | Expected Result | Screenshot | -|------|--------|-----------------|------------| -| 1 | เปิด Chrome DevTools > Network Tab | แสดง requests | [ ] | -| 2 | เปิด Throttling > Offline | จำลอง Bangkok GIS down | [ ] | -| 3 | เปิดแอป HRMS | แสดงหน้าแรก | [ ] | -| 4 | อนุญาต Location Permission | รับสิทธิ์สำเร็จ | [ ] | -| 5 | กดปุ่มขอตำแหน่ง | เริ่มรับพิกัด GPS | [ ] | -| 6 | รับพิกัดสำเร็จ | แสดงแผนที่ | [ ] | -| 7 | ปิด Throttling (กลับสู่ Online) | Bangkok GIS ยังไม่พร้อม | [ ] | -| 8 | ตรวจสอบ Console | Bangkok GIS API fail | [ ] | -| 9 | ตรวจสอบ fallback | เรียก ArcGIS API | [ ] | -| 10 | ตรวจสอบ response | GET `https://geocode.arcgis.com/...` | [ ] | -| 11 | ตรวจสอบ poiPlaceName | poiPlaceName มีค่า | [ ] | - -**Verification Checklist:** - -- [ ] Bangkok GIS API fail -- [ ] Fallback ไป ArcGIS API สำเร็จ -- [ ] `poiPlaceName` มีค่า -- [ ] แสดงชื่อสถานที่ใกล้เคียง - ---- - -### 3.6 TC-LOC-06: Check-in with Location Testing - -#### Test Case: TC-LOC-06-01 - ลงเวลาเข้าสำเร็จ - ณ สถานที่ตั้ง (In-place) - -**Objective**: ทดสอบการลงเวลาเข้าสำเร็จเมื่ออยู่ ณ สถานที่ตั้ง - -| Step | Action | Expected Result | Screenshot | -|------|--------|-----------------|------------| -| 1 | เปิดแอป HRMS | แสดงหน้าแรก | [ ] | -| 2 | ตรวจสอบ statusCheckin | statusCheckin = true (เข้างาน) | [ ] | -| 3 | อนุญาต Location Permission | รับสิทธิ์สำเร็จ | [ ] | -| 4 | กดปุ่มขอตำแหน่ง | รับพิกัดสำเร็จ | [ ] | -| 5 | ถ่ายรูปภาพ | แสดงรูปที่ถ่าย | [ ] | -| 6 | เลือก "ในสถานที่" | workplace = 'in-place' | [ ] | -| 7 | กด "ลงเวลาเข้างาน" | แสดง dialog ยืนยัน | [ ] | -| 8 | ตรวจสอบ dialog message | "ยืนยันการลงเวลาเข้างาน ในสถานที่..." | [ ] | -| 9 | กด "ยืนยัน" | ส่งข้อมูลไป API | [ ] | -| 10 | เปิด Network Tab | POST `/leave/check-in` | [ ] | -| 11 | ตรวจสอบ payload | { lat, lon, POI, isLocation: true, locationName: '', img, remark } | [ ] | -| 12 | ตรวจสอบ response | status = 200 | [ ] | -| 13 | ตรวจสอบ modal | แสดง modal ลงเวลาเข้างานสำเร็จ | [ ] | -| 14 | ตรวจสอบ statusCheckin | statusCheckin = false | [ ] | - -**Verification Checklist:** - -- [ ] API call ถูกต้อง -- [ ] Payload ถูกต้อง (isLocation: true, locationName: '') -- [ ] Response status 200 -- [ ] แสดง modal สำเร็จ -- [ ] `statusCheckin = false` - ---- - -#### Test Case: TC-LOC-06-02 - ลงเวลาเข้าสำเร็จ - นอกสถานที่ตั้ง (Off-site) - -**Objective**: ทดสอบการลงเวลาเข้าสำเร็จเมื่ออยู่นอกสถานที่ตั้ง - -| Step | Action | Expected Result | Screenshot | -|------|--------|-----------------|------------| -| 1 | เปิดแอป HRMS | แสดงหน้าแรก | [ ] | -| 2 | ตรวจสอบ statusCheckin | statusCheckin = true (เข้างาน) | [ ] | -| 3 | อนุญาต Location Permission | รับสิทธิ์สำเร็จ | [ ] | -| 4 | กดปุ่มขอตำแหน่ง | รับพิกัดสำเร็จ | [ ] | -| 5 | ถ่ายรูปภาพ | แสดงรูปที่ถ่าย | [ ] | -| 6 | เลือก "นอกสถานที่" | workplace = 'off-site' | [ ] | -| 7 | เลือก "ปฏิบัติงานที่บ้าน (WFH)" | model = 'ปฏิบัติงานที่บ้าน (WFH)' | [ ] | -| 8 | ตรวจสอบ useLocation | useLocation = 'ปฏิบัติงานที่บ้าน (WFH)' | [ ] | -| 9 | กด "ลงเวลาเข้างาน" | แสดง dialog ยืนยัน | [ ] | -| 10 | ตรวจสอบ dialog message | "ยืนยันการลงเวลาเข้างาน นอกสถานที่ (ปฏิบัติงานที่บ้าน (WFH))..." | [ ] | -| 11 | กด "ยืนยัน" | ส่งข้อมูลไป API | [ ] | -| 12 | เปิด Network Tab | POST `/leave/check-in` | [ ] | -| 13 | ตรวจสอบ payload | { lat, lon, POI, isLocation: false, locationName: 'ปฏิบัติงานที่บ้าน (WFH)', img, remark } | [ ] | -| 14 | ตรวจสอบ response | status = 200 | [ ] | - -**Verification Checklist:** - -- [ ] API call ถูกต้อง -- [ ] Payload ถูกต้อง (isLocation: false, locationName มีค่า) -- [ ] Response status 200 -- [ ] แสดง modal สำเร็จ - ---- - -#### Test Case: TC-LOC-06-03 - ลงเวลาเข้าล้มเหลว - Mock Location detected - -**Objective**: ทดสอบการลงเวลาเข้าล้มเหลวเมื่อตรวจพบ mock location - -| Step | Action | Expected Result | Screenshot | -|------|--------|-----------------|------------| -| 1 | เปิด Fake GPS Location App | เปิดแอป | [ ] | -| 2 | ตั้งค่าตำแหน่งปลอง | ตั้งค่าสำเร็จ | [ ] | -| 3 | เปิดแอป HRMS | แสดงหน้าแรก | [ ] | -| 4 | อนุญาต Location Permission | รับสิทธิ์สำเร็จ | [ ] | -| 5 | กดปุ่มขอตำแหน่ง | เริ่มรับพิกัด GPS | [ ] | -| 6 | รับพิกัดจาก Fake GPS | ตรวจจับ mock location | [ ] | -| 7 | ตรวจสอบ error message | "ตรวจพบว่าตำแหน่ง GPS อาจไม่ถูกต้อง..." | [ ] | -| 8 | ตรวจสอบ isMockLocationDetected | isMockLocationDetected = true | [ ] | -| 9 | ตรวจสอบ disabledBtn | disabledBtn = true | [ ] | -| 10 | ถ่ายรูปภาพ | แสดงรูปที่ถ่าย | [ ] | -| 11 | พยายามกด "ลงเวลาเข้างาน" | กดไม่ได้ (ปุ่ม disabled) | [ ] | - -**Verification Checklist:** - -- [ ] ตรวจจับ mock location สำเร็จ -- [ ] `isMockLocationDetected = true` -- [ ] `disabledBtn = true` -- [ ] ไม่สามารถลงเวลาเข้างานได้ - ---- - -### 3.7 TC-LOC-07: Check-out with Location Testing - -#### Test Case: TC-LOC-07-01 - ลงเวลาออกสำเร็จ - -**Objective**: ทดสอบการลงเวลาออกสำเร็จพร้อมตำแหน่ง - -| Step | Action | Expected Result | Screenshot | -|------|--------|-----------------|------------| -| 1 | เปิดแอป HRMS | แสดงหน้าแรก | [ ] | -| 2 | ตรวจสอบ statusCheckin | statusCheckin = false (ออกงาน) | [ ] | -| 3 | อนุญาต Location Permission | รับสิทธิ์สำเร็จ | [ ] | -| 4 | กดปุ่มขอตำแหน่ง | รับพิกัดสำเร็จ | [ ] | -| 5 | ถ่ายรูปภาพ | แสดงรูปที่ถ่าย | [ ] | -| 6 | กด "ลงเวลาออกงาน" | เรียก checkoutCheck API | [ ] | -| 7 | เปิด Network Tab | GET `/leave/user/checkout-check/N` | [ ] | -| 8 | ตรวจสอบ response | status = 'NORMAL' หรือ 'ABSENT' | [ ] | -| 9 | ตรวจสอบ dialog | แสดง dialog ยืนยันการลงเวลาออกงาน | [ ] | -| 10 | กด "ยืนยัน" | ส่งข้อมูลไป API | [ ] | -| 11 | เปิด Network Tab | POST `/leave/check-in` | [ ] | -| 12 | ตรวจสอบ payload | { lat, lon, POI, isLocation, locationName, img, remark, checkInId } | [ ] | -| 13 | ตรวจสอบ response | status = 200 | [ ] | -| 14 | ตรวจสอบ modal | แสดง modal ลงเวลาออกงานสำเร็จ | [ ] | -| 15 | ตรวจสอบ statusCheckin | statusCheckin = true | [ ] | - -**Verification Checklist:** - -- [ ] checkoutCheck API ถูกต้อง -- [ ] API call ถูกต้อง (มี checkInId) -- [ ] Response status 200 -- [ ] แสดง modal สำเร็จ -- [ ] `statusCheckin = true` - ---- - -### 3.8 TC-LOC-09: Privacy Consent Testing - -#### Test Case: TC-LOC-09-01 - แสดง Privacy Modal ก่อนใช้ Location - -**Objective**: ทดสอบการแสดง Privacy Modal ก่อนใช้ Location - -| Step | Action | Expected Result | Screenshot | -|------|--------|-----------------|------------| -| 1 | เปิดแอป HRMS ครั้งแรก (หรือ clear localStorage) | แสดงหน้าแรก | [ ] | -| 2 | ตรวจสอบ privacyStore.isAccepted | isAccepted = false | [ ] | -| 3 | กดปุ่มที่ต้องการ Location (แผนที่/กล้อง) | checkPrivacyAccepted() return false | [ ] | -| 4 | ตรวจสอบ privacyStore.modalPrivacy | modalPrivacy = true | [ ] | -| 5 | ตรวจสอบ Privacy Modal | แสดง Privacy Modal | [ ] | -| 6 | ตรวจสอบว่าไม่สามารถใช้งาน Location ได้ | แผนที่/กล้องไม่ทำงาน | [ ] | - -**Verification Checklist:** - -- [ ] `isAccepted = false` -- [ ] `modalPrivacy = true` -- [ ] แสดง Privacy Modal -- [ ] ไม่สามารถใช้งาน Location ได้ - ---- - -#### Test Case: TC-LOC-09-02 - ยอมรับ Privacy Policy - -**Objective**: ทดสอบการยอมรับ Privacy Policy - -| Step | Action | Expected Result | Screenshot | -|------|--------|-----------------|------------| -| 1 | เปิดแอป HRMS ครั้งแรก | แสดงหน้าแรก | [ ] | -| 2 | แสดง Privacy Modal | แสดง Privacy Modal | [ ] | -| 3 | กด "ยอมรับ" | เรียก privacyStore.setAccepted(true) | [ ] | -| 4 | ตรวจสอบ privacyStore.isAccepted | isAccepted = true | [ ] | -| 5 | ตรวจสอบ privacyStore.modalPrivacy | modalPrivacy = false | [ ] | -| 6 | ตรวจสอบ Privacy Modal | Modal ปิด | [ ] | -| 7 | กดปุ่มขอตำแหน่ง | เรียก mapRef.value?.requestLocationPermission() | [ ] | -| 8 | อนุญาต Location Permission | รับสิทธิ์สำเร็จ | [ ] | -| 9 | รับพิกัดสำเร็จ | แสดงแผนที่ | [ ] | - -**Verification Checklist:** - -- [ ] `isAccepted = true` -- [ ] `modalPrivacy = false` -- [ ] Modal ปิด -- [ ] สามารถใช้งาน Location ได้ - ---- - -#### Test Case: TC-LOC-09-03 - ปฏิเสธ Privacy Policy - -**Objective**: ทดสอบการปฏิเสธ Privacy Policy - -| Step | Action | Expected Result | Screenshot | -|------|--------|-----------------|------------| -| 1 | เปิดแอป HRMS ครั้งแรก | แสดงหน้าแรก | [ ] | -| 2 | แสดง Privacy Modal | แสดง Privacy Modal | [ ] | -| 3 | กด "ปฏิเสธ" หรือปิด Modal | เรียก privacyStore.modalPrivacy = false | [ ] | -| 4 | ตรวจสอบ privacyStore.isAccepted | isAccepted = false | [ ] | -| 5 | ตรวจสอบ privacyStore.modalPrivacy | modalPrivacy = false | [ ] | -| 6 | ตรวจสอบ Privacy Modal | Modal ปิด | [ ] | -| 7 | กดปุ่มขอตำแหน่ง | checkPrivacyAccepted() return false | [ ] | -| 8 | ตรวจสอบ | Privacy Modal แสดงอีกครั้ง | [ ] | - -**Verification Checklist:** - -- [ ] `isAccepted = false` -- [ ] `modalPrivacy = false` -- [ ] Modal ปิด -- [ ] ไม่สามารถใช้งาน Location ได้ -- [ ] แจ้งเตือนเมื่อพยายามใช้งาน Location - ---- - -## 4. Mock Location Testing - -### 4.1 Fake GPS Apps for Testing - -#### 4.1.1 Android Fake GPS Apps - -| App Name | Link | Features | -|----------|------|----------| -| Fake GPS Location | [Play Store](https://play.google.com/store/apps/details?id=com.lexa.fakegps) | Simple location spoofing | -| GPS Joystick | [Play Store](https://play.google.com/store/apps/details?id=com.theappninjas.android.gpsjoke) | Joystick control, route simulation | -| Mock GPS | [Play Store](https://play.google.com/store/apps/details?id=com.incorporateapps.fakegps) | Multiple location saving | - -#### 4.1.2 iOS Fake GPS Apps - -| App Name | Link | Features | -|----------|------|----------| -| Location Changer | [App Store](https://apps.apple.com/app/location-changer/id1383869695) | Location spoofing (needs jailbreak) | -| Xcode Simulation | - | Built-in location simulation | - -### 4.2 Mock Location Test Scenarios - -#### Scenario 1: Basic Mock Location - -1. **Setup** - - เปิด Fake GPS Location App - - ตั้งค่าตำแหน่ง: 13.7563, 100.5018 (Sanam Suea Pa) - - ตั้งค่าความแม่นยำ: 10 meters - -2. **Execute** - - เปิดแอป HRMS - - กดปุ่มขอตำแหน่ง - - ตรวจสอบผลลัพธ์ - -3. **Expected** - - ตรวจจับ mock location (indicators >= 3) - - แสดง error message - - ปุ่มลงเวลา disabled - -#### Scenario 2: Rapid Location Change - -1. **Setup** - - เปิดแอป HRMS ที่ตำแหน่ง A (13.7563, 100.5018) - - รอสักครู่เพื่อให้บันทึกตำแหน่ง A ลง history - -2. **Execute** - - เปิด Fake GPS App - - เปลี่ยนตำแหน่งไป B (15.8700, 100.9925) - - รีเฟรชแอป HRMS ทันที (ภายใน 60 วินาที) - -3. **Expected** - - คำนวณความเร็ว = ~3000 km/h (> 100 m/s) - - mockIndicators += 3 - - แสดง error: "ตรวจพบการเคลื่อนที่ด้วยความเร็วผิดปกติ..." - -#### Scenario 3: Null Island (0,0) - -1. **Setup** - - เปิด Fake GPS App - - ตั้งค่าตำแหน่ง: 0, 0 (Null Island) - -2. **Execute** - - เปิดแอป HRMS - - กดปุ่มขอตำแหน่ง - -3. **Expected** - - validateCoordinates(0, 0) = false - - mockIndicators += 3 - - แสดง error: "พิกัดตำแหน่งไม่ถูกต้อง กรุณาลองใหม่" - ---- - -## 5. Developer Tools Testing - -### 5.1 Chrome DevTools Sensor Simulation - -#### 5.1.1 Enable Sensors - -1. เปิด Chrome DevTools (F12) -2. เลือก More tools > Sensors -3. ตั้งค่า Location - -#### 5.1.2 Pre-defined Locations - -| Location | Latitude | Longitude | Use Case | -|----------|----------|-----------|----------| -| Sanam Suea Pa | 13.7563 | 100.5018 | Bangkok office | -| Suvarnabhumi Airport | 13.6900 | 100.7501 | Outdoor high accuracy | -| Central World | 13.7944 | 100.5439 | Indoor | -| Null Island | 0 | 0 | Invalid coordinates | -| Out of Range | 91 | 181 | Out of range test | - -#### 5.1.3 Custom Location - -1. เลือก "Other..." ใน Sensors tab -2. ป้อน Latitude และ Longitude -3. กด Enter - -### 5.2 Network Throttling - -#### 5.2.1 Enable Throttling - -1. เปิด Chrome DevTools (F12) -2. เลือก Network tab -3. เลือก Throttling -4. เลือก preset หรือ custom - -#### 5.2.2 Throttling Presets - -| Preset | Download | Upload | Latency | Use Case | -|--------|----------|--------|---------|----------| -| Offline | 0 | 0 | - | API failure test | -| Slow 3G | 500 Kbps | 500 Kbps | 2000ms | Slow network | -| Fast 3G | 1.6 Mbps | 750 Kbps | 100ms | Normal mobile | - -### 5.3 Console Testing - -#### 5.3.1 Test Location Validation - -```javascript -// Test 1: Valid coordinates -const validPosition = { - coords: { latitude: 13.7563, longitude: 100.5018, accuracy: 10 }, - timestamp: Date.now() -} -console.log(validateLocation(validPosition)) -// Expected: { isValid: true, isMockDetected: false } - -// Test 2: Invalid coordinates (0,0) -const invalidPosition = { - coords: { latitude: 0, longitude: 0, accuracy: 10 }, - timestamp: Date.now() -} -console.log(validateLocation(invalidPosition)) -// Expected: { isValid: false, isMockDetected: true, confidence: 'high' } - -// Test 3: Stale timestamp -const stalePosition = { - coords: { latitude: 13.7563, longitude: 100.5018, accuracy: 10 }, - timestamp: Date.now() - 70000 // 70 seconds ago -} -console.log(validateLocation(stalePosition)) -// Expected: errors = ["ข้อมูลตำแหน่งเก่าเกินไป..."] - -// Test 4: Poor accuracy -const poorAccuracyPosition = { - coords: { latitude: 13.7563, longitude: 100.5018, accuracy: 150 }, - timestamp: Date.now() -} -console.log(validateLocation(poorAccuracyPosition)) -// Expected: warnings = ["ความแม่นยำตำแหน่งต่ำเกินไป..."] -``` - -#### 5.3.2 Test Distance Calculation - -```javascript -// Test haversineDistance -const distance = haversineDistance(13.7563, 100.5018, 15.8700, 100.9925) -console.log(`Distance: ${distance} meters`) -// Expected: ~180,000 meters (180 km) - -// Test speed calculation -const pos1 = { latitude: 13.7563, longitude: 100.5018, timestamp: Date.now() - 60000 } -const pos2 = { latitude: 15.8700, longitude: 100.9925, timestamp: Date.now() } -const speed = calculateSpeed(pos1, pos2) -console.log(`Speed: ${speed} m/s (${speed * 3.6} km/h)`) -// Expected: ~3000 km/h (> 100 m/s = 360 km/h) -``` - ---- - -## 6. Device-Specific Testing - -### 6.1 Mobile Device Testing - -#### 6.1.1 Android Testing - -| Step | Action | Notes | -|------|--------|-------| -| 1 | Install Fake GPS App | Google Play Store | -| 2 | Enable Developer Options | Settings > About Phone > Build Number (7 taps) | -| 3 | Enable Mock Location | Settings > Developer Options > Select mock location app | -| 4 | Set Fake Location | Fake GPS App > Set Location | -| 5 | Open HRMS App | Test mock detection | - -#### 6.1.2 iOS Testing - -| Step | Action | Notes | -|------|--------|-------| -| 1 | Connect iPhone to Mac | USB cable | -| 2 | Open Xcode | Mac | -| 3 | Select Device > Debug Location | Xcode menu | -| 4 | Set Custom Location | Enter lat/lon | -| 5 | Open HRMS App | Test mock detection | - -### 6.2 Desktop Browser Testing - -#### 6.2.1 Chrome Testing - -| Step | Action | Notes | -|------|--------|-------| -| 1 | Open Chrome | - | -| 2 | Open DevTools | F12 | -| 3 | More tools > Sensors | - | -| 4 | Set Location | Choose or custom | -| 5 | Open HRMS App | Test location | - -#### 6.2.2 Edge Testing - -| Step | Action | Notes | -|------|--------|-------| -| 1 | Open Edge | - | -| 2 | Open DevTools | F12 | -| 3 | More tools > Sensors | - | -| 4 | Set Location | Choose or custom | -| 5 | Open HRMS App | Test location | - ---- - -## 7. Test Data & Scenarios - -### 7.1 Valid Locations - -| Name | Latitude | Longitude | Accuracy | Expected | -|------|----------|-----------|----------|----------| -| Sanam Suea Pa | 13.7563 | 100.5018 | <= 100m | Pass | -| Suvarnabhumi | 13.6900 | 100.7501 | <= 100m | Pass | -| Central World | 13.7944 | 100.5439 | <= 100m | Pass | -| Chiang Mai | 18.7883 | 98.9853 | <= 100m | Pass | - -### 7.2 Invalid Locations - -| Name | Latitude | Longitude | Accuracy | Expected | -|------|----------|-----------|----------|----------| -| Null Island | 0 | 0 | Any | Fail (invalid coordinates) | -| Out of Range (lat) | 91 | 100.5018 | Any | Fail (invalid coordinates) | -| Out of Range (lon) | 13.7563 | 181 | Any | Fail (invalid coordinates) | -| NaN | NaN | NaN | Any | Fail (invalid coordinates) | - -### 7.3 Edge Cases - -| Scenario | Latitude | Longitude | Accuracy | Timestamp | Expected | -|----------|----------|-----------|----------|-----------|----------| -| Stale Data | 13.7563 | 100.5018 | 10 | t - 70s | Fail (stale) | -| Poor Accuracy | 13.7563 | 100.5018 | 150 | current | Warning | -| Impossible Speed | 15.8700 | 100.9925 | 10 | t + 60s (from 13.7563, 100.5018) | Fail (speed) | - ---- - -## 8. Troubleshooting - -### 8.1 Common Issues - -| Issue | Cause | Solution | -|-------|-------|----------| -| ไม่ได้รับพิกัด GPS | GPS ปิดอยู่ | เปิด Location Service | -| แผนที่ไม่แสดง | ยังไม่ยอมรับ Privacy Policy | ยอมรับ Privacy Policy | -| ปุ่มลงเวลา disabled | Mock location detected | ปิด Fake GPS App และรีเฟรช | -| POI ไม่แสดง | Bangkok GIS down | รอ fallback ไป ArcGIS | -| ความแม่นยำต่ำ | อยู่ในอาคาร | ไปยังพื้นที่เปิดกว้าง | - -### 8.2 Debug Mode - -#### Enable Debug Mode - -1. เปิด Console ใน Browser -2. ตรวจสอบค่าต่าง ๆ: - -```javascript -// Check validation result -console.log('Validation:', validationResult) - -// Check location granted -console.log('Location Granted:', locationGranted) - -// Check mock detected -console.log('Mock Detected:', isMockLocationDetected) - -// Check form location -console.log('Form Location:', formLocation) - -// Check privacy accepted -console.log('Privacy Accepted:', privacyStore.isAccepted) -``` - ---- - -## 9. Test Execution Checklist - -### 9.1 Pre-Test Checklist - -- [ ] ติดตั้ง Fake GPS App (สำหรับการทดสอบ mock location) -- [ ] เปิด Developer Options (Android) หรือเตรียม Xcode (iOS) -- [ ] เตรียม Test Data และ Test Scenarios -- [ ] เตรียม Screenshot/Screen Recording tool -- [ ] ตรวจสอบ Test Environment (Network, GPS, etc.) - -### 9.2 During Test Checklist - -- [ ] บันทึกผลลัพธ์ทุก Test Case -- [ ] ถ่าย Screenshot สำคัญ -- [ ] บันทึกข้อผิดพลาดที่พบ -- [ ] ตรวจสอบ Console logs -- [ ] ตรวจสอบ Network requests - -### 9.3 Post-Test Checklist - -- [ ] สรุปผลการทดสอบ -- [ ] รายงาน bugs ที่พบ -- [ ] อัปเดต Test Cases document -- [ ] เตรียม Test Evidence (Screenshots, Videos) -- [ ] ส่งรายงานให้ทีมที่เกี่ยวข้อง - ---- - -## 10. References - -### 10.1 Related Documents - -- [Test Cases: Location Features](./test-cases-location.md) -- [Location Validation Logic](../src/composables/useLocationValidation.ts) -- [Home View Component](../src/views/HomeView.vue) -- [AscGISMap Component](../src/components/AscGISMap.vue) - -### 10.2 External Resources - -- [W3C Geolocation API](https://www.w3.org/TR/geolocation-API/) -- [MDN Geolocation API](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API) -- [Chrome DevTools Sensors](https://developer.chrome.com/docs/devtools/device-mode/override-device-metrics/) - ---- - -## 11. Revision History - -| Version | Date | Author | Changes | -|---------|------|--------|---------| -| 1.0.0 | 2025-03-09 | QA Team | Initial version - Detailed test steps for Location features | - ---- - -## 12. Approval - -| Role | Name | Signature | Date | -|------|------|-----------|------| -| QA Lead | | | | -| Developer | | | | -| Product Owner | | | | diff --git a/src/components/AscGISMap.vue b/src/components/AscGISMap.vue index 7651b7c..81da108 100644 --- a/src/components/AscGISMap.vue +++ b/src/components/AscGISMap.vue @@ -5,14 +5,19 @@ import axios from 'axios' import { useCounterMixin } from '@/stores/mixin' import { useQuasar } from 'quasar' import { usePrivacyStore } from '@/stores/privacy' +import { useLocationValidation } from '@/composables/useLocationValidation' const mixin = useCounterMixin() const { messageError } = mixin const privacyStore = usePrivacyStore() -const emit = defineEmits(['update:location']) +// import type { LocationObject } from '@/interface/index/Main' +const mapElement = ref(null) +const emit = defineEmits(['update:location', 'locationStatus', 'mockDetected']) const $q = useQuasar() +const { validateLocation, showMockWarning } = useLocationValidation() + function updateLocation(latitude: number, longitude: number, namePOI: string) { // ส่ง event ไปยัง parent component เพื่ออัพเดทค่า props emit('update:location', latitude, longitude, namePOI) @@ -221,8 +226,30 @@ const requestLocationPermission = () => { navigator.geolocation.getCurrentPosition( async (position) => { - // Permission granted - locationGranted.value = true + // Validate location first + const validationResult = validateLocation(position) + + // Always emit mockDetected event (regardless of result) + if (validationResult.isMockDetected) { + showMockWarning(validationResult) + emit('mockDetected', validationResult) + } + + // Check for critical errors (invalid coordinates) that prevent showing location + const hasCriticalErrors = validationResult.errors.some(error => + error.includes('พิกัดตำแหน่งไม่ถูกต้อง') + ) + + if (hasCriticalErrors) { + locationGranted.value = false + emit('locationStatus', false) + messageError($q, '', validationResult.errors[0]) + return + } + + // Permission granted based on mock detection + locationGranted.value = !validationResult.isMockDetected + emit('locationStatus', !validationResult.isMockDetected) const { latitude, longitude } = position.coords // console.log('Current position:', latitude, longitude) @@ -240,6 +267,7 @@ const requestLocationPermission = () => { (error) => { // Permission denied locationGranted.value = false + emit('locationStatus', false) switch (error.code) { case error.PERMISSION_DENIED: @@ -270,6 +298,7 @@ const requestLocationPermission = () => { defineExpose({ requestLocationPermission, + locationGranted, }) diff --git a/src/components/DialogDebug.vue b/src/components/DialogDebug.vue index 798ce47..5fa1f3f 100644 --- a/src/components/DialogDebug.vue +++ b/src/components/DialogDebug.vue @@ -11,7 +11,6 @@ import http from '@/plugins/http' import config from '@/app.config' import DialogHeader from '@/components/DialogHeader.vue' -import FooterContact from '@/components/FooterContact.vue' const $q = useQuasar() const store = usePositionKeycloakStore() @@ -176,7 +175,7 @@ function onClose() { - - - - diff --git a/src/components/PopupPrivacy.vue b/src/components/PopupPrivacy.vue index 4493fab..a72dde3 100644 --- a/src/components/PopupPrivacy.vue +++ b/src/components/PopupPrivacy.vue @@ -108,7 +108,6 @@ const checkIfScrollable = () => { transition-show="slide-up" transition-hide="slide-down" :maximized="$q.screen.lt.sm" - @show="checkIfScrollable" > diff --git a/src/composables/useLocationValidation.ts b/src/composables/useLocationValidation.ts new file mode 100644 index 0000000..27222df --- /dev/null +++ b/src/composables/useLocationValidation.ts @@ -0,0 +1,169 @@ +import { ref } from 'vue' +import { useQuasar } from 'quasar' +import { useCounterMixin } from '@/stores/mixin' + +export interface LocationValidationResult { + isValid: boolean + isMockDetected: boolean + confidence: 'low' | 'medium' | 'high' + warnings: string[] + errors: string[] +} + +export interface PositionSnapshot { + latitude: number + longitude: number + timestamp: number +} + +// Configuration constants - exported for documentation and testing purposes +export const VALIDATION_CONFIG = { + MAX_TIMESTAMP_AGE_MS: 60_000, // 60 seconds - maximum acceptable age of location data + MAX_ACCURACY_METERS: 100, // 100 meters - maximum acceptable GPS accuracy + MAX_SPEED_MS: 100, // 100 m/s (~360 km/h) - maximum plausible movement speed + POSITION_HISTORY_SIZE: 5, // number of positions to keep for pattern detection + MOCK_INDICATOR_THRESHOLD: 3, // threshold for mock detection (indicators >= 3 = mock) +} as const + +export function useLocationValidation() { + const $q = useQuasar() + const { messageError } = useCounterMixin() + + // Thai error messages - exported for i18n consistency + const errorMessages = { + MOCK_DETECTED: 'ตรวจพบว่าตำแหน่ง GPS อาจไม่ถูกต้อง กรุณาปิดแอปจำลองตำแหน่งและลองใหม่', + INVALID_COORDINATES: 'พิกัดตำแหน่งไม่ถูกต้อง กรุณาลองใหม่', + STALE_TIMESTAMP: 'ข้อมูลตำแหน่งเก่าเกินไป กรุณารับสัญญาณ GPS ใหม่', + POOR_ACCURACY: 'ความแม่นยำตำแหน่งต่ำเกินไป กรุณาตรวจสอบการรับสัญญาณ GPS', + IMPOSSIBLE_SPEED: 'ตรวจพบการเคลื่อนที่ด้วยความเร็วผิดปกติ อาจเป็นการจำลองตำแหน่ง', + } + + const previousPositions = ref([]) + + // คำนวณระยะห่างระหว่าง 2 จุด (Haversine formula) + const haversineDistance = (lat1: number, lon1: number, lat2: number, lon2: number): number => { + const R = 6371e3 // Earth's radius in meters + const φ1 = (lat1 * Math.PI) / 180 + const φ2 = (lat2 * Math.PI) / 180 + const Δφ = ((lat2 - lat1) * Math.PI) / 180 + const Δλ = ((lon2 - lon1) * Math.PI) / 180 + + const a = + Math.sin(Δφ / 2) * Math.sin(Δφ / 2) + + Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) * Math.sin(Δλ / 2) + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)) + + return R * c + } + + // ตรวจสอบพิกัดถูกต้อง + const validateCoordinates = (lat: number, lon: number): boolean => { + return ( + lat >= -90 && lat <= 90 && + lon >= -180 && lon <= 180 && + !isNaN(lat) && !isNaN(lon) && + !(lat === 0 && lon === 0) // Mock มักใช้ 0,0 + ) + } + + // ตรวจสอบความแม่นยำ + const validateAccuracy = (accuracy: number | null): boolean => { + if (accuracy === null) return true + return accuracy <= VALIDATION_CONFIG.MAX_ACCURACY_METERS + } + + // ตรวจสอบ Timestamp + const validateTimestamp = (timestamp: number): boolean => { + const now = Date.now() + const age = Math.abs(now - timestamp) + return age <= VALIDATION_CONFIG.MAX_TIMESTAMP_AGE_MS + } + + // คำนวณความเร็ว + const calculateSpeed = (pos1: PositionSnapshot, pos2: PositionSnapshot): number => { + const distance = haversineDistance(pos1.latitude, pos1.longitude, pos2.latitude, pos2.longitude) + const timeDiff = Math.abs(pos2.timestamp - pos1.timestamp) / 1000 // seconds + return timeDiff > 0 ? distance / timeDiff : 0 + } + + // ตรวจสอบความเร็วปกติ + const validateSpeed = (current: PositionSnapshot, previous: PositionSnapshot): boolean => { + const speed = calculateSpeed(previous, current) + return speed <= VALIDATION_CONFIG.MAX_SPEED_MS + } + + // Main validation function + const validateLocation = (position: GeolocationPosition): LocationValidationResult => { + const warnings: string[] = [] + const errors: string[] = [] + let mockIndicators = 0 + + const { latitude, longitude, accuracy } = position.coords + const { timestamp } = position + + // 1. Coordinate validation + if (!validateCoordinates(latitude, longitude)) { + errors.push(errorMessages.INVALID_COORDINATES) + mockIndicators += 3 + } + + // 2. Timestamp validation + if (!validateTimestamp(timestamp)) { + errors.push(errorMessages.STALE_TIMESTAMP) + mockIndicators += 2 + } + + // 3. Accuracy validation + if (!validateAccuracy(accuracy)) { + warnings.push(errorMessages.POOR_ACCURACY) + mockIndicators += 1 + } + + // 4. Compare with previous positions + if (previousPositions.value.length > 0) { + const previous = previousPositions.value[previousPositions.value.length - 1] + + if (!validateSpeed({ latitude, longitude, timestamp }, previous)) { + errors.push(errorMessages.IMPOSSIBLE_SPEED) + mockIndicators += 3 + } + } + + // Store current position + previousPositions.value.push({ latitude, longitude, timestamp }) + if (previousPositions.value.length > VALIDATION_CONFIG.POSITION_HISTORY_SIZE) { + previousPositions.value.shift() + } + + // Determine result + const isMockDetected = mockIndicators >= VALIDATION_CONFIG.MOCK_INDICATOR_THRESHOLD + const isValid = errors.length === 0 + + let confidence: 'low' | 'medium' | 'high' = 'low' + if (mockIndicators >= 5) confidence = 'high' + else if (mockIndicators >= 3) confidence = 'medium' + + return { + isValid, + isMockDetected, + confidence, + warnings, + errors, + } + } + + const showMockWarning = (result: LocationValidationResult) => { + if (!result.isMockDetected) return + messageError($q, null, errorMessages.MOCK_DETECTED) + } + + const resetValidation = () => { + previousPositions.value = [] + } + + return { + validateLocation, + showMockWarning, + resetValidation, + } +} diff --git a/src/stores/mixin.ts b/src/stores/mixin.ts index fffa00c..3ff5636 100644 --- a/src/stores/mixin.ts +++ b/src/stores/mixin.ts @@ -5,56 +5,6 @@ import { logout } from '@/plugins/auth' import { format, utcToZonedTime } from 'date-fns-tz' export const useCounterMixin = defineStore('mixin', () => { - let activeErrorDialog: any = null - - const clearActiveErrorDialog = () => { - if (!activeErrorDialog) return - try { - if (typeof activeErrorDialog.hide === 'function') { - activeErrorDialog.hide() - } else if (typeof activeErrorDialog.destroy === 'function') { - activeErrorDialog.destroy() - } - } finally { - activeErrorDialog = null - } - } - - const openSingleErrorDialog = ( - q: any, - message: string, - onCancel?: () => void | Promise - ) => { - clearActiveErrorDialog() - - const dialog = q.dialog({ - component: CustomComponent, - componentProps: { - title: `พบข้อผิดพลาด`, - message, - icon: 'warning', - color: 'red', - onlycancel: true, - }, - }) - - activeErrorDialog = dialog - - dialog.onDismiss(() => { - if (activeErrorDialog === dialog) { - activeErrorDialog = null - } - }) - - if (onCancel) { - dialog.onCancel(() => { - void onCancel() - }) - } - - return dialog - } - function date2Thai(srcDate: Date, isFullMonth = false, isTime = false) { if (srcDate == null) { return null @@ -183,30 +133,53 @@ export const useCounterMixin = defineStore('mixin', () => { } const messageError = (q: any, e: any = '', msg: string = '') => { - // Keep only one active warning popup to prevent dialog overlap. + // q.dialog.hide(); if (e.response !== undefined) { if (e.response.data.status !== undefined) { if (e.response.data.status == 401) { //invalid_token - openSingleErrorDialog( - q, - 'ล็อกอินหมดอายุ กรุณาล็อกอินใหม่อีกครั้ง', - async () => { - showLoader() - await logout() - setTimeout(() => { - hideLoader() - }, 1000) - } - ) + q.dialog({ + component: CustomComponent, + componentProps: { + title: `พบข้อผิดพลาด`, + message: `ล็อกอินหมดอายุ กรุณาล็อกอินใหม่อีกครั้ง`, + icon: 'warning', + color: 'red', + onlycancel: true, + }, + }).onCancel(async () => { + showLoader() + await logout() + setTimeout(() => { + hideLoader() + }, 1000) + }) } else { const message = e.response.data.result ?? e.response.data.message - openSingleErrorDialog(q, `${message}`) + q.dialog({ + component: CustomComponent, + componentProps: { + title: `พบข้อผิดพลาด`, + message: `${message}`, + icon: 'warning', + color: 'red', + onlycancel: true, + }, + }) } } else { if (e.response.status == 401) { if (msg !== '') { - openSingleErrorDialog(q, msg, async () => { + q.dialog({ + component: CustomComponent, + componentProps: { + title: `พบข้อผิดพลาด`, + message: msg, + icon: 'warning', + color: 'red', + onlycancel: true, + }, + }).onCancel(async () => { showLoader() await logout() setTimeout(() => { @@ -215,35 +188,70 @@ export const useCounterMixin = defineStore('mixin', () => { }) } else { //invalid_token - openSingleErrorDialog( - q, - 'ล็อกอินหมดอายุ กรุณาล็อกอินใหม่อีกครั้ง', - async () => { - showLoader() - await logout() - setTimeout(() => { - hideLoader() - }, 1000) - } - ) + q.dialog({ + component: CustomComponent, + componentProps: { + title: `พบข้อผิดพลาด`, + message: `ล็อกอินหมดอายุ กรุณาล็อกอินใหม่อีกครั้ง`, + icon: 'warning', + color: 'red', + onlycancel: true, + }, + }).onCancel(async () => { + showLoader() + await logout() + setTimeout(() => { + hideLoader() + }, 1000) + }) } } else if (e.response.data.successful === false) { - openSingleErrorDialog(q, e.response.data.message) + q.dialog({ + component: CustomComponent, + componentProps: { + title: `พบข้อผิดพลาด`, + message: e.response.data.message, + icon: 'warning', + color: 'red', + onlycancel: true, + }, + }) } else { - openSingleErrorDialog( - q, - 'ข้อมูลผิดพลาดทำให้เกิดการไม่ตอบสนองต่อการเรียกใช้งานดูเว็บไซต์' - ) + q.dialog({ + component: CustomComponent, + componentProps: { + title: `พบข้อผิดพลาด`, + message: `ข้อมูลผิดพลาดทำให้เกิดการไม่ตอบสนองต่อการเรียกใช้งานดูเว็บไซต์`, + icon: 'warning', + color: 'red', + onlycancel: true, + }, + }) } } } else { if (msg !== '') { - return openSingleErrorDialog(q, msg) + return q.dialog({ + component: CustomComponent, + componentProps: { + title: `พบข้อผิดพลาด`, + message: msg, + icon: 'warning', + color: 'red', + onlycancel: true, + }, + }) } - openSingleErrorDialog( - q, - 'ข้อมูลผิดพลาดทำให้เกิดการไม่ตอบสนองต่อการเรียกใช้งานดูเว็บไซต์' - ) + q.dialog({ + component: CustomComponent, + componentProps: { + title: `พบข้อผิดพลาด`, + message: `ข้อมูลผิดพลาดทำให้เกิดการไม่ตอบสนองต่อการเรียกใช้งานดูเว็บไซต์`, + icon: 'warning', + color: 'red', + onlycancel: true, + }, + }) } } diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 5df3c0c..379b970 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -19,7 +19,6 @@ const { date2Thai, showLoader, hideLoader, messageError, dialogConfirm } = mixin const $q = useQuasar() const { checkPrivacyAccepted } = usePermissions() const privacyStore = usePrivacyStore() -const MOCK_CHECK_DELAY_MS = 800 const modalTime = ref(false) // Dailog ลงเวลาเข้างานของคุณ const checkStatus = ref('') @@ -33,6 +32,8 @@ const endTimeAfternoon = ref('12:00:00') //เวลาเช็คเ const isLoadingCheckTime = ref(false) // ตัวแปรสำหรับการโหลด const disabledBtn = ref(false) +const locationGranted = ref(false) +const isMockLocationDetected = ref(false) /** * fetch เช็คเวลาต้องลงเวลาเข้าหรือออกงาน @@ -114,40 +115,19 @@ async function updateLocation( formLocation.POI = namePOI } -function resetLocationForRetry() { - formLocation.lat = 0 - formLocation.lng = 0 - formLocation.POI = '' +/** + * รับค่าสถานะ location จาก AscGISMap + */ +function onLocationStatus(status: boolean) { + locationGranted.value = status } -function getCurrentPositionAsync(options?: PositionOptions) { - return new Promise((resolve, reject) => { - navigator.geolocation.getCurrentPosition(resolve, reject, options) - }) -} - -function wait(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)) -} - -async function getDelayedFreshPosition() { - const firstPosition = await getCurrentPositionAsync({ - enableHighAccuracy: true, - timeout: 10000, - maximumAge: 0, - }) - - await wait(MOCK_CHECK_DELAY_MS) - - try { - return await getCurrentPositionAsync({ - enableHighAccuracy: true, - timeout: 10000, - maximumAge: 0, - }) - } catch (error) { - return firstPosition - } +/** + * รับค่า mock location detection จาก AscGISMap + */ +function onMockDetected(result: any) { + isMockLocationDetected.value = true + disabledBtn.value = true } const location = ref('') // พื้นที่ใกล้เคียง @@ -187,9 +167,6 @@ const cameraIsOn = ref(false) const img = ref(undefined) const photoWidth = ref(350) const photoHeight = ref(350) -const availableCameras = ref([]) -const currentCameraIndex = ref(0) -const currentCameraType = ref<'front' | 'back' | 'unknown'>('unknown') const intervalId = ref(undefined) // ต้องใช้ตัวแปรเก็บค่า interval @@ -305,16 +282,6 @@ async function stopChecking() { } } -function identifyCameraType(label: string): 'front' | 'back' | 'unknown' { - const lowerLabel = label.toLowerCase() - if (lowerLabel.includes('front') || lowerLabel.includes('user') || lowerLabel.includes('face')) { - return 'front' - } else if (lowerLabel.includes('back') || lowerLabel.includes('environment') || lowerLabel.includes('rear')) { - return 'back' - } - return 'unknown' -} - /** function เปิดกล้อง*/ async function openCamera() { // เช็คสิทธิ์ privacy ก่อนเปิดกล้อง @@ -328,11 +295,7 @@ async function openCamera() { await camera.value?.stop() } else { await camera.value?.start() - const devices: any = await camera.value?.devices(['videoinput']) - if (devices) { - availableCameras.value = devices - await changeCamera() - } + await changeCamera() // ต้องรอให้ start() เสร็จก่อน } cameraIsOn.value = !cameraIsOn.value } else { @@ -346,59 +309,10 @@ async function openCamera() { } /** change camera device*/ -async function changeCamera(targetCameraType?: 'front' | 'back') { - try { - const devices: any = await camera.value?.devices(['videoinput']) - - if (!devices || devices.length === 0) { - console.warn('No cameras found') - return - } - - availableCameras.value = devices - - if (devices.length === 1 || !targetCameraType) { - const device = devices[0] - await camera.value?.changeCamera(device.deviceId) - currentCameraIndex.value = 0 - currentCameraType.value = identifyCameraType(device.label || '') - return - } - - const matchingCameras = devices.filter((device: any) => - identifyCameraType(device.label || '') === targetCameraType - ) - - if (matchingCameras.length > 0) { - const targetDevice = matchingCameras[0] - await camera.value?.changeCamera(targetDevice.deviceId) - currentCameraIndex.value = devices.indexOf(targetDevice) - currentCameraType.value = targetCameraType - } else { - const nextIndex = (currentCameraIndex.value + 1) % devices.length - const nextDevice = devices[nextIndex] - await camera.value?.changeCamera(nextDevice.deviceId) - currentCameraIndex.value = nextIndex - currentCameraType.value = identifyCameraType(nextDevice.label || '') - } - } catch (error) { - console.error('Error switching camera:', error) - } -} - -/** switch camera device*/ -async function switchCamera() { - if (availableCameras.value.length <= 1) { - return - } - - // สลับแค่ระหว่างกล้อง 2 ตัวแรก (กล้องหน้าและหลังหลัก) - const targetIndex = currentCameraIndex.value === 0 ? 1 : 0 - const targetDevice = availableCameras.value[targetIndex] - - await camera.value?.changeCamera(targetDevice.deviceId) - currentCameraIndex.value = targetIndex - currentCameraType.value = identifyCameraType(targetDevice.label || '') +async function changeCamera() { + const devices: any = await camera.value?.devices(['videoinput']) + const device = await devices[0] + camera.value?.changeCamera(device.deviceId) } /** function ถ่ายรูป*/ @@ -434,9 +348,7 @@ const objectRef: FormRef = { } /** function ตรวจสอบค่าว่างของ input*/ -async function validateForm() { - disabledBtn.value = true - +function validateForm() { const hasError = [] for (const key in objectRef) { if (Object.prototype.hasOwnProperty.call(objectRef, key)) { @@ -463,15 +375,11 @@ async function validateForm() { model.value === 'อื่นๆ' ? useLocation.value : model.value })` } คุณต้องการยืนยันการลงเวลาเข้างาน?`, - () => { - disabledBtn.value = false - }, + () => {}, 'red', 'ยืนยัน' ) } - } else { - disabledBtn.value = false } } @@ -482,16 +390,14 @@ const timeChickin = ref('') //เวลาเข้างาน,เว async function confirm() { // เช็คสิทธิ์ privacy ก่อนใช้งานแผนที่และกล้อง if (!checkPrivacyAccepted()) { - disabledBtn.value = false return } if (!formLocation.POI || !formLocation.lat || !formLocation.lng) { - disabledBtn.value = false mapRef.value?.requestLocationPermission() return } - + disabledBtn.value = true showLoader() const isLocation = workplace.value === 'in-place' //*true คือ ณ สถานที่ตั้ง, false คือ นอกสถานที่ตั้ง const locationName = workplace.value === 'in-place' ? '' : useLocation.value @@ -534,7 +440,6 @@ async function confirm() { async function getCheck() { if (!formLocation.POI || !formLocation.lat || !formLocation.lng) { - disabledBtn.value = false mapRef.value?.requestLocationPermission() return } @@ -567,9 +472,7 @@ async function getCheck() { () => confirm(), 'ยืนยันการลงเวลาออกงาน', `เวลาออกจากงานของคุณคือ ${endTimeAfternoonVal} แต่ขณะนี้เป็นเวลา ${timeVal} น. หากคุณออกจากงานในเวลานี้สถานะการลงเวลาจะเป็น "${res.data.result.statusText}" คุณแน่ใจว่าจะลงเวลาออกงานในตอนนี้ใช่หรือไม่?`, - () => { - disabledBtn.value = false - }, + () => {}, 'red', 'ยืนยัน' ) @@ -578,7 +481,6 @@ async function getCheck() { } }) .catch((e) => { - disabledBtn.value = false messageError($q, e) }) .finally(() => { @@ -710,6 +612,16 @@ watch( } } ) + +watch( + () => locationGranted.value, + (newVal) => { + // Removed auto-reset of isMockLocationDetected to prevent + // clearing mock detection state when permission is granted. + // Mock detection state should only be reset after explicit user action + // or after a successful validation without mock indicators. + } +)