facecog-liveness-showcase 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) hide show
  1. package/.browserslistrc +15 -0
  2. package/.dockerignore +48 -0
  3. package/.editorconfig +16 -0
  4. package/.eslintrc.json +47 -0
  5. package/.vercelignore +7 -0
  6. package/.vscode/extensions.json +5 -0
  7. package/.vscode/settings.json +3 -0
  8. package/DOCKER.md +221 -0
  9. package/Dockerfile +33 -0
  10. package/README.md +268 -0
  11. package/angular.json +156 -0
  12. package/capacitor.config.ts +9 -0
  13. package/docker-compose.dev.yml +20 -0
  14. package/docker-compose.yml +18 -0
  15. package/ionic.config.json +7 -0
  16. package/jest.config.js +38 -0
  17. package/nginx.conf +50 -0
  18. package/package.json +131 -0
  19. package/patches/ng-packagr+20.3.2.patch +60 -0
  20. package/projects/facecog-liveness-verification/README.md +295 -0
  21. package/projects/facecog-liveness-verification/ng-package.json +7 -0
  22. package/projects/facecog-liveness-verification/package.json +48 -0
  23. package/projects/facecog-liveness-verification/scripts/build-with-wrapper-copy.js +38 -0
  24. package/projects/facecog-liveness-verification/scripts/copy-wrapper-after-ngc.js +35 -0
  25. package/projects/facecog-liveness-verification/sources/FaceLivenessReactWrapper.tsx +320 -0
  26. package/projects/facecog-liveness-verification/src/lib/components/aws-face-liveness/FaceLivenessReactWrapper.generated.d.ts +28 -0
  27. package/projects/facecog-liveness-verification/src/lib/components/aws-face-liveness/FaceLivenessReactWrapper.generated.js +247 -0
  28. package/projects/facecog-liveness-verification/src/lib/components/aws-face-liveness/FaceLivenessReactWrapper.generated.js.map +1 -0
  29. package/projects/facecog-liveness-verification/src/lib/components/aws-face-liveness/FaceLivenessReactWrapper.js.map +1 -0
  30. package/projects/facecog-liveness-verification/src/lib/components/aws-face-liveness/FaceLivenessReactWrapper.ts +5 -0
  31. package/projects/facecog-liveness-verification/src/lib/components/aws-face-liveness/aws-face-liveness.component.ts +500 -0
  32. package/projects/facecog-liveness-verification/src/lib/components/camera-permission/camera-permission.component.html +41 -0
  33. package/projects/facecog-liveness-verification/src/lib/components/camera-permission/camera-permission.component.scss +234 -0
  34. package/projects/facecog-liveness-verification/src/lib/components/camera-permission/camera-permission.component.spec.ts +158 -0
  35. package/projects/facecog-liveness-verification/src/lib/components/camera-permission/camera-permission.component.ts +58 -0
  36. package/projects/facecog-liveness-verification/src/lib/components/camera-verification/camera-verification.component.html +34 -0
  37. package/projects/facecog-liveness-verification/src/lib/components/camera-verification/camera-verification.component.ts +210 -0
  38. package/projects/facecog-liveness-verification/src/lib/components/dialogs/save-custom-pose-dialog.component.ts +174 -0
  39. package/projects/facecog-liveness-verification/src/lib/components/facetec-scan/facetec-scan.component.html +45 -0
  40. package/projects/facecog-liveness-verification/src/lib/components/facetec-scan/facetec-scan.component.scss +87 -0
  41. package/projects/facecog-liveness-verification/src/lib/components/facetec-scan/facetec-scan.component.ts +182 -0
  42. package/projects/facecog-liveness-verification/src/lib/components/intro/intro.component.html +394 -0
  43. package/projects/facecog-liveness-verification/src/lib/components/intro/intro.component.scss +1567 -0
  44. package/projects/facecog-liveness-verification/src/lib/components/intro/intro.component.spec.ts +699 -0
  45. package/projects/facecog-liveness-verification/src/lib/components/intro/intro.component.ts +721 -0
  46. package/projects/facecog-liveness-verification/src/lib/components/live-preview/live-preview.component.html +120 -0
  47. package/projects/facecog-liveness-verification/src/lib/components/live-preview/live-preview.component.scss +611 -0
  48. package/projects/facecog-liveness-verification/src/lib/components/live-preview/live-preview.component.spec.ts +605 -0
  49. package/projects/facecog-liveness-verification/src/lib/components/live-preview/live-preview.component.ts +524 -0
  50. package/projects/facecog-liveness-verification/src/lib/components/liveness-flow/liveness-flow.component.html +73 -0
  51. package/projects/facecog-liveness-verification/src/lib/components/liveness-flow/liveness-flow.component.scss +19 -0
  52. package/projects/facecog-liveness-verification/src/lib/components/liveness-flow/liveness-flow.component.spec.ts +673 -0
  53. package/projects/facecog-liveness-verification/src/lib/components/liveness-flow/liveness-flow.component.ts +963 -0
  54. package/projects/facecog-liveness-verification/src/lib/components/liveness-verification/liveness-verification.component.html +38 -0
  55. package/projects/facecog-liveness-verification/src/lib/components/liveness-verification/liveness-verification.component.scss +10 -0
  56. package/projects/facecog-liveness-verification/src/lib/components/liveness-verification/liveness-verification.component.ts +233 -0
  57. package/projects/facecog-liveness-verification/src/lib/components/pose-selection/pose-selection.component.html +17 -0
  58. package/projects/facecog-liveness-verification/src/lib/components/pose-selection/pose-selection.component.spec.ts +35 -0
  59. package/projects/facecog-liveness-verification/src/lib/components/pose-selection/pose-selection.component.ts +33 -0
  60. package/projects/facecog-liveness-verification/src/lib/components/processing/processing.component.html +17 -0
  61. package/projects/facecog-liveness-verification/src/lib/components/processing/processing.component.scss +156 -0
  62. package/projects/facecog-liveness-verification/src/lib/components/processing/processing.component.spec.ts +46 -0
  63. package/projects/facecog-liveness-verification/src/lib/components/processing/processing.component.ts +18 -0
  64. package/projects/facecog-liveness-verification/src/lib/components/verification-result/verification-result.component.html +190 -0
  65. package/projects/facecog-liveness-verification/src/lib/components/verification-result/verification-result.component.scss +534 -0
  66. package/projects/facecog-liveness-verification/src/lib/components/verification-result/verification-result.component.spec.ts +286 -0
  67. package/projects/facecog-liveness-verification/src/lib/components/verification-result/verification-result.component.ts +155 -0
  68. package/projects/facecog-liveness-verification/src/lib/interfaces/analyze-response.interface.ts +16 -0
  69. package/projects/facecog-liveness-verification/src/lib/interfaces/aws-face-liveness.interface.ts +46 -0
  70. package/projects/facecog-liveness-verification/src/lib/interfaces/backend-adapter.interface.ts +21 -0
  71. package/projects/facecog-liveness-verification/src/lib/interfaces/backend-http-client.interface.ts +93 -0
  72. package/projects/facecog-liveness-verification/src/lib/interfaces/backend-response.interface.ts +9 -0
  73. package/projects/facecog-liveness-verification/src/lib/interfaces/camera-provider.interface.ts +107 -0
  74. package/projects/facecog-liveness-verification/src/lib/interfaces/category-info.interface.ts +9 -0
  75. package/projects/facecog-liveness-verification/src/lib/interfaces/custom-pose-data.interface.ts +14 -0
  76. package/projects/facecog-liveness-verification/src/lib/interfaces/custom-pose-repository.interface.ts +48 -0
  77. package/projects/facecog-liveness-verification/src/lib/interfaces/custom-pose-response.interface.ts +14 -0
  78. package/projects/facecog-liveness-verification/src/lib/interfaces/index.ts +52 -0
  79. package/projects/facecog-liveness-verification/src/lib/interfaces/liveness-action-result.interface.ts +13 -0
  80. package/projects/facecog-liveness-verification/src/lib/interfaces/liveness-config.interface.ts +17 -0
  81. package/projects/facecog-liveness-verification/src/lib/interfaces/liveness-metadata.interface.ts +17 -0
  82. package/projects/facecog-liveness-verification/src/lib/interfaces/liveness-result.interface.ts +24 -0
  83. package/projects/facecog-liveness-verification/src/lib/interfaces/liveness-verification-config.interface.ts +41 -0
  84. package/projects/facecog-liveness-verification/src/lib/interfaces/multi-backend-analyze-response.interface.ts +21 -0
  85. package/projects/facecog-liveness-verification/src/lib/interfaces/multi-backend-liveness-result.interface.ts +14 -0
  86. package/projects/facecog-liveness-verification/src/lib/interfaces/pose-definition.interface.ts +35 -0
  87. package/projects/facecog-liveness-verification/src/lib/interfaces/pose-keypoint.interface.ts +12 -0
  88. package/projects/facecog-liveness-verification/src/lib/interfaces/pose-match-result.interface.ts +9 -0
  89. package/projects/facecog-liveness-verification/src/lib/interfaces/pose-verify-response.interface.ts +8 -0
  90. package/projects/facecog-liveness-verification/src/lib/interfaces/scan-results.interface.ts +29 -0
  91. package/projects/facecog-liveness-verification/src/lib/interfaces/verification-plan.interface.ts +42 -0
  92. package/projects/facecog-liveness-verification/src/lib/interfaces/verification-progress-event.interface.ts +12 -0
  93. package/projects/facecog-liveness-verification/src/lib/interfaces/verification-session.interface.ts +72 -0
  94. package/projects/facecog-liveness-verification/src/lib/interfaces/verification-step-change-event.interface.ts +11 -0
  95. package/projects/facecog-liveness-verification/src/lib/interfaces/video-recording.interface.ts +9 -0
  96. package/projects/facecog-liveness-verification/src/lib/liveness-verification.module.ts +123 -0
  97. package/projects/facecog-liveness-verification/src/lib/models/constants/aws-face-liveness-component.token.ts +23 -0
  98. package/projects/facecog-liveness-verification/src/lib/models/constants/category-info.constant.ts +14 -0
  99. package/projects/facecog-liveness-verification/src/lib/models/constants/default-liveness-config.constant.ts +18 -0
  100. package/projects/facecog-liveness-verification/src/lib/models/constants/index.ts +5 -0
  101. package/projects/facecog-liveness-verification/src/lib/models/constants/liveness-verification-config.token.ts +16 -0
  102. package/projects/facecog-liveness-verification/src/lib/models/constants/pose-definitions.constant.ts +377 -0
  103. package/projects/facecog-liveness-verification/src/lib/models/index.ts +5 -0
  104. package/projects/facecog-liveness-verification/src/lib/models/utils/index.ts +2 -0
  105. package/projects/facecog-liveness-verification/src/lib/models/utils/pose.utils.spec.ts +76 -0
  106. package/projects/facecog-liveness-verification/src/lib/models/utils/pose.utils.ts +59 -0
  107. package/projects/facecog-liveness-verification/src/lib/services/aws-face-liveness.service.ts +49 -0
  108. package/projects/facecog-liveness-verification/src/lib/services/backend-http.service.spec.ts +111 -0
  109. package/projects/facecog-liveness-verification/src/lib/services/backend-http.service.ts +130 -0
  110. package/projects/facecog-liveness-verification/src/lib/services/backends/azure-backend.service.spec.ts +69 -0
  111. package/projects/facecog-liveness-verification/src/lib/services/backends/azure-backend.service.ts +72 -0
  112. package/projects/facecog-liveness-verification/src/lib/services/backends/facetec-backend.service.spec.ts +24 -0
  113. package/projects/facecog-liveness-verification/src/lib/services/backends/facetec-backend.service.ts +35 -0
  114. package/projects/facecog-liveness-verification/src/lib/services/backends/mock-backend.service.spec.ts +36 -0
  115. package/projects/facecog-liveness-verification/src/lib/services/backends/mock-backend.service.ts +39 -0
  116. package/projects/facecog-liveness-verification/src/lib/services/backends/openpose-backend.service.spec.ts +81 -0
  117. package/projects/facecog-liveness-verification/src/lib/services/backends/openpose-backend.service.ts +72 -0
  118. package/projects/facecog-liveness-verification/src/lib/services/backends/rekognition-analysis-backend.service.spec.ts +69 -0
  119. package/projects/facecog-liveness-verification/src/lib/services/backends/rekognition-analysis-backend.service.ts +83 -0
  120. package/projects/facecog-liveness-verification/src/lib/services/camera.service.spec.ts +200 -0
  121. package/projects/facecog-liveness-verification/src/lib/services/camera.service.ts +155 -0
  122. package/projects/facecog-liveness-verification/src/lib/services/custom-poses-api.service.ts +117 -0
  123. package/projects/facecog-liveness-verification/src/lib/services/index.ts +18 -0
  124. package/projects/facecog-liveness-verification/src/lib/services/liveness-backend.service.spec.ts +103 -0
  125. package/projects/facecog-liveness-verification/src/lib/services/liveness-backend.service.ts +61 -0
  126. package/projects/facecog-liveness-verification/src/lib/services/liveness-config.service.spec.ts +109 -0
  127. package/projects/facecog-liveness-verification/src/lib/services/liveness-config.service.ts +70 -0
  128. package/projects/facecog-liveness-verification/src/lib/services/liveness-orchestrator.service.spec.ts +144 -0
  129. package/projects/facecog-liveness-verification/src/lib/services/liveness-orchestrator.service.ts +162 -0
  130. package/projects/facecog-liveness-verification/src/lib/services/pose-detection/hand-gesture-detection.service.ts +315 -0
  131. package/projects/facecog-liveness-verification/src/lib/services/pose-detection/index.ts +5 -0
  132. package/projects/facecog-liveness-verification/src/lib/services/pose-detection/openpose.service.ts +287 -0
  133. package/projects/facecog-liveness-verification/src/lib/services/pose-detection/pose-comparison.service.ts +353 -0
  134. package/projects/facecog-liveness-verification/src/lib/services/pose-detection/pose-matching.service.ts +2370 -0
  135. package/projects/facecog-liveness-verification/src/lib/services/pose-detection/reference-pose.service.ts +271 -0
  136. package/projects/facecog-liveness-verification/src/lib/services/pose-selection.service.spec.ts +183 -0
  137. package/projects/facecog-liveness-verification/src/lib/services/pose-selection.service.ts +179 -0
  138. package/projects/facecog-liveness-verification/src/lib/services/verification-api.service.spec.ts +159 -0
  139. package/projects/facecog-liveness-verification/src/lib/services/verification-api.service.ts +151 -0
  140. package/projects/facecog-liveness-verification/src/lib/services/verification-plan.service.spec.ts +184 -0
  141. package/projects/facecog-liveness-verification/src/lib/services/verification-plan.service.ts +94 -0
  142. package/projects/facecog-liveness-verification/src/lib/services/video-recorder.service.spec.ts +52 -0
  143. package/projects/facecog-liveness-verification/src/lib/services/video-recorder.service.ts +117 -0
  144. package/projects/facecog-liveness-verification/src/lib/types/detection-strategy.type.ts +5 -0
  145. package/projects/facecog-liveness-verification/src/lib/types/index.ts +7 -0
  146. package/projects/facecog-liveness-verification/src/lib/types/liveness-action.type.ts +31 -0
  147. package/projects/facecog-liveness-verification/src/lib/types/liveness-backend.type.ts +5 -0
  148. package/projects/facecog-liveness-verification/src/lib/types/pose-category.type.ts +5 -0
  149. package/projects/facecog-liveness-verification/src/lib/types/pose-difficulty.type.ts +5 -0
  150. package/projects/facecog-liveness-verification/src/lib/types/verification-flow-step.type.ts +5 -0
  151. package/projects/facecog-liveness-verification/src/lib/types/verification-step-kind.type.ts +4 -0
  152. package/projects/facecog-liveness-verification/src/public-api.ts +150 -0
  153. package/projects/facecog-liveness-verification/tsconfig.lib.json +20 -0
  154. package/projects/facecog-liveness-verification/tsconfig.lib.prod.json +11 -0
  155. package/projects/facecog-liveness-verification/tsconfig.spec.json +13 -0
  156. package/projects/facecog-liveness-verification/tsconfig.wrapper.json +15 -0
  157. package/projects/facecog-liveness-verification-test/src/app/app-routing.module.ts +22 -0
  158. package/projects/facecog-liveness-verification-test/src/app/app.component.html +3 -0
  159. package/projects/facecog-liveness-verification-test/src/app/app.component.scss +0 -0
  160. package/projects/facecog-liveness-verification-test/src/app/app.component.ts +11 -0
  161. package/projects/facecog-liveness-verification-test/src/app/app.module.ts +27 -0
  162. package/projects/facecog-liveness-verification-test/src/app/home/home-routing.module.ts +16 -0
  163. package/projects/facecog-liveness-verification-test/src/app/home/home.module.ts +19 -0
  164. package/projects/facecog-liveness-verification-test/src/app/home/home.page.html +39 -0
  165. package/projects/facecog-liveness-verification-test/src/app/home/home.page.scss +97 -0
  166. package/projects/facecog-liveness-verification-test/src/app/home/home.page.spec.ts +24 -0
  167. package/projects/facecog-liveness-verification-test/src/app/home/home.page.ts +92 -0
  168. package/projects/facecog-liveness-verification-test/src/app/home/verification-modal.component.ts +106 -0
  169. package/projects/facecog-liveness-verification-test/src/assets/fonts/gilroy/Gilroy-Bold_0.ttf +0 -0
  170. package/projects/facecog-liveness-verification-test/src/assets/fonts/gilroy/Gilroy-Medium_0.ttf +0 -0
  171. package/projects/facecog-liveness-verification-test/src/assets/fonts/gilroy/Gilroy-Regular_0.ttf +0 -0
  172. package/projects/facecog-liveness-verification-test/src/assets/fonts/gilroy/Gilroy-SemiBold_0.ttf +0 -0
  173. package/projects/facecog-liveness-verification-test/src/assets/fonts/gilroy/Gilroy-Thin_0.ttf +0 -0
  174. package/projects/facecog-liveness-verification-test/src/assets/icon/favicon.png +0 -0
  175. package/projects/facecog-liveness-verification-test/src/assets/images/poses/Five_Fingers_Left.jpg +0 -0
  176. package/projects/facecog-liveness-verification-test/src/assets/images/poses/Left_Palm.jpg +0 -0
  177. package/projects/facecog-liveness-verification-test/src/assets/images/poses/Ok_Sign_Right.jpg +0 -0
  178. package/projects/facecog-liveness-verification-test/src/assets/images/poses/Peace_Sign_Left.jpg +0 -0
  179. package/projects/facecog-liveness-verification-test/src/assets/images/poses/README.md +77 -0
  180. package/projects/facecog-liveness-verification-test/src/assets/images/poses/Right_Palm.jpg +0 -0
  181. package/projects/facecog-liveness-verification-test/src/assets/images/poses/Speak_Phrase.jpg +0 -0
  182. package/projects/facecog-liveness-verification-test/src/assets/images/poses/Three_Fingers_Right.jpg +0 -0
  183. package/projects/facecog-liveness-verification-test/src/assets/images/poses/Thumbs_Up_Left.jpg +0 -0
  184. package/projects/facecog-liveness-verification-test/src/assets/images/poses/Thumbs_Up_Right.jpg +0 -0
  185. package/projects/facecog-liveness-verification-test/src/assets/images/poses/Wave_Right.jpg +0 -0
  186. package/projects/facecog-liveness-verification-test/src/assets/images/poses/blink.jpeg +0 -0
  187. package/projects/facecog-liveness-verification-test/src/assets/images/poses/blink_twice.jpeg +0 -0
  188. package/projects/facecog-liveness-verification-test/src/assets/images/poses/center_face.png +0 -0
  189. package/projects/facecog-liveness-verification-test/src/assets/images/poses/clap.jpeg +0 -0
  190. package/projects/facecog-liveness-verification-test/src/assets/images/poses/cover_mouth.png +0 -0
  191. package/projects/facecog-liveness-verification-test/src/assets/images/poses/cover_right_eye.png +0 -0
  192. package/projects/facecog-liveness-verification-test/src/assets/images/poses/cross_arms.png +0 -0
  193. package/projects/facecog-liveness-verification-test/src/assets/images/poses/face_straight.png +0 -0
  194. package/projects/facecog-liveness-verification-test/src/assets/images/poses/follow_dot.png +0 -0
  195. package/projects/facecog-liveness-verification-test/src/assets/images/poses/look_down.png +0 -0
  196. package/projects/facecog-liveness-verification-test/src/assets/images/poses/look_up.png +0 -0
  197. package/projects/facecog-liveness-verification-test/src/assets/images/poses/move_closer.png +0 -0
  198. package/projects/facecog-liveness-verification-test/src/assets/images/poses/nod.png +0 -0
  199. package/projects/facecog-liveness-verification-test/src/assets/images/poses/open_mouth.png +0 -0
  200. package/projects/facecog-liveness-verification-test/src/assets/images/poses/raise_eyebrow.png +0 -0
  201. package/projects/facecog-liveness-verification-test/src/assets/images/poses/rotate_face.jpeg +0 -0
  202. package/projects/facecog-liveness-verification-test/src/assets/images/poses/shake_head.jpeg +0 -0
  203. package/projects/facecog-liveness-verification-test/src/assets/images/poses/smile.png +0 -0
  204. package/projects/facecog-liveness-verification-test/src/assets/images/poses/tilt_left.png +0 -0
  205. package/projects/facecog-liveness-verification-test/src/assets/images/poses/tilt_right.png +0 -0
  206. package/projects/facecog-liveness-verification-test/src/assets/images/poses/touch_chin_left.jpg +0 -0
  207. package/projects/facecog-liveness-verification-test/src/assets/images/poses/touch_left_cheek.jpeg +0 -0
  208. package/projects/facecog-liveness-verification-test/src/assets/images/poses/touch_nose_right.png +0 -0
  209. package/projects/facecog-liveness-verification-test/src/assets/images/poses/touch_right_cheek.jpeg +0 -0
  210. package/projects/facecog-liveness-verification-test/src/assets/images/poses/turn_left.png +0 -0
  211. package/projects/facecog-liveness-verification-test/src/assets/images/poses/turn_right.png +0 -0
  212. package/projects/facecog-liveness-verification-test/src/assets/images/poses/wink.jpeg +0 -0
  213. package/projects/facecog-liveness-verification-test/src/assets/images/reference-pose.jpg +0 -0
  214. package/projects/facecog-liveness-verification-test/src/assets/shapes.svg +1 -0
  215. package/projects/facecog-liveness-verification-test/src/environments/environment.prod.ts +4 -0
  216. package/projects/facecog-liveness-verification-test/src/environments/environment.ts +17 -0
  217. package/projects/facecog-liveness-verification-test/src/global.scss +288 -0
  218. package/projects/facecog-liveness-verification-test/src/index.html +31 -0
  219. package/projects/facecog-liveness-verification-test/src/main.ts +6 -0
  220. package/projects/facecog-liveness-verification-test/src/polyfills.ts +55 -0
  221. package/projects/facecog-liveness-verification-test/src/theme/nextsapien-theme.scss +174 -0
  222. package/projects/facecog-liveness-verification-test/src/theme/variables.scss +2 -0
  223. package/projects/facecog-liveness-verification-test/src/zone-flags.ts +6 -0
  224. package/projects/facecog-liveness-verification-test/tsconfig.app.json +15 -0
  225. package/projects/facecog-liveness-verification-test/tsconfig.spec.json +14 -0
  226. package/setup-jest.ts +118 -0
  227. package/tsconfig.json +41 -0
  228. package/tsconfig.spec.json +15 -0
  229. package/vercel.json +24 -0
@@ -0,0 +1,605 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+ import { NgZone } from '@angular/core';
3
+ import { NO_ERRORS_SCHEMA } from '@angular/core';
4
+ import { IonicModule } from '@ionic/angular';
5
+ import { CommonModule } from '@angular/common';
6
+
7
+ import { LivePreviewComponent } from './live-preview.component';
8
+ import { VideoRecorderService } from '../../services/video-recorder.service';
9
+ import { PoseMatchingService } from '../../services/pose-detection/pose-matching.service';
10
+ import { LivenessConfigService } from '../../services/liveness-config.service';
11
+ import { CAMERA_PROVIDER } from '../../interfaces/camera-provider.interface';
12
+ import type { SelectedPoseInfo } from '../../services/pose-selection.service';
13
+ import type { PoseDefinition } from '../../interfaces/pose-definition.interface';
14
+
15
+ describe('LivePreviewComponent', () => {
16
+ let component: LivePreviewComponent;
17
+ let fixture: ComponentFixture<LivePreviewComponent>;
18
+ let videoRecorderSpy: jasmine.SpyObj<VideoRecorderService>;
19
+ let poseMatchingSpy: jasmine.SpyObj<PoseMatchingService>;
20
+ let configServiceSpy: jasmine.SpyObj<LivenessConfigService>;
21
+ let ngZoneRunSpy: jasmine.Spy;
22
+ const mockPose: PoseDefinition = {
23
+ id: 1,
24
+ name: 'Smile',
25
+ description: 'Smile naturally',
26
+ category: 'face',
27
+ imagePath: 'assets/smile.png',
28
+ detectionStrategy: 'face-api',
29
+ difficulty: 'easy'
30
+ };
31
+
32
+ const mockSelectedPose: SelectedPoseInfo = {
33
+ pose: mockPose,
34
+ imageUrl: 'assets/smile.png',
35
+ timestamp: new Date()
36
+ };
37
+
38
+ beforeEach(async () => {
39
+ videoRecorderSpy = jasmine.createSpyObj('VideoRecorderService', ['startRecording', 'stopRecording', 'isRecording', 'cleanup']);
40
+ videoRecorderSpy.isRecording.and.returnValue(false);
41
+
42
+ poseMatchingSpy = jasmine.createSpyObj('PoseMatchingService', [
43
+ 'loadModels',
44
+ 'loadReferenceImage',
45
+ 'resetActionState',
46
+ 'setStaticPoseKeypoints',
47
+ 'setExpectedGesture',
48
+ 'setCombinedPoseType',
49
+ 'detectAction',
50
+ 'frameToBase64',
51
+ 'cleanup'
52
+ ]);
53
+ poseMatchingSpy.loadModels.and.returnValue(Promise.resolve());
54
+ poseMatchingSpy.loadReferenceImage.and.returnValue(Promise.resolve());
55
+ poseMatchingSpy.detectAction.and.returnValue(Promise.resolve({ action: 'smile', progress: 0.5, completed: false, message: 'Hold...' }));
56
+ poseMatchingSpy.frameToBase64.and.returnValue('base64frame');
57
+
58
+ configServiceSpy = jasmine.createSpyObj('LivenessConfigService', ['getConfig']);
59
+ configServiceSpy.getConfig.and.returnValue({
60
+ exampleImageUrl: undefined,
61
+ enableVideoRecording: false,
62
+ captureFrameCount: 5
63
+ });
64
+
65
+ const mockCameraProvider = {
66
+ startBrightnessMonitoring: jasmine.createSpy('startBrightnessMonitoring').and.returnValue(12345),
67
+ stopBrightnessMonitoring: jasmine.createSpy('stopBrightnessMonitoring')
68
+ };
69
+
70
+ await TestBed.configureTestingModule({
71
+ imports: [CommonModule, IonicModule.forRoot(), LivePreviewComponent],
72
+ providers: [
73
+ { provide: VideoRecorderService, useValue: videoRecorderSpy },
74
+ { provide: PoseMatchingService, useValue: poseMatchingSpy },
75
+ { provide: LivenessConfigService, useValue: configServiceSpy },
76
+ { provide: CAMERA_PROVIDER, useValue: mockCameraProvider }
77
+ ],
78
+ schemas: [NO_ERRORS_SCHEMA]
79
+ }).compileComponents();
80
+
81
+ Object.defineProperty(navigator, 'mediaDevices', {
82
+ value: {
83
+ getUserMedia: jasmine.createSpy('getUserMedia').and.returnValue(
84
+ Promise.resolve(new MediaStream())
85
+ )
86
+ },
87
+ configurable: true
88
+ });
89
+
90
+ const ngZone = TestBed.inject(NgZone);
91
+ ngZoneRunSpy = spyOn(ngZone, 'run').and.callFake((fn: (...args: any[]) => any) => fn());
92
+
93
+ fixture = TestBed.createComponent(LivePreviewComponent);
94
+ component = fixture.componentInstance;
95
+ });
96
+
97
+ it('should create', () => {
98
+ expect(component).toBeTruthy();
99
+ });
100
+
101
+ it('ngOnInit calls poseMatching.loadModels() and sets statusMessage', async () => {
102
+ fixture.detectChanges();
103
+ await fixture.whenStable();
104
+
105
+ expect(poseMatchingSpy.loadModels).toHaveBeenCalled();
106
+ expect(component.statusMessage()).toBeDefined();
107
+ });
108
+
109
+ it('startPoseMatching sets showPoseOverlay to true', () => {
110
+ component['videoElement'] = { nativeElement: document.createElement('video') } as any;
111
+ component.stream.set({ getTracks: () => [] } as any);
112
+
113
+ component.startPoseMatching();
114
+
115
+ expect(component.showPoseOverlay()).toBe(true);
116
+ expect(poseMatchingSpy.resetActionState).toHaveBeenCalled();
117
+ });
118
+
119
+ it('startPoseMatching uses correct pose action based on selectedPose (smile)', () => {
120
+ component.selectedPose = mockSelectedPose;
121
+ component['videoElement'] = { nativeElement: document.createElement('video') } as any;
122
+ component.stream.set({ getTracks: () => [] } as any);
123
+
124
+ component.startPoseMatching();
125
+
126
+ expect(component.currentPoseAction()).toBe('smile');
127
+ expect(component.currentPoseDescription()).toBe('Smile naturally');
128
+ });
129
+
130
+ it('startPoseMatching uses custom-pose when pose has keypoints', () => {
131
+ const poseWithKeypoints = {
132
+ ...mockPose,
133
+ poseKeypoints: [{ x: 0, y: 0, confidence: 1 }, { x: 1, y: 1, confidence: 1 }]
134
+ };
135
+ component.selectedPose = { ...mockSelectedPose, pose: poseWithKeypoints };
136
+ component['videoElement'] = { nativeElement: document.createElement('video') } as any;
137
+ component.stream.set({ getTracks: () => [] } as any);
138
+
139
+ component.startPoseMatching();
140
+
141
+ expect(component.currentPoseAction()).toBe('custom-pose');
142
+ expect(poseMatchingSpy.setStaticPoseKeypoints).toHaveBeenCalledWith(poseWithKeypoints.poseKeypoints);
143
+ });
144
+
145
+ it('captureFrames clears matching interval, sets isMatching, emits captureComplete', async () => {
146
+ component['matchingInterval'] = setInterval(() => {}, 999) as any;
147
+ component['videoElement'] = { nativeElement: document.createElement('video') } as any;
148
+ configServiceSpy.getConfig.and.returnValue({ captureFrameCount: 2, enableVideoRecording: false });
149
+
150
+ let emitted: { frames: string[]; videoBlob?: Blob } | undefined;
151
+ component.captureComplete.subscribe((data) => (emitted = data));
152
+
153
+ await component.captureFrames();
154
+
155
+ expect(component.isMatching()).toBe(true);
156
+ expect(emitted).toBeDefined();
157
+ expect(emitted!.frames.length).toBe(2);
158
+ expect(ngZoneRunSpy).toHaveBeenCalled();
159
+ });
160
+
161
+ it('onCancel emits cancelled', () => {
162
+ let emitted = false;
163
+ component.cancelled.subscribe(() => (emitted = true));
164
+ component.onCancel();
165
+ expect(emitted).toBe(true);
166
+ });
167
+
168
+ it('handleBrightnessData updates lighting state', () => {
169
+ (component as any).handleBrightnessData({
170
+ brightness: 50,
171
+ isLowLight: true,
172
+ isTooLight: false
173
+ });
174
+
175
+ expect(component.brightnessLevel()).toBe(50);
176
+ expect(component.isLowLight()).toBe(true);
177
+ expect(component.lightingWarning()).toContain('Low light');
178
+ });
179
+
180
+ it('ngOnDestroy cleans up intervals and streams', () => {
181
+ const trackStopSpy = jasmine.createSpy('stop');
182
+ component.stream.set({ getTracks: () => [{ stop: trackStopSpy }] } as any);
183
+ component['matchingInterval'] = setInterval(() => {}, 1000) as any;
184
+
185
+ component.ngOnDestroy();
186
+
187
+ expect(poseMatchingSpy.cleanup).toHaveBeenCalled();
188
+ expect(videoRecorderSpy.cleanup).toHaveBeenCalled();
189
+ expect(trackStopSpy).toHaveBeenCalled();
190
+ });
191
+
192
+ it('handleBrightnessData with too-bright light', () => {
193
+ (component as any).handleBrightnessData({
194
+ brightness: 210,
195
+ isLowLight: false,
196
+ isTooLight: true
197
+ });
198
+ expect(component.isTooLight()).toBe(true);
199
+ expect(component.lightingWarning()).toContain('Too bright');
200
+ });
201
+
202
+ it('handleBrightnessData with good lighting', () => {
203
+ (component as any).handleBrightnessData({
204
+ brightness: 128,
205
+ isLowLight: false,
206
+ isTooLight: false
207
+ });
208
+ expect(component.isLowLight()).toBe(false);
209
+ expect(component.isTooLight()).toBe(false);
210
+ expect(component.lightingWarning()).toBe('');
211
+ });
212
+
213
+ it('captureFrames with video recording stops recorder', async () => {
214
+ component['matchingInterval'] = setInterval(() => {}, 999) as any;
215
+ component['videoElement'] = { nativeElement: document.createElement('video') } as any;
216
+ configServiceSpy.getConfig.and.returnValue({ captureFrameCount: 2, enableVideoRecording: true });
217
+ videoRecorderSpy.isRecording.and.returnValue(true);
218
+ videoRecorderSpy.stopRecording.and.returnValue(Promise.resolve({ blob: new Blob(), sizeMB: 0.5, duration: 3 }));
219
+
220
+ let emitted: any;
221
+ component.captureComplete.subscribe((data) => (emitted = data));
222
+
223
+ await component.captureFrames();
224
+ await new Promise(r => setTimeout(r, 50));
225
+
226
+ expect(videoRecorderSpy.stopRecording).toHaveBeenCalled();
227
+ });
228
+
229
+ it('stopDotAnimation clears dot interval', () => {
230
+ component['dotAnimationInterval'] = setInterval(() => {}, 1000) as any;
231
+ component['stopDotAnimation']();
232
+ expect(component['dotAnimationInterval']).toBeNull();
233
+ });
234
+
235
+ it('startPoseMatching with no selectedPose uses default action', () => {
236
+ component.selectedPose = undefined;
237
+ component['videoElement'] = { nativeElement: document.createElement('video') } as any;
238
+ component.stream.set({ getTracks: () => [] } as any);
239
+
240
+ component.startPoseMatching();
241
+
242
+ expect(component.currentPoseAction()).toBeDefined();
243
+ });
244
+
245
+ it('ngOnDestroy is safe with no stream', () => {
246
+ component.stream.set(null);
247
+ expect(() => component.ngOnDestroy()).not.toThrow();
248
+ });
249
+
250
+ it('progressOffset computes correctly', () => {
251
+ component.matchProgress.set(50);
252
+ const offset = component.progressOffset();
253
+ expect(offset).toBe(1515 * 0.5);
254
+ });
255
+
256
+ it('progressOffset is full circumference at 0%', () => {
257
+ component.matchProgress.set(0);
258
+ expect(component.progressOffset()).toBe(1515);
259
+ });
260
+
261
+ it('startCamera sets up stream and starts pose matching', async () => {
262
+ component['videoElement'] = { nativeElement: document.createElement('video') } as any;
263
+ await component.startCamera();
264
+
265
+ expect(navigator.mediaDevices.getUserMedia).toHaveBeenCalled();
266
+ expect(component.stream()).toBeDefined();
267
+ expect(component.statusMessage()).toBeDefined();
268
+ expect(component.showPoseOverlay()).toBe(true);
269
+ });
270
+
271
+ it('startCamera handles NotAllowedError', async () => {
272
+ (navigator.mediaDevices.getUserMedia as jasmine.Spy).and.returnValue(
273
+ Promise.reject(Object.assign(new Error('Denied'), { name: 'NotAllowedError' }))
274
+ );
275
+ await component.startCamera();
276
+ expect(component.statusMessage()).toContain('Camera access denied');
277
+ });
278
+
279
+ it('startCamera handles NotFoundError', async () => {
280
+ (navigator.mediaDevices.getUserMedia as jasmine.Spy).and.returnValue(
281
+ Promise.reject(Object.assign(new Error('Not found'), { name: 'NotFoundError' }))
282
+ );
283
+ await component.startCamera();
284
+ expect(component.statusMessage()).toContain('No camera found');
285
+ });
286
+
287
+ it('startCamera handles NotReadableError', async () => {
288
+ (navigator.mediaDevices.getUserMedia as jasmine.Spy).and.returnValue(
289
+ Promise.reject(Object.assign(new Error('In use'), { name: 'NotReadableError' }))
290
+ );
291
+ await component.startCamera();
292
+ expect(component.statusMessage()).toContain('already in use');
293
+ });
294
+
295
+ it('startCamera handles generic error', async () => {
296
+ (navigator.mediaDevices.getUserMedia as jasmine.Spy).and.returnValue(Promise.reject(new Error('Unknown')));
297
+ await component.startCamera();
298
+ expect(component.statusMessage()).toContain('Camera error');
299
+ });
300
+
301
+ it('startCamera with video recording enabled starts recorder', async () => {
302
+ configServiceSpy.getConfig.and.returnValue({ enableVideoRecording: true, captureFrameCount: 5 });
303
+ component['videoElement'] = { nativeElement: document.createElement('video') } as any;
304
+ videoRecorderSpy.startRecording.and.returnValue(Promise.resolve());
305
+
306
+ await component.startCamera();
307
+
308
+ expect(videoRecorderSpy.startRecording).toHaveBeenCalled();
309
+ });
310
+
311
+ it('ngOnInit loads models and starts camera', async () => {
312
+ component['videoElement'] = { nativeElement: document.createElement('video') } as any;
313
+ fixture.detectChanges();
314
+ await fixture.whenStable();
315
+
316
+ expect(poseMatchingSpy.loadModels).toHaveBeenCalled();
317
+ expect(component.statusMessage()).toBeDefined();
318
+ });
319
+
320
+ it('ngOnInit with reference image loads it', async () => {
321
+ configServiceSpy.getConfig.and.returnValue({ exampleImageUrl: 'http://example.com/ref.png', captureFrameCount: 5 });
322
+ component['videoElement'] = { nativeElement: document.createElement('video') } as any;
323
+
324
+ await component.ngOnInit();
325
+
326
+ expect(poseMatchingSpy.loadReferenceImage).toHaveBeenCalledWith('http://example.com/ref.png');
327
+ });
328
+
329
+ it('ngOnInit handles model load failure', async () => {
330
+ poseMatchingSpy.loadModels.and.returnValue(Promise.reject(new Error('Model load failed')));
331
+
332
+ await component.ngOnInit();
333
+
334
+ expect(component.statusMessage()).toContain('Failed to initialize');
335
+ });
336
+
337
+ it('startPoseMatching with referencePose uses custom-pose action', () => {
338
+ component.selectedPose = undefined;
339
+ component.referencePose = { poseDescription: 'Custom pose', keypoints: [], imageData: '' } as any;
340
+ component['videoElement'] = { nativeElement: document.createElement('video') } as any;
341
+ component.stream.set({ getTracks: () => [] } as any);
342
+
343
+ component.startPoseMatching();
344
+
345
+ expect(component.currentPoseAction()).toBe('custom-pose');
346
+ expect(component.currentPoseDescription()).toBe('Custom pose');
347
+ });
348
+
349
+ it('startPoseMatching with follow-dot pose starts dot animation', () => {
350
+ const followDotPose = {
351
+ ...mockPose,
352
+ name: 'Follow Dot',
353
+ description: 'Follow the dot with your eyes'
354
+ };
355
+ component.selectedPose = { ...mockSelectedPose, pose: followDotPose };
356
+ component['videoElement'] = { nativeElement: document.createElement('video') } as any;
357
+ component.stream.set({ getTracks: () => [] } as any);
358
+
359
+ const startDotSpy = spyOn(component as any, 'startDotAnimation');
360
+ component.startPoseMatching();
361
+
362
+ expect(startDotSpy).toHaveBeenCalled();
363
+ });
364
+
365
+ it('startPoseMatching with no keypoints uses fallback detection', () => {
366
+ const poseNoKeypoints = { ...mockPose, poseKeypoints: undefined };
367
+ component.selectedPose = { ...mockSelectedPose, pose: poseNoKeypoints };
368
+ component['videoElement'] = { nativeElement: document.createElement('video') } as any;
369
+ component.stream.set({ getTracks: () => [] } as any);
370
+
371
+ component.startPoseMatching();
372
+
373
+ expect(poseMatchingSpy.setStaticPoseKeypoints).toHaveBeenCalledWith(null);
374
+ });
375
+
376
+ it('getPoseAction maps palm pose to hand-gesture', () => {
377
+ const palmPose = { ...mockPose, name: 'Right Palm', poseKeypoints: undefined };
378
+ const action = (component as any).getPoseAction(palmPose);
379
+ expect(action).toBe('hand-gesture');
380
+ expect(poseMatchingSpy.setExpectedGesture).toHaveBeenCalledWith('palm', 'Right');
381
+ });
382
+
383
+ it('getPoseAction maps thumbs up to hand-gesture', () => {
384
+ const thumbsPose = { ...mockPose, name: 'Thumbs Up', poseKeypoints: undefined };
385
+ const action = (component as any).getPoseAction(thumbsPose);
386
+ expect(action).toBe('hand-gesture');
387
+ expect(poseMatchingSpy.setExpectedGesture).toHaveBeenCalledWith('thumbs_up', 'Any');
388
+ });
389
+
390
+ it('getPoseAction maps peace to hand-gesture', () => {
391
+ const peacePose = { ...mockPose, name: 'Left Peace Sign', poseKeypoints: undefined };
392
+ const action = (component as any).getPoseAction(peacePose);
393
+ expect(action).toBe('hand-gesture');
394
+ expect(poseMatchingSpy.setExpectedGesture).toHaveBeenCalledWith('peace', 'Left');
395
+ });
396
+
397
+ it('getPoseAction maps ok sign to hand-gesture', () => {
398
+ const pose = { ...mockPose, name: 'OK Sign', poseKeypoints: undefined };
399
+ expect((component as any).getPoseAction(pose)).toBe('hand-gesture');
400
+ expect(poseMatchingSpy.setExpectedGesture).toHaveBeenCalledWith('ok', 'Any');
401
+ });
402
+
403
+ it('getPoseAction maps fist to hand-gesture', () => {
404
+ const pose = { ...mockPose, name: 'Right Fist', poseKeypoints: undefined };
405
+ expect((component as any).getPoseAction(pose)).toBe('hand-gesture');
406
+ expect(poseMatchingSpy.setExpectedGesture).toHaveBeenCalledWith('fist', 'Right');
407
+ });
408
+
409
+ it('getPoseAction maps three fingers to hand-gesture', () => {
410
+ const pose = { ...mockPose, name: 'Three Fingers', poseKeypoints: undefined };
411
+ expect((component as any).getPoseAction(pose)).toBe('hand-gesture');
412
+ expect(poseMatchingSpy.setExpectedGesture).toHaveBeenCalledWith('three', 'Any');
413
+ });
414
+
415
+ it('getPoseAction maps five fingers to hand-gesture', () => {
416
+ const pose = { ...mockPose, name: 'Five Fingers', poseKeypoints: undefined };
417
+ expect((component as any).getPoseAction(pose)).toBe('hand-gesture');
418
+ expect(poseMatchingSpy.setExpectedGesture).toHaveBeenCalledWith('palm', 'Any');
419
+ });
420
+
421
+ it('getPoseAction maps face expressions', () => {
422
+ expect((component as any).getPoseAction({ ...mockPose, name: 'Open Mouth', poseKeypoints: undefined })).toBe('open-mouth');
423
+ expect((component as any).getPoseAction({ ...mockPose, name: 'Raise Eyebrows', poseKeypoints: undefined })).toBe('raise-eyebrows');
424
+ expect((component as any).getPoseAction({ ...mockPose, name: 'Blink Twice', poseKeypoints: undefined })).toBe('blink-twice');
425
+ expect((component as any).getPoseAction({ ...mockPose, name: 'Blink', poseKeypoints: undefined })).toBe('blink');
426
+ expect((component as any).getPoseAction({ ...mockPose, name: 'Wink', poseKeypoints: undefined })).toBe('wink');
427
+ });
428
+
429
+ it('getPoseAction maps head movements', () => {
430
+ expect((component as any).getPoseAction({ ...mockPose, name: 'Nod', poseKeypoints: undefined })).toBe('nod');
431
+ expect((component as any).getPoseAction({ ...mockPose, name: 'Turn Left', poseKeypoints: undefined })).toBe('turn-left');
432
+ expect((component as any).getPoseAction({ ...mockPose, name: 'Turn Right', poseKeypoints: undefined })).toBe('turn-right');
433
+ expect((component as any).getPoseAction({ ...mockPose, name: 'Tilt Left', poseKeypoints: undefined })).toBe('tilt-left');
434
+ expect((component as any).getPoseAction({ ...mockPose, name: 'Tilt Right', poseKeypoints: undefined })).toBe('tilt-right');
435
+ expect((component as any).getPoseAction({ ...mockPose, name: 'Look Up', poseKeypoints: undefined })).toBe('look-up');
436
+ expect((component as any).getPoseAction({ ...mockPose, name: 'Look Down', poseKeypoints: undefined })).toBe('look-down');
437
+ expect((component as any).getPoseAction({ ...mockPose, name: 'Shake Head', poseKeypoints: undefined })).toBe('shake-head');
438
+ expect((component as any).getPoseAction({ ...mockPose, name: 'Rotate Face', poseKeypoints: undefined })).toBe('rotate-face');
439
+ expect((component as any).getPoseAction({ ...mockPose, name: 'Face Straight Center', poseKeypoints: undefined })).toBe('face-center');
440
+ });
441
+
442
+ it('getPoseAction maps movement gestures', () => {
443
+ expect((component as any).getPoseAction({ ...mockPose, name: 'Wave', poseKeypoints: undefined })).toBe('wave');
444
+ expect((component as any).getPoseAction({ ...mockPose, name: 'Clap', poseKeypoints: undefined })).toBe('clap');
445
+ expect((component as any).getPoseAction({ ...mockPose, name: 'Cross Arms', poseKeypoints: undefined })).toBe('cross-arms');
446
+ expect((component as any).getPoseAction({ ...mockPose, name: 'Point', poseKeypoints: undefined })).toBe('point');
447
+ });
448
+
449
+ it('getPoseAction maps combined poses', () => {
450
+ expect((component as any).getPoseAction({ ...mockPose, name: 'Touch Nose', poseKeypoints: undefined })).toBe('combined-pose');
451
+ expect((component as any).getPoseAction({ ...mockPose, name: 'Cover Eyes', poseKeypoints: undefined })).toBe('combined-pose');
452
+ expect((component as any).getPoseAction({ ...mockPose, name: 'Frame Face', poseKeypoints: undefined })).toBe('combined-pose');
453
+ expect((component as any).getPoseAction({ ...mockPose, name: 'Hand to Ear', poseKeypoints: undefined })).toBe('combined-pose');
454
+ });
455
+
456
+ it('getPoseAction maps follow dot', () => {
457
+ expect((component as any).getPoseAction({ ...mockPose, name: 'Follow Dot', poseKeypoints: undefined })).toBe('follow-dot');
458
+ });
459
+
460
+ it('getPoseAction maps speak phrase', () => {
461
+ expect((component as any).getPoseAction({ ...mockPose, name: 'Speak Phrase', poseKeypoints: undefined })).toBe('speak-phrase');
462
+ });
463
+
464
+ it('getPoseAction defaults face category to smile', () => {
465
+ expect((component as any).getPoseAction({ ...mockPose, name: 'Unknown Face', category: 'face', poseKeypoints: undefined })).toBe('smile');
466
+ });
467
+
468
+ it('getPoseAction defaults head category to face-center', () => {
469
+ expect((component as any).getPoseAction({ ...mockPose, name: 'Unknown Head', category: 'head', poseKeypoints: undefined })).toBe('face-center');
470
+ });
471
+
472
+ it('getPoseAction falls back to custom-pose for unmapped', () => {
473
+ expect((component as any).getPoseAction({ ...mockPose, name: 'Totally Unknown', category: 'gesture', poseKeypoints: undefined })).toBe('custom-pose');
474
+ });
475
+
476
+ it('startDotAnimation sets up interval', () => {
477
+ (component as any).startDotAnimation();
478
+ expect(component.showFollowDot()).toBe(true);
479
+ expect(component['dotAnimationInterval']).toBeDefined();
480
+ (component as any).stopDotAnimation();
481
+ });
482
+
483
+ it('stopDotAnimation clears interval and hides dot', () => {
484
+ (component as any).startDotAnimation();
485
+ (component as any).stopDotAnimation();
486
+ expect(component.showFollowDot()).toBe(false);
487
+ expect(component['dotAnimationInterval']).toBeNull();
488
+ });
489
+
490
+ it('manualCapture calls captureFrames', () => {
491
+ const spy = spyOn(component, 'captureFrames');
492
+ spy.and.returnValue(Promise.resolve());
493
+ component.manualCapture();
494
+ expect(spy).toHaveBeenCalled();
495
+ });
496
+
497
+ it('onCancel emits cancelled event', () => {
498
+ const spy = jest.fn();
499
+ component.cancelled.subscribe(spy);
500
+ component.onCancel();
501
+ expect(spy).toHaveBeenCalled();
502
+ });
503
+
504
+ it('startBrightnessMonitoring without video element returns early', () => {
505
+ component['videoElement'] = null as any;
506
+ (component as any).startBrightnessMonitoring();
507
+ expect(component['brightnessMonitoringId']).toBeNull();
508
+ });
509
+
510
+ it('startBrightnessMonitoring without cameraProvider uses fallback interval', () => {
511
+ component['videoElement'] = { nativeElement: document.createElement('video') } as any;
512
+ component['cameraProvider'] = undefined;
513
+ (component as any).startBrightnessMonitoring();
514
+ expect(component['brightnessMonitoringId']).toBeDefined();
515
+ clearInterval(component['brightnessMonitoringId']!);
516
+ });
517
+
518
+ it('startBrightnessMonitoring with cameraProvider uses provider', () => {
519
+ const mockProvider = { startBrightnessMonitoring: jasmine.createSpy('startBrightnessMonitoring').and.returnValue(42) };
520
+ component['videoElement'] = { nativeElement: document.createElement('video') } as any;
521
+ component['cameraProvider'] = mockProvider as any;
522
+ (component as any).startBrightnessMonitoring();
523
+ expect(mockProvider.startBrightnessMonitoring).toHaveBeenCalled();
524
+ expect(component['brightnessMonitoringId']).toBe(42);
525
+ });
526
+
527
+ it('stopBrightnessMonitoring with cameraProvider calls provider stop', () => {
528
+ const mockProvider = { stopBrightnessMonitoring: jasmine.createSpy('stopBrightnessMonitoring') };
529
+ component['cameraProvider'] = mockProvider as any;
530
+ component['brightnessMonitoringId'] = 42;
531
+ (component as any).stopBrightnessMonitoring();
532
+ expect(mockProvider.stopBrightnessMonitoring).toHaveBeenCalledWith(42);
533
+ expect(component['brightnessMonitoringId']).toBeNull();
534
+ });
535
+
536
+ it('stopBrightnessMonitoring without cameraProvider clears interval', () => {
537
+ component['cameraProvider'] = undefined;
538
+ component['brightnessMonitoringId'] = setInterval(() => {}, 9999) as any;
539
+ (component as any).stopBrightnessMonitoring();
540
+ expect(component['brightnessMonitoringId']).toBeNull();
541
+ });
542
+
543
+ it('analyzeBrightness returns brightness data', () => {
544
+ const video = document.createElement('video');
545
+ const result = (component as any).analyzeBrightness(video);
546
+ expect(result).toHaveProperty('brightness');
547
+ expect(result).toHaveProperty('isLowLight');
548
+ expect(result).toHaveProperty('isTooLight');
549
+ });
550
+
551
+ it('captureFrames captures correct number of frames and emits', async () => {
552
+ component['matchingInterval'] = setInterval(() => {}, 999) as any;
553
+ component['videoElement'] = { nativeElement: document.createElement('video') } as any;
554
+ configServiceSpy.getConfig.and.returnValue({ captureFrameCount: 2, enableVideoRecording: false });
555
+ poseMatchingSpy.frameToBase64.and.returnValue('data:image/png;base64,frame');
556
+
557
+ let emitted: any;
558
+ component.captureComplete.subscribe((data: any) => (emitted = data));
559
+
560
+ await component.captureFrames();
561
+ await new Promise(r => setTimeout(r, 50));
562
+
563
+ expect((component as any).capturedFrames.length).toBe(2);
564
+ expect(component.isMatching()).toBe(true);
565
+ });
566
+
567
+ it('pose matching interval handles detection error', async () => {
568
+ component['videoElement'] = { nativeElement: document.createElement('video') } as any;
569
+ component.stream.set({ getTracks: () => [] } as any);
570
+ poseMatchingSpy.detectAction.and.returnValue(Promise.reject(new Error('Detection failed')));
571
+
572
+ component.startPoseMatching();
573
+ await new Promise(r => setTimeout(r, 300));
574
+ clearInterval(component['matchingInterval']);
575
+
576
+ expect(component.statusMessage()).toContain('Unable to detect');
577
+ });
578
+
579
+ it('pose matching interval completes when result.completed is true', async () => {
580
+ component['videoElement'] = { nativeElement: document.createElement('video') } as any;
581
+ component.stream.set({ getTracks: () => [] } as any);
582
+ poseMatchingSpy.detectAction.and.returnValue(Promise.resolve({ completed: true, progress: 1, message: 'Done' }));
583
+ poseMatchingSpy.frameToBase64.and.returnValue('data:image/png;base64,frame');
584
+ configServiceSpy.getConfig.and.returnValue({ captureFrameCount: 1, enableVideoRecording: false });
585
+
586
+ component.startPoseMatching();
587
+ await new Promise(r => setTimeout(r, 400));
588
+
589
+ expect(component.statusMessage()).toContain('Capturing');
590
+ });
591
+
592
+ it('ngOnDestroy cleans up canvasAnimationFrame', () => {
593
+ component['canvasAnimationFrame'] = 123;
594
+ component.stream.set(new MediaStream());
595
+ component.ngOnDestroy();
596
+ expect(component['canvasAnimationFrame']).toBe(123);
597
+ });
598
+
599
+ it('updateDotPosition sets dotX and dotY', () => {
600
+ component['currentDotIndex'] = 0;
601
+ (component as any).updateDotPosition(1000, 800);
602
+ expect(component.dotX()).toBe(500);
603
+ expect(component.dotY()).toBe(160);
604
+ });
605
+ });