@smileid/usesmileid 12.0.1 → 12.0.2
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.
- package/CHANGELOG.md +4 -0
- package/README.md +1 -1
- package/dist/camera/camera_defaults.d.ts +2 -0
- package/dist/camera/camera_defaults.d.ts.map +1 -1
- package/dist/camera/camera_defaults.js +2 -0
- package/dist/camera/camera_defaults.js.map +1 -1
- package/dist/core/exception/flow_exceptions.d.ts +4 -2
- package/dist/core/exception/flow_exceptions.d.ts.map +1 -1
- package/dist/core/exception/flow_exceptions.js +3 -2
- package/dist/core/exception/flow_exceptions.js.map +1 -1
- package/dist/core/exception/missing_job_type_exception.d.ts +1 -3
- package/dist/core/exception/missing_job_type_exception.d.ts.map +1 -1
- package/dist/core/exception/missing_job_type_exception.js +2 -4
- package/dist/core/exception/missing_job_type_exception.js.map +1 -1
- package/dist/data/api/interceptors/smile_auth_interceptor.js +1 -1
- package/dist/data/api/interceptors/smile_auth_interceptor.js.map +1 -1
- package/dist/data/api/smile_id_api.d.ts +4 -0
- package/dist/data/api/smile_id_api.d.ts.map +1 -1
- package/dist/data/api/smile_id_api.js +30 -27
- package/dist/data/api/smile_id_api.js.map +1 -1
- package/dist/data/model/job_request.d.ts +28 -12
- package/dist/data/model/job_request.d.ts.map +1 -1
- package/dist/data/model/token_payload.d.ts +78 -0
- package/dist/data/model/token_payload.d.ts.map +1 -0
- package/dist/data/model/token_payload.js +132 -0
- package/dist/data/model/token_payload.js.map +1 -0
- package/dist/data/model/user_details_payload.d.ts +39 -0
- package/dist/data/model/user_details_payload.d.ts.map +1 -0
- package/dist/data/model/user_details_payload.js +53 -0
- package/dist/data/model/user_details_payload.js.map +1 -0
- package/dist/data/util/use_smile_id_token_decoder.d.ts +15 -3
- package/dist/data/util/use_smile_id_token_decoder.d.ts.map +1 -1
- package/dist/data/util/use_smile_id_token_decoder.js +20 -8
- package/dist/data/util/use_smile_id_token_decoder.js.map +1 -1
- package/dist/l10n/strings_en.d.ts +4 -1
- package/dist/l10n/strings_en.d.ts.map +1 -1
- package/dist/l10n/strings_en.js +4 -1
- package/dist/l10n/strings_en.js.map +1 -1
- package/dist/l10n/use_smile_id_localizations.d.ts +10 -0
- package/dist/l10n/use_smile_id_localizations.d.ts.map +1 -1
- package/dist/l10n/use_smile_id_localizations.js +59 -3
- package/dist/l10n/use_smile_id_localizations.js.map +1 -1
- package/dist/metadata/metadata_configuration_mappers.js +1 -1
- package/dist/metadata/metadata_configuration_mappers.js.map +1 -1
- package/dist/metadata/use_smile_id_metadata_factory.js +1 -1
- package/dist/metadata/use_smile_id_metadata_factory.js.map +1 -1
- package/dist/presentation/components/use_smile_id_button.d.ts.map +1 -1
- package/dist/presentation/components/use_smile_id_button.js +8 -2
- package/dist/presentation/components/use_smile_id_button.js.map +1 -1
- package/dist/presentation/flow/dsl/builder/screens_builder.d.ts +3 -6
- package/dist/presentation/flow/dsl/builder/screens_builder.d.ts.map +1 -1
- package/dist/presentation/flow/dsl/builder/screens_builder.js +4 -6
- package/dist/presentation/flow/dsl/builder/screens_builder.js.map +1 -1
- package/dist/presentation/flow/dsl/use_smile_id_flow_builder.d.ts.map +1 -1
- package/dist/presentation/flow/dsl/use_smile_id_flow_builder.js +23 -1
- package/dist/presentation/flow/dsl/use_smile_id_flow_builder.js.map +1 -1
- package/dist/presentation/flow/flow_navigation_manager.d.ts +17 -2
- package/dist/presentation/flow/flow_navigation_manager.d.ts.map +1 -1
- package/dist/presentation/flow/flow_navigation_manager.js +77 -45
- package/dist/presentation/flow/flow_navigation_manager.js.map +1 -1
- package/dist/presentation/flow/screen/orchestrated_processing_screen.d.ts.map +1 -1
- package/dist/presentation/flow/screen/orchestrated_processing_screen.js +25 -5
- package/dist/presentation/flow/screen/orchestrated_processing_screen.js.map +1 -1
- package/dist/presentation/flow/use_smile_id_flow.d.ts.map +1 -1
- package/dist/presentation/flow/use_smile_id_flow.js +35 -8
- package/dist/presentation/flow/use_smile_id_flow.js.map +1 -1
- package/dist/presentation/flow/validation/flow_validator.d.ts +16 -2
- package/dist/presentation/flow/validation/flow_validator.d.ts.map +1 -1
- package/dist/presentation/flow/validation/flow_validator.js +19 -27
- package/dist/presentation/flow/validation/flow_validator.js.map +1 -1
- package/dist/presentation/flow/validation/job_type_validator.d.ts +7 -0
- package/dist/presentation/flow/validation/job_type_validator.d.ts.map +1 -1
- package/dist/presentation/flow/validation/job_type_validator.js +52 -27
- package/dist/presentation/flow/validation/job_type_validator.js.map +1 -1
- package/dist/presentation/flow/validation/user_details_validator.d.ts +9 -0
- package/dist/presentation/flow/validation/user_details_validator.d.ts.map +1 -0
- package/dist/presentation/flow/validation/user_details_validator.js +40 -0
- package/dist/presentation/flow/validation/user_details_validator.js.map +1 -0
- package/dist/presentation/screen/helpers/capture_screen_helpers.d.ts.map +1 -1
- package/dist/presentation/screen/helpers/capture_screen_helpers.js +1 -0
- package/dist/presentation/screen/helpers/capture_screen_helpers.js.map +1 -1
- package/dist/presentation/screen/use_smile_id_capture_screen.d.ts.map +1 -1
- package/dist/presentation/screen/use_smile_id_capture_screen.js +64 -18
- package/dist/presentation/screen/use_smile_id_capture_screen.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/react_native_mock.tsx +22 -0
- package/src/__tests__/snapshots/__snapshots__/screens_structural.test.tsx.snap +14 -8
- package/src/camera/__tests__/camera_permissions.test.ts +72 -0
- package/src/camera/__tests__/camera_system_focus.test.ts +173 -0
- package/src/camera/__tests__/use_smile_id_camera_preview.test.tsx +461 -0
- package/src/camera/__tests__/use_smile_id_frame_stream_hook.test.tsx +229 -0
- package/src/camera/camera_defaults.ts +3 -0
- package/src/core/exception/__tests__/builder_validation_exceptions.test.ts +14 -0
- package/src/core/exception/flow_exceptions.test.ts +10 -0
- package/src/core/exception/flow_exceptions.ts +3 -2
- package/src/core/exception/missing_job_type_exception.ts +2 -4
- package/src/data/api/__tests__/smile_id_api.test.ts +275 -1
- package/src/data/api/interceptors/__tests__/credential_header_redaction.test.ts +144 -0
- package/src/data/api/interceptors/smile_auth_interceptor.ts +1 -1
- package/src/data/api/smile_id_api.ts +36 -49
- package/src/data/dsl/__tests__/use_smile_id_network_builder.test.ts +157 -0
- package/src/data/factory/__tests__/signed_job_submission_headers.test.ts +1 -1
- package/src/data/model/__tests__/token_payload.test.ts +224 -0
- package/src/data/model/__tests__/user_details_payload.test.ts +100 -0
- package/src/data/model/job_request.ts +27 -12
- package/src/data/model/token_payload.ts +182 -0
- package/src/data/model/user_details_payload.ts +84 -0
- package/src/data/repository/__tests__/network_retries.test.ts +1 -1
- package/src/data/repository/__tests__/repository_dispatch.test.ts +366 -0
- package/src/data/repository/__tests__/use_smile_id_repository.test.ts +1 -1
- package/src/data/security/__tests__/signing_crypto.test.ts +203 -0
- package/src/data/util/__tests__/use_smile_id_token_decoder.test.ts +179 -0
- package/src/data/util/use_smile_id_token_decoder.ts +29 -8
- package/src/l10n/__tests__/use_smile_id_localizations.test.ts +122 -5
- package/src/l10n/__tests__/use_smile_id_localizations_provider.test.tsx +112 -0
- package/src/l10n/__tests__/use_smile_id_localizations_surface.test.ts +231 -0
- package/src/l10n/strings_en.ts +5 -1
- package/src/l10n/use_smile_id_localizations.ts +80 -3
- package/src/metadata/__tests__/device_orientation_sampling.test.ts +165 -0
- package/src/metadata/__tests__/geolocation.test.ts +150 -0
- package/src/metadata/__tests__/metadata_configuration_objects.test.ts +1 -1
- package/src/metadata/__tests__/metadata_device_context.test.ts +3 -3
- package/src/metadata/__tests__/metadata_golden_fixture.json +1 -1
- package/src/metadata/__tests__/metadata_payload_golden.test.ts +4 -4
- package/src/metadata/__tests__/optional_native_modules.test.ts +234 -0
- package/src/metadata/__tests__/use_smile_id_metadata.test.ts +1 -1
- package/src/metadata/__tests__/use_smile_id_metadata_factory.test.ts +615 -0
- package/src/metadata/metadata_configuration_mappers.ts +1 -1
- package/src/metadata/use_smile_id_metadata_factory.ts +1 -1
- package/src/presentation/components/__tests__/use_smile_id_content_card.test.tsx +112 -0
- package/src/presentation/components/__tests__/use_smile_id_face_oval_image.test.tsx +190 -0
- package/src/presentation/components/__tests__/use_smile_id_learn_more_content.test.tsx +159 -0
- package/src/presentation/components/__tests__/use_smile_id_processing_center_indicator.test.tsx +88 -0
- package/src/presentation/components/use_smile_id_button.tsx +7 -2
- package/src/presentation/flow/__tests__/flow_navigation_manager.test.ts +323 -1
- package/src/presentation/flow/__tests__/use_smile_id_flow.test.tsx +651 -0
- package/src/presentation/flow/dsl/__tests__/use_smile_id_builder_component.test.tsx +120 -0
- package/src/presentation/flow/dsl/builder/screens_builder.ts +4 -6
- package/src/presentation/flow/dsl/use_smile_id_flow_builder.ts +27 -1
- package/src/presentation/flow/flow_navigation_manager.ts +102 -92
- package/src/presentation/flow/graph/__tests__/screen_renderer.test.tsx +102 -0
- package/src/presentation/flow/screen/__tests__/orchestrated_capture_screen.test.tsx +836 -0
- package/src/presentation/flow/screen/__tests__/orchestrated_consent_screen.test.tsx +165 -0
- package/src/presentation/flow/screen/__tests__/orchestrated_instructions_screen.test.tsx +163 -0
- package/src/presentation/flow/screen/__tests__/orchestrated_preview_screen.test.tsx +224 -0
- package/src/presentation/flow/screen/__tests__/orchestrated_processing_screen.test.tsx +338 -0
- package/src/presentation/flow/screen/orchestrated_processing_screen.tsx +42 -5
- package/src/presentation/flow/use_smile_id_flow.tsx +52 -8
- package/src/presentation/flow/validation/__tests__/flow_validator.test.ts +158 -0
- package/src/presentation/flow/validation/__tests__/job_type_validator.test.ts +200 -0
- package/src/presentation/flow/validation/flow_validator.ts +30 -31
- package/src/presentation/flow/validation/job_type_validator.ts +73 -39
- package/src/presentation/flow/validation/user_details_validator.ts +53 -0
- package/src/presentation/screen/__tests__/feedback_copy_table.test.ts +145 -0
- package/src/presentation/screen/__tests__/use_smile_id_capture_screen.test.tsx +348 -0
- package/src/presentation/screen/__tests__/use_smile_id_document_back_instructions.test.tsx +3 -1
- package/src/presentation/screen/__tests__/use_smile_id_permission_screen.test.tsx +68 -0
- package/src/presentation/screen/helpers/capture_screen_helpers.ts +1 -0
- package/src/presentation/screen/use_smile_id_capture_screen.tsx +69 -18
- package/src/presentation/theme/__tests__/use_use_smile_id_fonts.test.tsx +166 -0
|
@@ -0,0 +1,836 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { create as createRenderer, act } from "react-test-renderer";
|
|
4
|
+
|
|
5
|
+
// `vi.mock` is hoisted above the imports, so anything its factory closes over
|
|
6
|
+
// must be created with `vi.hoisted` or it is still in the TDZ when the factory
|
|
7
|
+
// runs.
|
|
8
|
+
const {
|
|
9
|
+
captureScreenProps,
|
|
10
|
+
backInstructionsProps,
|
|
11
|
+
orientationLockCalls,
|
|
12
|
+
setContext,
|
|
13
|
+
finalize,
|
|
14
|
+
signals,
|
|
15
|
+
} = vi.hoisted(() => ({
|
|
16
|
+
captureScreenProps: {
|
|
17
|
+
current: undefined as Record<string, unknown> | undefined,
|
|
18
|
+
},
|
|
19
|
+
backInstructionsProps: {
|
|
20
|
+
current: undefined as Record<string, unknown> | undefined,
|
|
21
|
+
},
|
|
22
|
+
orientationLockCalls: { current: [] as unknown[][] },
|
|
23
|
+
setContext: vi.fn(),
|
|
24
|
+
finalize: vi.fn(),
|
|
25
|
+
signals: vi.fn(),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
vi.mock("@smileid/usesmileid_bridge", async () => {
|
|
29
|
+
const actual = await vi.importActual<
|
|
30
|
+
typeof import("@smileid/usesmileid_bridge")
|
|
31
|
+
>("@smileid/usesmileid_bridge");
|
|
32
|
+
return { ...actual, DeviceIntegrityProvider: { signals } };
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
vi.mock("@smileid/usesmileid_bridge/src/exif/smile_exif_tagger", () => ({
|
|
36
|
+
SmileExifTagger: { setContext, finalize },
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
// The real hook drives `expo-screen-orientation`; stand in for it so the
|
|
40
|
+
// orientation the orchestrator *asks* for is observable without a device.
|
|
41
|
+
vi.mock("../../../screen/helpers/use_capture_orientation_lock", () => ({
|
|
42
|
+
useCaptureOrientationLock: (...args: unknown[]) => {
|
|
43
|
+
orientationLockCalls.current.push(args);
|
|
44
|
+
},
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
// Stand in for the leaf screens so these tests exercise the orchestration
|
|
48
|
+
// (analyzer lifecycle, side tracking, EXIF + persistence, navigation) rather
|
|
49
|
+
// than re-testing the camera UI, which has its own component suite.
|
|
50
|
+
vi.mock("../../../screen/use_smile_id_capture_screen", () => ({
|
|
51
|
+
UseSmileIDCaptureScreen: (props: Record<string, unknown>) => {
|
|
52
|
+
captureScreenProps.current = props;
|
|
53
|
+
return null;
|
|
54
|
+
},
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
vi.mock("../../../screen/use_smile_id_document_back_instructions", () => ({
|
|
58
|
+
UseSmileIDDocumentBackInstructions: (props: Record<string, unknown>) => {
|
|
59
|
+
backInstructionsProps.current = props;
|
|
60
|
+
return null;
|
|
61
|
+
},
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
import {
|
|
65
|
+
CaptureReadyEventKind,
|
|
66
|
+
DocumentSide,
|
|
67
|
+
ImageType,
|
|
68
|
+
} from "@smileid/usesmileid_bridge";
|
|
69
|
+
import type { CaptureReadyEvent } from "@smileid/usesmileid_bridge";
|
|
70
|
+
import { OrchestratedCaptureScreen } from "../orchestrated_capture_screen";
|
|
71
|
+
import { FlowNavigationProvider } from "../../flow_navigation_context";
|
|
72
|
+
import {
|
|
73
|
+
CaptureConfigBuilder,
|
|
74
|
+
CaptureType,
|
|
75
|
+
} from "../../dsl/builder/screens_builder";
|
|
76
|
+
import { DocumentType } from "../../../../core/models/document_type";
|
|
77
|
+
import type { FlowNavigationManager } from "../../flow_navigation_manager";
|
|
78
|
+
|
|
79
|
+
type Fn = ReturnType<typeof vi.fn>;
|
|
80
|
+
|
|
81
|
+
type FakeManager = FlowNavigationManager & {
|
|
82
|
+
navigateToNext: Fn;
|
|
83
|
+
navigateBack: Fn;
|
|
84
|
+
exit: Fn;
|
|
85
|
+
setCapturedSelfie: Fn;
|
|
86
|
+
setCapturedFrontDocument: Fn;
|
|
87
|
+
setCapturedBackDocument: Fn;
|
|
88
|
+
skipDocumentBack: Fn;
|
|
89
|
+
recordDocumentImageOrigin: Fn;
|
|
90
|
+
recordSelfieCaptureTelemetry: Fn;
|
|
91
|
+
recordDocumentCaptureTelemetry: Fn;
|
|
92
|
+
documentPreviewFollowsCapture: Fn;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
function makeAnalyzer(key?: string) {
|
|
96
|
+
const analyzer: Record<string, unknown> = {
|
|
97
|
+
configure: vi.fn(() => Promise.resolve()),
|
|
98
|
+
close: vi.fn(() => Promise.resolve()),
|
|
99
|
+
};
|
|
100
|
+
if (key !== undefined) analyzer.key = key;
|
|
101
|
+
return analyzer as { configure: Fn; close: Fn };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
type ManagerOverrides = {
|
|
105
|
+
readonly selfieAnalyzer?: unknown;
|
|
106
|
+
readonly documentAnalyzer?: unknown;
|
|
107
|
+
readonly selfieConfig?: Record<string, unknown>;
|
|
108
|
+
readonly frontDocumentPath?: string;
|
|
109
|
+
readonly previewFollows?: boolean;
|
|
110
|
+
readonly resolvedSdkConfig?: Record<string, unknown>;
|
|
111
|
+
readonly exitOrientations?: unknown;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
function makeManager(overrides: ManagerOverrides = {}): FakeManager {
|
|
115
|
+
const analyzers: Record<string, unknown> = {};
|
|
116
|
+
if (overrides.selfieAnalyzer !== undefined) {
|
|
117
|
+
analyzers.selfie = overrides.selfieAnalyzer;
|
|
118
|
+
}
|
|
119
|
+
if (overrides.documentAnalyzer !== undefined) {
|
|
120
|
+
analyzers.document = overrides.documentAnalyzer;
|
|
121
|
+
}
|
|
122
|
+
if (overrides.selfieConfig !== undefined) {
|
|
123
|
+
analyzers.selfieConfig = overrides.selfieConfig;
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
navigateToNext: vi.fn(),
|
|
127
|
+
navigateBack: vi.fn(),
|
|
128
|
+
exit: vi.fn(),
|
|
129
|
+
setCapturedSelfie: vi.fn(),
|
|
130
|
+
setCapturedFrontDocument: vi.fn(),
|
|
131
|
+
setCapturedBackDocument: vi.fn(),
|
|
132
|
+
skipDocumentBack: vi.fn(),
|
|
133
|
+
recordDocumentImageOrigin: vi.fn(),
|
|
134
|
+
recordSelfieCaptureTelemetry: vi.fn(),
|
|
135
|
+
recordDocumentCaptureTelemetry: vi.fn(),
|
|
136
|
+
documentPreviewFollowsCapture: vi.fn(
|
|
137
|
+
() => overrides.previewFollows ?? false,
|
|
138
|
+
),
|
|
139
|
+
partnerId: "002",
|
|
140
|
+
resolvedSdkConfig: overrides.resolvedSdkConfig,
|
|
141
|
+
captured: {
|
|
142
|
+
...(overrides.frontDocumentPath !== undefined
|
|
143
|
+
? { frontDocumentPath: overrides.frontDocumentPath }
|
|
144
|
+
: {}),
|
|
145
|
+
},
|
|
146
|
+
configuration: {
|
|
147
|
+
ml: { analyzers },
|
|
148
|
+
config:
|
|
149
|
+
overrides.exitOrientations !== undefined
|
|
150
|
+
? { exitOrientations: overrides.exitOrientations }
|
|
151
|
+
: {},
|
|
152
|
+
},
|
|
153
|
+
} as unknown as FakeManager;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function selfieConfiguration(
|
|
157
|
+
block: (selfie: {
|
|
158
|
+
allowAgentMode: boolean;
|
|
159
|
+
enableEnhancedLiveness: boolean;
|
|
160
|
+
}) => void = () => {},
|
|
161
|
+
): CaptureConfigBuilder {
|
|
162
|
+
const configuration = new CaptureConfigBuilder();
|
|
163
|
+
configuration.captureType = CaptureType.selfie;
|
|
164
|
+
configuration.selfie(block);
|
|
165
|
+
return configuration;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function documentConfiguration(
|
|
169
|
+
block: (document: {
|
|
170
|
+
documentType?: unknown;
|
|
171
|
+
captureBothSides: boolean;
|
|
172
|
+
allowSkipBack: boolean;
|
|
173
|
+
allowGalleryUpload: boolean;
|
|
174
|
+
knownIdAspectRatio?: number;
|
|
175
|
+
}) => void,
|
|
176
|
+
): CaptureConfigBuilder {
|
|
177
|
+
const configuration = new CaptureConfigBuilder();
|
|
178
|
+
configuration.captureType = CaptureType.document;
|
|
179
|
+
configuration.document(
|
|
180
|
+
block as unknown as Parameters<CaptureConfigBuilder["document"]>[0],
|
|
181
|
+
);
|
|
182
|
+
return configuration;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function renderWith(
|
|
186
|
+
manager: FlowNavigationManager,
|
|
187
|
+
configuration: CaptureConfigBuilder,
|
|
188
|
+
) {
|
|
189
|
+
let renderer!: ReturnType<typeof createRenderer>;
|
|
190
|
+
act(() => {
|
|
191
|
+
renderer = createRenderer(
|
|
192
|
+
<FlowNavigationProvider manager={manager}>
|
|
193
|
+
<OrchestratedCaptureScreen configuration={configuration} />
|
|
194
|
+
</FlowNavigationProvider>,
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
return renderer;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Drain the EXIF tagging promise chain kicked off by `onCaptureComplete`. */
|
|
201
|
+
async function settle() {
|
|
202
|
+
await act(async () => {
|
|
203
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function readCaptureProps(): Record<string, unknown> {
|
|
208
|
+
const props = captureScreenProps.current;
|
|
209
|
+
if (!props) throw new Error("UseSmileIDCaptureScreen was not rendered");
|
|
210
|
+
return props;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function readBackInstructionsProps(): Record<string, unknown> {
|
|
214
|
+
const props = backInstructionsProps.current;
|
|
215
|
+
if (!props) {
|
|
216
|
+
throw new Error("UseSmileIDDocumentBackInstructions was not rendered");
|
|
217
|
+
}
|
|
218
|
+
return props;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function completeCapture(
|
|
222
|
+
events: readonly CaptureReadyEvent[],
|
|
223
|
+
telemetry: Record<string, unknown> = { durationMs: 1200, retries: 0 },
|
|
224
|
+
) {
|
|
225
|
+
const onCaptureComplete = readCaptureProps().onCaptureComplete as (
|
|
226
|
+
events: readonly CaptureReadyEvent[],
|
|
227
|
+
telemetry: unknown,
|
|
228
|
+
) => void;
|
|
229
|
+
await act(async () => {
|
|
230
|
+
onCaptureComplete(events, telemetry);
|
|
231
|
+
});
|
|
232
|
+
await settle();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const selfieEvent = {
|
|
236
|
+
kind: CaptureReadyEventKind.Selfie,
|
|
237
|
+
url: "file:///selfie.jpg",
|
|
238
|
+
} as CaptureReadyEvent;
|
|
239
|
+
const livenessEvents = [
|
|
240
|
+
{ kind: CaptureReadyEventKind.Liveness, url: "file:///liveness-0.jpg" },
|
|
241
|
+
{ kind: CaptureReadyEventKind.Liveness, url: "file:///liveness-1.jpg" },
|
|
242
|
+
] as CaptureReadyEvent[];
|
|
243
|
+
const documentEvent = {
|
|
244
|
+
kind: CaptureReadyEventKind.Document,
|
|
245
|
+
url: "file:///document.jpg",
|
|
246
|
+
} as CaptureReadyEvent;
|
|
247
|
+
|
|
248
|
+
beforeEach(() => {
|
|
249
|
+
captureScreenProps.current = undefined;
|
|
250
|
+
backInstructionsProps.current = undefined;
|
|
251
|
+
orientationLockCalls.current = [];
|
|
252
|
+
setContext.mockReset();
|
|
253
|
+
setContext.mockResolvedValue(undefined);
|
|
254
|
+
finalize.mockReset();
|
|
255
|
+
finalize.mockResolvedValue(undefined);
|
|
256
|
+
signals.mockReset();
|
|
257
|
+
signals.mockResolvedValue({ fingerprint: "device-fp" });
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe("OrchestratedCaptureScreen — selfie", () => {
|
|
261
|
+
it("configures the selfie analyzer with the screen's enhanced-liveness flag", () => {
|
|
262
|
+
const analyzer = makeAnalyzer("vision-face");
|
|
263
|
+
const manager = makeManager({ selfieAnalyzer: analyzer });
|
|
264
|
+
|
|
265
|
+
renderWith(
|
|
266
|
+
manager,
|
|
267
|
+
selfieConfiguration((selfie) => {
|
|
268
|
+
selfie.enableEnhancedLiveness = true;
|
|
269
|
+
}),
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
expect(analyzer.configure).toHaveBeenCalledTimes(1);
|
|
273
|
+
const [config] = analyzer.configure.mock.calls[0] as [
|
|
274
|
+
Record<string, unknown>,
|
|
275
|
+
];
|
|
276
|
+
expect(config.enableEnhancedLiveness).toBe(true);
|
|
277
|
+
expect(config.analyzerFactoryKey).toBe("vision-face");
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("merges the registry-built base selfie config rather than replacing it", () => {
|
|
281
|
+
const analyzer = makeAnalyzer("vision-face");
|
|
282
|
+
const manager = makeManager({
|
|
283
|
+
selfieAnalyzer: analyzer,
|
|
284
|
+
selfieConfig: { analyzerFactoryKey: "vision-face", numSamples: 8 },
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
renderWith(manager, selfieConfiguration());
|
|
288
|
+
|
|
289
|
+
const [config] = analyzer.configure.mock.calls[0] as [
|
|
290
|
+
Record<string, unknown>,
|
|
291
|
+
];
|
|
292
|
+
expect(config.numSamples).toBe(8);
|
|
293
|
+
expect(config.enableEnhancedLiveness).toBe(false);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("closes the analyzer when the screen unmounts", () => {
|
|
297
|
+
const analyzer = makeAnalyzer("vision-face");
|
|
298
|
+
const renderer = renderWith(
|
|
299
|
+
makeManager({ selfieAnalyzer: analyzer }),
|
|
300
|
+
selfieConfiguration(),
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
expect(analyzer.close).not.toHaveBeenCalled();
|
|
304
|
+
act(() => {
|
|
305
|
+
renderer.unmount();
|
|
306
|
+
});
|
|
307
|
+
expect(analyzer.close).toHaveBeenCalledTimes(1);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("locks portrait for selfie capture", () => {
|
|
311
|
+
renderWith(
|
|
312
|
+
makeManager({ selfieAnalyzer: makeAnalyzer("vision-face") }),
|
|
313
|
+
selfieConfiguration(),
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
expect(orientationLockCalls.current[0]?.[0]).toBe("Portrait");
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("threads the partner's exitOrientations into the orientation lock", () => {
|
|
320
|
+
renderWith(
|
|
321
|
+
makeManager({
|
|
322
|
+
selfieAnalyzer: makeAnalyzer("vision-face"),
|
|
323
|
+
exitOrientations: 5,
|
|
324
|
+
}),
|
|
325
|
+
selfieConfiguration(),
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
expect(orientationLockCalls.current[0]?.[1]).toEqual({ exitLock: 5 });
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it("persists the selfie plus its liveness frames, then advances", async () => {
|
|
332
|
+
const manager = makeManager({
|
|
333
|
+
selfieAnalyzer: makeAnalyzer("vision-face"),
|
|
334
|
+
});
|
|
335
|
+
renderWith(manager, selfieConfiguration());
|
|
336
|
+
|
|
337
|
+
await completeCapture([selfieEvent, ...livenessEvents], {
|
|
338
|
+
durationMs: 900,
|
|
339
|
+
retries: 2,
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
expect(manager.setCapturedSelfie).toHaveBeenCalledWith(
|
|
343
|
+
"file:///selfie.jpg",
|
|
344
|
+
["file:///liveness-0.jpg", "file:///liveness-1.jpg"],
|
|
345
|
+
);
|
|
346
|
+
expect(manager.recordSelfieCaptureTelemetry).toHaveBeenCalledWith({
|
|
347
|
+
durationMs: 900,
|
|
348
|
+
retries: 2,
|
|
349
|
+
});
|
|
350
|
+
expect(manager.navigateToNext).toHaveBeenCalledTimes(1);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("stamps EXIF on the selfie and on every liveness frame", async () => {
|
|
354
|
+
renderWith(
|
|
355
|
+
makeManager({ selfieAnalyzer: makeAnalyzer("vision-face") }),
|
|
356
|
+
selfieConfiguration(),
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
await completeCapture([selfieEvent, ...livenessEvents]);
|
|
360
|
+
|
|
361
|
+
expect(setContext).toHaveBeenCalledWith({
|
|
362
|
+
partnerId: "002",
|
|
363
|
+
fingerprint: "device-fp",
|
|
364
|
+
});
|
|
365
|
+
expect(finalize).toHaveBeenCalledWith(
|
|
366
|
+
"file:///selfie.jpg",
|
|
367
|
+
ImageType.Selfie,
|
|
368
|
+
);
|
|
369
|
+
expect(finalize).toHaveBeenCalledWith(
|
|
370
|
+
"file:///liveness-0.jpg",
|
|
371
|
+
ImageType.Liveness,
|
|
372
|
+
);
|
|
373
|
+
expect(finalize).toHaveBeenCalledWith(
|
|
374
|
+
"file:///liveness-1.jpg",
|
|
375
|
+
ImageType.Liveness,
|
|
376
|
+
);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("omits a blank device fingerprint so it can't shadow a real one", async () => {
|
|
380
|
+
signals.mockResolvedValue({ fingerprint: "" });
|
|
381
|
+
renderWith(
|
|
382
|
+
makeManager({ selfieAnalyzer: makeAnalyzer("vision-face") }),
|
|
383
|
+
selfieConfiguration(),
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
await completeCapture([selfieEvent]);
|
|
387
|
+
|
|
388
|
+
expect(setContext).toHaveBeenCalledWith({ partnerId: "002" });
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("stays on the screen when the pipeline emits no selfie artifact", async () => {
|
|
392
|
+
const manager = makeManager({
|
|
393
|
+
selfieAnalyzer: makeAnalyzer("vision-face"),
|
|
394
|
+
});
|
|
395
|
+
renderWith(manager, selfieConfiguration());
|
|
396
|
+
|
|
397
|
+
await completeCapture(livenessEvents);
|
|
398
|
+
|
|
399
|
+
expect(manager.setCapturedSelfie).not.toHaveBeenCalled();
|
|
400
|
+
expect(manager.navigateToNext).not.toHaveBeenCalled();
|
|
401
|
+
expect(setContext).not.toHaveBeenCalled();
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it("still persists and advances when EXIF tagging rejects (fail-open)", async () => {
|
|
405
|
+
finalize.mockRejectedValue(new Error("native tagger unavailable"));
|
|
406
|
+
const manager = makeManager({
|
|
407
|
+
selfieAnalyzer: makeAnalyzer("vision-face"),
|
|
408
|
+
});
|
|
409
|
+
renderWith(manager, selfieConfiguration());
|
|
410
|
+
|
|
411
|
+
await completeCapture([selfieEvent]);
|
|
412
|
+
|
|
413
|
+
expect(manager.setCapturedSelfie).toHaveBeenCalledWith(
|
|
414
|
+
"file:///selfie.jpg",
|
|
415
|
+
[],
|
|
416
|
+
);
|
|
417
|
+
expect(manager.navigateToNext).toHaveBeenCalledTimes(1);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it("forwards agent mode and enhanced liveness to the capture screen", () => {
|
|
421
|
+
renderWith(
|
|
422
|
+
makeManager({ selfieAnalyzer: makeAnalyzer("vision-face") }),
|
|
423
|
+
selfieConfiguration((selfie) => {
|
|
424
|
+
selfie.allowAgentMode = true;
|
|
425
|
+
selfie.enableEnhancedLiveness = true;
|
|
426
|
+
}),
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
const props = readCaptureProps();
|
|
430
|
+
expect(props.allowAgentMode).toBe(true);
|
|
431
|
+
expect(props.enableEnhancedLiveness).toBe(true);
|
|
432
|
+
expect(props.captureType).toBe(CaptureType.selfie);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it("does not pass document-only props on a selfie capture", () => {
|
|
436
|
+
renderWith(
|
|
437
|
+
makeManager({ selfieAnalyzer: makeAnalyzer("vision-face") }),
|
|
438
|
+
selfieConfiguration(),
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
const props = readCaptureProps();
|
|
442
|
+
expect("documentSide" in props).toBe(false);
|
|
443
|
+
expect("knownIdAspectRatio" in props).toBe(false);
|
|
444
|
+
expect("onClose" in props).toBe(false);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it("omits the startButton slot when the partner supplies none", () => {
|
|
448
|
+
renderWith(
|
|
449
|
+
makeManager({ selfieAnalyzer: makeAnalyzer("vision-face") }),
|
|
450
|
+
selfieConfiguration(),
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
expect("startButton" in readCaptureProps()).toBe(false);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it("passes a partner startButton slot straight through", () => {
|
|
457
|
+
const startButton = vi.fn(() => null);
|
|
458
|
+
const configuration = selfieConfiguration();
|
|
459
|
+
configuration.startButton =
|
|
460
|
+
startButton as unknown as CaptureConfigBuilder["startButton"];
|
|
461
|
+
|
|
462
|
+
renderWith(
|
|
463
|
+
makeManager({ selfieAnalyzer: makeAnalyzer("vision-face") }),
|
|
464
|
+
configuration,
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
expect(readCaptureProps().startButton).toBe(startButton);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it("back on a selfie capture rewinds the flow", () => {
|
|
471
|
+
const manager = makeManager({
|
|
472
|
+
selfieAnalyzer: makeAnalyzer("vision-face"),
|
|
473
|
+
});
|
|
474
|
+
renderWith(manager, selfieConfiguration());
|
|
475
|
+
|
|
476
|
+
act(() => {
|
|
477
|
+
(readCaptureProps().onBack as () => void)();
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
expect(manager.navigateBack).toHaveBeenCalledTimes(1);
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
describe("OrchestratedCaptureScreen — single-sided document", () => {
|
|
485
|
+
const greenBook = DocumentType.GenericDocument({
|
|
486
|
+
displayName: "Booklet",
|
|
487
|
+
hasBackSide: false,
|
|
488
|
+
orientation: "Portrait",
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
function singleSided() {
|
|
492
|
+
return documentConfiguration((document) => {
|
|
493
|
+
document.documentType = greenBook;
|
|
494
|
+
document.captureBothSides = false;
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
it("configures the document analyzer for the front side", () => {
|
|
499
|
+
const analyzer = makeAnalyzer();
|
|
500
|
+
renderWith(makeManager({ documentAnalyzer: analyzer }), singleSided());
|
|
501
|
+
|
|
502
|
+
expect(analyzer.configure).toHaveBeenCalledWith({
|
|
503
|
+
documentSide: DocumentSide.Front,
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it("applies the sdk_config document JPEG quality when it is in range", () => {
|
|
508
|
+
const analyzer = makeAnalyzer();
|
|
509
|
+
renderWith(
|
|
510
|
+
makeManager({
|
|
511
|
+
documentAnalyzer: analyzer,
|
|
512
|
+
resolvedSdkConfig: { document: { quality: 72 } },
|
|
513
|
+
}),
|
|
514
|
+
singleSided(),
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
expect(analyzer.configure).toHaveBeenCalledWith({
|
|
518
|
+
documentSide: DocumentSide.Front,
|
|
519
|
+
documentJpegQuality: 72,
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it("locks the document type's own orientation", () => {
|
|
524
|
+
renderWith(
|
|
525
|
+
makeManager({ documentAnalyzer: makeAnalyzer() }),
|
|
526
|
+
singleSided(),
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
expect(orientationLockCalls.current[0]?.[0]).toBe("Portrait");
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
it("persists the front document and advances", async () => {
|
|
533
|
+
const manager = makeManager({ documentAnalyzer: makeAnalyzer() });
|
|
534
|
+
renderWith(manager, singleSided());
|
|
535
|
+
|
|
536
|
+
await completeCapture([documentEvent], { durationMs: 400, retries: 1 });
|
|
537
|
+
|
|
538
|
+
expect(finalize).toHaveBeenCalledWith(
|
|
539
|
+
"file:///document.jpg",
|
|
540
|
+
ImageType.DocumentFront,
|
|
541
|
+
);
|
|
542
|
+
expect(manager.recordDocumentCaptureTelemetry).toHaveBeenCalledWith(
|
|
543
|
+
DocumentSide.Front,
|
|
544
|
+
{ durationMs: 400, retries: 1 },
|
|
545
|
+
);
|
|
546
|
+
expect(manager.setCapturedFrontDocument).toHaveBeenCalledWith(
|
|
547
|
+
"file:///document.jpg",
|
|
548
|
+
);
|
|
549
|
+
expect(manager.setCapturedBackDocument).not.toHaveBeenCalled();
|
|
550
|
+
expect(manager.navigateToNext).toHaveBeenCalledTimes(1);
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
it("falls back to the orientation default when the type has no known ratio", () => {
|
|
554
|
+
renderWith(
|
|
555
|
+
makeManager({ documentAnalyzer: makeAnalyzer() }),
|
|
556
|
+
singleSided(),
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
expect(readCaptureProps().knownIdAspectRatio).toBeCloseTo(0.65, 5);
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it("prefers the document type's known aspect ratio", () => {
|
|
563
|
+
renderWith(
|
|
564
|
+
makeManager({ documentAnalyzer: makeAnalyzer() }),
|
|
565
|
+
documentConfiguration((document) => {
|
|
566
|
+
document.documentType = DocumentType.Passport;
|
|
567
|
+
document.captureBothSides = false;
|
|
568
|
+
}),
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
expect(readCaptureProps().knownIdAspectRatio).toBeCloseTo(356 / 272, 5);
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
it("passes the document kind through so the frame can specialise", () => {
|
|
575
|
+
renderWith(
|
|
576
|
+
makeManager({ documentAnalyzer: makeAnalyzer() }),
|
|
577
|
+
documentConfiguration((document) => {
|
|
578
|
+
document.documentType = DocumentType.Passport;
|
|
579
|
+
document.captureBothSides = false;
|
|
580
|
+
}),
|
|
581
|
+
);
|
|
582
|
+
|
|
583
|
+
expect(readCaptureProps().documentTypeKind).toBe("passport");
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
it("records the image origin against the side being captured", () => {
|
|
587
|
+
const manager = makeManager({ documentAnalyzer: makeAnalyzer() });
|
|
588
|
+
renderWith(manager, singleSided());
|
|
589
|
+
|
|
590
|
+
(readCaptureProps().onDocumentOrigin as (origin: string) => void)(
|
|
591
|
+
"gallery",
|
|
592
|
+
);
|
|
593
|
+
|
|
594
|
+
expect(manager.recordDocumentImageOrigin).toHaveBeenCalledWith(
|
|
595
|
+
DocumentSide.Front,
|
|
596
|
+
"gallery",
|
|
597
|
+
);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
it("closing the document capture exits the flow", () => {
|
|
601
|
+
const manager = makeManager({ documentAnalyzer: makeAnalyzer() });
|
|
602
|
+
renderWith(manager, singleSided());
|
|
603
|
+
|
|
604
|
+
(readCaptureProps().onClose as () => void)();
|
|
605
|
+
|
|
606
|
+
expect(manager.exit).toHaveBeenCalledTimes(1);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
it("stays on the screen when the pipeline emits no document artifact", async () => {
|
|
610
|
+
const manager = makeManager({ documentAnalyzer: makeAnalyzer() });
|
|
611
|
+
renderWith(manager, singleSided());
|
|
612
|
+
|
|
613
|
+
await completeCapture([selfieEvent]);
|
|
614
|
+
|
|
615
|
+
expect(manager.setCapturedFrontDocument).not.toHaveBeenCalled();
|
|
616
|
+
expect(manager.navigateToNext).not.toHaveBeenCalled();
|
|
617
|
+
});
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
describe("OrchestratedCaptureScreen — two-sided document, in-screen flip", () => {
|
|
621
|
+
function twoSided(allowSkipBack = false) {
|
|
622
|
+
return documentConfiguration((document) => {
|
|
623
|
+
document.documentType = DocumentType.Passport;
|
|
624
|
+
document.captureBothSides = true;
|
|
625
|
+
document.allowSkipBack = allowSkipBack;
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
it("shows the flip interstitial after the front capture instead of advancing", async () => {
|
|
630
|
+
const manager = makeManager({ documentAnalyzer: makeAnalyzer() });
|
|
631
|
+
renderWith(manager, twoSided());
|
|
632
|
+
|
|
633
|
+
await completeCapture([documentEvent]);
|
|
634
|
+
|
|
635
|
+
expect(manager.setCapturedFrontDocument).toHaveBeenCalledWith(
|
|
636
|
+
"file:///document.jpg",
|
|
637
|
+
);
|
|
638
|
+
expect(manager.navigateToNext).not.toHaveBeenCalled();
|
|
639
|
+
expect(readBackInstructionsProps().documentTypeDisplayName).toBe(
|
|
640
|
+
"Passport",
|
|
641
|
+
);
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
it("forces portrait while the flip interstitial shows, then reverts", async () => {
|
|
645
|
+
renderWith(makeManager({ documentAnalyzer: makeAnalyzer() }), twoSided());
|
|
646
|
+
expect(orientationLockCalls.current[0]?.[0]).toBe("Landscape");
|
|
647
|
+
|
|
648
|
+
await completeCapture([documentEvent]);
|
|
649
|
+
expect(orientationLockCalls.current.at(-1)?.[0]).toBe("Portrait");
|
|
650
|
+
|
|
651
|
+
act(() => {
|
|
652
|
+
(readBackInstructionsProps().onCaptureBack as () => void)();
|
|
653
|
+
});
|
|
654
|
+
expect(orientationLockCalls.current.at(-1)?.[0]).toBe("Landscape");
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
it("reconfigures the analyzer for the back side so it can't overwrite the front", async () => {
|
|
658
|
+
const analyzer = makeAnalyzer();
|
|
659
|
+
renderWith(makeManager({ documentAnalyzer: analyzer }), twoSided());
|
|
660
|
+
|
|
661
|
+
await completeCapture([documentEvent]);
|
|
662
|
+
act(() => {
|
|
663
|
+
(readBackInstructionsProps().onCaptureBack as () => void)();
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
expect(analyzer.configure).toHaveBeenLastCalledWith({
|
|
667
|
+
documentSide: DocumentSide.Back,
|
|
668
|
+
});
|
|
669
|
+
expect(analyzer.close).toHaveBeenCalled();
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
it("persists the back document and advances after the second capture", async () => {
|
|
673
|
+
const manager = makeManager({ documentAnalyzer: makeAnalyzer() });
|
|
674
|
+
renderWith(manager, twoSided());
|
|
675
|
+
|
|
676
|
+
await completeCapture([documentEvent]);
|
|
677
|
+
act(() => {
|
|
678
|
+
(readBackInstructionsProps().onCaptureBack as () => void)();
|
|
679
|
+
});
|
|
680
|
+
await completeCapture([
|
|
681
|
+
{ kind: CaptureReadyEventKind.Document, url: "file:///back.jpg" },
|
|
682
|
+
] as CaptureReadyEvent[]);
|
|
683
|
+
|
|
684
|
+
expect(finalize).toHaveBeenCalledWith(
|
|
685
|
+
"file:///back.jpg",
|
|
686
|
+
ImageType.DocumentBack,
|
|
687
|
+
);
|
|
688
|
+
expect(manager.setCapturedBackDocument).toHaveBeenCalledWith(
|
|
689
|
+
"file:///back.jpg",
|
|
690
|
+
);
|
|
691
|
+
expect(manager.recordDocumentCaptureTelemetry).toHaveBeenLastCalledWith(
|
|
692
|
+
DocumentSide.Back,
|
|
693
|
+
{ durationMs: 1200, retries: 0 },
|
|
694
|
+
);
|
|
695
|
+
expect(manager.navigateToNext).toHaveBeenCalledTimes(1);
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
it("hides the skip action unless the partner opted in", async () => {
|
|
699
|
+
renderWith(makeManager({ documentAnalyzer: makeAnalyzer() }), twoSided());
|
|
700
|
+
await completeCapture([documentEvent]);
|
|
701
|
+
|
|
702
|
+
expect(readBackInstructionsProps().allowSkip).toBe(false);
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
it("skipping the back routes past its capture and preview", async () => {
|
|
706
|
+
const manager = makeManager({ documentAnalyzer: makeAnalyzer() });
|
|
707
|
+
renderWith(manager, twoSided(true));
|
|
708
|
+
await completeCapture([documentEvent]);
|
|
709
|
+
|
|
710
|
+
expect(readBackInstructionsProps().allowSkip).toBe(true);
|
|
711
|
+
act(() => {
|
|
712
|
+
(readBackInstructionsProps().onSkip as () => void)();
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
expect(manager.skipDocumentBack).toHaveBeenCalledTimes(1);
|
|
716
|
+
expect(manager.navigateToNext).toHaveBeenCalledTimes(1);
|
|
717
|
+
expect(manager.setCapturedBackDocument).not.toHaveBeenCalled();
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
it("back from the flip interstitial re-captures the front in place", async () => {
|
|
721
|
+
const manager = makeManager({ documentAnalyzer: makeAnalyzer() });
|
|
722
|
+
renderWith(manager, twoSided());
|
|
723
|
+
await completeCapture([documentEvent]);
|
|
724
|
+
|
|
725
|
+
act(() => {
|
|
726
|
+
(readBackInstructionsProps().onBack as () => void)();
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
expect(manager.navigateBack).not.toHaveBeenCalled();
|
|
730
|
+
expect(readCaptureProps().documentSide).toBe(DocumentSide.Front);
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
it("back from the back-side capture returns to the flip interstitial", async () => {
|
|
734
|
+
const manager = makeManager({ documentAnalyzer: makeAnalyzer() });
|
|
735
|
+
renderWith(manager, twoSided());
|
|
736
|
+
await completeCapture([documentEvent]);
|
|
737
|
+
act(() => {
|
|
738
|
+
(readBackInstructionsProps().onCaptureBack as () => void)();
|
|
739
|
+
});
|
|
740
|
+
expect(readCaptureProps().documentSide).toBe(DocumentSide.Back);
|
|
741
|
+
|
|
742
|
+
backInstructionsProps.current = undefined;
|
|
743
|
+
act(() => {
|
|
744
|
+
(readCaptureProps().onBack as () => void)();
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
expect(manager.navigateBack).not.toHaveBeenCalled();
|
|
748
|
+
expect(readBackInstructionsProps().documentTypeDisplayName).toBe(
|
|
749
|
+
"Passport",
|
|
750
|
+
);
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
it("records back-side telemetry against the back, not the front", async () => {
|
|
754
|
+
const manager = makeManager({ documentAnalyzer: makeAnalyzer() });
|
|
755
|
+
renderWith(manager, twoSided());
|
|
756
|
+
await completeCapture([documentEvent], { durationMs: 100, retries: 0 });
|
|
757
|
+
act(() => {
|
|
758
|
+
(readBackInstructionsProps().onCaptureBack as () => void)();
|
|
759
|
+
});
|
|
760
|
+
await completeCapture([documentEvent], { durationMs: 250, retries: 3 });
|
|
761
|
+
|
|
762
|
+
expect(manager.recordDocumentCaptureTelemetry.mock.calls).toEqual([
|
|
763
|
+
[DocumentSide.Front, { durationMs: 100, retries: 0 }],
|
|
764
|
+
[DocumentSide.Back, { durationMs: 250, retries: 3 }],
|
|
765
|
+
]);
|
|
766
|
+
});
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
describe("OrchestratedCaptureScreen — two-sided document with a preview between sides", () => {
|
|
770
|
+
function twoSided() {
|
|
771
|
+
return documentConfiguration((document) => {
|
|
772
|
+
document.documentType = DocumentType.Passport;
|
|
773
|
+
document.captureBothSides = true;
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
it("exits to the front preview instead of flipping in-screen", async () => {
|
|
778
|
+
const manager = makeManager({
|
|
779
|
+
documentAnalyzer: makeAnalyzer(),
|
|
780
|
+
previewFollows: true,
|
|
781
|
+
});
|
|
782
|
+
renderWith(manager, twoSided());
|
|
783
|
+
|
|
784
|
+
await completeCapture([documentEvent]);
|
|
785
|
+
|
|
786
|
+
expect(manager.setCapturedFrontDocument).toHaveBeenCalledWith(
|
|
787
|
+
"file:///document.jpg",
|
|
788
|
+
);
|
|
789
|
+
expect(manager.navigateToNext).toHaveBeenCalledTimes(1);
|
|
790
|
+
expect(backInstructionsProps.current).toBeUndefined();
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
it("re-entry with the front already captured starts on the back with the flip prompt", () => {
|
|
794
|
+
const analyzer = makeAnalyzer();
|
|
795
|
+
renderWith(
|
|
796
|
+
makeManager({
|
|
797
|
+
documentAnalyzer: analyzer,
|
|
798
|
+
previewFollows: true,
|
|
799
|
+
frontDocumentPath: "file:///document.jpg",
|
|
800
|
+
}),
|
|
801
|
+
twoSided(),
|
|
802
|
+
);
|
|
803
|
+
|
|
804
|
+
expect(analyzer.configure).toHaveBeenCalledWith({
|
|
805
|
+
documentSide: DocumentSide.Back,
|
|
806
|
+
});
|
|
807
|
+
expect(readBackInstructionsProps().documentTypeDisplayName).toBe(
|
|
808
|
+
"Passport",
|
|
809
|
+
);
|
|
810
|
+
expect(captureScreenProps.current).toBeUndefined();
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
it("back from the flip prompt rewinds to the front preview", () => {
|
|
814
|
+
const manager = makeManager({
|
|
815
|
+
documentAnalyzer: makeAnalyzer(),
|
|
816
|
+
previewFollows: true,
|
|
817
|
+
frontDocumentPath: "file:///document.jpg",
|
|
818
|
+
});
|
|
819
|
+
renderWith(manager, twoSided());
|
|
820
|
+
|
|
821
|
+
act(() => {
|
|
822
|
+
(readBackInstructionsProps().onBack as () => void)();
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
expect(manager.navigateBack).toHaveBeenCalledTimes(1);
|
|
826
|
+
expect(captureScreenProps.current).toBeUndefined();
|
|
827
|
+
});
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
describe("OrchestratedCaptureScreen — missing analyzer", () => {
|
|
831
|
+
it("fails loudly rather than rendering an empty preview", () => {
|
|
832
|
+
expect(() => renderWith(makeManager(), selfieConfiguration())).toThrow(
|
|
833
|
+
/no selfie analyzer registered/,
|
|
834
|
+
);
|
|
835
|
+
});
|
|
836
|
+
});
|