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,699 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+ import { NO_ERRORS_SCHEMA } from '@angular/core';
3
+ import { CommonModule } from '@angular/common';
4
+ import { IonicModule, ModalController, AlertController, ToastController, LoadingController } from '@ionic/angular';
5
+ import { FormsModule } from '@angular/forms';
6
+ import { of, throwError } from 'rxjs';
7
+ import { IntroComponent } from './intro.component';
8
+ import { ReferencePoseService } from '../../services/pose-detection/reference-pose.service';
9
+ import { OpenposeService } from '../../services/pose-detection/openpose.service';
10
+ import { PoseSelectionService } from '../../services/pose-selection.service';
11
+ import { LivenessConfigService } from '../../services/liveness-config.service';
12
+ import { CUSTOM_POSE_REPOSITORY } from '../../interfaces/custom-pose-repository.interface';
13
+
14
+ describe('IntroComponent', () => {
15
+ let component: IntroComponent;
16
+ let fixture: ComponentFixture<IntroComponent>;
17
+ let poseSelectionSpy: any;
18
+ let configServiceSpy: any;
19
+ let openposeServiceSpy: any;
20
+ let referencePoseServiceSpy: any;
21
+ let modalCtrlSpy: any;
22
+ let alertCtrlSpy: any;
23
+ let toastCtrlSpy: any;
24
+ let loadingCtrlSpy: any;
25
+
26
+ beforeEach(async () => {
27
+ poseSelectionSpy = jasmine.createSpyObj('PoseSelectionService', [
28
+ 'selectPoseByObject', 'getSelectedPose', 'clearSelection', 'selectedPose$'
29
+ ]);
30
+ poseSelectionSpy.selectedPose$ = { subscribe: jest.fn((cb: any) => cb(null)) };
31
+
32
+ configServiceSpy = jasmine.createSpyObj('LivenessConfigService', ['enableBackends', 'getConfig']);
33
+ configServiceSpy.getConfig.and.returnValue({ backends: ['amazon'] });
34
+
35
+ openposeServiceSpy = jasmine.createSpyObj('OpenposeService', ['loadModel']);
36
+ openposeServiceSpy.loadModel.and.returnValue(Promise.resolve());
37
+
38
+ referencePoseServiceSpy = jasmine.createSpyObj('ReferencePoseService', ['extractPoseFromImage', 'clearReferencePose']);
39
+
40
+ modalCtrlSpy = jasmine.createSpyObj('ModalController', ['create']);
41
+ alertCtrlSpy = jasmine.createSpyObj('AlertController', ['create']);
42
+ toastCtrlSpy = jasmine.createSpyObj('ToastController', ['create']);
43
+ loadingCtrlSpy = jasmine.createSpyObj('LoadingController', ['create']);
44
+
45
+ await TestBed.configureTestingModule({
46
+ imports: [CommonModule, IonicModule.forRoot(), FormsModule, IntroComponent],
47
+ providers: [
48
+ { provide: ReferencePoseService, useValue: referencePoseServiceSpy },
49
+ { provide: OpenposeService, useValue: openposeServiceSpy },
50
+ { provide: PoseSelectionService, useValue: poseSelectionSpy },
51
+ { provide: LivenessConfigService, useValue: configServiceSpy },
52
+ { provide: ModalController, useValue: modalCtrlSpy },
53
+ { provide: AlertController, useValue: alertCtrlSpy },
54
+ { provide: ToastController, useValue: toastCtrlSpy },
55
+ { provide: LoadingController, useValue: loadingCtrlSpy },
56
+ ],
57
+ schemas: [NO_ERRORS_SCHEMA]
58
+ }).compileComponents();
59
+
60
+ fixture = TestBed.createComponent(IntroComponent);
61
+ component = fixture.componentInstance;
62
+ });
63
+
64
+ it('should create', () => {
65
+ expect(component).toBeTruthy();
66
+ });
67
+
68
+ it('has correct default state', () => {
69
+ expect(component.poseSpecificMode).toBe(false);
70
+ expect(component.viewMode).toBe('grid');
71
+ expect(component.showFilters).toBe(false);
72
+ expect(component.poseSelectionMode).toBe('browse');
73
+ expect(component.useAwsFaceLiveness).toBe(false);
74
+ });
75
+
76
+ it('displayImage returns exampleImageBase64 first', () => {
77
+ component.exampleImageBase64 = 'base64data';
78
+ component.exampleImageUrl = 'http://example.com/img.png';
79
+ expect(component.displayImage).toBe('base64data');
80
+ });
81
+
82
+ it('displayImage returns exampleImageUrl when no base64', () => {
83
+ component.exampleImageUrl = 'http://example.com/img.png';
84
+ expect(component.displayImage).toBe('http://example.com/img.png');
85
+ });
86
+
87
+ it('customPosesEnabled returns false when no repo', () => {
88
+ expect(component.customPosesEnabled).toBe(false);
89
+ });
90
+
91
+ it('categories returns expected array', () => {
92
+ expect(component.categories.length).toBeGreaterThan(0);
93
+ expect(component.categories[0].key).toBe('all');
94
+ });
95
+
96
+ it('difficulties returns expected array', () => {
97
+ expect(component.difficulties.length).toBe(4);
98
+ });
99
+
100
+ it('filteredPoses returns all poses by default', () => {
101
+ expect(component.filteredPoses.length).toBe(component.allPoses.length);
102
+ });
103
+
104
+ it('filteredPoses filters by category', () => {
105
+ component.selectedCategory = 'face';
106
+ const filtered = component.filteredPoses;
107
+ expect(filtered.every(p => p.category === 'face')).toBe(true);
108
+ });
109
+
110
+ it('filteredPoses filters by difficulty', () => {
111
+ component.selectedDifficulty = 'easy';
112
+ const filtered = component.filteredPoses;
113
+ expect(filtered.every(p => p.difficulty === 'easy')).toBe(true);
114
+ });
115
+
116
+ it('filteredPoses filters by search query', () => {
117
+ component.searchQuery = 'smile';
118
+ const filtered = component.filteredPoses;
119
+ expect(filtered.every(p =>
120
+ p.name.toLowerCase().includes('smile') || p.description.toLowerCase().includes('smile')
121
+ )).toBe(true);
122
+ });
123
+
124
+ it('ngOnInit loads OpenPose model', async () => {
125
+ await component.ngOnInit();
126
+ expect(openposeServiceSpy.loadModel).toHaveBeenCalled();
127
+ });
128
+
129
+ it('ngOnInit handles model load failure', async () => {
130
+ openposeServiceSpy.loadModel.and.returnValue(Promise.reject(new Error('Load failed')));
131
+ await component.ngOnInit();
132
+ expect(openposeServiceSpy.loadModel).toHaveBeenCalled();
133
+ });
134
+
135
+ it('onStart emits startClicked', () => {
136
+ const spy = jest.fn();
137
+ component.startClicked.subscribe(spy);
138
+ component.onStart();
139
+ expect(spy).toHaveBeenCalled();
140
+ });
141
+
142
+ it('onCancel emits cancelClicked', () => {
143
+ const spy = jest.fn();
144
+ component.cancelClicked.subscribe(spy);
145
+ component.onCancel();
146
+ expect(spy).toHaveBeenCalled();
147
+ });
148
+
149
+ it('toggleFilters toggles showFilters', () => {
150
+ expect(component.showFilters).toBe(false);
151
+ component.toggleFilters();
152
+ expect(component.showFilters).toBe(true);
153
+ component.toggleFilters();
154
+ expect(component.showFilters).toBe(false);
155
+ });
156
+
157
+ it('setViewMode changes viewMode', () => {
158
+ component.setViewMode('list');
159
+ expect(component.viewMode).toBe('list');
160
+ component.setViewMode('grid');
161
+ expect(component.viewMode).toBe('grid');
162
+ });
163
+
164
+ it('filterByCategory sets selectedCategory', () => {
165
+ component.filterByCategory('face');
166
+ expect(component.selectedCategory).toBe('face');
167
+ });
168
+
169
+ it('filterByDifficulty sets selectedDifficulty', () => {
170
+ component.filterByDifficulty('hard');
171
+ expect(component.selectedDifficulty).toBe('hard');
172
+ });
173
+
174
+ it('onSearchChange updates searchQuery', () => {
175
+ component.onSearchChange({ target: { value: 'test' } });
176
+ expect(component.searchQuery).toBe('test');
177
+ });
178
+
179
+ it('clearFilters resets all filters', () => {
180
+ component.selectedCategory = 'face';
181
+ component.selectedDifficulty = 'hard';
182
+ component.searchQuery = 'test';
183
+ component.clearFilters();
184
+ expect(component.selectedCategory).toBe('all');
185
+ expect(component.selectedDifficulty).toBe('all');
186
+ expect(component.searchQuery).toBe('');
187
+ });
188
+
189
+ it('getCategoryIcon returns icon string', () => {
190
+ expect(typeof component.getCategoryIcon('face')).toBe('string');
191
+ });
192
+
193
+ it('getCategoryColor returns color string', () => {
194
+ expect(typeof component.getCategoryColor('face')).toBe('string');
195
+ });
196
+
197
+ it('getDifficultyColor returns correct colors', () => {
198
+ expect(component.getDifficultyColor('easy')).toBe('success');
199
+ expect(component.getDifficultyColor('medium')).toBe('warning');
200
+ expect(component.getDifficultyColor('hard')).toBe('danger');
201
+ });
202
+
203
+ it('onImageError hides the image element', () => {
204
+ const mockEvent = { target: { src: 'broken.png', style: { display: '' } } };
205
+ component.onImageError(mockEvent);
206
+ expect(mockEvent.target.style.display).toBe('none');
207
+ });
208
+
209
+ it('applyBackendConfig applies selected backends', () => {
210
+ component.selectedBackends = { amazon: true, openpose: false, facetec: false };
211
+ component.applyBackendConfig();
212
+ expect(configServiceSpy.enableBackends).toHaveBeenCalledWith(['amazon']);
213
+ });
214
+
215
+ it('applyBackendConfig defaults to amazon when none selected', () => {
216
+ component.selectedBackends = { amazon: false, openpose: false, facetec: false };
217
+ component.applyBackendConfig();
218
+ expect(configServiceSpy.enableBackends).toHaveBeenCalledWith(['amazon']);
219
+ expect(component.selectedBackends['amazon']).toBe(true);
220
+ });
221
+
222
+ it('onBackendChange calls applyBackendConfig', () => {
223
+ const spy = spyOn(component, 'applyBackendConfig');
224
+ component.onBackendChange();
225
+ expect(spy).toHaveBeenCalled();
226
+ });
227
+
228
+ it('getSelectedBackendCount returns correct count', () => {
229
+ component.selectedBackends = { amazon: true, openpose: true, facetec: false };
230
+ expect(component.getSelectedBackendCount()).toBe(2);
231
+ });
232
+
233
+ it('getSelectedBackendName returns backend name', () => {
234
+ component.selectedBackends = { amazon: true, openpose: false, facetec: false };
235
+ expect(component.getSelectedBackendName()).toBe('Amazon Rekognition');
236
+ });
237
+
238
+ it('toggleBackendSettings toggles visibility', () => {
239
+ expect(component.showBackendSettings).toBe(false);
240
+ component.toggleBackendSettings();
241
+ expect(component.showBackendSettings).toBe(true);
242
+ });
243
+
244
+ it('onAwsFaceLivenessChange emits toggle value', () => {
245
+ const spy = jest.fn();
246
+ component.awsFaceLivenessToggled.subscribe(spy);
247
+ component.useAwsFaceLiveness = true;
248
+ component.onAwsFaceLivenessChange();
249
+ expect(spy).toHaveBeenCalledWith(true);
250
+ });
251
+
252
+ it('onModeChange updates poseSelectionMode', () => {
253
+ component.onModeChange({ detail: { value: 'upload' } });
254
+ expect(component.poseSelectionMode).toBe('upload');
255
+ });
256
+
257
+ it('onDragOver sets isDragging', () => {
258
+ const event = { preventDefault: jest.fn(), stopPropagation: jest.fn() } as any;
259
+ component.onDragOver(event);
260
+ expect(component.isDragging).toBe(true);
261
+ });
262
+
263
+ it('onDragLeave clears isDragging', () => {
264
+ component.isDragging = true;
265
+ const event = { preventDefault: jest.fn(), stopPropagation: jest.fn() } as any;
266
+ component.onDragLeave(event);
267
+ expect(component.isDragging).toBe(false);
268
+ });
269
+
270
+ it('onDrop with no files does nothing', async () => {
271
+ const event = { preventDefault: jest.fn(), stopPropagation: jest.fn(), dataTransfer: { files: [] } } as any;
272
+ await component.onDrop(event);
273
+ expect(component.isDragging).toBe(false);
274
+ });
275
+
276
+ it('onDrop with non-image file sets error status', async () => {
277
+ const event = {
278
+ preventDefault: jest.fn(),
279
+ stopPropagation: jest.fn(),
280
+ dataTransfer: { files: [{ type: 'text/plain', size: 100 }] }
281
+ } as any;
282
+ await component.onDrop(event);
283
+ expect(component.poseDetectionStatus?.success).toBe(false);
284
+ expect(component.poseDetectionStatus?.message).toContain('valid image');
285
+ });
286
+
287
+ it('onDrop with oversized file sets error status', async () => {
288
+ const event = {
289
+ preventDefault: jest.fn(),
290
+ stopPropagation: jest.fn(),
291
+ dataTransfer: { files: [{ type: 'image/png', size: 20 * 1024 * 1024 }] }
292
+ } as any;
293
+ await component.onDrop(event);
294
+ expect(component.poseDetectionStatus?.success).toBe(false);
295
+ expect(component.poseDetectionStatus?.message).toContain('too large');
296
+ });
297
+
298
+ it('onSelectPose selects pose and emits', () => {
299
+ const pose = component.allPoses[0];
300
+ poseSelectionSpy.getSelectedPose.and.returnValue({ pose, imageUrl: '', timestamp: new Date() });
301
+ const spy = jest.fn();
302
+ component.poseSelected.subscribe(spy);
303
+
304
+ component.onSelectPose(pose);
305
+
306
+ expect(poseSelectionSpy.selectPoseByObject).toHaveBeenCalledWith(pose);
307
+ expect(spy).toHaveBeenCalled();
308
+ });
309
+
310
+ it('clearPoseSelection clears selection', () => {
311
+ component.selectedPose = { pose: component.allPoses[0], imageUrl: '', timestamp: new Date() };
312
+ component.clearPoseSelection();
313
+ expect(poseSelectionSpy.clearSelection).toHaveBeenCalled();
314
+ expect(component.selectedPose).toBeNull();
315
+ });
316
+
317
+ it('isPoseSelected returns correct value', () => {
318
+ const pose = component.allPoses[0];
319
+ component.selectedPose = { pose, imageUrl: '', timestamp: new Date() };
320
+ expect(component.isPoseSelected(pose)).toBe(true);
321
+ expect(component.isPoseSelected(component.allPoses[1])).toBe(false);
322
+ });
323
+
324
+ it('clearReferencePose resets all reference state', () => {
325
+ component.referencePoseImage = 'data:image/png;base64,abc';
326
+ component.referencePoseDescription = 'Test';
327
+ component.poseDetectionStatus = { success: true, message: 'OK' };
328
+ component.isSavingCustomPose = true;
329
+
330
+ component.clearReferencePose();
331
+
332
+ expect(component.referencePoseImage).toBeNull();
333
+ expect(component.referencePoseDescription).toBeNull();
334
+ expect(component.poseDetectionStatus).toBeNull();
335
+ expect(component.isSavingCustomPose).toBe(false);
336
+ expect(component.poseSelectionMode).toBe('browse');
337
+ expect(referencePoseServiceSpy.clearReferencePose).toHaveBeenCalled();
338
+ });
339
+
340
+ it('onFileSelected with no files does nothing', async () => {
341
+ const event = { target: { files: null } } as any;
342
+ await component.onFileSelected(event);
343
+ expect(component.isProcessingImage).toBe(false);
344
+ });
345
+
346
+ it('onFileSelected with non-image file sets error', async () => {
347
+ const event = { target: { files: [{ type: 'text/plain', size: 100 }] } } as any;
348
+ await component.onFileSelected(event);
349
+ expect(component.poseDetectionStatus?.success).toBe(false);
350
+ expect(component.poseDetectionStatus?.message).toContain('valid image');
351
+ });
352
+
353
+ it('onFileSelected with oversized file sets error', async () => {
354
+ const event = { target: { files: [{ type: 'image/png', size: 20 * 1024 * 1024 }] } } as any;
355
+ await component.onFileSelected(event);
356
+ expect(component.poseDetectionStatus?.success).toBe(false);
357
+ expect(component.poseDetectionStatus?.message).toContain('too large');
358
+ });
359
+
360
+ it('onFileSelected with valid image processes it', async () => {
361
+ referencePoseServiceSpy.extractPoseFromImage.and.returnValue(Promise.resolve({
362
+ imageData: 'base64img',
363
+ poseDescription: 'Detected pose',
364
+ keypoints: []
365
+ }));
366
+ const file = new File(['data'], 'test.png', { type: 'image/png' });
367
+ const event = { target: { files: [file] } } as any;
368
+ await component.onFileSelected(event);
369
+ expect(component.referencePoseImage).toBe('base64img');
370
+ expect(component.referencePoseDescription).toBe('Detected pose');
371
+ expect(component.poseDetectionStatus?.success).toBe(true);
372
+ expect(component.isProcessingImage).toBe(false);
373
+ });
374
+
375
+ it('processReferenceImage handles extraction failure', async () => {
376
+ referencePoseServiceSpy.extractPoseFromImage.and.returnValue(Promise.reject(new Error('No pose found')));
377
+ const file = new File(['data'], 'test.png', { type: 'image/png' });
378
+ const event = { target: { files: [file] } } as any;
379
+ await component.onFileSelected(event);
380
+ expect(component.poseDetectionStatus?.success).toBe(false);
381
+ expect(component.poseDetectionStatus?.message).toBe('No pose found');
382
+ expect(component.referencePoseImage).toBeNull();
383
+ expect(component.isProcessingImage).toBe(false);
384
+ });
385
+
386
+ it('onDrop with valid image processes it', async () => {
387
+ referencePoseServiceSpy.extractPoseFromImage.and.returnValue(Promise.resolve({
388
+ imageData: 'base64img',
389
+ poseDescription: 'Detected pose',
390
+ keypoints: []
391
+ }));
392
+ const file = new File(['data'], 'test.png', { type: 'image/png' });
393
+ const event = {
394
+ preventDefault: jest.fn(),
395
+ stopPropagation: jest.fn(),
396
+ dataTransfer: { files: [file] }
397
+ } as any;
398
+ await component.onDrop(event);
399
+ expect(component.referencePoseImage).toBe('base64img');
400
+ });
401
+
402
+ it('onSelectPose clears reference pose if one exists', () => {
403
+ component.referencePoseImage = 'base64img';
404
+ const pose = component.allPoses[0];
405
+ poseSelectionSpy.getSelectedPose.and.returnValue({ pose, imageUrl: '', timestamp: new Date() });
406
+
407
+ component.onSelectPose(pose);
408
+
409
+ expect(referencePoseServiceSpy.clearReferencePose).toHaveBeenCalled();
410
+ expect(component.referencePoseImage).toBeNull();
411
+ });
412
+
413
+ it('onSaveCustomPose returns early when already saving', async () => {
414
+ component.isSavingCustomPose = true;
415
+ await component.onSaveCustomPose();
416
+ expect(toastCtrlSpy.create).not.toHaveBeenCalled();
417
+ });
418
+
419
+ it('onSaveCustomPose shows warning when no pose', async () => {
420
+ component.referencePoseImage = null;
421
+ toastCtrlSpy.create.and.returnValue(Promise.resolve({ present: jest.fn() }));
422
+ await component.onSaveCustomPose();
423
+ expect(toastCtrlSpy.create).toHaveBeenCalled();
424
+ });
425
+
426
+ it('onDeleteCustomPose returns early for non-custom pose', async () => {
427
+ const pose = { ...component.allPoses[0], isCustom: false };
428
+ const event = { stopPropagation: jest.fn() } as any;
429
+ await component.onDeleteCustomPose(pose, event);
430
+ expect(event.stopPropagation).toHaveBeenCalled();
431
+ expect(alertCtrlSpy.create).not.toHaveBeenCalled();
432
+ });
433
+
434
+ it('getSelectedBackendName returns Unknown for no selection', () => {
435
+ component.selectedBackends = { amazon: false, openpose: false, facetec: false };
436
+ expect(component.getSelectedBackendName()).toBe('Unknown');
437
+ });
438
+
439
+ it('onStart warns when no pose selected', () => {
440
+ component.selectedPose = null;
441
+ const spy = jest.fn();
442
+ component.startClicked.subscribe(spy);
443
+ component.onStart();
444
+ expect(spy).toHaveBeenCalled();
445
+ });
446
+
447
+ it('processReferenceImage clears existing pose selection', async () => {
448
+ component.selectedPose = { pose: component.allPoses[0], imageUrl: '', timestamp: new Date() };
449
+ referencePoseServiceSpy.extractPoseFromImage.and.returnValue(Promise.resolve({
450
+ imageData: 'base64img',
451
+ poseDescription: 'Test',
452
+ keypoints: []
453
+ }));
454
+ const file = new File(['data'], 'test.png', { type: 'image/png' });
455
+ const event = { target: { files: [file] } } as any;
456
+ await component.onFileSelected(event);
457
+ expect(poseSelectionSpy.clearSelection).toHaveBeenCalled();
458
+ });
459
+
460
+ it('customPosesEnabled returns false when no repo', () => {
461
+ expect(component.customPosesEnabled).toBe(false);
462
+ });
463
+
464
+ it('onSaveCustomPose shows warning when customPoses not enabled', async () => {
465
+ component.referencePoseImage = 'data:image/png;base64,abc';
466
+ component['referencePose'] = { poseDescription: 'Test', keypoints: [], imageData: '' } as any;
467
+ toastCtrlSpy.create.and.returnValue(Promise.resolve({ present: jest.fn() }));
468
+ await component.onSaveCustomPose();
469
+ expect(toastCtrlSpy.create).toHaveBeenCalled();
470
+ });
471
+
472
+ it('detectSafariBrowser sets isSafariBrowser', () => {
473
+ (component as any).detectSafariBrowser();
474
+ expect(typeof component.isSafariBrowser).toBe('boolean');
475
+ });
476
+ });
477
+
478
+ describe('IntroComponent with CustomPoseRepository', () => {
479
+ let component: IntroComponent;
480
+ let fixture: ComponentFixture<IntroComponent>;
481
+ let customPoseRepoSpy: any;
482
+ let modalCtrlSpy2: any;
483
+ let alertCtrlSpy2: any;
484
+ let toastCtrlSpy2: any;
485
+ let loadingCtrlSpy2: any;
486
+
487
+ beforeEach(async () => {
488
+ const poseSelectionSpy = {
489
+ selectPoseByObject: jest.fn(),
490
+ getSelectedPose: jest.fn().mockReturnValue(null),
491
+ clearSelection: jest.fn(),
492
+ selectedPose$: { subscribe: jest.fn((cb: any) => cb(null)) }
493
+ };
494
+
495
+ const configServiceSpy = {
496
+ enableBackends: jest.fn(),
497
+ getConfig: jest.fn().mockReturnValue({ backends: ['amazon'] })
498
+ };
499
+
500
+ const openposeServiceSpy = {
501
+ loadModel: jest.fn().mockResolvedValue(undefined)
502
+ };
503
+
504
+ const referencePoseServiceSpy = {
505
+ extractPoseFromImage: jest.fn(),
506
+ clearReferencePose: jest.fn()
507
+ };
508
+
509
+ customPoseRepoSpy = {
510
+ loadCustomPoses: jest.fn().mockReturnValue(of({ success: false, poses: [] })),
511
+ createCustomPoseFromBase64: jest.fn(),
512
+ deleteCustomPose: jest.fn()
513
+ };
514
+
515
+ modalCtrlSpy2 = { create: jest.fn() };
516
+ alertCtrlSpy2 = { create: jest.fn() };
517
+ toastCtrlSpy2 = { create: jest.fn() };
518
+ loadingCtrlSpy2 = { create: jest.fn() };
519
+
520
+ await TestBed.configureTestingModule({
521
+ imports: [CommonModule, IonicModule.forRoot(), FormsModule, IntroComponent],
522
+ providers: [
523
+ { provide: ReferencePoseService, useValue: referencePoseServiceSpy },
524
+ { provide: OpenposeService, useValue: openposeServiceSpy },
525
+ { provide: PoseSelectionService, useValue: poseSelectionSpy },
526
+ { provide: LivenessConfigService, useValue: configServiceSpy },
527
+ { provide: ModalController, useValue: modalCtrlSpy2 },
528
+ { provide: AlertController, useValue: alertCtrlSpy2 },
529
+ { provide: ToastController, useValue: toastCtrlSpy2 },
530
+ { provide: LoadingController, useValue: loadingCtrlSpy2 },
531
+ { provide: CUSTOM_POSE_REPOSITORY, useValue: customPoseRepoSpy },
532
+ ],
533
+ schemas: [NO_ERRORS_SCHEMA]
534
+ })
535
+ .overrideComponent(IntroComponent, {
536
+ set: {
537
+ providers: [
538
+ { provide: ModalController, useValue: modalCtrlSpy2 },
539
+ { provide: AlertController, useValue: alertCtrlSpy2 },
540
+ { provide: ToastController, useValue: toastCtrlSpy2 },
541
+ { provide: LoadingController, useValue: loadingCtrlSpy2 },
542
+ ]
543
+ }
544
+ })
545
+ .compileComponents();
546
+
547
+ fixture = TestBed.createComponent(IntroComponent);
548
+ component = fixture.componentInstance;
549
+ });
550
+
551
+ it('customPosesEnabled returns true when repo provided', () => {
552
+ expect(component.customPosesEnabled).toBe(true);
553
+ });
554
+
555
+ it('onSaveCustomPose saves new pose successfully', async () => {
556
+ component.referencePoseImage = 'data:image/png;base64,abc';
557
+ component['referencePose'] = { poseDescription: 'Test', keypoints: [], imageData: '' } as any;
558
+
559
+ modalCtrlSpy2.create.mockResolvedValue({
560
+ present: jest.fn().mockResolvedValue(undefined),
561
+ onWillDismiss: jest.fn().mockResolvedValue({
562
+ data: { name: 'My Pose', description: 'Test', category: 'face', difficulty: 'easy' }
563
+ })
564
+ });
565
+ loadingCtrlSpy2.create.mockResolvedValue({
566
+ present: jest.fn().mockResolvedValue(undefined),
567
+ dismiss: jest.fn().mockResolvedValue(undefined)
568
+ });
569
+ customPoseRepoSpy.createCustomPoseFromBase64.mockResolvedValue({
570
+ pose: { id: 1, name: 'My Pose', description: 'Test', category: 'face', difficulty: 'easy', imageUrl: '' },
571
+ isNew: true
572
+ });
573
+ toastCtrlSpy2.create.mockResolvedValue({ present: jest.fn().mockResolvedValue(undefined) });
574
+
575
+ await component.onSaveCustomPose();
576
+
577
+ expect(customPoseRepoSpy.createCustomPoseFromBase64).toHaveBeenCalled();
578
+ expect(toastCtrlSpy2.create).toHaveBeenCalled();
579
+ expect(component.isSavingCustomPose).toBe(false);
580
+ });
581
+
582
+ it('onSaveCustomPose handles duplicate pose', async () => {
583
+ component.referencePoseImage = 'data:image/png;base64,abc';
584
+ component['referencePose'] = { poseDescription: 'Test', keypoints: [], imageData: '' } as any;
585
+
586
+ modalCtrlSpy2.create.mockResolvedValue({
587
+ present: jest.fn().mockResolvedValue(undefined),
588
+ onWillDismiss: jest.fn().mockResolvedValue({
589
+ data: { name: 'My Pose', description: 'Test', category: 'face', difficulty: 'easy' }
590
+ })
591
+ });
592
+ loadingCtrlSpy2.create.mockResolvedValue({
593
+ present: jest.fn().mockResolvedValue(undefined),
594
+ dismiss: jest.fn().mockResolvedValue(undefined)
595
+ });
596
+ customPoseRepoSpy.createCustomPoseFromBase64.mockResolvedValue({
597
+ pose: { id: 1, name: 'Existing Pose' },
598
+ isNew: false
599
+ });
600
+ toastCtrlSpy2.create.mockResolvedValue({ present: jest.fn().mockResolvedValue(undefined) });
601
+
602
+ await component.onSaveCustomPose();
603
+
604
+ expect(toastCtrlSpy2.create).toHaveBeenCalled();
605
+ });
606
+
607
+ it('onSaveCustomPose handles user cancelling dialog', async () => {
608
+ component.referencePoseImage = 'data:image/png;base64,abc';
609
+ component['referencePose'] = { poseDescription: 'Test', keypoints: [], imageData: '' } as any;
610
+
611
+ modalCtrlSpy2.create.mockResolvedValue({
612
+ present: jest.fn().mockResolvedValue(undefined),
613
+ onWillDismiss: jest.fn().mockResolvedValue({ data: null })
614
+ });
615
+
616
+ await component.onSaveCustomPose();
617
+
618
+ expect(customPoseRepoSpy.createCustomPoseFromBase64).not.toHaveBeenCalled();
619
+ });
620
+
621
+ it('onSaveCustomPose handles API error', async () => {
622
+ component.referencePoseImage = 'data:image/png;base64,abc';
623
+ component['referencePose'] = { poseDescription: 'Test', keypoints: [], imageData: '' } as any;
624
+
625
+ modalCtrlSpy2.create.mockResolvedValue({
626
+ present: jest.fn().mockResolvedValue(undefined),
627
+ onWillDismiss: jest.fn().mockResolvedValue({
628
+ data: { name: 'My Pose', description: 'Test', category: 'face', difficulty: 'easy' }
629
+ })
630
+ });
631
+ loadingCtrlSpy2.create.mockResolvedValue({
632
+ present: jest.fn().mockResolvedValue(undefined),
633
+ dismiss: jest.fn().mockResolvedValue(undefined)
634
+ });
635
+ customPoseRepoSpy.createCustomPoseFromBase64.mockRejectedValue(new Error('API error'));
636
+ toastCtrlSpy2.create.mockResolvedValue({ present: jest.fn().mockResolvedValue(undefined) });
637
+
638
+ await component.onSaveCustomPose();
639
+
640
+ expect(component.isSavingCustomPose).toBe(false);
641
+ expect(toastCtrlSpy2.create).toHaveBeenCalled();
642
+ });
643
+
644
+ it('onDeleteCustomPose deletes custom pose', async () => {
645
+ const customPose = { id: 10001, name: 'Custom', isCustom: true } as any;
646
+
647
+ alertCtrlSpy2.create.mockResolvedValue({
648
+ present: jest.fn().mockResolvedValue(undefined),
649
+ onDidDismiss: jest.fn().mockResolvedValue({ role: 'destructive' })
650
+ });
651
+ loadingCtrlSpy2.create.mockResolvedValue({
652
+ present: jest.fn().mockResolvedValue(undefined),
653
+ dismiss: jest.fn().mockResolvedValue(undefined)
654
+ });
655
+ customPoseRepoSpy.deleteCustomPose.mockReturnValue({ toPromise: () => Promise.resolve() });
656
+ toastCtrlSpy2.create.mockResolvedValue({ present: jest.fn().mockResolvedValue(undefined) });
657
+
658
+ component.allPoses = [customPose];
659
+ const event = { stopPropagation: jest.fn() } as any;
660
+ await component.onDeleteCustomPose(customPose, event);
661
+
662
+ expect(customPoseRepoSpy.deleteCustomPose).toHaveBeenCalledWith(1);
663
+ expect(component.allPoses.length).toBe(0);
664
+ });
665
+
666
+ it('onDeleteCustomPose handles user cancellation', async () => {
667
+ const customPose = { id: 10001, name: 'Custom', isCustom: true } as any;
668
+
669
+ alertCtrlSpy2.create.mockResolvedValue({
670
+ present: jest.fn().mockResolvedValue(undefined),
671
+ onDidDismiss: jest.fn().mockResolvedValue({ role: 'cancel' })
672
+ });
673
+
674
+ const event = { stopPropagation: jest.fn() } as any;
675
+ await component.onDeleteCustomPose(customPose, event);
676
+
677
+ expect(customPoseRepoSpy.deleteCustomPose).not.toHaveBeenCalled();
678
+ });
679
+
680
+ it('onDeleteCustomPose handles API error', async () => {
681
+ const customPose = { id: 10001, name: 'Custom', isCustom: true } as any;
682
+
683
+ alertCtrlSpy2.create.mockResolvedValue({
684
+ present: jest.fn().mockResolvedValue(undefined),
685
+ onDidDismiss: jest.fn().mockResolvedValue({ role: 'destructive' })
686
+ });
687
+ loadingCtrlSpy2.create.mockResolvedValue({
688
+ present: jest.fn().mockResolvedValue(undefined),
689
+ dismiss: jest.fn().mockResolvedValue(undefined)
690
+ });
691
+ customPoseRepoSpy.deleteCustomPose.mockReturnValue({ toPromise: () => Promise.reject(new Error('Delete failed')) });
692
+ toastCtrlSpy2.create.mockResolvedValue({ present: jest.fn().mockResolvedValue(undefined) });
693
+
694
+ const event = { stopPropagation: jest.fn() } as any;
695
+ await component.onDeleteCustomPose(customPose, event);
696
+
697
+ expect(toastCtrlSpy2.create).toHaveBeenCalled();
698
+ });
699
+ });