From f0d4eba9d38574091c8ba767e337a152d68d9ae7 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Sat, 7 Mar 2026 00:17:36 +0700 Subject: [PATCH 01/14] fix --- .forgejo/workflows/ci-cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/ci-cd.yml b/.forgejo/workflows/ci-cd.yml index 4c508ef..62a125b 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: From 7fdece0a2896b08e61332e4d9f296cd13939ec41 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Mon, 9 Mar 2026 11:43:57 +0700 Subject: [PATCH 02/14] add test case and test step location --- docs/test-cases-location.md | 860 +++++++++++++++++++++++++++++ docs/test-steps-location.md | 1026 +++++++++++++++++++++++++++++++++++ 2 files changed, 1886 insertions(+) create mode 100644 docs/test-cases-location.md create mode 100644 docs/test-steps-location.md diff --git a/docs/test-cases-location.md b/docs/test-cases-location.md new file mode 100644 index 0000000..ed19dcd --- /dev/null +++ b/docs/test-cases-location.md @@ -0,0 +1,860 @@ +# 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 new file mode 100644 index 0000000..f62a2fa --- /dev/null +++ b/docs/test-steps-location.md @@ -0,0 +1,1026 @@ +# 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 | | | | From 859d74056a44bf105d18357ffcf283006d20f7ff Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Wed, 11 Mar 2026 15:29:43 +0700 Subject: [PATCH 03/14] fix track mock location --- src/components/AscGISMap.vue | 275 +++++++++++------------ src/composables/useLocationValidation.ts | 89 ++++++-- 2 files changed, 206 insertions(+), 158 deletions(-) diff --git a/src/components/AscGISMap.vue b/src/components/AscGISMap.vue index 81da108..d78b1bc 100644 --- a/src/components/AscGISMap.vue +++ b/src/components/AscGISMap.vue @@ -32,7 +32,7 @@ const apiKey = ref( ) const zoomMap = ref(18) -async function initializeMap() { +async function initializeMap(position: GeolocationPosition) { try { // Load modules of ArcGIS loadModules([ @@ -43,159 +43,146 @@ async function initializeMap() { 'esri/Graphic', 'esri/layers/TileLayer', ]).then(async ([esriConfig, Map, MapView, Point, Graphic, TileLayer]) => { - // Set apiKey - // esriConfig.apiKey = - // 'AAPK4f700a4324d04e9f8a1a134e0771ac45FXWawdCl-OotFfr52gz9XKxTDJTpDzw_YYcwbmKDDyAJswf14FoPyw0qBkN64DvP' - - // Create a FeatureLayer using a custom server URL - // const hillshadeLayer = new TileLayer({ - // url: `https://bmagis.bangkok.go.th/arcgis/rest/services/cache/BMA_3D_2D_Cache/MapServer`, - // }) - const map = new Map({ basemap: 'streets', - // basemap: 'arcgis-topographic', - // layers: [hillshadeLayer], }) - navigator.geolocation.getCurrentPosition(async (position) => { - const { latitude, longitude } = position.coords + const { latitude, longitude } = position.coords - const mapView = new MapView({ - container: 'mapViewDisplay', - map: map, - center: { - latitude: latitude, - longitude: longitude, - }, // Set the initial map center current position + const mapView = new MapView({ + container: 'mapViewDisplay', + map: map, + center: { + latitude: latitude, + longitude: longitude, + }, // Set the initial map center current position - zoom: zoomMap.value, - constraints: { - snapToZoom: false, // Disables snapping to the zoom level - minZoom: zoomMap.value, // Set minimum zoom level - maxZoom: zoomMap.value, // Set maximum zoom level (same as minZoom for fixed zoom) - }, + zoom: zoomMap.value, + constraints: { + snapToZoom: false, // Disables snapping to the zoom level + minZoom: zoomMap.value, // Set minimum zoom level + maxZoom: zoomMap.value, // Set maximum zoom level (same as minZoom for fixed zoom) + }, - ui: { - components: [], // Empty array to remove all default UI components - }, - }) + ui: { + components: [], // Empty array to remove all default UI components + }, + }) - // ตำแหน่งของผู้ใช้ - const userPoint = new Point({ longitude, latitude }) - const userSymbol = { - type: 'picture-marker', - url: 'http://maps.google.com/mapfiles/ms/icons/red.png', - width: '32px', - height: '32px', - } - const userGraphic = new Graphic({ - geometry: userPoint, - symbol: userSymbol, - }) - mapView.graphics.add(userGraphic) - // Get POI place ยิงไปขอที่ server ของกทม.ก่อน - await axios - .get( - 'https://bmagis.bangkok.go.th/portal/sharing/servers/e4732c3a9fe549ab8bc697573b468f68/rest/services/World/GeocodeServer/reverseGeocode/', - { - params: { - f: 'json', // Format JSON response - distance: 2000, - category: 'POI', - location: { - spatialReference: { wkid: 4326 }, - x: longitude, - y: latitude, - }, - token: apiKey.value, + // ตำแหน่งของผู้ใช้ + const userPoint = new Point({ longitude, latitude }) + const userSymbol = { + type: 'picture-marker', + url: 'http://maps.google.com/mapfiles/ms/icons/red.png', + width: '32px', + height: '32px', + } + const userGraphic = new Graphic({ + geometry: userPoint, + symbol: userSymbol, + }) + mapView.graphics.add(userGraphic) + // Get POI place ยิงไปขอที่ server ของกทม.ก่อน + await axios + .get( + 'https://bmagis.bangkok.go.th/portal/sharing/servers/e4732c3a9fe549ab8bc697573b468f68/rest/services/World/GeocodeServer/reverseGeocode/', + { + params: { + f: 'json', // Format JSON response + distance: 2000, + category: 'POI', + location: { + spatialReference: { wkid: 4326 }, + x: longitude, + y: latitude, }, - } - ) - .then((response) => { - // console.log('poi', response.data.location) - poiPlaceName.value = response.data.address - ? response.data.address.PlaceName === '' - ? response.data.address.ShortLabel - : response.data.address.PlaceName - : 'ไม่พบข้อมูล' - const poiPoint = new Point({ - longitude: response.data.location.x, - latitude: response.data.location.y, - }) - const poiSymbol = { - type: 'picture-marker', - url: 'http://maps.google.com/mapfiles/ms/icons/blue.png', - width: '32px', - height: '32px', - } - const poiGraphic = new Graphic({ - geometry: poiPoint, - symbol: poiSymbol, - }) - mapView.graphics.add(poiGraphic) - // อัปเดตการแสดงผลให้แสดงทั้งตำแหน่งของผู้ใช้และ POI - mapView.goTo({ - target: [userPoint, poiPoint], - zoom: zoomMap.value, - }) - - updateLocation(latitude, longitude, poiPlaceName.value) + token: apiKey.value, + }, + } + ) + .then((response) => { + // console.log('poi', response.data.location) + poiPlaceName.value = response.data.address + ? response.data.address.PlaceName === '' + ? response.data.address.ShortLabel + : response.data.address.PlaceName + : 'ไม่พบข้อมูล' + const poiPoint = new Point({ + longitude: response.data.location.x, + latitude: response.data.location.y, }) - .catch(async (error) => { - // console.error('Error fetching points of interest:', error) - // Get POI place ยิงไปขอที่ server arcgis ไม่ต้องใช้ token - await axios - .get( - 'https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/reverseGeocode/', - { - params: { - f: 'json', // Format JSON response - distance: 2000, - category: 'POI', - location: { - spatialReference: { wkid: 4326 }, - x: longitude, - y: latitude, - }, + const poiSymbol = { + type: 'picture-marker', + url: 'http://maps.google.com/mapfiles/ms/icons/blue.png', + width: '32px', + height: '32px', + } + const poiGraphic = new Graphic({ + geometry: poiPoint, + symbol: poiSymbol, + }) + mapView.graphics.add(poiGraphic) + // อัปเดตการแสดงผลให้แสดงทั้งตำแหน่งของผู้ใช้และ POI + mapView.goTo({ + target: [userPoint, poiPoint], + zoom: zoomMap.value, + }) + + updateLocation(latitude, longitude, poiPlaceName.value) + }) + .catch(async (error) => { + // console.error('Error fetching points of interest:', error) + // Get POI place ยิงไปขอที่ server arcgis ไม่ต้องใช้ token + await axios + .get( + 'https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/reverseGeocode/', + { + params: { + f: 'json', // Format JSON response + distance: 2000, + category: 'POI', + location: { + spatialReference: { wkid: 4326 }, + x: longitude, + y: latitude, }, - } - ) - .then((response) => { - // console.log('poi', response.data.location) - poiPlaceName.value = response.data.address - ? response.data.address.PlaceName === '' - ? response.data.address.ShortLabel - : response.data.address.PlaceName - : 'ไม่พบข้อมูล' - const poiPoint = new Point({ - longitude: response.data.location.x, - latitude: response.data.location.y, - }) - const poiSymbol = { - type: 'picture-marker', - url: 'http://maps.google.com/mapfiles/ms/icons/blue.png', - width: '32px', - height: '32px', - } - const poiGraphic = new Graphic({ - geometry: poiPoint, - symbol: poiSymbol, - }) - mapView.graphics.add(poiGraphic) - // อัปเดตการแสดงผลให้แสดงทั้งตำแหน่งของผู้ใช้และ POI - mapView.goTo({ - target: [userPoint, poiPoint], - zoom: zoomMap.value, - }) + }, + } + ) + .then((response) => { + // console.log('poi', response.data.location) + poiPlaceName.value = response.data.address + ? response.data.address.PlaceName === '' + ? response.data.address.ShortLabel + : response.data.address.PlaceName + : 'ไม่พบข้อมูล' + const poiPoint = new Point({ + longitude: response.data.location.x, + latitude: response.data.location.y, + }) + const poiSymbol = { + type: 'picture-marker', + url: 'http://maps.google.com/mapfiles/ms/icons/blue.png', + width: '32px', + height: '32px', + } + const poiGraphic = new Graphic({ + geometry: poiPoint, + symbol: poiSymbol, + }) + mapView.graphics.add(poiGraphic) + // อัปเดตการแสดงผลให้แสดงทั้งตำแหน่งของผู้ใช้และ POI + mapView.goTo({ + target: [userPoint, poiPoint], + zoom: zoomMap.value, + }) - updateLocation(latitude, longitude, poiPlaceName.value) - }) - .catch((error) => { - // console.error('Error fetching points of interest:', error) - }) - }) - }) + updateLocation(latitude, longitude, poiPlaceName.value) + }) + .catch((error) => { + // console.error('Error fetching points of interest:', error) + }) + }) }) } catch (error) { console.error('Error loading the map', error) @@ -236,7 +223,7 @@ const requestLocationPermission = () => { } // Check for critical errors (invalid coordinates) that prevent showing location - const hasCriticalErrors = validationResult.errors.some(error => + const hasCriticalErrors = validationResult.errors.some((error) => error.includes('พิกัดตำแหน่งไม่ถูกต้อง') ) @@ -261,7 +248,7 @@ const requestLocationPermission = () => { // Center map on user's location if map is initialized if (privacyStore.isAccepted) { - await initializeMap() + await initializeMap(position) } }, (error) => { diff --git a/src/composables/useLocationValidation.ts b/src/composables/useLocationValidation.ts index 27222df..2522d07 100644 --- a/src/composables/useLocationValidation.ts +++ b/src/composables/useLocationValidation.ts @@ -23,6 +23,7 @@ export const VALIDATION_CONFIG = { 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) + SUSPICIOUS_ACCURACY_MAX: 5, // accuracy ≤ 5m AND integer = suspicious (real GPS never rounds to whole number) } as const export function useLocationValidation() { @@ -31,17 +32,26 @@ export function useLocationValidation() { // Thai error messages - exported for i18n consistency const errorMessages = { - MOCK_DETECTED: 'ตรวจพบว่าตำแหน่ง GPS อาจไม่ถูกต้อง กรุณาปิดแอปจำลองตำแหน่งและลองใหม่', + MOCK_DETECTED: + 'ตรวจพบว่าตำแหน่ง GPS อาจไม่ถูกต้อง กรุณาปิดแอปจำลองตำแหน่งและลองใหม่', INVALID_COORDINATES: 'พิกัดตำแหน่งไม่ถูกต้อง กรุณาลองใหม่', STALE_TIMESTAMP: 'ข้อมูลตำแหน่งเก่าเกินไป กรุณารับสัญญาณ GPS ใหม่', POOR_ACCURACY: 'ความแม่นยำตำแหน่งต่ำเกินไป กรุณาตรวจสอบการรับสัญญาณ GPS', - IMPOSSIBLE_SPEED: 'ตรวจพบการเคลื่อนที่ด้วยความเร็วผิดปกติ อาจเป็นการจำลองตำแหน่ง', + IMPOSSIBLE_SPEED: + 'ตรวจพบการเคลื่อนที่ด้วยความเร็วผิดปกติ อาจเป็นการจำลองตำแหน่ง', + SUSPICIOUS_ACCURACY: 'ตรวจพบค่าความแม่นยำที่ผิดปกติ อาจเป็นการจำลองตำแหน่ง', + DUPLICATE_POSITION: 'ตรวจพบพิกัดตำแหน่งซ้ำกัน อาจเป็นการจำลองตำแหน่ง', } const previousPositions = ref([]) // คำนวณระยะห่างระหว่าง 2 จุด (Haversine formula) - const haversineDistance = (lat1: number, lon1: number, lat2: number, lon2: number): number => { + 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 @@ -59,9 +69,12 @@ export function useLocationValidation() { // ตรวจสอบพิกัดถูกต้อง const validateCoordinates = (lat: number, lon: number): boolean => { return ( - lat >= -90 && lat <= 90 && - lon >= -180 && lon <= 180 && - !isNaN(lat) && !isNaN(lon) && + lat >= -90 && + lat <= 90 && + lon >= -180 && + lon <= 180 && + !isNaN(lat) && + !isNaN(lon) && !(lat === 0 && lon === 0) // Mock มักใช้ 0,0 ) } @@ -80,20 +93,49 @@ export function useLocationValidation() { } // คำนวณความเร็ว - const calculateSpeed = (pos1: PositionSnapshot, pos2: PositionSnapshot): number => { - const distance = haversineDistance(pos1.latitude, pos1.longitude, pos2.latitude, pos2.longitude) + 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 validateSpeed = ( + current: PositionSnapshot, + previous: PositionSnapshot + ): boolean => { const speed = calculateSpeed(previous, current) return speed <= VALIDATION_CONFIG.MAX_SPEED_MS } + // ตรวจสอบค่าความแม่นยำที่ผิดปกติ (mock apps มักรายงานค่าที่เป็นเลขจำนวนเต็มสวยงาม เช่น 5.0, 3.0) + const hasSuspiciousPerfectAccuracy = (accuracy: number | null): boolean => { + if (accuracy === null) return false + return ( + Number.isInteger(accuracy) && + accuracy <= VALIDATION_CONFIG.SUSPICIOUS_ACCURACY_MAX + ) + } + + // ตรวจสอบพิกัดซ้ำกันทุกค่า (real GPS มีการสั่นเล็กน้อย mock app มักคืนพิกัดเดิมซ้ำ) + const hasDuplicateCoordinates = (lat: number, lon: number): boolean => { + return previousPositions.value.some( + (p) => p.latitude === lat && p.longitude === lon + ) + } + // Main validation function - const validateLocation = (position: GeolocationPosition): LocationValidationResult => { + const validateLocation = ( + position: GeolocationPosition + ): LocationValidationResult => { const warnings: string[] = [] const errors: string[] = [] let mockIndicators = 0 @@ -121,7 +163,8 @@ export function useLocationValidation() { // 4. Compare with previous positions if (previousPositions.value.length > 0) { - const previous = previousPositions.value[previousPositions.value.length - 1] + const previous = + previousPositions.value[previousPositions.value.length - 1] if (!validateSpeed({ latitude, longitude, timestamp }, previous)) { errors.push(errorMessages.IMPOSSIBLE_SPEED) @@ -129,14 +172,32 @@ export function useLocationValidation() { } } + // 5. Suspiciously perfect accuracy — real GPS never rounds to a whole-number ≤ 5m + if (hasSuspiciousPerfectAccuracy(accuracy)) { + warnings.push(errorMessages.SUSPICIOUS_ACCURACY) + mockIndicators += 2 + } + + // 6. Exact duplicate coordinates — real GPS always drifts slightly between readings + if ( + previousPositions.value.length > 0 && + hasDuplicateCoordinates(latitude, longitude) + ) { + warnings.push(errorMessages.DUPLICATE_POSITION) + mockIndicators += 2 + } + // Store current position previousPositions.value.push({ latitude, longitude, timestamp }) - if (previousPositions.value.length > VALIDATION_CONFIG.POSITION_HISTORY_SIZE) { + if ( + previousPositions.value.length > VALIDATION_CONFIG.POSITION_HISTORY_SIZE + ) { previousPositions.value.shift() } // Determine result - const isMockDetected = mockIndicators >= VALIDATION_CONFIG.MOCK_INDICATOR_THRESHOLD + const isMockDetected = + mockIndicators >= VALIDATION_CONFIG.MOCK_INDICATOR_THRESHOLD const isValid = errors.length === 0 let confidence: 'low' | 'medium' | 'high' = 'low' @@ -154,7 +215,7 @@ export function useLocationValidation() { const showMockWarning = (result: LocationValidationResult) => { if (!result.isMockDetected) return - messageError($q, null, errorMessages.MOCK_DETECTED) + messageError($q, '', errorMessages.MOCK_DETECTED) } const resetValidation = () => { From 3ae6e6eeaced6f3ab07996c178c0c92dd7a9ad9e Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Wed, 11 Mar 2026 17:44:39 +0700 Subject: [PATCH 04/14] fix fack location complated --- src/components/AscGISMap.vue | 66 ++++++--- src/composables/useLocationValidation.ts | 25 ++++ src/stores/mixin.ts | 178 +++++++++++------------ src/views/HomeView.vue | 133 ++++++++++++++++- 4 files changed, 287 insertions(+), 115 deletions(-) diff --git a/src/components/AscGISMap.vue b/src/components/AscGISMap.vue index d78b1bc..7ed48a7 100644 --- a/src/components/AscGISMap.vue +++ b/src/components/AscGISMap.vue @@ -16,7 +16,32 @@ const mapElement = ref(null) const emit = defineEmits(['update:location', 'locationStatus', 'mockDetected']) const $q = useQuasar() -const { validateLocation, showMockWarning } = useLocationValidation() +const { validateLocation } = useLocationValidation() +const MOCK_CHECK_DELAY_MS = 800 + +function wait(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)) +} + +function getCurrentPositionAsync(options?: PositionOptions) { + return new Promise((resolve, reject) => { + navigator.geolocation.getCurrentPosition(resolve, reject, options) + }) +} + +async function getPositionForValidation(fallback: GeolocationPosition) { + await wait(MOCK_CHECK_DELAY_MS) + try { + return await getCurrentPositionAsync({ + enableHighAccuracy: true, + timeout: 10000, + maximumAge: 0, + }) + } catch (error) { + // Keep the original reading if the second sampling fails. + return fallback + } +} function updateLocation(latitude: number, longitude: number, namePOI: string) { // ส่ง event ไปยัง parent component เพื่ออัพเดทค่า props @@ -24,6 +49,7 @@ function updateLocation(latitude: number, longitude: number, namePOI: string) { } const poiPlaceName = ref('') // ชื่อพื้นที่ใกล้เคียง +const isMapReady = ref(false) // Replace ArcGIS api key const apiKey = ref( @@ -68,6 +94,7 @@ async function initializeMap(position: GeolocationPosition) { components: [], // Empty array to remove all default UI components }, }) + isMapReady.value = true // ตำแหน่งของผู้ใช้ const userPoint = new Point({ longitude, latitude }) @@ -180,7 +207,9 @@ async function initializeMap(position: GeolocationPosition) { updateLocation(latitude, longitude, poiPlaceName.value) }) .catch((error) => { - // console.error('Error fetching points of interest:', error) + // Keep map visible even when POI lookup fails. + poiPlaceName.value = 'ไม่พบข้อมูล' + updateLocation(latitude, longitude, poiPlaceName.value) }) }) }) @@ -213,13 +242,16 @@ const requestLocationPermission = () => { navigator.geolocation.getCurrentPosition( async (position) => { - // Validate location first - const validationResult = validateLocation(position) + const sampledPosition = await getPositionForValidation(position) - // Always emit mockDetected event (regardless of result) + // Validate location first + const validationResult = validateLocation(sampledPosition) + + // Do not block map preview on initial mock detection. + // The hard-stop warning is enforced at submit time in HomeView. if (validationResult.isMockDetected) { - showMockWarning(validationResult) - emit('mockDetected', validationResult) + locationGranted.value = true + emit('locationStatus', true) } // Check for critical errors (invalid coordinates) that prevent showing location @@ -234,11 +266,11 @@ const requestLocationPermission = () => { return } - // Permission granted based on mock detection - locationGranted.value = !validationResult.isMockDetected - emit('locationStatus', !validationResult.isMockDetected) + // Permission granted for map preview state. + locationGranted.value = true + emit('locationStatus', true) - const { latitude, longitude } = position.coords + const { latitude, longitude } = sampledPosition.coords // console.log('Current position:', latitude, longitude) if (!latitude || !longitude) { @@ -248,7 +280,7 @@ const requestLocationPermission = () => { // Center map on user's location if map is initialized if (privacyStore.isAccepted) { - await initializeMap(position) + await initializeMap(sampledPosition) } }, (error) => { @@ -279,7 +311,7 @@ const requestLocationPermission = () => { break } }, - { enableHighAccuracy: true } + { enableHighAccuracy: true, timeout: 10000, maximumAge: 0 } ) } @@ -291,7 +323,7 @@ defineExpose({ diff --git a/src/composables/useLocationValidation.ts b/src/composables/useLocationValidation.ts index 2522d07..689af41 100644 --- a/src/composables/useLocationValidation.ts +++ b/src/composables/useLocationValidation.ts @@ -24,6 +24,13 @@ export const VALIDATION_CONFIG = { POSITION_HISTORY_SIZE: 5, // number of positions to keep for pattern detection MOCK_INDICATOR_THRESHOLD: 3, // threshold for mock detection (indicators >= 3 = mock) SUSPICIOUS_ACCURACY_MAX: 5, // accuracy ≤ 5m AND integer = suspicious (real GPS never rounds to whole number) + // Geographic service area (Thailand) to reduce spoofing risk from remote fake coordinates. + SERVICE_AREA: { + MIN_LAT: 5.0, + MAX_LAT: 21.0, + MIN_LON: 97.0, + MAX_LON: 106.0, + }, } as const export function useLocationValidation() { @@ -41,6 +48,8 @@ export function useLocationValidation() { 'ตรวจพบการเคลื่อนที่ด้วยความเร็วผิดปกติ อาจเป็นการจำลองตำแหน่ง', SUSPICIOUS_ACCURACY: 'ตรวจพบค่าความแม่นยำที่ผิดปกติ อาจเป็นการจำลองตำแหน่ง', DUPLICATE_POSITION: 'ตรวจพบพิกัดตำแหน่งซ้ำกัน อาจเป็นการจำลองตำแหน่ง', + OUT_OF_SERVICE_AREA: + 'ตรวจพบตำแหน่งนอกพื้นที่ใช้งานของระบบ กรุณาปิดแอปจำลองตำแหน่งและลองใหม่', } const previousPositions = ref([]) @@ -132,6 +141,16 @@ export function useLocationValidation() { ) } + const isWithinServiceArea = (lat: number, lon: number): boolean => { + const area = VALIDATION_CONFIG.SERVICE_AREA + return ( + lat >= area.MIN_LAT && + lat <= area.MAX_LAT && + lon >= area.MIN_LON && + lon <= area.MAX_LON + ) + } + // Main validation function const validateLocation = ( position: GeolocationPosition @@ -149,6 +168,12 @@ export function useLocationValidation() { mockIndicators += 3 } + // 1.1 Service-area validation (critical for remote spoofed coordinates) + if (!isWithinServiceArea(latitude, longitude)) { + errors.push(errorMessages.OUT_OF_SERVICE_AREA) + mockIndicators += 3 + } + // 2. Timestamp validation if (!validateTimestamp(timestamp)) { errors.push(errorMessages.STALE_TIMESTAMP) diff --git a/src/stores/mixin.ts b/src/stores/mixin.ts index 3ff5636..fffa00c 100644 --- a/src/stores/mixin.ts +++ b/src/stores/mixin.ts @@ -5,6 +5,56 @@ 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 @@ -133,53 +183,30 @@ export const useCounterMixin = defineStore('mixin', () => { } const messageError = (q: any, e: any = '', msg: string = '') => { - // q.dialog.hide(); + // Keep only one active warning popup to prevent dialog overlap. if (e.response !== undefined) { if (e.response.data.status !== undefined) { if (e.response.data.status == 401) { //invalid_token - q.dialog({ - component: CustomComponent, - componentProps: { - title: `พบข้อผิดพลาด`, - message: `ล็อกอินหมดอายุ กรุณาล็อกอินใหม่อีกครั้ง`, - icon: 'warning', - color: 'red', - onlycancel: true, - }, - }).onCancel(async () => { - showLoader() - await logout() - setTimeout(() => { - hideLoader() - }, 1000) - }) + openSingleErrorDialog( + q, + 'ล็อกอินหมดอายุ กรุณาล็อกอินใหม่อีกครั้ง', + async () => { + showLoader() + await logout() + setTimeout(() => { + hideLoader() + }, 1000) + } + ) } else { const message = e.response.data.result ?? e.response.data.message - q.dialog({ - component: CustomComponent, - componentProps: { - title: `พบข้อผิดพลาด`, - message: `${message}`, - icon: 'warning', - color: 'red', - onlycancel: true, - }, - }) + openSingleErrorDialog(q, `${message}`) } } else { if (e.response.status == 401) { if (msg !== '') { - q.dialog({ - component: CustomComponent, - componentProps: { - title: `พบข้อผิดพลาด`, - message: msg, - icon: 'warning', - color: 'red', - onlycancel: true, - }, - }).onCancel(async () => { + openSingleErrorDialog(q, msg, async () => { showLoader() await logout() setTimeout(() => { @@ -188,70 +215,35 @@ export const useCounterMixin = defineStore('mixin', () => { }) } else { //invalid_token - q.dialog({ - component: CustomComponent, - componentProps: { - title: `พบข้อผิดพลาด`, - message: `ล็อกอินหมดอายุ กรุณาล็อกอินใหม่อีกครั้ง`, - icon: 'warning', - color: 'red', - onlycancel: true, - }, - }).onCancel(async () => { - showLoader() - await logout() - setTimeout(() => { - hideLoader() - }, 1000) - }) + openSingleErrorDialog( + q, + 'ล็อกอินหมดอายุ กรุณาล็อกอินใหม่อีกครั้ง', + async () => { + showLoader() + await logout() + setTimeout(() => { + hideLoader() + }, 1000) + } + ) } } else if (e.response.data.successful === false) { - q.dialog({ - component: CustomComponent, - componentProps: { - title: `พบข้อผิดพลาด`, - message: e.response.data.message, - icon: 'warning', - color: 'red', - onlycancel: true, - }, - }) + openSingleErrorDialog(q, e.response.data.message) } else { - q.dialog({ - component: CustomComponent, - componentProps: { - title: `พบข้อผิดพลาด`, - message: `ข้อมูลผิดพลาดทำให้เกิดการไม่ตอบสนองต่อการเรียกใช้งานดูเว็บไซต์`, - icon: 'warning', - color: 'red', - onlycancel: true, - }, - }) + openSingleErrorDialog( + q, + 'ข้อมูลผิดพลาดทำให้เกิดการไม่ตอบสนองต่อการเรียกใช้งานดูเว็บไซต์' + ) } } } else { if (msg !== '') { - return q.dialog({ - component: CustomComponent, - componentProps: { - title: `พบข้อผิดพลาด`, - message: msg, - icon: 'warning', - color: 'red', - onlycancel: true, - }, - }) + return openSingleErrorDialog(q, msg) } - q.dialog({ - component: CustomComponent, - componentProps: { - title: `พบข้อผิดพลาด`, - message: `ข้อมูลผิดพลาดทำให้เกิดการไม่ตอบสนองต่อการเรียกใช้งานดูเว็บไซต์`, - icon: 'warning', - color: 'red', - onlycancel: true, - }, - }) + openSingleErrorDialog( + q, + 'ข้อมูลผิดพลาดทำให้เกิดการไม่ตอบสนองต่อการเรียกใช้งานดูเว็บไซต์' + ) } } diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 379b970..43c14c0 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -8,6 +8,7 @@ import config from '@/app.config' import http from '@/plugins/http' import { useCounterMixin } from '@/stores/mixin' import { usePermissions } from '@/composables/usePermissions' +import { useLocationValidation } from '@/composables/useLocationValidation' import { usePrivacyStore } from '@/stores/privacy' import type { FormRef, OptionReason } from '@/interface/response/checkin' @@ -18,7 +19,10 @@ const mixin = useCounterMixin() const { date2Thai, showLoader, hideLoader, messageError, dialogConfirm } = mixin const $q = useQuasar() const { checkPrivacyAccepted } = usePermissions() +const { validateLocation, showMockWarning, resetValidation } = + useLocationValidation() const privacyStore = usePrivacyStore() +const MOCK_CHECK_DELAY_MS = 800 const modalTime = ref(false) // Dailog ลงเวลาเข้างานของคุณ const checkStatus = ref('') @@ -120,14 +124,110 @@ async function updateLocation( */ function onLocationStatus(status: boolean) { locationGranted.value = status + if (status) { + isMockLocationDetected.value = false + } } /** * รับค่า mock location detection จาก AscGISMap */ function onMockDetected(result: any) { - isMockLocationDetected.value = true - disabledBtn.value = true + isMockLocationDetected.value = !!result?.isMockDetected + disabledBtn.value = false +} + +function resetLocationForRetry() { + locationGranted.value = false + formLocation.lat = 0 + formLocation.lng = 0 + formLocation.POI = '' +} + +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 + } +} + +async function revalidateLocationBeforeSubmit() { + if (!navigator.geolocation) { + messageError( + $q, + '', + 'ไม่สามารถระบุตำแหน่งปัจจุบันได้ เบราว์เซอร์ของคุณไม่รองรับ Geolocation' + ) + return false + } + + try { + // If previous attempt was mock, clear history so fresh GPS is not compared + // against spoofed coordinates and incorrectly flagged as impossible speed. + if (isMockLocationDetected.value) { + resetValidation() + } + + const position = await getDelayedFreshPosition() + + const validationResult = validateLocation(position) + + if (validationResult.isMockDetected) { + isMockLocationDetected.value = true + disabledBtn.value = false + resetValidation() + resetLocationForRetry() + showMockWarning(validationResult) + mapRef.value?.requestLocationPermission() + return false + } + + if (validationResult.errors.length > 0) { + disabledBtn.value = false + resetValidation() + resetLocationForRetry() + messageError($q, '', validationResult.errors[0]) + mapRef.value?.requestLocationPermission() + return false + } + + locationGranted.value = true + isMockLocationDetected.value = false + return true + } catch (error) { + disabledBtn.value = false + resetLocationForRetry() + messageError( + $q, + '', + 'ไม่สามารถตรวจสอบตำแหน่งล่าสุดก่อนลงเวลาได้ กรุณาลองใหม่อีกครั้ง' + ) + mapRef.value?.requestLocationPermission() + return false + } } const location = ref('') // พื้นที่ใกล้เคียง @@ -348,7 +448,7 @@ const objectRef: FormRef = { } /** function ตรวจสอบค่าว่างของ input*/ -function validateForm() { +async function validateForm() { const hasError = [] for (const key in objectRef) { if (Object.prototype.hasOwnProperty.call(objectRef, key)) { @@ -360,6 +460,11 @@ function validateForm() { } } if (hasError.every((result) => result === true)) { + const isLocationValid = await revalidateLocationBeforeSubmit() + if (!isLocationValid) { + return + } + if (statusCheckin.value == false) { getCheck() } else if (statusCheckin.value) { @@ -397,6 +502,12 @@ async function confirm() { mapRef.value?.requestLocationPermission() return } + + const isLocationValid = await revalidateLocationBeforeSubmit() + if (!isLocationValid) { + return + } + disabledBtn.value = true showLoader() const isLocation = workplace.value === 'in-place' //*true คือ ณ สถานที่ตั้ง, false คือ นอกสถานที่ตั้ง @@ -1016,7 +1127,13 @@ watch( push size="18px" :class="$q.screen.gt.xs ? 'q-px-md' : 'full-width q-pa-sm'" - :disable="disabledBtn || !locationGranted || isMockLocationDetected ? true : camera && img ? false : true" + :disable=" + disabledBtn || !locationGranted || isMockLocationDetected + ? true + : camera && img + ? false + : true + " @click="validateForm" :loading="inQueue" /> @@ -1131,7 +1248,13 @@ watch( push size="18px" :class="$q.screen.gt.xs ? 'q-px-md' : 'full-width q-pa-sm'" - :disable="disabledBtn || !locationGranted || isMockLocationDetected ? true : camera && img ? false : true" + :disable=" + disabledBtn || !locationGranted || isMockLocationDetected + ? true + : camera && img + ? false + : true + " @click="validateForm" :loading="inQueue" /> From 33da60ec026ced73776f13b72c5749713efe277a Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Thu, 12 Mar 2026 00:51:22 +0700 Subject: [PATCH 05/14] =?UTF-8?q?fix=20disable=20=E0=B8=9B=E0=B8=B8?= =?UTF-8?q?=E0=B9=88=E0=B8=A1=E0=B8=A5=E0=B8=87=E0=B9=80=E0=B8=A7=E0=B8=A5?= =?UTF-8?q?=E0=B8=B2=E0=B8=81=E0=B8=A3=E0=B8=93=E0=B8=B5=E0=B9=80=E0=B8=84?= =?UTF-8?q?=E0=B8=A3=E0=B8=B7=E0=B9=88=E0=B8=AD=E0=B8=87=E0=B8=8A=E0=B9=89?= =?UTF-8?q?=E0=B8=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/HomeView.vue | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 43c14c0..7021e65 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -449,6 +449,8 @@ const objectRef: FormRef = { /** function ตรวจสอบค่าว่างของ input*/ async function validateForm() { + disabledBtn.value = true + const hasError = [] for (const key in objectRef) { if (Object.prototype.hasOwnProperty.call(objectRef, key)) { @@ -480,11 +482,15 @@ async function validateForm() { model.value === 'อื่นๆ' ? useLocation.value : model.value })` } คุณต้องการยืนยันการลงเวลาเข้างาน?`, - () => {}, + () => { + disabledBtn.value = false + }, 'red', 'ยืนยัน' ) } + } else { + disabledBtn.value = false } } @@ -495,10 +501,12 @@ 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 } @@ -508,7 +516,6 @@ async function confirm() { return } - disabledBtn.value = true showLoader() const isLocation = workplace.value === 'in-place' //*true คือ ณ สถานที่ตั้ง, false คือ นอกสถานที่ตั้ง const locationName = workplace.value === 'in-place' ? '' : useLocation.value @@ -551,6 +558,7 @@ async function confirm() { async function getCheck() { if (!formLocation.POI || !formLocation.lat || !formLocation.lng) { + disabledBtn.value = false mapRef.value?.requestLocationPermission() return } @@ -583,7 +591,9 @@ async function getCheck() { () => confirm(), 'ยืนยันการลงเวลาออกงาน', `เวลาออกจากงานของคุณคือ ${endTimeAfternoonVal} แต่ขณะนี้เป็นเวลา ${timeVal} น. หากคุณออกจากงานในเวลานี้สถานะการลงเวลาจะเป็น "${res.data.result.statusText}" คุณแน่ใจว่าจะลงเวลาออกงานในตอนนี้ใช่หรือไม่?`, - () => {}, + () => { + disabledBtn.value = false + }, 'red', 'ยืนยัน' ) @@ -592,6 +602,7 @@ async function getCheck() { } }) .catch((e) => { + disabledBtn.value = false messageError($q, e) }) .finally(() => { From e2f22cc9c056c5eacabbdc7427fb0888d9ed4b35 Mon Sep 17 00:00:00 2001 From: "DESKTOP-1R2VSQH\\Lenovo ThinkPad E490" Date: Wed, 25 Mar 2026 11:06:00 +0700 Subject: [PATCH 06/14] fix --- src/components/DialogDebug.vue | 5 ++++- src/components/FooterContact.vue | 13 +++++++++++++ src/views/MainView.vue | 6 ++++++ 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 src/components/FooterContact.vue diff --git a/src/components/DialogDebug.vue b/src/components/DialogDebug.vue index 5fa1f3f..ab55a0e 100644 --- a/src/components/DialogDebug.vue +++ b/src/components/DialogDebug.vue @@ -11,6 +11,7 @@ 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() @@ -343,7 +344,9 @@ function onClose() { - + + + +
+ + + พบปัญหาการใช้งานกรุณาติดต่อผู้ดูแลระบบ + 088-264-9800 + +
+ + + + + diff --git a/src/views/MainView.vue b/src/views/MainView.vue index eda5edc..e34ab53 100644 --- a/src/views/MainView.vue +++ b/src/views/MainView.vue @@ -17,6 +17,7 @@ import { usePositionKeycloakStore } from '@/stores/positionKeycloak' import DialogHeader from '@/components/DialogHeader.vue' import PopupPrivacy from '@/components/PopupPrivacy.vue' import DialogDebug from '@/components/DialogDebug.vue' +import FooterContact from '@/components/FooterContact.vue' const mixin = useCounterMixin() const privacyStore = usePrivacyStore() @@ -561,6 +562,11 @@ onMounted(async () => { + + + + + From 66a48f38302946708ea52af51dcd4f2da33dd864 Mon Sep 17 00:00:00 2001 From: "DESKTOP-1R2VSQH\\Lenovo ThinkPad E490" Date: Wed, 25 Mar 2026 11:58:46 +0700 Subject: [PATCH 07/14] fix:tel --- src/components/FooterContact.vue | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/FooterContact.vue b/src/components/FooterContact.vue index fe21234..025aed3 100644 --- a/src/components/FooterContact.vue +++ b/src/components/FooterContact.vue @@ -3,7 +3,9 @@ พบปัญหาการใช้งานกรุณาติดต่อผู้ดูแลระบบ - 088-264-9800 + 088-264-9800 From fa855b30c84b55f797725f8e3861554f1ef8c07f Mon Sep 17 00:00:00 2001 From: "DESKTOP-1R2VSQH\\Lenovo ThinkPad E490" Date: Wed, 25 Mar 2026 12:12:40 +0700 Subject: [PATCH 08/14] fix:style --- src/components/FooterContact.vue | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/FooterContact.vue b/src/components/FooterContact.vue index 025aed3..4df5d53 100644 --- a/src/components/FooterContact.vue +++ b/src/components/FooterContact.vue @@ -4,7 +4,9 @@ พบปัญหาการใช้งานกรุณาติดต่อผู้ดูแลระบบ 088-264-9800088-264-9800 From f53327e2d9b8d71373783d4f5844082c81763d4d Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Fri, 27 Mar 2026 20:57:24 +0700 Subject: [PATCH 09/14] fix on mobile hidden contact --- src/views/MainView.vue | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/views/MainView.vue b/src/views/MainView.vue index e34ab53..c8bc1d8 100644 --- a/src/views/MainView.vue +++ b/src/views/MainView.vue @@ -564,7 +564,10 @@ onMounted(async () => { - + @@ -720,4 +723,8 @@ onMounted(async () => { background-color: #016987; color: #fff; } + +.hidden { + display: none !important; +} From 7edfaa4e4fb50dc12428855d7c329e7b6d9053d9 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Mon, 30 Mar 2026 10:47:36 +0700 Subject: [PATCH 10/14] fix remove code check fake location --- src/components/AscGISMap.vue | 354 ++++++++++------------- src/components/DialogDebug.vue | 2 +- src/composables/useLocationValidation.ts | 255 ---------------- src/views/HomeView.vue | 125 +------- 4 files changed, 157 insertions(+), 579 deletions(-) delete mode 100644 src/composables/useLocationValidation.ts diff --git a/src/components/AscGISMap.vue b/src/components/AscGISMap.vue index 7ed48a7..7651b7c 100644 --- a/src/components/AscGISMap.vue +++ b/src/components/AscGISMap.vue @@ -5,51 +5,20 @@ 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() -// import type { LocationObject } from '@/interface/index/Main' -const mapElement = ref(null) -const emit = defineEmits(['update:location', 'locationStatus', 'mockDetected']) +const emit = defineEmits(['update:location']) const $q = useQuasar() -const { validateLocation } = useLocationValidation() -const MOCK_CHECK_DELAY_MS = 800 - -function wait(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)) -} - -function getCurrentPositionAsync(options?: PositionOptions) { - return new Promise((resolve, reject) => { - navigator.geolocation.getCurrentPosition(resolve, reject, options) - }) -} - -async function getPositionForValidation(fallback: GeolocationPosition) { - await wait(MOCK_CHECK_DELAY_MS) - try { - return await getCurrentPositionAsync({ - enableHighAccuracy: true, - timeout: 10000, - maximumAge: 0, - }) - } catch (error) { - // Keep the original reading if the second sampling fails. - return fallback - } -} - function updateLocation(latitude: number, longitude: number, namePOI: string) { // ส่ง event ไปยัง parent component เพื่ออัพเดทค่า props emit('update:location', latitude, longitude, namePOI) } const poiPlaceName = ref('') // ชื่อพื้นที่ใกล้เคียง -const isMapReady = ref(false) // Replace ArcGIS api key const apiKey = ref( @@ -58,7 +27,7 @@ const apiKey = ref( ) const zoomMap = ref(18) -async function initializeMap(position: GeolocationPosition) { +async function initializeMap() { try { // Load modules of ArcGIS loadModules([ @@ -69,149 +38,159 @@ async function initializeMap(position: GeolocationPosition) { 'esri/Graphic', 'esri/layers/TileLayer', ]).then(async ([esriConfig, Map, MapView, Point, Graphic, TileLayer]) => { + // Set apiKey + // esriConfig.apiKey = + // 'AAPK4f700a4324d04e9f8a1a134e0771ac45FXWawdCl-OotFfr52gz9XKxTDJTpDzw_YYcwbmKDDyAJswf14FoPyw0qBkN64DvP' + + // Create a FeatureLayer using a custom server URL + // const hillshadeLayer = new TileLayer({ + // url: `https://bmagis.bangkok.go.th/arcgis/rest/services/cache/BMA_3D_2D_Cache/MapServer`, + // }) + const map = new Map({ basemap: 'streets', + // basemap: 'arcgis-topographic', + // layers: [hillshadeLayer], }) - const { latitude, longitude } = position.coords + navigator.geolocation.getCurrentPosition(async (position) => { + const { latitude, longitude } = position.coords - const mapView = new MapView({ - container: 'mapViewDisplay', - map: map, - center: { - latitude: latitude, - longitude: longitude, - }, // Set the initial map center current position + const mapView = new MapView({ + container: 'mapViewDisplay', + map: map, + center: { + latitude: latitude, + longitude: longitude, + }, // Set the initial map center current position - zoom: zoomMap.value, - constraints: { - snapToZoom: false, // Disables snapping to the zoom level - minZoom: zoomMap.value, // Set minimum zoom level - maxZoom: zoomMap.value, // Set maximum zoom level (same as minZoom for fixed zoom) - }, + zoom: zoomMap.value, + constraints: { + snapToZoom: false, // Disables snapping to the zoom level + minZoom: zoomMap.value, // Set minimum zoom level + maxZoom: zoomMap.value, // Set maximum zoom level (same as minZoom for fixed zoom) + }, - ui: { - components: [], // Empty array to remove all default UI components - }, - }) - isMapReady.value = true - - // ตำแหน่งของผู้ใช้ - const userPoint = new Point({ longitude, latitude }) - const userSymbol = { - type: 'picture-marker', - url: 'http://maps.google.com/mapfiles/ms/icons/red.png', - width: '32px', - height: '32px', - } - const userGraphic = new Graphic({ - geometry: userPoint, - symbol: userSymbol, - }) - mapView.graphics.add(userGraphic) - // Get POI place ยิงไปขอที่ server ของกทม.ก่อน - await axios - .get( - 'https://bmagis.bangkok.go.th/portal/sharing/servers/e4732c3a9fe549ab8bc697573b468f68/rest/services/World/GeocodeServer/reverseGeocode/', - { - params: { - f: 'json', // Format JSON response - distance: 2000, - category: 'POI', - location: { - spatialReference: { wkid: 4326 }, - x: longitude, - y: latitude, - }, - token: apiKey.value, - }, - } - ) - .then((response) => { - // console.log('poi', response.data.location) - poiPlaceName.value = response.data.address - ? response.data.address.PlaceName === '' - ? response.data.address.ShortLabel - : response.data.address.PlaceName - : 'ไม่พบข้อมูล' - const poiPoint = new Point({ - longitude: response.data.location.x, - latitude: response.data.location.y, - }) - const poiSymbol = { - type: 'picture-marker', - url: 'http://maps.google.com/mapfiles/ms/icons/blue.png', - width: '32px', - height: '32px', - } - const poiGraphic = new Graphic({ - geometry: poiPoint, - symbol: poiSymbol, - }) - mapView.graphics.add(poiGraphic) - // อัปเดตการแสดงผลให้แสดงทั้งตำแหน่งของผู้ใช้และ POI - mapView.goTo({ - target: [userPoint, poiPoint], - zoom: zoomMap.value, - }) - - updateLocation(latitude, longitude, poiPlaceName.value) + ui: { + components: [], // Empty array to remove all default UI components + }, }) - .catch(async (error) => { - // console.error('Error fetching points of interest:', error) - // Get POI place ยิงไปขอที่ server arcgis ไม่ต้องใช้ token - await axios - .get( - 'https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/reverseGeocode/', - { - params: { - f: 'json', // Format JSON response - distance: 2000, - category: 'POI', - location: { - spatialReference: { wkid: 4326 }, - x: longitude, - y: latitude, - }, + + // ตำแหน่งของผู้ใช้ + const userPoint = new Point({ longitude, latitude }) + const userSymbol = { + type: 'picture-marker', + url: 'http://maps.google.com/mapfiles/ms/icons/red.png', + width: '32px', + height: '32px', + } + const userGraphic = new Graphic({ + geometry: userPoint, + symbol: userSymbol, + }) + mapView.graphics.add(userGraphic) + // Get POI place ยิงไปขอที่ server ของกทม.ก่อน + await axios + .get( + 'https://bmagis.bangkok.go.th/portal/sharing/servers/e4732c3a9fe549ab8bc697573b468f68/rest/services/World/GeocodeServer/reverseGeocode/', + { + params: { + f: 'json', // Format JSON response + distance: 2000, + category: 'POI', + location: { + spatialReference: { wkid: 4326 }, + x: longitude, + y: latitude, }, - } - ) - .then((response) => { - // console.log('poi', response.data.location) - poiPlaceName.value = response.data.address - ? response.data.address.PlaceName === '' - ? response.data.address.ShortLabel - : response.data.address.PlaceName - : 'ไม่พบข้อมูล' - const poiPoint = new Point({ - longitude: response.data.location.x, - latitude: response.data.location.y, - }) - const poiSymbol = { - type: 'picture-marker', - url: 'http://maps.google.com/mapfiles/ms/icons/blue.png', - width: '32px', - height: '32px', - } - const poiGraphic = new Graphic({ - geometry: poiPoint, - symbol: poiSymbol, - }) - mapView.graphics.add(poiGraphic) - // อัปเดตการแสดงผลให้แสดงทั้งตำแหน่งของผู้ใช้และ POI - mapView.goTo({ - target: [userPoint, poiPoint], - zoom: zoomMap.value, - }) + token: apiKey.value, + }, + } + ) + .then((response) => { + // console.log('poi', response.data.location) + poiPlaceName.value = response.data.address + ? response.data.address.PlaceName === '' + ? response.data.address.ShortLabel + : response.data.address.PlaceName + : 'ไม่พบข้อมูล' + const poiPoint = new Point({ + longitude: response.data.location.x, + latitude: response.data.location.y, + }) + const poiSymbol = { + type: 'picture-marker', + url: 'http://maps.google.com/mapfiles/ms/icons/blue.png', + width: '32px', + height: '32px', + } + const poiGraphic = new Graphic({ + geometry: poiPoint, + symbol: poiSymbol, + }) + mapView.graphics.add(poiGraphic) + // อัปเดตการแสดงผลให้แสดงทั้งตำแหน่งของผู้ใช้และ POI + mapView.goTo({ + target: [userPoint, poiPoint], + zoom: zoomMap.value, + }) - updateLocation(latitude, longitude, poiPlaceName.value) - }) - .catch((error) => { - // Keep map visible even when POI lookup fails. - poiPlaceName.value = 'ไม่พบข้อมูล' - updateLocation(latitude, longitude, poiPlaceName.value) - }) - }) + updateLocation(latitude, longitude, poiPlaceName.value) + }) + .catch(async (error) => { + // console.error('Error fetching points of interest:', error) + // Get POI place ยิงไปขอที่ server arcgis ไม่ต้องใช้ token + await axios + .get( + 'https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/reverseGeocode/', + { + params: { + f: 'json', // Format JSON response + distance: 2000, + category: 'POI', + location: { + spatialReference: { wkid: 4326 }, + x: longitude, + y: latitude, + }, + }, + } + ) + .then((response) => { + // console.log('poi', response.data.location) + poiPlaceName.value = response.data.address + ? response.data.address.PlaceName === '' + ? response.data.address.ShortLabel + : response.data.address.PlaceName + : 'ไม่พบข้อมูล' + const poiPoint = new Point({ + longitude: response.data.location.x, + latitude: response.data.location.y, + }) + const poiSymbol = { + type: 'picture-marker', + url: 'http://maps.google.com/mapfiles/ms/icons/blue.png', + width: '32px', + height: '32px', + } + const poiGraphic = new Graphic({ + geometry: poiPoint, + symbol: poiSymbol, + }) + mapView.graphics.add(poiGraphic) + // อัปเดตการแสดงผลให้แสดงทั้งตำแหน่งของผู้ใช้และ POI + mapView.goTo({ + target: [userPoint, poiPoint], + zoom: zoomMap.value, + }) + + updateLocation(latitude, longitude, poiPlaceName.value) + }) + .catch((error) => { + // console.error('Error fetching points of interest:', error) + }) + }) + }) }) } catch (error) { console.error('Error loading the map', error) @@ -242,35 +221,10 @@ const requestLocationPermission = () => { navigator.geolocation.getCurrentPosition( async (position) => { - const sampledPosition = await getPositionForValidation(position) - - // Validate location first - const validationResult = validateLocation(sampledPosition) - - // Do not block map preview on initial mock detection. - // The hard-stop warning is enforced at submit time in HomeView. - if (validationResult.isMockDetected) { - locationGranted.value = true - emit('locationStatus', true) - } - - // 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 for map preview state. + // Permission granted locationGranted.value = true - emit('locationStatus', true) - const { latitude, longitude } = sampledPosition.coords + const { latitude, longitude } = position.coords // console.log('Current position:', latitude, longitude) if (!latitude || !longitude) { @@ -280,13 +234,12 @@ const requestLocationPermission = () => { // Center map on user's location if map is initialized if (privacyStore.isAccepted) { - await initializeMap(sampledPosition) + await initializeMap() } }, (error) => { // Permission denied locationGranted.value = false - emit('locationStatus', false) switch (error.code) { case error.PERMISSION_DENIED: @@ -311,19 +264,18 @@ const requestLocationPermission = () => { break } }, - { enableHighAccuracy: true, timeout: 10000, maximumAge: 0 } + { enableHighAccuracy: true } ) } defineExpose({ requestLocationPermission, - locationGranted, }) diff --git a/src/components/DialogDebug.vue b/src/components/DialogDebug.vue index ab55a0e..fb151e3 100644 --- a/src/components/DialogDebug.vue +++ b/src/components/DialogDebug.vue @@ -176,7 +176,7 @@ function onClose() {