@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,112 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { create as createRenderer, act } from "react-test-renderer";
|
|
4
|
+
import {
|
|
5
|
+
UseSmileIDLocalizationsProvider,
|
|
6
|
+
useStableLocalizations,
|
|
7
|
+
useUseSmileIDLocalizations,
|
|
8
|
+
} from "../use_smile_id_localizations_provider";
|
|
9
|
+
import { UseSmileIDLocalizations } from "../use_smile_id_localizations";
|
|
10
|
+
|
|
11
|
+
function renderReader(children: React.ReactNode) {
|
|
12
|
+
let renderer!: ReturnType<typeof createRenderer>;
|
|
13
|
+
act(() => {
|
|
14
|
+
renderer = createRenderer(<>{children}</>);
|
|
15
|
+
});
|
|
16
|
+
return renderer;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("useUseSmileIDLocalizations", () => {
|
|
20
|
+
it("returns the English default when no provider wraps the caller", () => {
|
|
21
|
+
const seen: string[] = [];
|
|
22
|
+
function Reader() {
|
|
23
|
+
seen.push(useUseSmileIDLocalizations().siConsentAllow);
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
renderReader(<Reader />);
|
|
28
|
+
|
|
29
|
+
expect(seen).toEqual(["Allow"]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("returns the provider's instance when one is mounted", () => {
|
|
33
|
+
const localizations = new UseSmileIDLocalizations({
|
|
34
|
+
si_consent_allow: "Autoriser",
|
|
35
|
+
});
|
|
36
|
+
const seen: UseSmileIDLocalizations[] = [];
|
|
37
|
+
function Reader() {
|
|
38
|
+
seen.push(useUseSmileIDLocalizations());
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
renderReader(
|
|
43
|
+
<UseSmileIDLocalizationsProvider value={localizations}>
|
|
44
|
+
<Reader />
|
|
45
|
+
</UseSmileIDLocalizationsProvider>,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
expect(seen[0]).toBe(localizations);
|
|
49
|
+
expect(seen[0]?.siConsentAllow).toBe("Autoriser");
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("useStableLocalizations", () => {
|
|
54
|
+
it("builds a localizations instance from the overrides map", () => {
|
|
55
|
+
const overrides = { si_consent_allow: "Autoriser" };
|
|
56
|
+
const seen: UseSmileIDLocalizations[] = [];
|
|
57
|
+
function Reader() {
|
|
58
|
+
seen.push(useStableLocalizations(overrides));
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
renderReader(<Reader />);
|
|
63
|
+
|
|
64
|
+
expect(seen[0]).toBeInstanceOf(UseSmileIDLocalizations);
|
|
65
|
+
expect(seen[0]?.siConsentAllow).toBe("Autoriser");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("keeps one instance across re-renders for a stable overrides object", () => {
|
|
69
|
+
const overrides = { si_consent_allow: "Autoriser" };
|
|
70
|
+
const seen: UseSmileIDLocalizations[] = [];
|
|
71
|
+
function Reader(_props: { tick: number }) {
|
|
72
|
+
seen.push(useStableLocalizations(overrides));
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let renderer!: ReturnType<typeof createRenderer>;
|
|
77
|
+
act(() => {
|
|
78
|
+
renderer = createRenderer(<Reader tick={0} />);
|
|
79
|
+
});
|
|
80
|
+
act(() => {
|
|
81
|
+
renderer.update(<Reader tick={1} />);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
expect(seen).toHaveLength(2);
|
|
85
|
+
expect(seen[0]).toBe(seen[1]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("rebuilds when the partner passes a fresh overrides object", () => {
|
|
89
|
+
const seen: UseSmileIDLocalizations[] = [];
|
|
90
|
+
function Reader({
|
|
91
|
+
overrides,
|
|
92
|
+
}: {
|
|
93
|
+
overrides: Partial<Record<string, string>>;
|
|
94
|
+
}) {
|
|
95
|
+
seen.push(useStableLocalizations(overrides));
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let renderer!: ReturnType<typeof createRenderer>;
|
|
100
|
+
act(() => {
|
|
101
|
+
renderer = createRenderer(
|
|
102
|
+
<Reader overrides={{ si_consent_allow: "Autoriser" }} />,
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
act(() => {
|
|
106
|
+
renderer.update(<Reader overrides={{ si_consent_allow: "Permitir" }} />);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
expect(seen[0]).not.toBe(seen[1]);
|
|
110
|
+
expect(seen[1]?.siConsentAllow).toBe("Permitir");
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { UseSmileIDLocalizations } from "../use_smile_id_localizations";
|
|
3
|
+
import { enStrings } from "../strings_en";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Every localized accessor is a two-line `_s(key, enStrings[key])` /
|
|
7
|
+
* `_t(key, enStrings[key], params)` wrapper. Hand-writing one test per
|
|
8
|
+
* accessor would be ~130 near-identical tests, so this suite drives the
|
|
9
|
+
* whole surface off the prototype: a Proxy overrides map reports which
|
|
10
|
+
* `si_*` key each accessor asks for, which is then checked against the
|
|
11
|
+
* English default it returns. A typo'd key (the realistic defect — a
|
|
12
|
+
* copy-pasted getter reading its neighbour's key) fails here.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const PARAM_VALUE = "SENTINEL-PARAM";
|
|
16
|
+
|
|
17
|
+
type Accessor = {
|
|
18
|
+
readonly name: string;
|
|
19
|
+
readonly arity: number;
|
|
20
|
+
readonly call: (instance: UseSmileIDLocalizations) => string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const accessors: readonly Accessor[] = Object.entries(
|
|
24
|
+
Object.getOwnPropertyDescriptors(UseSmileIDLocalizations.prototype),
|
|
25
|
+
)
|
|
26
|
+
.filter(([name]) => name.startsWith("si"))
|
|
27
|
+
.map(([name, descriptor]): Accessor => {
|
|
28
|
+
if (descriptor.get) {
|
|
29
|
+
return {
|
|
30
|
+
name,
|
|
31
|
+
arity: 0,
|
|
32
|
+
call: (instance) =>
|
|
33
|
+
(instance as unknown as Record<string, string>)[name] as string,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const method = descriptor.value as (...args: string[]) => string;
|
|
37
|
+
const arity = method.length;
|
|
38
|
+
return {
|
|
39
|
+
name,
|
|
40
|
+
arity,
|
|
41
|
+
call: (instance) =>
|
|
42
|
+
method.apply(
|
|
43
|
+
instance,
|
|
44
|
+
Array.from({ length: arity }, () => PARAM_VALUE),
|
|
45
|
+
),
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/** Overrides map that reports which key an accessor looked up. */
|
|
50
|
+
function keyProbe(): Partial<Record<string, string>> {
|
|
51
|
+
return new Proxy(
|
|
52
|
+
{},
|
|
53
|
+
{
|
|
54
|
+
get: (_target, property) =>
|
|
55
|
+
typeof property === "string" ? `<<${property}>>` : undefined,
|
|
56
|
+
},
|
|
57
|
+
) as Partial<Record<string, string>>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function lookedUpKey(accessor: Accessor): string {
|
|
61
|
+
const probed = accessor.call(new UseSmileIDLocalizations(keyProbe()));
|
|
62
|
+
const match = /^<<(.+)>>$/.exec(probed);
|
|
63
|
+
if (!match?.[1]) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`${accessor.name} did not resolve through the overrides map (got ${probed})`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
return match[1];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
describe("UseSmileIDLocalizations — accessor surface", () => {
|
|
72
|
+
it("exposes an accessor for every localized string", () => {
|
|
73
|
+
expect(accessors.length).toBeGreaterThan(100);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it.each(accessors.map((accessor) => [accessor.name, accessor] as const))(
|
|
77
|
+
"%s reads its own si_ key and defaults to the English string",
|
|
78
|
+
(_name, accessor) => {
|
|
79
|
+
const key = lookedUpKey(accessor);
|
|
80
|
+
|
|
81
|
+
expect(key).toMatch(/^si_/);
|
|
82
|
+
expect(enStrings).toHaveProperty(key);
|
|
83
|
+
|
|
84
|
+
const english = (enStrings as Record<string, string>)[key] as string;
|
|
85
|
+
const actual = accessor.call(new UseSmileIDLocalizations());
|
|
86
|
+
const expected =
|
|
87
|
+
accessor.arity === 0
|
|
88
|
+
? english
|
|
89
|
+
: english.replace(
|
|
90
|
+
/\{[A-Za-z_][A-Za-z0-9_]*\}/g,
|
|
91
|
+
`\u2068${PARAM_VALUE}\u2069`,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
expect(actual).toBe(expected);
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
it("maps each accessor to a distinct key — no copy-paste collisions", () => {
|
|
99
|
+
const keys = accessors.map(lookedUpKey);
|
|
100
|
+
|
|
101
|
+
expect(new Set(keys).size).toBe(keys.length);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("leaves no canonical string unreachable through the public surface", () => {
|
|
105
|
+
const reached = new Set(accessors.map(lookedUpKey));
|
|
106
|
+
const orphans = Object.keys(enStrings).filter((key) => !reached.has(key));
|
|
107
|
+
|
|
108
|
+
expect(orphans).toEqual([]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("substitutes named tokens only in the parameterized accessors", () => {
|
|
112
|
+
const parameterized = accessors.filter((accessor) => accessor.arity > 0);
|
|
113
|
+
expect(parameterized.length).toBeGreaterThan(0);
|
|
114
|
+
|
|
115
|
+
for (const accessor of parameterized) {
|
|
116
|
+
const rendered = accessor.call(new UseSmileIDLocalizations());
|
|
117
|
+
expect(rendered).toContain(PARAM_VALUE);
|
|
118
|
+
expect(rendered).not.toMatch(/\{[A-Za-z_][A-Za-z0-9_]*\}/);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("UseSmileIDLocalizations — locale registry", () => {
|
|
124
|
+
const originalResolver = UseSmileIDLocalizations.localeResolver;
|
|
125
|
+
|
|
126
|
+
beforeEach(() => {
|
|
127
|
+
UseSmileIDLocalizations.clearRegistry();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
afterEach(() => {
|
|
131
|
+
UseSmileIDLocalizations.clearRegistry();
|
|
132
|
+
UseSmileIDLocalizations.localeResolver = originalResolver;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("reports an empty registry until a locale is registered", () => {
|
|
136
|
+
expect(UseSmileIDLocalizations.hasRegisteredLocales()).toBe(false);
|
|
137
|
+
|
|
138
|
+
UseSmileIDLocalizations.register("fr", { si_consent_allow: "Autoriser" });
|
|
139
|
+
|
|
140
|
+
expect(UseSmileIDLocalizations.hasRegisteredLocales()).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("clearRegistry empties it again", () => {
|
|
144
|
+
UseSmileIDLocalizations.register("fr", { si_consent_allow: "Autoriser" });
|
|
145
|
+
UseSmileIDLocalizations.clearRegistry();
|
|
146
|
+
|
|
147
|
+
expect(UseSmileIDLocalizations.hasRegisteredLocales()).toBe(false);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("strips ARB metadata and non-string values on register", () => {
|
|
151
|
+
UseSmileIDLocalizations.register("fr", {
|
|
152
|
+
"@@locale": "fr",
|
|
153
|
+
"@si_consent_allow": { description: "Allow" },
|
|
154
|
+
si_consent_allow: "Autoriser",
|
|
155
|
+
si_consent_deny: 42,
|
|
156
|
+
});
|
|
157
|
+
UseSmileIDLocalizations.localeResolver = () => "fr";
|
|
158
|
+
|
|
159
|
+
const resolved = UseSmileIDLocalizations.forActiveLocale();
|
|
160
|
+
|
|
161
|
+
expect(resolved.siConsentAllow).toBe("Autoriser");
|
|
162
|
+
expect(resolved.siConsentDeny).toBe("Deny");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("re-registering a locale replaces the prior translations", () => {
|
|
166
|
+
UseSmileIDLocalizations.register("fr", { si_consent_allow: "Ancien" });
|
|
167
|
+
UseSmileIDLocalizations.register("fr", { si_consent_allow: "Autoriser" });
|
|
168
|
+
UseSmileIDLocalizations.localeResolver = () => "fr";
|
|
169
|
+
|
|
170
|
+
expect(UseSmileIDLocalizations.forActiveLocale().siConsentAllow).toBe(
|
|
171
|
+
"Autoriser",
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("prefers an exact locale match over the language-only entry", () => {
|
|
176
|
+
UseSmileIDLocalizations.register("pt", { si_consent_allow: "Permitir" });
|
|
177
|
+
UseSmileIDLocalizations.register("pt-BR", {
|
|
178
|
+
si_consent_allow: "Permitir (BR)",
|
|
179
|
+
});
|
|
180
|
+
UseSmileIDLocalizations.localeResolver = () => "pt-BR";
|
|
181
|
+
|
|
182
|
+
expect(UseSmileIDLocalizations.forActiveLocale().siConsentAllow).toBe(
|
|
183
|
+
"Permitir (BR)",
|
|
184
|
+
);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("falls back to the language subtag when the region variant is unregistered", () => {
|
|
188
|
+
UseSmileIDLocalizations.register("pt", { si_consent_allow: "Permitir" });
|
|
189
|
+
UseSmileIDLocalizations.localeResolver = () => "pt-BR";
|
|
190
|
+
|
|
191
|
+
expect(UseSmileIDLocalizations.forActiveLocale().siConsentAllow).toBe(
|
|
192
|
+
"Permitir",
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("falls back to English when neither the locale nor its language is registered", () => {
|
|
197
|
+
UseSmileIDLocalizations.register("fr", { si_consent_allow: "Autoriser" });
|
|
198
|
+
UseSmileIDLocalizations.localeResolver = () => "sw-KE";
|
|
199
|
+
|
|
200
|
+
expect(UseSmileIDLocalizations.forActiveLocale().siConsentAllow).toBe(
|
|
201
|
+
"Allow",
|
|
202
|
+
);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("falls back to English when the registry is empty", () => {
|
|
206
|
+
UseSmileIDLocalizations.localeResolver = () => "fr";
|
|
207
|
+
|
|
208
|
+
expect(UseSmileIDLocalizations.forActiveLocale().siConsentAllow).toBe(
|
|
209
|
+
"Allow",
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("resolves the device language subtag by default", () => {
|
|
214
|
+
const language = UseSmileIDLocalizations.localeResolver();
|
|
215
|
+
|
|
216
|
+
expect(language).toMatch(/^[a-z]{2,3}$/);
|
|
217
|
+
expect(language).not.toContain("-");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("falls back to English when Intl is unavailable", () => {
|
|
221
|
+
const originalIntl = globalThis.Intl;
|
|
222
|
+
// Concrete scenario: a JS engine (or a locked-down Hermes build)
|
|
223
|
+
// without full-ICU — the resolver must not throw at flow mount.
|
|
224
|
+
(globalThis as { Intl?: unknown }).Intl = undefined;
|
|
225
|
+
try {
|
|
226
|
+
expect(originalResolver()).toBe("en");
|
|
227
|
+
} finally {
|
|
228
|
+
globalThis.Intl = originalIntl;
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
});
|
package/src/l10n/strings_en.ts
CHANGED
|
@@ -81,6 +81,8 @@ export const enStrings = {
|
|
|
81
81
|
si_capture_hold_steady: "Hold still…",
|
|
82
82
|
si_capture_timeout: "Capture timed out. Try again.",
|
|
83
83
|
si_capture_failed: "Capture failed. Please try again.",
|
|
84
|
+
si_capture_camera_unavailable:
|
|
85
|
+
"No camera frames received. Close other apps using the camera, then try again.",
|
|
84
86
|
si_capture_adjusting: "Adjusting…",
|
|
85
87
|
si_capture_bright_light: "Please move away from direct light",
|
|
86
88
|
si_processing: "Processing…",
|
|
@@ -104,6 +106,8 @@ export const enStrings = {
|
|
|
104
106
|
"Thank you for your submission. We are currently verifying your details.",
|
|
105
107
|
si_processing_failed_title: "Submission Failed",
|
|
106
108
|
si_processing_failed_message: "Reasons for failure",
|
|
109
|
+
si_processing_error_no_connection: "No internet connection.",
|
|
110
|
+
si_processing_error_generic: "Something went wrong. Try again.",
|
|
107
111
|
si_processing_continue: "Continue",
|
|
108
112
|
si_processing_retry: "Retry",
|
|
109
113
|
si_processing_exit: "Exit",
|
|
@@ -169,4 +173,4 @@ export const enStrings = {
|
|
|
169
173
|
export type SiKey = keyof typeof enStrings;
|
|
170
174
|
|
|
171
175
|
/** Total canonical key count — pinned in tests to catch a missed addition/deletion at PR time. */
|
|
172
|
-
export const CANONICAL_KEY_COUNT =
|
|
176
|
+
export const CANONICAL_KEY_COUNT = 122 as const;
|
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
import { enStrings, type SiKey } from "./strings_en";
|
|
2
2
|
|
|
3
|
+
// Unicode bidi isolates: without them a substituted value of the opposite direction reorders the whole sentence.
|
|
4
|
+
const FIRST_STRONG_ISOLATE = "";
|
|
5
|
+
const POP_DIRECTIONAL_ISOLATE = "";
|
|
6
|
+
|
|
7
|
+
// `__DEV__` is bundler-injected; outside Metro-like environments a bare reference throws rather than reading falsy.
|
|
8
|
+
function isDevBuild(): boolean {
|
|
9
|
+
return typeof __DEV__ !== "undefined" && __DEV__;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const warnedLocales = new Set<string>();
|
|
13
|
+
|
|
14
|
+
function warnLocaleFallback(locale: string): void {
|
|
15
|
+
if (!isDevBuild() || warnedLocales.has(locale)) return;
|
|
16
|
+
warnedLocales.add(locale);
|
|
17
|
+
// eslint-disable-next-line no-console
|
|
18
|
+
console.warn(
|
|
19
|
+
`[UseSmileID] No translations registered for locale '${locale}' — the SDK ` +
|
|
20
|
+
"is rendering English. Call UseSmileIDLocalizations.register('" +
|
|
21
|
+
locale +
|
|
22
|
+
"', …) at startup, or set b.localizations explicitly.",
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
3
26
|
/**
|
|
4
27
|
* Localized string surface for the SDK. Canonical `si_*` keys default to English
|
|
5
28
|
* (`strings_en.ts`); partners override per-locale by passing an `overrides` map to
|
|
@@ -42,6 +65,7 @@ export class UseSmileIDLocalizations {
|
|
|
42
65
|
// JS hook for discovering bundled translation assets.
|
|
43
66
|
|
|
44
67
|
private static _registry = new Map<string, Partial<Record<string, string>>>();
|
|
68
|
+
private static _registryRevision = 0;
|
|
45
69
|
|
|
46
70
|
/**
|
|
47
71
|
* Default device-locale resolver: reads the BCP 47 locale via
|
|
@@ -71,11 +95,13 @@ export class UseSmileIDLocalizations {
|
|
|
71
95
|
if (typeof value === "string") filtered[key] = value;
|
|
72
96
|
}
|
|
73
97
|
UseSmileIDLocalizations._registry.set(locale, filtered);
|
|
98
|
+
UseSmileIDLocalizations._registryRevision += 1;
|
|
74
99
|
}
|
|
75
100
|
|
|
76
101
|
/** Clear the locale registry. Useful for tests; partners rarely call. */
|
|
77
102
|
static clearRegistry(): void {
|
|
78
103
|
UseSmileIDLocalizations._registry.clear();
|
|
104
|
+
UseSmileIDLocalizations._registryRevision += 1;
|
|
79
105
|
}
|
|
80
106
|
|
|
81
107
|
/** Whether any locale has been registered. */
|
|
@@ -83,6 +109,11 @@ export class UseSmileIDLocalizations {
|
|
|
83
109
|
return UseSmileIDLocalizations._registry.size > 0;
|
|
84
110
|
}
|
|
85
111
|
|
|
112
|
+
/** Identity of the translations `forActiveLocale()` would resolve right now; it changes on a locale switch or a late `register(...)`. */
|
|
113
|
+
static get activeLocaleKey(): string {
|
|
114
|
+
return `${UseSmileIDLocalizations.localeResolver()}#${UseSmileIDLocalizations._registryRevision}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
86
117
|
/**
|
|
87
118
|
* Resolve the active locale via `localeResolver()` and return the matching
|
|
88
119
|
* `UseSmileIDLocalizations`. Lookup order: exact match (`'pt-BR'`), then
|
|
@@ -99,26 +130,54 @@ export class UseSmileIDLocalizations {
|
|
|
99
130
|
const fallback = UseSmileIDLocalizations._registry.get(language);
|
|
100
131
|
if (fallback) return new UseSmileIDLocalizations(fallback);
|
|
101
132
|
}
|
|
133
|
+
warnLocaleFallback(locale);
|
|
102
134
|
return new UseSmileIDLocalizations();
|
|
103
135
|
}
|
|
104
136
|
|
|
137
|
+
/** One dev-build warning per instance, so a partly translated bundle can't flood the console. */
|
|
138
|
+
private _warnedMissingKey = false;
|
|
139
|
+
|
|
140
|
+
private _lookup(key: SiKey, fallback: string): string {
|
|
141
|
+
const override = this._overrides[key];
|
|
142
|
+
if (override !== undefined) return override;
|
|
143
|
+
if (
|
|
144
|
+
isDevBuild() &&
|
|
145
|
+
!this._warnedMissingKey &&
|
|
146
|
+
Object.keys(this._overrides).length > 0
|
|
147
|
+
) {
|
|
148
|
+
this._warnedMissingKey = true;
|
|
149
|
+
// eslint-disable-next-line no-console
|
|
150
|
+
console.warn(
|
|
151
|
+
`[UseSmileID] '${key}' is missing from the supplied translations and is ` +
|
|
152
|
+
"rendering English; other keys may be too. Compare your overrides " +
|
|
153
|
+
"against the si_* key set in the SDK's English strings.",
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
return fallback;
|
|
157
|
+
}
|
|
158
|
+
|
|
105
159
|
/** @internal Static-fallback string lookup. */
|
|
106
160
|
private _s(key: SiKey, fallback: string): string {
|
|
107
|
-
return this.
|
|
161
|
+
return this._lookup(key, fallback);
|
|
108
162
|
}
|
|
109
163
|
|
|
110
164
|
/**
|
|
111
165
|
* @internal Templated lookup; replaces every `{name}` with `params[name]`. A
|
|
112
166
|
* placeholder absent from the translation is left as-is (the `{name}` literal).
|
|
167
|
+
* Substituted values are bidi-isolated so they cannot reorder the sentence around them.
|
|
113
168
|
*/
|
|
114
169
|
private _t(
|
|
115
170
|
key: SiKey,
|
|
116
171
|
fallback: string,
|
|
117
172
|
params: Readonly<Record<string, string>>,
|
|
118
173
|
): string {
|
|
119
|
-
let value = this.
|
|
174
|
+
let value = this._lookup(key, fallback);
|
|
120
175
|
for (const [name, replacement] of Object.entries(params)) {
|
|
121
|
-
|
|
176
|
+
const isolated =
|
|
177
|
+
replacement === ""
|
|
178
|
+
? replacement
|
|
179
|
+
: `${FIRST_STRONG_ISOLATE}${replacement}${POP_DIRECTIONAL_ISOLATE}`;
|
|
180
|
+
value = value.split(`{${name}}`).join(isolated);
|
|
122
181
|
}
|
|
123
182
|
return value;
|
|
124
183
|
}
|
|
@@ -389,6 +448,12 @@ export class UseSmileIDLocalizations {
|
|
|
389
448
|
get siCaptureHoldSteady(): string {
|
|
390
449
|
return this._s("si_capture_hold_steady", enStrings.si_capture_hold_steady);
|
|
391
450
|
}
|
|
451
|
+
get siCaptureCameraUnavailable(): string {
|
|
452
|
+
return this._s(
|
|
453
|
+
"si_capture_camera_unavailable",
|
|
454
|
+
enStrings.si_capture_camera_unavailable,
|
|
455
|
+
);
|
|
456
|
+
}
|
|
392
457
|
get siCaptureTimeout(): string {
|
|
393
458
|
return this._s("si_capture_timeout", enStrings.si_capture_timeout);
|
|
394
459
|
}
|
|
@@ -492,6 +557,18 @@ export class UseSmileIDLocalizations {
|
|
|
492
557
|
enStrings.si_processing_failed_message,
|
|
493
558
|
);
|
|
494
559
|
}
|
|
560
|
+
get siProcessingErrorNoConnection(): string {
|
|
561
|
+
return this._s(
|
|
562
|
+
"si_processing_error_no_connection",
|
|
563
|
+
enStrings.si_processing_error_no_connection,
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
get siProcessingErrorGeneric(): string {
|
|
567
|
+
return this._s(
|
|
568
|
+
"si_processing_error_generic",
|
|
569
|
+
enStrings.si_processing_error_generic,
|
|
570
|
+
);
|
|
571
|
+
}
|
|
495
572
|
get siProcessingContinue(): string {
|
|
496
573
|
return this._s("si_processing_continue", enStrings.si_processing_continue);
|
|
497
574
|
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
// `vi.mock` is hoisted above the imports, so anything its factory closes over
|
|
4
|
+
// must be created with `vi.hoisted` or it is still in the TDZ when the factory
|
|
5
|
+
// runs.
|
|
6
|
+
const { loadExpoSensors } = vi.hoisted(() => ({ loadExpoSensors: vi.fn() }));
|
|
7
|
+
|
|
8
|
+
vi.mock("../optional_native_modules", () => ({ loadExpoSensors }));
|
|
9
|
+
|
|
10
|
+
import { DeviceOrientationCollector } from "../device_orientation_collector";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Sampling behaviour of the accelerometer collector: the ~15 Hz throttle,
|
|
14
|
+
* the 90 s session cap, the movement-span computation, and the graceful
|
|
15
|
+
* no-op when `expo-sensors` is absent. The empty-session contract is pinned
|
|
16
|
+
* separately in `device_orientation_collector.test.ts`.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
type Sample = { x: number; y: number; z: number };
|
|
20
|
+
|
|
21
|
+
function makeSensors() {
|
|
22
|
+
const listeners: Array<(data: Sample) => void> = [];
|
|
23
|
+
const remove = vi.fn(() => {
|
|
24
|
+
listeners.length = 0;
|
|
25
|
+
});
|
|
26
|
+
const setUpdateInterval = vi.fn();
|
|
27
|
+
const addListener = vi.fn((listener: (data: Sample) => void) => {
|
|
28
|
+
listeners.push(listener);
|
|
29
|
+
return { remove };
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
module: { Accelerometer: { setUpdateInterval, addListener } },
|
|
33
|
+
emit: (sample: Sample) => {
|
|
34
|
+
for (const listener of [...listeners]) listener(sample);
|
|
35
|
+
},
|
|
36
|
+
setUpdateInterval,
|
|
37
|
+
addListener,
|
|
38
|
+
remove,
|
|
39
|
+
listenerCount: () => listeners.length,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe("DeviceOrientationCollector — sampling", () => {
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
vi.useFakeTimers();
|
|
46
|
+
vi.setSystemTime(new Date("2026-07-30T00:00:00.000Z"));
|
|
47
|
+
loadExpoSensors.mockReset();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
vi.useRealTimers();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("subscribes at the ~15 Hz sampling interval on start", () => {
|
|
55
|
+
const sensors = makeSensors();
|
|
56
|
+
loadExpoSensors.mockReturnValue(sensors.module);
|
|
57
|
+
|
|
58
|
+
new DeviceOrientationCollector().start();
|
|
59
|
+
|
|
60
|
+
expect(sensors.setUpdateInterval).toHaveBeenCalledWith(66);
|
|
61
|
+
expect(sensors.addListener).toHaveBeenCalledTimes(1);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("records epoch-stamped samples and the movement span", () => {
|
|
65
|
+
const sensors = makeSensors();
|
|
66
|
+
loadExpoSensors.mockReturnValue(sensors.module);
|
|
67
|
+
const collector = new DeviceOrientationCollector();
|
|
68
|
+
collector.start();
|
|
69
|
+
|
|
70
|
+
const firstStamp = Date.now();
|
|
71
|
+
sensors.emit({ x: 0, y: 0, z: 1 });
|
|
72
|
+
vi.advanceTimersByTime(100);
|
|
73
|
+
const secondStamp = Date.now();
|
|
74
|
+
sensors.emit({ x: 0, y: 0, z: 2 });
|
|
75
|
+
|
|
76
|
+
expect(collector.devicePositions).toEqual([
|
|
77
|
+
`${firstStamp},0,0,1`,
|
|
78
|
+
`${secondStamp},0,0,2`,
|
|
79
|
+
]);
|
|
80
|
+
// |1 − 0.981| = 0.019 and |2 − 0.981| = 1.019 → span 1.
|
|
81
|
+
expect(collector.deviceMovementDetected).toBeCloseTo(1, 10);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("throttles samples arriving faster than the interval", () => {
|
|
85
|
+
const sensors = makeSensors();
|
|
86
|
+
loadExpoSensors.mockReturnValue(sensors.module);
|
|
87
|
+
const collector = new DeviceOrientationCollector();
|
|
88
|
+
collector.start();
|
|
89
|
+
|
|
90
|
+
sensors.emit({ x: 1, y: 0, z: 0 });
|
|
91
|
+
vi.advanceTimersByTime(10);
|
|
92
|
+
sensors.emit({ x: 2, y: 0, z: 0 });
|
|
93
|
+
vi.advanceTimersByTime(10);
|
|
94
|
+
sensors.emit({ x: 3, y: 0, z: 0 });
|
|
95
|
+
|
|
96
|
+
expect(collector.devicePositions).toHaveLength(1);
|
|
97
|
+
expect(collector.devicePositions[0]).toContain(",1,0,0");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("stops sampling once the 90 s session cap is exceeded", () => {
|
|
101
|
+
const sensors = makeSensors();
|
|
102
|
+
loadExpoSensors.mockReturnValue(sensors.module);
|
|
103
|
+
const collector = new DeviceOrientationCollector();
|
|
104
|
+
collector.start();
|
|
105
|
+
|
|
106
|
+
sensors.emit({ x: 0, y: 0, z: 1 });
|
|
107
|
+
vi.advanceTimersByTime(90 * 1000 + 1);
|
|
108
|
+
sensors.emit({ x: 0, y: 0, z: 9 });
|
|
109
|
+
|
|
110
|
+
expect(collector.devicePositions).toHaveLength(1);
|
|
111
|
+
expect(sensors.remove).toHaveBeenCalledTimes(1);
|
|
112
|
+
expect(sensors.listenerCount()).toBe(0);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("start() is idempotent while a session is already running", () => {
|
|
116
|
+
const sensors = makeSensors();
|
|
117
|
+
loadExpoSensors.mockReturnValue(sensors.module);
|
|
118
|
+
const collector = new DeviceOrientationCollector();
|
|
119
|
+
|
|
120
|
+
collector.start();
|
|
121
|
+
collector.start();
|
|
122
|
+
|
|
123
|
+
expect(sensors.addListener).toHaveBeenCalledTimes(1);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("stop() unsubscribes but preserves the collected data", () => {
|
|
127
|
+
const sensors = makeSensors();
|
|
128
|
+
loadExpoSensors.mockReturnValue(sensors.module);
|
|
129
|
+
const collector = new DeviceOrientationCollector();
|
|
130
|
+
collector.start();
|
|
131
|
+
sensors.emit({ x: 0, y: 0, z: 1 });
|
|
132
|
+
|
|
133
|
+
collector.stop();
|
|
134
|
+
|
|
135
|
+
expect(sensors.remove).toHaveBeenCalledTimes(1);
|
|
136
|
+
expect(collector.devicePositions).toHaveLength(1);
|
|
137
|
+
// A second stop on an already-stopped collector is harmless.
|
|
138
|
+
collector.stop();
|
|
139
|
+
expect(sensors.remove).toHaveBeenCalledTimes(1);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("a restart clears the previous session's samples", () => {
|
|
143
|
+
const sensors = makeSensors();
|
|
144
|
+
loadExpoSensors.mockReturnValue(sensors.module);
|
|
145
|
+
const collector = new DeviceOrientationCollector();
|
|
146
|
+
collector.start();
|
|
147
|
+
sensors.emit({ x: 0, y: 0, z: 1 });
|
|
148
|
+
collector.stop();
|
|
149
|
+
|
|
150
|
+
collector.start();
|
|
151
|
+
expect(collector.devicePositions).toHaveLength(0);
|
|
152
|
+
expect(collector.deviceMovementDetected).toBeNull();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("no-ops when expo-sensors is not installed", () => {
|
|
156
|
+
loadExpoSensors.mockReturnValue(null);
|
|
157
|
+
const collector = new DeviceOrientationCollector();
|
|
158
|
+
|
|
159
|
+
collector.start();
|
|
160
|
+
collector.stop();
|
|
161
|
+
|
|
162
|
+
expect(collector.devicePositions).toHaveLength(0);
|
|
163
|
+
expect(collector.deviceMovementDetected).toBeNull();
|
|
164
|
+
});
|
|
165
|
+
});
|