@symbiote-native/tracking-transparency 0.0.1 → 0.1.0
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/README.md +6 -1
- package/build/angular/index.js +2 -2
- package/build/angular/services/permissions.service/index.js +1 -1
- package/build/core/index.js +1 -1
- package/build/core/tracking-transparency.js +1 -1
- package/build/react/hooks/use-permissions/index.js +1 -1
- package/build/react/index.js +2 -2
- package/build/vue/composables/use-permissions/index.js +1 -1
- package/build/vue/index.js +2 -2
- package/package.json +9 -6
- package/src/angular/services/permissions.service/permissions.service.test.ts +0 -107
- package/src/core/tracking-transparency.test.ts +0 -137
- package/src/react/hooks/use-permissions/use-permissions.test.tsx +0 -100
- package/src/vue/composables/use-permissions/use-permissions.test.ts +0 -99
package/README.md
CHANGED
|
@@ -142,6 +142,12 @@ export class TrackingScreen {
|
|
|
142
142
|
}
|
|
143
143
|
```
|
|
144
144
|
|
|
145
|
+
All three examples mirror the real canary demo screens —
|
|
146
|
+
`examples/expo-react/screens/TrackingTransparencyScreen.tsx`,
|
|
147
|
+
`examples/expo-vue-sfc/screens/TrackingTransparencyScreen.vue`,
|
|
148
|
+
`examples/expo-vue-tsx/screens/TrackingTransparencyScreen.tsx`,
|
|
149
|
+
`examples/expo-angular/src/screens/TrackingTransparencyScreen.ts`.
|
|
150
|
+
|
|
145
151
|
## API
|
|
146
152
|
|
|
147
153
|
```ts
|
|
@@ -205,6 +211,5 @@ surface, never a view. Tests inject a fake native-module object in place of the
|
|
|
205
211
|
|
|
206
212
|
## Known gaps
|
|
207
213
|
|
|
208
|
-
- Not yet wired into any `examples/expo-*` canary.
|
|
209
214
|
- Tests exercise the JS layer only (fake native module, `vitest`) — no on-device/simulator
|
|
210
215
|
automated smoke test yet, only manual verification.
|
package/build/angular/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// PermissionsService is the Angular-only lifecycle half; getAdvertisingId/isAvailable are
|
|
2
2
|
// stateless free functions, shared with React/Vue.
|
|
3
|
-
export { PermissionsService } from './services/permissions.service';
|
|
4
|
-
export * from '../core';
|
|
3
|
+
export { PermissionsService } from './services/permissions.service/index.js';
|
|
4
|
+
export * from '../core/index.js';
|
|
@@ -40,7 +40,7 @@ var __runInitializers = (this && this.__runInitializers) || function (thisArg, i
|
|
|
40
40
|
// readonly status = inject(PermissionsService).connect();
|
|
41
41
|
// // template: {{ status()?.granted }}
|
|
42
42
|
import { Injectable, signal } from '@angular/core';
|
|
43
|
-
import { getTrackingPermissionsAsync, requestTrackingPermissionsAsync, } from '../../../core';
|
|
43
|
+
import { getTrackingPermissionsAsync, requestTrackingPermissionsAsync, } from '../../../core/index.js';
|
|
44
44
|
let PermissionsService = (() => {
|
|
45
45
|
let _classDecorators = [Injectable({ providedIn: 'root' })];
|
|
46
46
|
let _classDescriptor;
|
package/build/core/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { getAdvertisingId, requestTrackingPermissionsAsync, getTrackingPermissionsAsync, isAvailable, } from './tracking-transparency';
|
|
1
|
+
export { getAdvertisingId, requestTrackingPermissionsAsync, getTrackingPermissionsAsync, isAvailable, } from './tracking-transparency.js';
|
|
2
2
|
export { PermissionStatus, } from 'expo-modules-core';
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// convention is for each adapter to hand-roll its own permission hook instead (react/hooks,
|
|
7
7
|
// vue/composables, angular/services), same as brightness/cellular already do.
|
|
8
8
|
import { Platform, PermissionStatus, UnavailabilityError } from 'expo-modules-core';
|
|
9
|
-
import { expoTrackingTransparency } from './native-module';
|
|
9
|
+
import { expoTrackingTransparency } from './native-module.js';
|
|
10
10
|
const NATIVE_MODULE_NAME = 'TrackingTransparency';
|
|
11
11
|
/**
|
|
12
12
|
* Gets the advertising ID, a UUID string intended only for advertising. Returns `null` on the
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// callbacks. Mirrors the shape @symbiote-native/cellular's own usePermissions ships in parallel,
|
|
4
4
|
// so both stay byte-for-byte consistent.
|
|
5
5
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
6
|
-
import { getTrackingPermissionsAsync, requestTrackingPermissionsAsync, } from '../../../core';
|
|
6
|
+
import { getTrackingPermissionsAsync, requestTrackingPermissionsAsync, } from '../../../core/index.js';
|
|
7
7
|
export function usePermissions() {
|
|
8
8
|
const isMounted = useRef(true);
|
|
9
9
|
const [status, setStatus] = useState(null);
|
package/build/react/index.js
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
// core. Only the permission surface gets a lifecycle hook (there is per-instance state to seed
|
|
3
3
|
// and refresh); getAdvertisingId/isAvailable are stateless, re-exported verbatim — mirrors the
|
|
4
4
|
// lifecycle-bucket naming convention of adapters/react/src/hooks.
|
|
5
|
-
export { usePermissions } from './hooks/use-permissions';
|
|
6
|
-
export * from '../core';
|
|
5
|
+
export { usePermissions } from './hooks/use-permissions/index.js';
|
|
6
|
+
export * from '../core/index.js';
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// onMounted/onUnmounted style, but there is no ongoing subscription to tear down here, just a
|
|
4
4
|
// one-shot get), then exposes get/request as imperative callbacks.
|
|
5
5
|
import { onMounted, ref } from '@vue/runtime-core';
|
|
6
|
-
import { getTrackingPermissionsAsync, requestTrackingPermissionsAsync, } from '../../../core';
|
|
6
|
+
import { getTrackingPermissionsAsync, requestTrackingPermissionsAsync, } from '../../../core/index.js';
|
|
7
7
|
export function usePermissions() {
|
|
8
8
|
const status = ref(null);
|
|
9
9
|
const get = async () => {
|
package/build/vue/index.js
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
// Same reasoning as the React entry — only the permission surface gets a lifecycle composable,
|
|
3
3
|
// every other export is a plain re-export — mirrors the lifecycle-bucket naming convention of
|
|
4
4
|
// adapters/vue/src/composables (never `hooks/`, that's React's term).
|
|
5
|
-
export { usePermissions } from './composables/use-permissions';
|
|
6
|
-
export * from '../core';
|
|
5
|
+
export { usePermissions } from './composables/use-permissions/index.js';
|
|
6
|
+
export * from '../core/index.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@symbiote-native/tracking-transparency",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "expo-tracking-transparency wrapped for SymbioteNative — one framework-agnostic core, built once and reachable from the React, Vue, and Angular adapters. iOS App Tracking Transparency prompt (permission get/request, always-granted on Android/web) plus the advertising-ID getter.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -40,7 +40,10 @@
|
|
|
40
40
|
"src",
|
|
41
41
|
"build",
|
|
42
42
|
"build-ngc",
|
|
43
|
-
"native-link.json"
|
|
43
|
+
"native-link.json",
|
|
44
|
+
"!src/**/*.test.*",
|
|
45
|
+
"!src/**/*.spec.*",
|
|
46
|
+
"!src/**/*.detox.*"
|
|
44
47
|
],
|
|
45
48
|
"publishConfig": {
|
|
46
49
|
"access": "public"
|
|
@@ -56,7 +59,7 @@
|
|
|
56
59
|
"react": ">=19.0.0",
|
|
57
60
|
"react-native": ">=0.86",
|
|
58
61
|
"vue": ">=3.5.0",
|
|
59
|
-
"@symbiote-native/angular": "0.6.
|
|
62
|
+
"@symbiote-native/angular": "0.6.2",
|
|
60
63
|
"@symbiote-native/react": "0.2.8",
|
|
61
64
|
"@symbiote-native/vue": "0.3.8"
|
|
62
65
|
},
|
|
@@ -92,11 +95,11 @@
|
|
|
92
95
|
"@vue/runtime-core": "^3.5.13",
|
|
93
96
|
"react": "19.2.3",
|
|
94
97
|
"typescript": "~6.0.0",
|
|
95
|
-
"@symbiote-native/angular": "0.6.
|
|
98
|
+
"@symbiote-native/angular": "0.6.2",
|
|
96
99
|
"@symbiote-native/engine": "0.1.7",
|
|
97
|
-
"@symbiote-native/
|
|
100
|
+
"@symbiote-native/react": "0.2.8",
|
|
98
101
|
"@symbiote-native/test-utils": "0.1.6",
|
|
99
|
-
"@symbiote-native/
|
|
102
|
+
"@symbiote-native/vue": "0.3.8"
|
|
100
103
|
},
|
|
101
104
|
"scripts": {
|
|
102
105
|
"typecheck": "tsc --build",
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
// Co-located Angular-driven test (ADR 0025) for PermissionsService. See battery's
|
|
2
|
-
// battery-state.service.test.ts for the shared rationale.
|
|
3
|
-
|
|
4
|
-
import '@angular/compiler';
|
|
5
|
-
import { Component, inject, type Signal } from '@angular/core';
|
|
6
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
7
|
-
import { mount, unmount } from '@symbiote-native/angular';
|
|
8
|
-
import { installFabric } from '@symbiote-native/test-utils';
|
|
9
|
-
import { PermissionsService } from './index';
|
|
10
|
-
import { PermissionStatus, type PermissionResponse } from '../../../core';
|
|
11
|
-
|
|
12
|
-
const { getTrackingPermissionsAsync, requestTrackingPermissionsAsync } = vi.hoisted(() => ({
|
|
13
|
-
getTrackingPermissionsAsync: vi.fn(),
|
|
14
|
-
requestTrackingPermissionsAsync: vi.fn(),
|
|
15
|
-
}));
|
|
16
|
-
|
|
17
|
-
// Same enum-shaped-object mock trick packages/battery/src/angular/services/battery-state.service's
|
|
18
|
-
// test uses for BatteryState.
|
|
19
|
-
vi.mock('../../../core', () => ({
|
|
20
|
-
getTrackingPermissionsAsync,
|
|
21
|
-
requestTrackingPermissionsAsync,
|
|
22
|
-
PermissionStatus: { GRANTED: 'granted', DENIED: 'denied', UNDETERMINED: 'undetermined' },
|
|
23
|
-
}));
|
|
24
|
-
|
|
25
|
-
const GRANTED: PermissionResponse = {
|
|
26
|
-
status: PermissionStatus.GRANTED,
|
|
27
|
-
granted: true,
|
|
28
|
-
canAskAgain: true,
|
|
29
|
-
expires: 'never',
|
|
30
|
-
};
|
|
31
|
-
const DENIED: PermissionResponse = {
|
|
32
|
-
status: PermissionStatus.DENIED,
|
|
33
|
-
granted: false,
|
|
34
|
-
canAskAgain: true,
|
|
35
|
-
expires: 'never',
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const ROOT_TAG = 974;
|
|
39
|
-
const fabric = installFabric();
|
|
40
|
-
const tick = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 0));
|
|
41
|
-
|
|
42
|
-
let capturedStatus: Signal<PermissionResponse | null> | undefined;
|
|
43
|
-
let capturedService: PermissionsService | undefined;
|
|
44
|
-
|
|
45
|
-
@Component({
|
|
46
|
-
selector: 'symbiote-permissions-host',
|
|
47
|
-
standalone: true,
|
|
48
|
-
template: '',
|
|
49
|
-
})
|
|
50
|
-
class PermissionsHost {
|
|
51
|
-
readonly service = inject(PermissionsService);
|
|
52
|
-
readonly status = this.service.connect();
|
|
53
|
-
|
|
54
|
-
constructor() {
|
|
55
|
-
capturedStatus = this.status;
|
|
56
|
-
capturedService = this.service;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
beforeEach(() => {
|
|
61
|
-
capturedStatus = undefined;
|
|
62
|
-
capturedService = undefined;
|
|
63
|
-
vi.clearAllMocks();
|
|
64
|
-
getTrackingPermissionsAsync.mockResolvedValue(GRANTED);
|
|
65
|
-
requestTrackingPermissionsAsync.mockResolvedValue(GRANTED);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
afterEach(() => {
|
|
69
|
-
unmount(ROOT_TAG);
|
|
70
|
-
fabric.reset();
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
describe('PermissionsService.connect', () => {
|
|
74
|
-
it('reports null before the initial fetch resolves', () => {
|
|
75
|
-
mount(ROOT_TAG, PermissionsHost);
|
|
76
|
-
|
|
77
|
-
expect(capturedStatus?.()).toBe(null);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('reports the fetched status once getTrackingPermissionsAsync() resolves', async () => {
|
|
81
|
-
mount(ROOT_TAG, PermissionsHost);
|
|
82
|
-
await tick();
|
|
83
|
-
|
|
84
|
-
expect(capturedStatus?.()).toEqual(GRANTED);
|
|
85
|
-
expect(getTrackingPermissionsAsync).toHaveBeenCalledTimes(1);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('request() delegates to requestTrackingPermissionsAsync and updates the signal', async () => {
|
|
89
|
-
mount(ROOT_TAG, PermissionsHost);
|
|
90
|
-
await tick();
|
|
91
|
-
|
|
92
|
-
requestTrackingPermissionsAsync.mockResolvedValueOnce(DENIED);
|
|
93
|
-
await capturedService?.request();
|
|
94
|
-
|
|
95
|
-
expect(capturedStatus?.()).toEqual(DENIED);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('get() re-fetches and updates the signal', async () => {
|
|
99
|
-
mount(ROOT_TAG, PermissionsHost);
|
|
100
|
-
await tick();
|
|
101
|
-
|
|
102
|
-
getTrackingPermissionsAsync.mockResolvedValueOnce(DENIED);
|
|
103
|
-
await capturedService?.get();
|
|
104
|
-
|
|
105
|
-
expect(capturedStatus?.()).toEqual(DENIED);
|
|
106
|
-
});
|
|
107
|
-
});
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
|
|
3
|
-
const FAKE_NATIVE_TRACKING_TRANSPARENCY = {
|
|
4
|
-
getAdvertisingId: vi.fn(() => 'E9228286-4C4E-4789-9D95-15827DCB291B'),
|
|
5
|
-
requestPermissionsAsync: vi.fn(async () => ({ status: 'granted', granted: true })),
|
|
6
|
-
getPermissionsAsync: vi.fn(async () => ({ status: 'granted', granted: true })),
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
// The real ExpoTrackingTransparency native module only exists on device — resolving it via
|
|
10
|
-
// requireNativeModule() at import time would throw in this headless test run, so the
|
|
11
|
-
// module-lookup file is faked in place of expo-modules-core's runtime resolution, same pattern
|
|
12
|
-
// packages/brightness/src/core/brightness.test.ts and packages/device/src/core/device.test.ts use.
|
|
13
|
-
vi.mock('./native-module', () => ({
|
|
14
|
-
expoTrackingTransparency: FAKE_NATIVE_TRACKING_TRANSPARENCY,
|
|
15
|
-
}));
|
|
16
|
-
|
|
17
|
-
// expo-modules-core's real entry transitively imports 'react-native' for Platform/
|
|
18
|
-
// TurboModuleRegistry, whose Flow-typed source Vitest's Oxc transform can't parse — same fake
|
|
19
|
-
// packages/brightness/src/core/brightness.test.ts uses, since Platform.OS gates the
|
|
20
|
-
// Android/web-vs-iOS branch here.
|
|
21
|
-
const fakePlatform = { OS: 'ios' as 'ios' | 'android' };
|
|
22
|
-
|
|
23
|
-
vi.mock('expo-modules-core', () => ({
|
|
24
|
-
Platform: fakePlatform,
|
|
25
|
-
PermissionStatus: { GRANTED: 'granted', DENIED: 'denied', UNDETERMINED: 'undetermined' },
|
|
26
|
-
UnavailabilityError: class UnavailabilityError extends Error {
|
|
27
|
-
constructor(moduleName: string, propertyName: string) {
|
|
28
|
-
super(`${propertyName} is not available on ${moduleName}`);
|
|
29
|
-
}
|
|
30
|
-
},
|
|
31
|
-
}));
|
|
32
|
-
|
|
33
|
-
const {
|
|
34
|
-
getAdvertisingId,
|
|
35
|
-
requestTrackingPermissionsAsync,
|
|
36
|
-
getTrackingPermissionsAsync,
|
|
37
|
-
isAvailable,
|
|
38
|
-
} = await import('./tracking-transparency');
|
|
39
|
-
|
|
40
|
-
afterEach(() => {
|
|
41
|
-
fakePlatform.OS = 'ios';
|
|
42
|
-
vi.clearAllMocks();
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
describe('getAdvertisingId', () => {
|
|
46
|
-
it('returns the advertising ID straight from the native module', () => {
|
|
47
|
-
expect(getAdvertisingId()).toBe('E9228286-4C4E-4789-9D95-15827DCB291B');
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('maps the all-zero UUID sentinel to null', () => {
|
|
51
|
-
FAKE_NATIVE_TRACKING_TRANSPARENCY.getAdvertisingId.mockReturnValueOnce(
|
|
52
|
-
'00000000-0000-0000-0000-000000000000',
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
expect(getAdvertisingId()).toBeNull();
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
describe('requestTrackingPermissionsAsync', () => {
|
|
60
|
-
it('always resolves granted on Android without calling the native module', async () => {
|
|
61
|
-
fakePlatform.OS = 'android';
|
|
62
|
-
|
|
63
|
-
await expect(requestTrackingPermissionsAsync()).resolves.toEqual({
|
|
64
|
-
granted: true,
|
|
65
|
-
expires: 'never',
|
|
66
|
-
canAskAgain: true,
|
|
67
|
-
status: 'granted',
|
|
68
|
-
});
|
|
69
|
-
expect(FAKE_NATIVE_TRACKING_TRANSPARENCY.requestPermissionsAsync).not.toHaveBeenCalled();
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('delegates to the native module on iOS', async () => {
|
|
73
|
-
fakePlatform.OS = 'ios';
|
|
74
|
-
|
|
75
|
-
await expect(requestTrackingPermissionsAsync()).resolves.toEqual({
|
|
76
|
-
status: 'granted',
|
|
77
|
-
granted: true,
|
|
78
|
-
});
|
|
79
|
-
expect(FAKE_NATIVE_TRACKING_TRANSPARENCY.requestPermissionsAsync).toHaveBeenCalledTimes(1);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('throws an UnavailabilityError-shaped error on iOS when the native method is absent', async () => {
|
|
83
|
-
fakePlatform.OS = 'ios';
|
|
84
|
-
const { requestPermissionsAsync: native } = FAKE_NATIVE_TRACKING_TRANSPARENCY;
|
|
85
|
-
// @ts-expect-error -- simulating a native module build missing this method
|
|
86
|
-
FAKE_NATIVE_TRACKING_TRANSPARENCY.requestPermissionsAsync = undefined;
|
|
87
|
-
|
|
88
|
-
await expect(requestTrackingPermissionsAsync()).rejects.toThrow(
|
|
89
|
-
'requestPermissionsAsync is not available on TrackingTransparency',
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
FAKE_NATIVE_TRACKING_TRANSPARENCY.requestPermissionsAsync = native;
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
describe('getTrackingPermissionsAsync', () => {
|
|
97
|
-
it('always resolves granted on Android without calling the native module', async () => {
|
|
98
|
-
fakePlatform.OS = 'android';
|
|
99
|
-
|
|
100
|
-
await expect(getTrackingPermissionsAsync()).resolves.toEqual({
|
|
101
|
-
granted: true,
|
|
102
|
-
expires: 'never',
|
|
103
|
-
canAskAgain: true,
|
|
104
|
-
status: 'granted',
|
|
105
|
-
});
|
|
106
|
-
expect(FAKE_NATIVE_TRACKING_TRANSPARENCY.getPermissionsAsync).not.toHaveBeenCalled();
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it('delegates to the native module on iOS', async () => {
|
|
110
|
-
fakePlatform.OS = 'ios';
|
|
111
|
-
|
|
112
|
-
await expect(getTrackingPermissionsAsync()).resolves.toEqual({
|
|
113
|
-
status: 'granted',
|
|
114
|
-
granted: true,
|
|
115
|
-
});
|
|
116
|
-
expect(FAKE_NATIVE_TRACKING_TRANSPARENCY.getPermissionsAsync).toHaveBeenCalledTimes(1);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('throws an UnavailabilityError-shaped error on iOS when the native method is absent', async () => {
|
|
120
|
-
fakePlatform.OS = 'ios';
|
|
121
|
-
const { getPermissionsAsync: native } = FAKE_NATIVE_TRACKING_TRANSPARENCY;
|
|
122
|
-
// @ts-expect-error -- simulating a native module build missing this method
|
|
123
|
-
FAKE_NATIVE_TRACKING_TRANSPARENCY.getPermissionsAsync = undefined;
|
|
124
|
-
|
|
125
|
-
await expect(getTrackingPermissionsAsync()).rejects.toThrow(
|
|
126
|
-
'getPermissionsAsync is not available on TrackingTransparency',
|
|
127
|
-
);
|
|
128
|
-
|
|
129
|
-
FAKE_NATIVE_TRACKING_TRANSPARENCY.getPermissionsAsync = native;
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
describe('isAvailable', () => {
|
|
134
|
-
it('resolves true when the native module resolved', () => {
|
|
135
|
-
expect(isAvailable()).toBe(true);
|
|
136
|
-
});
|
|
137
|
-
});
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
// Co-located React-driven test (ADR 0025) for usePermissions. See battery's
|
|
2
|
-
// use-battery-state.test.tsx for the shared rationale (mocks `core`, not expo-modules-core
|
|
3
|
-
// internals).
|
|
4
|
-
|
|
5
|
-
import { createElement, type ReactElement } from 'react';
|
|
6
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
7
|
-
import { mount, unmount, View } from '@symbiote-native/react';
|
|
8
|
-
import { installFabric } from '@symbiote-native/test-utils';
|
|
9
|
-
import { usePermissions } from './index';
|
|
10
|
-
import { PermissionStatus, type PermissionResponse } from '../../../core';
|
|
11
|
-
|
|
12
|
-
const { getTrackingPermissionsAsync, requestTrackingPermissionsAsync } = vi.hoisted(() => ({
|
|
13
|
-
getTrackingPermissionsAsync: vi.fn(),
|
|
14
|
-
requestTrackingPermissionsAsync: vi.fn(),
|
|
15
|
-
}));
|
|
16
|
-
|
|
17
|
-
// Same enum-shaped-object mock trick packages/battery/src/react/hooks/use-battery-state's test
|
|
18
|
-
// uses for BatteryState — the mock factory's runtime object stands in for the real enum
|
|
19
|
-
// (imported here purely for its type), so `PermissionStatus.GRANTED` still type-checks.
|
|
20
|
-
vi.mock('../../../core', () => ({
|
|
21
|
-
getTrackingPermissionsAsync,
|
|
22
|
-
requestTrackingPermissionsAsync,
|
|
23
|
-
PermissionStatus: { GRANTED: 'granted', DENIED: 'denied', UNDETERMINED: 'undetermined' },
|
|
24
|
-
}));
|
|
25
|
-
|
|
26
|
-
const ROOT_TAG = 954;
|
|
27
|
-
|
|
28
|
-
const GRANTED: PermissionResponse = {
|
|
29
|
-
status: PermissionStatus.GRANTED,
|
|
30
|
-
granted: true,
|
|
31
|
-
canAskAgain: true,
|
|
32
|
-
expires: 'never',
|
|
33
|
-
};
|
|
34
|
-
const DENIED: PermissionResponse = {
|
|
35
|
-
status: PermissionStatus.DENIED,
|
|
36
|
-
granted: false,
|
|
37
|
-
canAskAgain: true,
|
|
38
|
-
expires: 'never',
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const results: (PermissionResponse | null)[] = [];
|
|
42
|
-
let latestRequest: (() => Promise<PermissionResponse>) | undefined;
|
|
43
|
-
let latestGet: (() => Promise<PermissionResponse>) | undefined;
|
|
44
|
-
|
|
45
|
-
function Probe(): ReactElement {
|
|
46
|
-
const [status, request, get] = usePermissions();
|
|
47
|
-
results.push(status);
|
|
48
|
-
latestRequest = request;
|
|
49
|
-
latestGet = get;
|
|
50
|
-
return createElement(View);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const fabric = installFabric();
|
|
54
|
-
|
|
55
|
-
beforeEach(() => {
|
|
56
|
-
fabric.reset();
|
|
57
|
-
results.length = 0;
|
|
58
|
-
latestRequest = undefined;
|
|
59
|
-
latestGet = undefined;
|
|
60
|
-
vi.clearAllMocks();
|
|
61
|
-
getTrackingPermissionsAsync.mockResolvedValue(GRANTED);
|
|
62
|
-
requestTrackingPermissionsAsync.mockResolvedValue(GRANTED);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
afterEach(() => unmount(ROOT_TAG));
|
|
66
|
-
|
|
67
|
-
describe('usePermissions', () => {
|
|
68
|
-
it('starts at null before the initial fetch resolves', () => {
|
|
69
|
-
mount(ROOT_TAG, createElement(Probe));
|
|
70
|
-
|
|
71
|
-
expect(results[results.length - 1]).toBe(null);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('fetches the permission status once on mount', async () => {
|
|
75
|
-
mount(ROOT_TAG, createElement(Probe));
|
|
76
|
-
|
|
77
|
-
await vi.waitFor(() => expect(results[results.length - 1]).toEqual(GRANTED));
|
|
78
|
-
expect(getTrackingPermissionsAsync).toHaveBeenCalledTimes(1);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('request() delegates to requestTrackingPermissionsAsync and updates the status', async () => {
|
|
82
|
-
mount(ROOT_TAG, createElement(Probe));
|
|
83
|
-
await vi.waitFor(() => expect(results[results.length - 1]).toEqual(GRANTED));
|
|
84
|
-
|
|
85
|
-
requestTrackingPermissionsAsync.mockResolvedValueOnce(DENIED);
|
|
86
|
-
await latestRequest?.();
|
|
87
|
-
|
|
88
|
-
await vi.waitFor(() => expect(results[results.length - 1]).toEqual(DENIED));
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('get() re-fetches and updates the status', async () => {
|
|
92
|
-
mount(ROOT_TAG, createElement(Probe));
|
|
93
|
-
await vi.waitFor(() => expect(results[results.length - 1]).toEqual(GRANTED));
|
|
94
|
-
|
|
95
|
-
getTrackingPermissionsAsync.mockResolvedValueOnce(DENIED);
|
|
96
|
-
await latestGet?.();
|
|
97
|
-
|
|
98
|
-
await vi.waitFor(() => expect(results[results.length - 1]).toEqual(DENIED));
|
|
99
|
-
});
|
|
100
|
-
});
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
// Co-located Vue-driven test (ADR 0025) for usePermissions. See battery's
|
|
2
|
-
// use-battery-state.test.ts for the shared rationale.
|
|
3
|
-
|
|
4
|
-
import { defineComponent, h } from '@vue/runtime-core';
|
|
5
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
-
import { mount, unmount } from '@symbiote-native/vue';
|
|
7
|
-
import { installFabric } from '@symbiote-native/test-utils';
|
|
8
|
-
import { usePermissions } from './index';
|
|
9
|
-
import { PermissionStatus, type PermissionResponse } from '../../../core';
|
|
10
|
-
|
|
11
|
-
const { getTrackingPermissionsAsync, requestTrackingPermissionsAsync } = vi.hoisted(() => ({
|
|
12
|
-
getTrackingPermissionsAsync: vi.fn(),
|
|
13
|
-
requestTrackingPermissionsAsync: vi.fn(),
|
|
14
|
-
}));
|
|
15
|
-
|
|
16
|
-
// Same enum-shaped-object mock trick packages/battery/src/vue/composables/use-battery-state's
|
|
17
|
-
// test uses for BatteryState.
|
|
18
|
-
vi.mock('../../../core', () => ({
|
|
19
|
-
getTrackingPermissionsAsync,
|
|
20
|
-
requestTrackingPermissionsAsync,
|
|
21
|
-
PermissionStatus: { GRANTED: 'granted', DENIED: 'denied', UNDETERMINED: 'undetermined' },
|
|
22
|
-
}));
|
|
23
|
-
|
|
24
|
-
const GRANTED: PermissionResponse = {
|
|
25
|
-
status: PermissionStatus.GRANTED,
|
|
26
|
-
granted: true,
|
|
27
|
-
canAskAgain: true,
|
|
28
|
-
expires: 'never',
|
|
29
|
-
};
|
|
30
|
-
const DENIED: PermissionResponse = {
|
|
31
|
-
status: PermissionStatus.DENIED,
|
|
32
|
-
granted: false,
|
|
33
|
-
canAskAgain: true,
|
|
34
|
-
expires: 'never',
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
const ROOT_TAG = 9954;
|
|
38
|
-
const fabric = installFabric();
|
|
39
|
-
|
|
40
|
-
beforeEach(() => {
|
|
41
|
-
fabric.reset();
|
|
42
|
-
vi.clearAllMocks();
|
|
43
|
-
getTrackingPermissionsAsync.mockResolvedValue(GRANTED);
|
|
44
|
-
requestTrackingPermissionsAsync.mockResolvedValue(GRANTED);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
afterEach(() => unmount(ROOT_TAG));
|
|
48
|
-
|
|
49
|
-
function mountPermissions(): ReturnType<typeof usePermissions> {
|
|
50
|
-
let result: ReturnType<typeof usePermissions> | undefined;
|
|
51
|
-
mount(
|
|
52
|
-
ROOT_TAG,
|
|
53
|
-
defineComponent({
|
|
54
|
-
setup: () => {
|
|
55
|
-
result = usePermissions();
|
|
56
|
-
return () => h('symbiote-text', {}, 'tracking-transparency');
|
|
57
|
-
},
|
|
58
|
-
}),
|
|
59
|
-
);
|
|
60
|
-
if (result === undefined) {
|
|
61
|
-
throw new Error('setup() did not run');
|
|
62
|
-
}
|
|
63
|
-
return result;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
describe('usePermissions (Vue)', () => {
|
|
67
|
-
it('starts at null before the initial fetch resolves', () => {
|
|
68
|
-
const { status } = mountPermissions();
|
|
69
|
-
|
|
70
|
-
expect(status.value).toBe(null);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('fetches the permission status once on mount', async () => {
|
|
74
|
-
const { status } = mountPermissions();
|
|
75
|
-
|
|
76
|
-
await vi.waitFor(() => expect(status.value).toEqual(GRANTED));
|
|
77
|
-
expect(getTrackingPermissionsAsync).toHaveBeenCalledTimes(1);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('request() delegates to requestTrackingPermissionsAsync and updates the status', async () => {
|
|
81
|
-
const { status, request } = mountPermissions();
|
|
82
|
-
await vi.waitFor(() => expect(status.value).toEqual(GRANTED));
|
|
83
|
-
|
|
84
|
-
requestTrackingPermissionsAsync.mockResolvedValueOnce(DENIED);
|
|
85
|
-
await request();
|
|
86
|
-
|
|
87
|
-
expect(status.value).toEqual(DENIED);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('get() re-fetches and updates the status', async () => {
|
|
91
|
-
const { status, get } = mountPermissions();
|
|
92
|
-
await vi.waitFor(() => expect(status.value).toEqual(GRANTED));
|
|
93
|
-
|
|
94
|
-
getTrackingPermissionsAsync.mockResolvedValueOnce(DENIED);
|
|
95
|
-
await get();
|
|
96
|
-
|
|
97
|
-
expect(status.value).toEqual(DENIED);
|
|
98
|
-
});
|
|
99
|
-
});
|