Compare commits
13 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 202318c169 | |||
|
|
e1962d79bb | ||
|
|
d4ae2f56a0 | ||
|
|
a45c1cae90 | ||
|
|
8240a0b3c5 | ||
| 7edfaa4e4f | |||
| 02f1fd417d | |||
| f53327e2d9 | |||
| 33da60ec02 | |||
| 3ae6e6eeac | |||
| 859d74056a | |||
| 7fdece0a28 | |||
| f0d4eba9d3 |
10 changed files with 2129 additions and 340 deletions
|
|
@ -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:
|
||||
|
|
|
|||
860
docs/test-cases-location.md
Normal file
860
docs/test-cases-location.md
Normal file
|
|
@ -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 แล้ว<br>2. เปิดแอปครั้งแรก หรือยังไม่เคยอนุญาต Location Permission |
|
||||
| **Test Data** | - |
|
||||
| **Test Steps** | 1. เปิดแอป HRMS<br>2. กดปุ่มขอตำแหน่ง (ถ้าจำเป็น)<br>3. เลือก "Allow" เมื่อระบบขอ Location Permission |
|
||||
| **Expected Result** | 1. แสดงแผนที่พร้อมตำแหน่งปัจจุบัน<br>2. แสดงชื่อสถานที่ใกล้เคียง<br>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 แล้ว<br>2. เปิดแอปครั้งแรก หรือยังไม่เคยอนุญาต Location Permission |
|
||||
| **Test Data** | - |
|
||||
| **Test Steps** | 1. เปิดแอป HRMS<br>2. กดปุ่มขอตำแหน่ง (ถ้าจำเป็น)<br>3. เลือก "Deny" เมื่อระบบขอ Location Permission |
|
||||
| **Expected Result** | 1. แสดงข้อความแจ้งเตือน: "ไม่สามารถระบุตำแหน่งปัจจุบันได้ เนื่องจากคุณปฏิเสธการเข้าถึงตำแหน่ง กรุณาเปิดการเข้าถึงตำแหน่ง"<br>2. แผนที่ไม่แสดง<BR>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 แล้ว<BR>2. ยอมรับ Privacy Policy แล้ว |
|
||||
| **Test Data** | Location: อาคารรัฐสภา แขวง ถนนนครไชยศรี เขตดุสิต กรุงเทพมหานคร (13.7563, 100.5018) |
|
||||
| **Test Steps** | 1. เปิดแอป HRMS<BR>2. อยู่ในพื้นที่เปิดกว้าง (Outdoor)<BR>3. รอรับพิกัด GPS |
|
||||
| **Expected Result** | 1. รับพิกัด GPS สำเร็จ<BR>2. แสดงแผนที่พร้อมตำแหน่ง<BR>3. แสดงชื่อสถานที่ใกล้เคียง<BR>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 แล้ว<BR>2. ยอมรับ Privacy Policy แล้ว |
|
||||
| **Test Data** | Location: ภายในอาคารสำนักงาน |
|
||||
| **Test Steps** | 1. เปิดแอป HRMS<BR>2. อยู่ในพื้นที่ปิด (Indoor)<BR>3. รอรับพิกัด GPS |
|
||||
| **Expected Result** | 1. รับพิกัด GPS สำเร็จ<BR>2. แสดงแผนที่พร้อมตำแหน่ง<BR>3. แสดงชื่อสถานที่ใกล้เคียง<BR>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 แล้ว<BR>2. ยอมรับ Privacy Policy แล้ว |
|
||||
| **Test Data** | - |
|
||||
| **Test Steps** | 1. เปิดแอป HRMS<BR>2. ปิด GPS ในอุปกรณ์<BR>3. พยายามขอรับพิกัด |
|
||||
| **Expected Result** | 1. แสดงข้อความแจ้งเตือน: "ไม่สามารถระบุตำแหน่งปัจจุบันได้"<BR>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 แล้ว<BR>2. ยอมรับ Privacy Policy แล้ว |
|
||||
| **Test Data** | - |
|
||||
| **Test Steps** | 1. เปิดแอป HRMS<BR>2. จำลองสภาวะที่ GPS ตอบสนองช้า (ใช้ Developer Tools)<BR>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 แล้ว<BR>2. ยอมรับ Privacy Policy แล้ว |
|
||||
| **Test Data** | Latitude: 13.7563, Longitude: 100.5018, Accuracy: 10m, Timestamp: current |
|
||||
| **Test Steps** | 1. เปิดแอป HRMS<BR>2. รับพิกัด GPS ที่ถูกต้อง |
|
||||
| **Expected Result** | 1. validation = { isValid: true, isMockDetected: false }<BR>2. locationGranted = true<BR>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 แล้ว<BR>2. ยอมรับ Privacy Policy แล้ว |
|
||||
| **Test Data** | Latitude: 0, Longitude: 0 |
|
||||
| **Test Steps** | 1. เปิดแอป HRMS<BR>2. จำลองพิกัด (0,0) ด้วย Mock Location App |
|
||||
| **Expected Result** | 1. validation = { isValid: false, isMockDetected: true, confidence: 'high' }<BR>2. แสดงข้อความ: "พิกัดตำแหน่งไม่ถูกต้อง กรุณาลองใหม่"<BR>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 แล้ว<BR>2. ยอมรับ Privacy Policy แล้ว |
|
||||
| **Test Data** | Latitude: 91, Longitude: 181 (out of range) |
|
||||
| **Test Steps** | 1. เปิดแอป HRMS<BR>2. จำลองพิกัดนอกช่วงด้วย Developer Tools |
|
||||
| **Expected Result** | 1. validation = { isValid: false }<BR>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 แล้ว<BR>2. ยอมรับ Privacy Policy แล้ว |
|
||||
| **Test Data** | Timestamp: Date.now() - 70,000 ms (70 seconds old) |
|
||||
| **Test Steps** | 1. เปิดแอป HRMS<BR>2. จำลอง timestamp เก่าเกิน 60 วินาที |
|
||||
| **Expected Result** | 1. validation.errors = ["ข้อมูลตำแหน่งเก่าเกินไป กรุณารับสัญญาณ GPS ใหม่"]<BR>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 แล้ว<BR>2. ยอมรับ Privacy Policy แล้ว |
|
||||
| **Test Data** | Accuracy: 150 meters |
|
||||
| **Test Steps** | 1. เปิดแอป HRMS<BR>2. จำลองความแม่นยำต่ำด้วย Developer Tools |
|
||||
| **Expected Result** | 1. validation.warnings = ["ความแม่นยำตำแหน่งต่ำเกินไป กรุณาตรวจสอบการรับสัญญาณ GPS"]<BR>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 แล้ว<BR>2. ยอมรับ Privacy Policy แล้ว<BR>3. มีตำแหน่งเดิมใน history |
|
||||
| **Test Data** | Position 1: (13.7563, 100.5018) at t=0<br>Position 2: (15.8700, 100.9925) at t=60 (~180 km in 60s = 3000 km/h) |
|
||||
| **Test Steps** | 1. เปิดแอป HRMS<BR>2. จำลองการเคลื่อนที่ด้วยความเร็วผิดปกติ |
|
||||
| **Expected Result** | 1. validation.errors = ["ตรวจพบการเคลื่อนที่ด้วยความเร็วผิดปกติ อาจเป็นการจำลองตำแหน่ง"]<BR>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 แล้ว<BR>2. ยอมรับ Privacy Policy แล้ว<BR>3. ไม่ได้เปิด Mock Location App |
|
||||
| **Test Data** | Normal GPS data |
|
||||
| **Test Steps** | 1. เปิดแอป HRMS<BR>2. ใช้งาน GPS ปกติ |
|
||||
| **Expected Result** | 1. validation = { isValid: true, isMockDetected: false }<BR>2. locationGranted = true<BR>3. isMockLocationDetected = false<BR>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 แล้ว<BR>2. ยอมรับ Privacy Policy แล้ว |
|
||||
| **Test Data** | Mock location from Fake GPS App |
|
||||
| **Test Steps** | 1. เปิด Fake GPS Location App<BR>2. ตั้งค่าตำแหน่งปลอม<BR>3. เปิดแอป HRMS<BR>4. ขอรับพิกัด |
|
||||
| **Expected Result** | 1. ตรวจพบ mock location (indicators >= 3)<BR>2. แสดงข้อความ: "ตรวจพบว่าตำแหน่ง GPS อาจไม่ถูกต้อง กรุณาปิดแอปจำลองตำแหน่งและลองใหม่"<BR>3. isMockLocationDetected = true<BR>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 แล้ว<BR>2. ยอมรับ Privacy Policy แล้ว |
|
||||
| **Test Data** | Latitude: 0, Longitude: 0 |
|
||||
| **Test Steps** | 1. จำลองพิกัด (0,0) ด้วย Mock Location App<BR>2. เปิดแอป HRMS<BR>3. ขอรับพิกัด |
|
||||
| **Expected Result** | 1. mockIndicators = 3 (จาก invalid coordinates)<BR>2. isMockDetected = true<BR>3. confidence = 'medium'<BR>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 แล้ว<BR>2. ยอมรับ Privacy Policy แล้ว |
|
||||
| **Test Data** | Timestamp: Date.now() - 70,000 ms |
|
||||
| **Test Steps** | 1. จำลอง timestamp เก่าเกิน 60 วินาที<BR>2. เปิดแอป HRMS<BR>3. ขอรับพิกัด |
|
||||
| **Expected Result** | 1. mockIndicators = 2 (จาก stale timestamp)<BR>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 แล้ว<BR>2. ยอมรับ Privacy Policy แล้ว<BR>3. มีตำแหน่งเดิมใน history |
|
||||
| **Test Data** | Speed > 100 m/s |
|
||||
| **Test Steps** | 1. จำลองการเคลื่อนที่ด้วยความเร็ว > 100 m/s<BR>2. เปิดแอป HRMS |
|
||||
| **Expected Result** | 1. mockIndicators = 3 (จาก impossible speed)<BR>2. isMockDetected = true<BR>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 แล้ว<BR>2. ยอมรับ Privacy Policy แล้ว |
|
||||
| **Test Data** | Combined: invalid coordinates (0,0) + stale timestamp + impossible speed |
|
||||
| **Test Steps** | 1. จำลอง multiple mock indicators<BR>2. เปิดแอป HRMS |
|
||||
| **Expected Result** | 1. mockIndicators >= 5<BR>2. isMockDetected = true<BR>3. confidence = 'high'<BR>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 แล้ว<BR>2. ยอมรับ Privacy Policy แล้ว<BR>3. รับพิกัดสำเร็จแล้ว |
|
||||
| **Test Data** | Latitude: 13.7563, Longitude: 100.5018 (Sanam Suea Pa, Bangkok) |
|
||||
| **Test Steps** | 1. เปิดแอป HRMS<BR>2. รับพิกัด GPS<BR>3. รอให้ระบบแปลงพิกัดเป็นชื่อสถานที่ |
|
||||
| **Expected Result** | 1. เรียก Bangkok GIS API: `https://bmagis.bangkok.go.th/...`<BR>2. แสดงชื่อสถานที่ (POI name)<BR>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 แล้ว<BR>2. ยอมรับ Privacy Policy แล้ว<BR>3. Bangkok GIS API ล้มเหลว |
|
||||
| **Test Data** | Latitude: 13.7563, Longitude: 100.5018 |
|
||||
| **Test Steps** | 1. เปิดแอป HRMS<BR>2. รับพิกัด GPS<BR>3. จำลอง Bangkok GIS API fail<BR>4. รอ fallback ไป ArcGIS |
|
||||
| **Expected Result** | 1. เรียก ArcGIS API: `https://geocode.arcgis.com/...`<BR>2. แสดงชื่อสถานที่ (POI name)<BR>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 แล้ว<BR>2. ยอมรับ Privacy Policy แล้ว |
|
||||
| **Test Data** | - |
|
||||
| **Test Steps** | 1. เปิดแอป HRMS<BR>2. จำลองทั้งสอง API fail |
|
||||
| **Expected Result** | 1. แอปยังทำงานต่อไปได้<BR>2. แผนที่แสดงตำแหน่งแต่ไม่มีชื่อสถานที่<BR>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 แล้ว<BR>2. ยอมรับ Privacy Policy แล้ว<BR>3. รับพิกัดและชื่อสถานที่สำเร็จ<BR>4. ถ่ายรูปภาพแล้ว<BR>5. statusCheckin = true (เข้างาน) |
|
||||
| **Test Data** | workplace: 'in-place', locationName: '', POI: 'สนามเสือป่า', img: (รูปถ่าย) |
|
||||
| **Test Steps** | 1. เลือก "ในสถานที่"<BR>2. กด "ลงเวลาเข้างาน"<BR>3. ยืนยันการลงเวลา |
|
||||
| **Expected Result** | 1. API call: POST `/leave/check-in`<BR>2. formdata: { lat, lon, POI, isLocation: true, locationName: '', img, remark, checkInId }<BR>3. แสดง modal ลงเวลาเข้างานสำเร็จ<BR>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 แล้ว<BR>2. ยอมรับ Privacy Policy แล้ว<BR>3. รับพิกัดและชื่อสถานที่สำเร็จ<BR>4. ถ่ายรูปภาพแล้ว<BR>5. statusCheckin = true (เข้างาน) |
|
||||
| **Test Data** | workplace: 'off-site', locationName: 'ปฏิบัติงานที่บ้าน (WFH)', POI: 'บ้าน', img: (รูปถ่าย) |
|
||||
| **Test Steps** | 1. เลือก "นอกสถานที่"<BR>2. เลือก "ปฏิบัติงานที่บ้าน (WFH)"<BR>3. กด "ลงเวลาเข้างาน"<BR>4. ยืนยันการลงเวลา |
|
||||
| **Expected Result** | 1. API call: POST `/leave/check-in`<BR>2. formdata: { lat, lon, POI, isLocation: false, locationName: 'ปฏิบัติงานที่บ้าน (WFH)', img, remark, checkInId }<BR>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 แล้ว<BR>2. ยอมรับ Privacy Policy แล้ว<BR>3. เปิด Mock Location App |
|
||||
| **Test Data** | Mock location enabled |
|
||||
| **Test Steps** | 1. เปิด Mock Location App<BR>2. ตั้งค่าตำแหน่งปลอง<BR>3. เปิดแอป HRMS<BR>4. พยายามลงเวลาเข้างาน |
|
||||
| **Expected Result** | 1. แสดงข้อความ: "ตรวจพบว่าตำแหน่ง GPS อาจไม่ถูกต้อง..."<BR>2. isMockLocationDetected = true<BR>3. disabledBtn = true<BR>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 แล้ว<BR>2. ปฏิเสธ Location Permission |
|
||||
| **Test Data** | - |
|
||||
| **Test Steps** | 1. เปิดแอป HRMS<BR>2. ปฏิเสธ Location Permission<BR>3. พยายามลงเวลาเข้างาน |
|
||||
| **Expected Result** | 1. locationGranted = false<BR>2. ปุ่มลงเวลาเข้า/ออก disabled<BR>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 แล้ว<BR>2. ยอมรับ Privacy Policy แล้ว<BR>3. เคยลงเวลาเข้างานแล้ว<BR>4. statusCheckin = false (ออกงาน) |
|
||||
| **Test Data** | POI: 'Office', img: (รูปถ่าย) |
|
||||
| **Test Steps** | 1. เปิดแอป HRMS<BR>2. ถ่ายรูปภาพ<BR>3. กด "ลงเวลาออกงาน" |
|
||||
| **Expected Result** | 1. API call: POST `/leave/check-in` (with checkInId)<BR>2. แสดง modal ลงเวลาออกงานสำเร็จ<BR>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 แล้ว<BR>2. ยอมรับ Privacy Policy แล้ว<BR>3. เคยลงเวลาเข้างานแล้ว<BR>4. เปิด Mock Location App |
|
||||
| **Test Data** | Mock location enabled |
|
||||
| **Test Steps** | 1. เปิด Mock Location App<BR>2. พยายามลงเวลาออกงาน |
|
||||
| **Expected Result** | 1. แสดงข้อความ: "ตรวจพบว่าตำแหน่ง GPS อาจไม่ถูกต้อง..."<BR>2. disabledBtn = true<BR>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 แล้ว<BR>2. ยอมรับ Privacy Policy แล้ว<BR>3. รับพิกัดสำเร็จ |
|
||||
| **Test Data** | POI: 'Meeting Room', img: (รูปถ่าย) |
|
||||
| **Test Steps** | 1. เปิดแอป HRMS<BR>2. ไปที่หน้าบันทึกเวลาพิเศษ<BR>3. กรอกข้อมูล<BR>4. บันทึก |
|
||||
| **Expected Result** | 1. บันทึกเวลาพิเศษสำเร็จ<BR>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 แล้ว<BR>2. ปฏิเสธ Location Permission |
|
||||
| **Test Data** | - |
|
||||
| **Test Steps** | 1. เปิดแอป HRMS<BR>2. ปฏิเสธ Location Permission<BR>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. เปิดแอปครั้งแรก<BR>2. ยังไม่ยอมรับ Privacy Policy |
|
||||
| **Test Data** | - |
|
||||
| **Test Steps** | 1. เปิดแอป HRMS<BR>2. กดปุ่มที่ต้องการ Location (แผนที่/กล้อง) |
|
||||
| **Expected Result** | 1. privacyStore.modalPrivacy = true<BR>2. แสดง Privacy Modal<BR>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. เปิดแอปครั้งแรก<BR>2. Privacy Modal แสดงขึ้นมา |
|
||||
| **Test Data** | - |
|
||||
| **Test Steps** | 1. เปิดแอป HRMS<BR>2. แสดง Privacy Modal<BR>3. กด "ยอมรับ" |
|
||||
| **Expected Result** | 1. privacyStore.isAccepted = true<BR>2. privacyStore.modalPrivacy = false<BR>3. สามารถใช้งาน Location ได้<BR>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. เปิดแอปครั้งแรก<BR>2. Privacy Modal แสดงขึ้นมา |
|
||||
| **Test Data** | - |
|
||||
| **Test Steps** | 1. เปิดแอป HRMS<BR>2. แสดง Privacy Modal<BR>3. กด "ปฏิเสธ" หรือปิด Modal |
|
||||
| **Expected Result** | 1. privacyStore.isAccepted = false<BR>2. privacyStore.modalPrivacy = false<BR>3. ไม่สามารถใช้งาน Location ได้<BR>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 | | | |
|
||||
1026
docs/test-steps-location.md
Normal file
1026
docs/test-steps-location.md
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -5,19 +5,14 @@ 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<HTMLElement | null>(null)
|
||||
const emit = defineEmits(['update:location', 'locationStatus', 'mockDetected'])
|
||||
const emit = defineEmits(['update:location'])
|
||||
const $q = useQuasar()
|
||||
|
||||
const { validateLocation, showMockWarning } = useLocationValidation()
|
||||
|
||||
function updateLocation(latitude: number, longitude: number, namePOI: string) {
|
||||
// ส่ง event ไปยัง parent component เพื่ออัพเดทค่า props
|
||||
emit('update:location', latitude, longitude, namePOI)
|
||||
|
|
@ -226,30 +221,8 @@ const requestLocationPermission = () => {
|
|||
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
async (position) => {
|
||||
// Validate location first
|
||||
const validationResult = validateLocation(position)
|
||||
|
||||
// Always emit mockDetected event (regardless of result)
|
||||
if (validationResult.isMockDetected) {
|
||||
showMockWarning(validationResult)
|
||||
emit('mockDetected', validationResult)
|
||||
}
|
||||
|
||||
// Check for critical errors (invalid coordinates) that prevent showing location
|
||||
const hasCriticalErrors = validationResult.errors.some(error =>
|
||||
error.includes('พิกัดตำแหน่งไม่ถูกต้อง')
|
||||
)
|
||||
|
||||
if (hasCriticalErrors) {
|
||||
locationGranted.value = false
|
||||
emit('locationStatus', false)
|
||||
messageError($q, '', validationResult.errors[0])
|
||||
return
|
||||
}
|
||||
|
||||
// Permission granted based on mock detection
|
||||
locationGranted.value = !validationResult.isMockDetected
|
||||
emit('locationStatus', !validationResult.isMockDetected)
|
||||
// Permission granted
|
||||
locationGranted.value = true
|
||||
|
||||
const { latitude, longitude } = position.coords
|
||||
// console.log('Current position:', latitude, longitude)
|
||||
|
|
@ -267,7 +240,6 @@ const requestLocationPermission = () => {
|
|||
(error) => {
|
||||
// Permission denied
|
||||
locationGranted.value = false
|
||||
emit('locationStatus', false)
|
||||
|
||||
switch (error.code) {
|
||||
case error.PERMISSION_DENIED:
|
||||
|
|
@ -298,7 +270,6 @@ const requestLocationPermission = () => {
|
|||
|
||||
defineExpose({
|
||||
requestLocationPermission,
|
||||
locationGranted,
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ function onClose() {
|
|||
|
||||
<template>
|
||||
<q-dialog v-model="modal" persistent>
|
||||
<q-card style="width: 700px; max-width: 80vw">
|
||||
<q-card style="width: 700px; max-width: 90vw">
|
||||
<q-form greedy @submit.prevent @validation-success="onSubmit">
|
||||
<DialogHeader :tittle="title" :close="onClose" />
|
||||
<q-separator />
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ const checkIfScrollable = () => {
|
|||
transition-show="slide-up"
|
||||
transition-hide="slide-down"
|
||||
:maximized="$q.screen.lt.sm"
|
||||
@show="checkIfScrollable"
|
||||
>
|
||||
<q-card class="privacy-card" style="max-width: 560px; max-height: 95vh">
|
||||
<!-- Header -->
|
||||
|
|
|
|||
|
|
@ -1,169 +0,0 @@
|
|||
import { ref } from 'vue'
|
||||
import { useQuasar } from 'quasar'
|
||||
import { useCounterMixin } from '@/stores/mixin'
|
||||
|
||||
export interface LocationValidationResult {
|
||||
isValid: boolean
|
||||
isMockDetected: boolean
|
||||
confidence: 'low' | 'medium' | 'high'
|
||||
warnings: string[]
|
||||
errors: string[]
|
||||
}
|
||||
|
||||
export interface PositionSnapshot {
|
||||
latitude: number
|
||||
longitude: number
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
// Configuration constants - exported for documentation and testing purposes
|
||||
export const VALIDATION_CONFIG = {
|
||||
MAX_TIMESTAMP_AGE_MS: 60_000, // 60 seconds - maximum acceptable age of location data
|
||||
MAX_ACCURACY_METERS: 100, // 100 meters - maximum acceptable GPS accuracy
|
||||
MAX_SPEED_MS: 100, // 100 m/s (~360 km/h) - maximum plausible movement speed
|
||||
POSITION_HISTORY_SIZE: 5, // number of positions to keep for pattern detection
|
||||
MOCK_INDICATOR_THRESHOLD: 3, // threshold for mock detection (indicators >= 3 = mock)
|
||||
} as const
|
||||
|
||||
export function useLocationValidation() {
|
||||
const $q = useQuasar()
|
||||
const { messageError } = useCounterMixin()
|
||||
|
||||
// Thai error messages - exported for i18n consistency
|
||||
const errorMessages = {
|
||||
MOCK_DETECTED: 'ตรวจพบว่าตำแหน่ง GPS อาจไม่ถูกต้อง กรุณาปิดแอปจำลองตำแหน่งและลองใหม่',
|
||||
INVALID_COORDINATES: 'พิกัดตำแหน่งไม่ถูกต้อง กรุณาลองใหม่',
|
||||
STALE_TIMESTAMP: 'ข้อมูลตำแหน่งเก่าเกินไป กรุณารับสัญญาณ GPS ใหม่',
|
||||
POOR_ACCURACY: 'ความแม่นยำตำแหน่งต่ำเกินไป กรุณาตรวจสอบการรับสัญญาณ GPS',
|
||||
IMPOSSIBLE_SPEED: 'ตรวจพบการเคลื่อนที่ด้วยความเร็วผิดปกติ อาจเป็นการจำลองตำแหน่ง',
|
||||
}
|
||||
|
||||
const previousPositions = ref<PositionSnapshot[]>([])
|
||||
|
||||
// คำนวณระยะห่างระหว่าง 2 จุด (Haversine formula)
|
||||
const haversineDistance = (lat1: number, lon1: number, lat2: number, lon2: number): number => {
|
||||
const R = 6371e3 // Earth's radius in meters
|
||||
const φ1 = (lat1 * Math.PI) / 180
|
||||
const φ2 = (lat2 * Math.PI) / 180
|
||||
const Δφ = ((lat2 - lat1) * Math.PI) / 180
|
||||
const Δλ = ((lon2 - lon1) * Math.PI) / 180
|
||||
|
||||
const a =
|
||||
Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
|
||||
Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) * Math.sin(Δλ / 2)
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
|
||||
|
||||
return R * c
|
||||
}
|
||||
|
||||
// ตรวจสอบพิกัดถูกต้อง
|
||||
const validateCoordinates = (lat: number, lon: number): boolean => {
|
||||
return (
|
||||
lat >= -90 && lat <= 90 &&
|
||||
lon >= -180 && lon <= 180 &&
|
||||
!isNaN(lat) && !isNaN(lon) &&
|
||||
!(lat === 0 && lon === 0) // Mock มักใช้ 0,0
|
||||
)
|
||||
}
|
||||
|
||||
// ตรวจสอบความแม่นยำ
|
||||
const validateAccuracy = (accuracy: number | null): boolean => {
|
||||
if (accuracy === null) return true
|
||||
return accuracy <= VALIDATION_CONFIG.MAX_ACCURACY_METERS
|
||||
}
|
||||
|
||||
// ตรวจสอบ Timestamp
|
||||
const validateTimestamp = (timestamp: number): boolean => {
|
||||
const now = Date.now()
|
||||
const age = Math.abs(now - timestamp)
|
||||
return age <= VALIDATION_CONFIG.MAX_TIMESTAMP_AGE_MS
|
||||
}
|
||||
|
||||
// คำนวณความเร็ว
|
||||
const calculateSpeed = (pos1: PositionSnapshot, pos2: PositionSnapshot): number => {
|
||||
const distance = haversineDistance(pos1.latitude, pos1.longitude, pos2.latitude, pos2.longitude)
|
||||
const timeDiff = Math.abs(pos2.timestamp - pos1.timestamp) / 1000 // seconds
|
||||
return timeDiff > 0 ? distance / timeDiff : 0
|
||||
}
|
||||
|
||||
// ตรวจสอบความเร็วปกติ
|
||||
const validateSpeed = (current: PositionSnapshot, previous: PositionSnapshot): boolean => {
|
||||
const speed = calculateSpeed(previous, current)
|
||||
return speed <= VALIDATION_CONFIG.MAX_SPEED_MS
|
||||
}
|
||||
|
||||
// Main validation function
|
||||
const validateLocation = (position: GeolocationPosition): LocationValidationResult => {
|
||||
const warnings: string[] = []
|
||||
const errors: string[] = []
|
||||
let mockIndicators = 0
|
||||
|
||||
const { latitude, longitude, accuracy } = position.coords
|
||||
const { timestamp } = position
|
||||
|
||||
// 1. Coordinate validation
|
||||
if (!validateCoordinates(latitude, longitude)) {
|
||||
errors.push(errorMessages.INVALID_COORDINATES)
|
||||
mockIndicators += 3
|
||||
}
|
||||
|
||||
// 2. Timestamp validation
|
||||
if (!validateTimestamp(timestamp)) {
|
||||
errors.push(errorMessages.STALE_TIMESTAMP)
|
||||
mockIndicators += 2
|
||||
}
|
||||
|
||||
// 3. Accuracy validation
|
||||
if (!validateAccuracy(accuracy)) {
|
||||
warnings.push(errorMessages.POOR_ACCURACY)
|
||||
mockIndicators += 1
|
||||
}
|
||||
|
||||
// 4. Compare with previous positions
|
||||
if (previousPositions.value.length > 0) {
|
||||
const previous = previousPositions.value[previousPositions.value.length - 1]
|
||||
|
||||
if (!validateSpeed({ latitude, longitude, timestamp }, previous)) {
|
||||
errors.push(errorMessages.IMPOSSIBLE_SPEED)
|
||||
mockIndicators += 3
|
||||
}
|
||||
}
|
||||
|
||||
// Store current position
|
||||
previousPositions.value.push({ latitude, longitude, timestamp })
|
||||
if (previousPositions.value.length > VALIDATION_CONFIG.POSITION_HISTORY_SIZE) {
|
||||
previousPositions.value.shift()
|
||||
}
|
||||
|
||||
// Determine result
|
||||
const isMockDetected = mockIndicators >= VALIDATION_CONFIG.MOCK_INDICATOR_THRESHOLD
|
||||
const isValid = errors.length === 0
|
||||
|
||||
let confidence: 'low' | 'medium' | 'high' = 'low'
|
||||
if (mockIndicators >= 5) confidence = 'high'
|
||||
else if (mockIndicators >= 3) confidence = 'medium'
|
||||
|
||||
return {
|
||||
isValid,
|
||||
isMockDetected,
|
||||
confidence,
|
||||
warnings,
|
||||
errors,
|
||||
}
|
||||
}
|
||||
|
||||
const showMockWarning = (result: LocationValidationResult) => {
|
||||
if (!result.isMockDetected) return
|
||||
messageError($q, null, errorMessages.MOCK_DETECTED)
|
||||
}
|
||||
|
||||
const resetValidation = () => {
|
||||
previousPositions.value = []
|
||||
}
|
||||
|
||||
return {
|
||||
validateLocation,
|
||||
showMockWarning,
|
||||
resetValidation,
|
||||
}
|
||||
}
|
||||
|
|
@ -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<void>
|
||||
) => {
|
||||
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,
|
||||
'ข้อมูลผิดพลาดทำให้เกิดการไม่ตอบสนองต่อการเรียกใช้งานดูเว็บไซต์'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ const { date2Thai, showLoader, hideLoader, messageError, dialogConfirm } = mixin
|
|||
const $q = useQuasar()
|
||||
const { checkPrivacyAccepted } = usePermissions()
|
||||
const privacyStore = usePrivacyStore()
|
||||
const MOCK_CHECK_DELAY_MS = 800
|
||||
|
||||
const modalTime = ref<boolean>(false) // Dailog ลงเวลาเข้างานของคุณ
|
||||
const checkStatus = ref<string>('')
|
||||
|
|
@ -32,8 +33,6 @@ const endTimeAfternoon = ref<string>('12:00:00') //เวลาเช็คเ
|
|||
|
||||
const isLoadingCheckTime = ref<boolean>(false) // ตัวแปรสำหรับการโหลด
|
||||
const disabledBtn = ref<boolean>(false)
|
||||
const locationGranted = ref<boolean>(false)
|
||||
const isMockLocationDetected = ref<boolean>(false)
|
||||
|
||||
/**
|
||||
* fetch เช็คเวลาต้องลงเวลาเข้าหรือออกงาน
|
||||
|
|
@ -115,19 +114,40 @@ async function updateLocation(
|
|||
formLocation.POI = namePOI
|
||||
}
|
||||
|
||||
/**
|
||||
* รับค่าสถานะ location จาก AscGISMap
|
||||
*/
|
||||
function onLocationStatus(status: boolean) {
|
||||
locationGranted.value = status
|
||||
function resetLocationForRetry() {
|
||||
formLocation.lat = 0
|
||||
formLocation.lng = 0
|
||||
formLocation.POI = ''
|
||||
}
|
||||
|
||||
/**
|
||||
* รับค่า mock location detection จาก AscGISMap
|
||||
*/
|
||||
function onMockDetected(result: any) {
|
||||
isMockLocationDetected.value = true
|
||||
disabledBtn.value = true
|
||||
function getCurrentPositionAsync(options?: PositionOptions) {
|
||||
return new Promise<GeolocationPosition>((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
|
||||
}
|
||||
}
|
||||
|
||||
const location = ref<string>('') // พื้นที่ใกล้เคียง
|
||||
|
|
@ -167,6 +187,9 @@ const cameraIsOn = ref<boolean>(false)
|
|||
const img = ref<any>(undefined)
|
||||
const photoWidth = ref<number>(350)
|
||||
const photoHeight = ref<number>(350)
|
||||
const availableCameras = ref<any[]>([])
|
||||
const currentCameraIndex = ref<number>(0)
|
||||
const currentCameraType = ref<'front' | 'back' | 'unknown'>('unknown')
|
||||
|
||||
const intervalId = ref<number | undefined>(undefined) // ต้องใช้ตัวแปรเก็บค่า interval
|
||||
|
||||
|
|
@ -282,6 +305,16 @@ async function stopChecking() {
|
|||
}
|
||||
}
|
||||
|
||||
function identifyCameraType(label: string): 'front' | 'back' | 'unknown' {
|
||||
const lowerLabel = label.toLowerCase()
|
||||
if (lowerLabel.includes('front') || lowerLabel.includes('user') || lowerLabel.includes('face')) {
|
||||
return 'front'
|
||||
} else if (lowerLabel.includes('back') || lowerLabel.includes('environment') || lowerLabel.includes('rear')) {
|
||||
return 'back'
|
||||
}
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
/** function เปิดกล้อง*/
|
||||
async function openCamera() {
|
||||
// เช็คสิทธิ์ privacy ก่อนเปิดกล้อง
|
||||
|
|
@ -295,7 +328,11 @@ async function openCamera() {
|
|||
await camera.value?.stop()
|
||||
} else {
|
||||
await camera.value?.start()
|
||||
await changeCamera() // ต้องรอให้ start() เสร็จก่อน
|
||||
const devices: any = await camera.value?.devices(['videoinput'])
|
||||
if (devices) {
|
||||
availableCameras.value = devices
|
||||
await changeCamera()
|
||||
}
|
||||
}
|
||||
cameraIsOn.value = !cameraIsOn.value
|
||||
} else {
|
||||
|
|
@ -309,10 +346,59 @@ async function openCamera() {
|
|||
}
|
||||
|
||||
/** change camera device*/
|
||||
async function changeCamera() {
|
||||
const devices: any = await camera.value?.devices(['videoinput'])
|
||||
const device = await devices[0]
|
||||
camera.value?.changeCamera(device.deviceId)
|
||||
async function changeCamera(targetCameraType?: 'front' | 'back') {
|
||||
try {
|
||||
const devices: any = await camera.value?.devices(['videoinput'])
|
||||
|
||||
if (!devices || devices.length === 0) {
|
||||
console.warn('No cameras found')
|
||||
return
|
||||
}
|
||||
|
||||
availableCameras.value = devices
|
||||
|
||||
if (devices.length === 1 || !targetCameraType) {
|
||||
const device = devices[0]
|
||||
await camera.value?.changeCamera(device.deviceId)
|
||||
currentCameraIndex.value = 0
|
||||
currentCameraType.value = identifyCameraType(device.label || '')
|
||||
return
|
||||
}
|
||||
|
||||
const matchingCameras = devices.filter((device: any) =>
|
||||
identifyCameraType(device.label || '') === targetCameraType
|
||||
)
|
||||
|
||||
if (matchingCameras.length > 0) {
|
||||
const targetDevice = matchingCameras[0]
|
||||
await camera.value?.changeCamera(targetDevice.deviceId)
|
||||
currentCameraIndex.value = devices.indexOf(targetDevice)
|
||||
currentCameraType.value = targetCameraType
|
||||
} else {
|
||||
const nextIndex = (currentCameraIndex.value + 1) % devices.length
|
||||
const nextDevice = devices[nextIndex]
|
||||
await camera.value?.changeCamera(nextDevice.deviceId)
|
||||
currentCameraIndex.value = nextIndex
|
||||
currentCameraType.value = identifyCameraType(nextDevice.label || '')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error switching camera:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/** switch camera device*/
|
||||
async function switchCamera() {
|
||||
if (availableCameras.value.length <= 1) {
|
||||
return
|
||||
}
|
||||
|
||||
// สลับแค่ระหว่างกล้อง 2 ตัวแรก (กล้องหน้าและหลังหลัก)
|
||||
const targetIndex = currentCameraIndex.value === 0 ? 1 : 0
|
||||
const targetDevice = availableCameras.value[targetIndex]
|
||||
|
||||
await camera.value?.changeCamera(targetDevice.deviceId)
|
||||
currentCameraIndex.value = targetIndex
|
||||
currentCameraType.value = identifyCameraType(targetDevice.label || '')
|
||||
}
|
||||
|
||||
/** function ถ่ายรูป*/
|
||||
|
|
@ -348,7 +434,9 @@ const objectRef: FormRef = {
|
|||
}
|
||||
|
||||
/** function ตรวจสอบค่าว่างของ input*/
|
||||
function validateForm() {
|
||||
async function validateForm() {
|
||||
disabledBtn.value = true
|
||||
|
||||
const hasError = []
|
||||
for (const key in objectRef) {
|
||||
if (Object.prototype.hasOwnProperty.call(objectRef, key)) {
|
||||
|
|
@ -375,11 +463,15 @@ function validateForm() {
|
|||
model.value === 'อื่นๆ' ? useLocation.value : model.value
|
||||
})`
|
||||
} คุณต้องการยืนยันการลงเวลาเข้างาน?`,
|
||||
() => {},
|
||||
() => {
|
||||
disabledBtn.value = false
|
||||
},
|
||||
'red',
|
||||
'ยืนยัน'
|
||||
)
|
||||
}
|
||||
} else {
|
||||
disabledBtn.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -390,14 +482,16 @@ const timeChickin = ref<string>('') //เวลาเข้างาน,เว
|
|||
async function confirm() {
|
||||
// เช็คสิทธิ์ privacy ก่อนใช้งานแผนที่และกล้อง
|
||||
if (!checkPrivacyAccepted()) {
|
||||
disabledBtn.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (!formLocation.POI || !formLocation.lat || !formLocation.lng) {
|
||||
disabledBtn.value = false
|
||||
mapRef.value?.requestLocationPermission()
|
||||
return
|
||||
}
|
||||
disabledBtn.value = true
|
||||
|
||||
showLoader()
|
||||
const isLocation = workplace.value === 'in-place' //*true คือ ณ สถานที่ตั้ง, false คือ นอกสถานที่ตั้ง
|
||||
const locationName = workplace.value === 'in-place' ? '' : useLocation.value
|
||||
|
|
@ -440,6 +534,7 @@ async function confirm() {
|
|||
|
||||
async function getCheck() {
|
||||
if (!formLocation.POI || !formLocation.lat || !formLocation.lng) {
|
||||
disabledBtn.value = false
|
||||
mapRef.value?.requestLocationPermission()
|
||||
return
|
||||
}
|
||||
|
|
@ -472,7 +567,9 @@ async function getCheck() {
|
|||
() => confirm(),
|
||||
'ยืนยันการลงเวลาออกงาน',
|
||||
`เวลาออกจากงานของคุณคือ ${endTimeAfternoonVal} แต่ขณะนี้เป็นเวลา ${timeVal} น. หากคุณออกจากงานในเวลานี้สถานะการลงเวลาจะเป็น "${res.data.result.statusText}" คุณแน่ใจว่าจะลงเวลาออกงานในตอนนี้ใช่หรือไม่?`,
|
||||
() => {},
|
||||
() => {
|
||||
disabledBtn.value = false
|
||||
},
|
||||
'red',
|
||||
'ยืนยัน'
|
||||
)
|
||||
|
|
@ -481,6 +578,7 @@ async function getCheck() {
|
|||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
disabledBtn.value = false
|
||||
messageError($q, e)
|
||||
})
|
||||
.finally(() => {
|
||||
|
|
@ -612,16 +710,6 @@ watch(
|
|||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => locationGranted.value,
|
||||
(newVal) => {
|
||||
// Removed auto-reset of isMockLocationDetected to prevent
|
||||
// clearing mock detection state when permission is granted.
|
||||
// Mock detection state should only be reset after explicit user action
|
||||
// or after a successful validation without mock indicators.
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -694,8 +782,6 @@ watch(
|
|||
v-if="$q.screen.gt.xs"
|
||||
ref="mapRef"
|
||||
@update:location="updateLocation"
|
||||
@location-status="onLocationStatus"
|
||||
@mock-detected="onMockDetected"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -739,6 +825,16 @@ watch(
|
|||
v-if="$q.screen.gt.xs"
|
||||
class="absolute-bottom-right q-ma-md"
|
||||
>
|
||||
<q-btn
|
||||
v-if="availableCameras.length > 1 && img == null"
|
||||
round
|
||||
push
|
||||
icon="flip_camera_ios"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
class="q-mr-sm"
|
||||
@click="switchCamera"
|
||||
/>
|
||||
<q-btn
|
||||
v-if="img == null"
|
||||
round
|
||||
|
|
@ -763,6 +859,16 @@ watch(
|
|||
class="absolute-bottom text-subtitle2 text-center q-py-sm"
|
||||
style="background: #00000021"
|
||||
>
|
||||
<q-btn
|
||||
v-if="availableCameras.length > 1 && img == null"
|
||||
round
|
||||
icon="flip_camera_ios"
|
||||
size="16px"
|
||||
style="background: #424242; color: white"
|
||||
@click="switchCamera"
|
||||
unelevated
|
||||
class="q-mr-xs"
|
||||
/>
|
||||
<q-btn
|
||||
round
|
||||
v-if="img == null"
|
||||
|
|
@ -862,12 +968,7 @@ watch(
|
|||
</div>
|
||||
|
||||
<div class="col-12" v-if="$q.screen.xs">
|
||||
<MapCheck
|
||||
ref="mapRef"
|
||||
@update:location="updateLocation"
|
||||
@location-status="onLocationStatus"
|
||||
@mock-detected="onMockDetected"
|
||||
/>
|
||||
<MapCheck ref="mapRef" @update:location="updateLocation" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- กรอกข้อมูล หน้ามือถือ -->
|
||||
|
|
@ -1016,7 +1117,7 @@ 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 ? true : camera && img ? false : true"
|
||||
@click="validateForm"
|
||||
:loading="inQueue"
|
||||
/>
|
||||
|
|
@ -1131,7 +1232,7 @@ 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 ? true : camera && img ? false : true"
|
||||
@click="validateForm"
|
||||
:loading="inQueue"
|
||||
/>
|
||||
|
|
@ -1268,4 +1369,4 @@ watch(
|
|||
rgba(2, 169, 152, 1) 100%
|
||||
);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
@ -564,7 +564,10 @@ onMounted(async () => {
|
|||
</q-page-container>
|
||||
|
||||
<!-- Footer -->
|
||||
<q-footer class="bg-grey-1 text-dark q-pa-md">
|
||||
<q-footer
|
||||
class="bg-grey-1 text-dark q-pa-md"
|
||||
:class="$q.screen.xs ? 'hidden' : ''"
|
||||
>
|
||||
<footer-contact />
|
||||
</q-footer>
|
||||
</q-layout>
|
||||
|
|
@ -720,4 +723,8 @@ onMounted(async () => {
|
|||
background-color: #016987;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue