@chipmobilesdk/rn-ads 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +57 -0
- package/README.md +310 -11
- package/package.json +1 -1
- package/skills/sdk-rn-ads/SKILL.md +125 -32
- package/src/config/types.ts +170 -8
- package/src/config/validate.ts +83 -7
- package/src/core/adOriginatedReturn.ts +134 -0
- package/src/core/adsInstance.ts +565 -42
- package/src/core/consentFlow.ts +167 -0
- package/src/core/enablement.ts +76 -0
- package/src/core/gates.ts +91 -0
- package/src/core/initMachine.ts +73 -7
- package/src/core/retryPolicy.ts +127 -0
- package/src/core/timers.ts +109 -10
- package/src/diagnostics/errors.ts +27 -3
- package/src/diagnostics/events.ts +41 -1
- package/src/engine/admobEngine.ts +144 -2
- package/src/engine/port.ts +44 -0
- package/src/engine/testAdUnits.ts +3 -0
- package/src/index.ts +23 -1
- package/src/react/AdsBanner.tsx +23 -1
- package/src/testing/index.ts +155 -0
package/src/testing/index.ts
CHANGED
|
@@ -17,13 +17,89 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import type { FullScreenFormat, Reward, Unsubscribe } from '../config/types';
|
|
20
|
+
import type { Scheduler } from '../core/timers';
|
|
20
21
|
import type {
|
|
21
22
|
AdsEngine,
|
|
23
|
+
ConsentPort,
|
|
22
24
|
EngineAdEvent,
|
|
23
25
|
EngineAdHandle,
|
|
26
|
+
EngineConsentInfo,
|
|
27
|
+
EngineConsentOptions,
|
|
24
28
|
EnginePrivacy,
|
|
25
29
|
} from '../engine/port';
|
|
26
30
|
|
|
31
|
+
/**
|
|
32
|
+
* A clock a test can move by hand.
|
|
33
|
+
*
|
|
34
|
+
* Retry backoff, app-open expiry, the undecided-state warning, and the
|
|
35
|
+
* ad-originated-return windows are all time-dependent, and all four must be
|
|
36
|
+
* assertable exactly rather than approximately. Advancing a clock makes "two
|
|
37
|
+
* retries, 2s then 4s" a precise assertion; sleeping would make it a flaky one.
|
|
38
|
+
*
|
|
39
|
+
* Callbacks fire in due order, and a callback scheduled *during* an advance is
|
|
40
|
+
* honored within the same advance if its deadline has also passed — matching how
|
|
41
|
+
* real timers behave when work chains.
|
|
42
|
+
*/
|
|
43
|
+
export interface TestClock {
|
|
44
|
+
now(): number;
|
|
45
|
+
advance(ms: number): void;
|
|
46
|
+
readonly scheduler: Scheduler;
|
|
47
|
+
pending(): number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function createTestClock(startMs = 1_000_000): TestClock {
|
|
51
|
+
let current = startMs;
|
|
52
|
+
let sequence = 0;
|
|
53
|
+
const timers = new Map<number, { at: number; callback: () => void }>();
|
|
54
|
+
|
|
55
|
+
const scheduler: Scheduler = {
|
|
56
|
+
setTimeout(callback, ms) {
|
|
57
|
+
sequence += 1;
|
|
58
|
+
timers.set(sequence, { at: current + ms, callback });
|
|
59
|
+
return sequence;
|
|
60
|
+
},
|
|
61
|
+
clearTimeout(handle) {
|
|
62
|
+
timers.delete(handle as number);
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
now: () => current,
|
|
68
|
+
advance(ms) {
|
|
69
|
+
const target = current + ms;
|
|
70
|
+
// Loop rather than iterate once: a fired callback may schedule another
|
|
71
|
+
// timer that is also due before `target`, which is exactly what a retry
|
|
72
|
+
// chain does.
|
|
73
|
+
for (;;) {
|
|
74
|
+
let nextId: number | null = null;
|
|
75
|
+
let nextAt = Number.POSITIVE_INFINITY;
|
|
76
|
+
for (const [id, timer] of timers) {
|
|
77
|
+
if (timer.at <= target && timer.at < nextAt) {
|
|
78
|
+
nextAt = timer.at;
|
|
79
|
+
nextId = id;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (nextId === null) {
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
const timer = timers.get(nextId)!;
|
|
86
|
+
timers.delete(nextId);
|
|
87
|
+
current = timer.at;
|
|
88
|
+
timer.callback();
|
|
89
|
+
}
|
|
90
|
+
current = target;
|
|
91
|
+
},
|
|
92
|
+
scheduler,
|
|
93
|
+
pending: () => timers.size,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export type FakeConsentCall =
|
|
98
|
+
| { readonly kind: 'requestInfoUpdate'; readonly options: EngineConsentOptions }
|
|
99
|
+
| { readonly kind: 'showFormIfRequired' }
|
|
100
|
+
| { readonly kind: 'getInfo' }
|
|
101
|
+
| { readonly kind: 'reset' };
|
|
102
|
+
|
|
27
103
|
export type FakeEngineCall =
|
|
28
104
|
| { readonly kind: 'initialize'; readonly privacy: EnginePrivacy }
|
|
29
105
|
| {
|
|
@@ -47,8 +123,18 @@ export interface FakeEngineOptions {
|
|
|
47
123
|
readonly neverCallBack?: boolean;
|
|
48
124
|
/** Rejects `show`, for the display-failure path (FR-044). */
|
|
49
125
|
readonly failShow?: unknown;
|
|
126
|
+
/** Initial consent information. Defaults to the not-required, allowed case. */
|
|
127
|
+
readonly consentInfo?: Partial<EngineConsentInfo>;
|
|
128
|
+
/** Drives time for retry, expiry, and the undecided warning. */
|
|
129
|
+
readonly clock?: TestClock;
|
|
50
130
|
}
|
|
51
131
|
|
|
132
|
+
const DEFAULT_CONSENT: EngineConsentInfo = {
|
|
133
|
+
status: 'not_required',
|
|
134
|
+
canRequestAds: true,
|
|
135
|
+
isFormAvailable: false,
|
|
136
|
+
};
|
|
137
|
+
|
|
52
138
|
export interface FakeAdsEngine extends AdsEngine {
|
|
53
139
|
/** Every call the package made, in order. */
|
|
54
140
|
readonly calls: readonly FakeEngineCall[];
|
|
@@ -66,10 +152,60 @@ export interface FakeAdsEngine extends AdsEngine {
|
|
|
66
152
|
listenerCount(): number;
|
|
67
153
|
/** Drops every recorded call, for tests that assert on a later window only. */
|
|
68
154
|
reset(): void;
|
|
155
|
+
|
|
156
|
+
/** The clock driving this engine's timers. Wire it into `createAds`. */
|
|
157
|
+
readonly clock: TestClock;
|
|
158
|
+
/** Moves the clock and fires anything due. */
|
|
159
|
+
advance(ms: number): void;
|
|
160
|
+
|
|
161
|
+
/** Every consent call the package made, in order (FR-113's assertion surface). */
|
|
162
|
+
readonly consentCalls: readonly FakeConsentCall[];
|
|
163
|
+
/** Sets what the consent platform will report from now on. */
|
|
164
|
+
setConsentInfo(info: Partial<EngineConsentInfo>): void;
|
|
165
|
+
/** Makes `requestInfoUpdate` reject — the flow must yield `error`, never granted. */
|
|
166
|
+
failConsentRequest(cause?: unknown): void;
|
|
167
|
+
/** Makes `showFormIfRequired` reject — dismissal is not consent (FR-106). */
|
|
168
|
+
failConsentForm(cause?: unknown): void;
|
|
69
169
|
}
|
|
70
170
|
|
|
71
171
|
export function createFakeAdsEngine(options: FakeEngineOptions = {}): FakeAdsEngine {
|
|
72
172
|
const calls: FakeEngineCall[] = [];
|
|
173
|
+
const consentCalls: FakeConsentCall[] = [];
|
|
174
|
+
const clock = options.clock ?? createTestClock();
|
|
175
|
+
let consentInfo: EngineConsentInfo = { ...DEFAULT_CONSENT, ...options.consentInfo };
|
|
176
|
+
let consentRequestFailure: unknown;
|
|
177
|
+
let consentFormFailure: unknown;
|
|
178
|
+
|
|
179
|
+
const consent: ConsentPort = {
|
|
180
|
+
async requestInfoUpdate(consentOptions) {
|
|
181
|
+
consentCalls.push({ kind: 'requestInfoUpdate', options: consentOptions });
|
|
182
|
+
if (consentRequestFailure !== undefined) {
|
|
183
|
+
throw consentRequestFailure;
|
|
184
|
+
}
|
|
185
|
+
return consentInfo;
|
|
186
|
+
},
|
|
187
|
+
async showFormIfRequired() {
|
|
188
|
+
consentCalls.push({ kind: 'showFormIfRequired' });
|
|
189
|
+
if (consentFormFailure !== undefined) {
|
|
190
|
+
throw consentFormFailure;
|
|
191
|
+
}
|
|
192
|
+
// A presented form that the user completes moves the status on. The
|
|
193
|
+
// fake only does this when a form was actually required, mirroring the
|
|
194
|
+
// provider: showing nothing cannot change an answer.
|
|
195
|
+
if (consentInfo.status === 'required' && consentInfo.isFormAvailable) {
|
|
196
|
+
consentInfo = { status: 'obtained', canRequestAds: true, isFormAvailable: false };
|
|
197
|
+
}
|
|
198
|
+
return consentInfo;
|
|
199
|
+
},
|
|
200
|
+
async getInfo() {
|
|
201
|
+
consentCalls.push({ kind: 'getInfo' });
|
|
202
|
+
return consentInfo;
|
|
203
|
+
},
|
|
204
|
+
reset() {
|
|
205
|
+
consentCalls.push({ kind: 'reset' });
|
|
206
|
+
consentInfo = { status: 'unknown', canRequestAds: false, isFormAvailable: true };
|
|
207
|
+
},
|
|
208
|
+
};
|
|
73
209
|
const listeners = new Map<string, Set<(event: EngineAdEvent) => void>>();
|
|
74
210
|
/** Callbacks queued before anyone subscribed; see `subscribe`. */
|
|
75
211
|
const pending = new Map<string, EngineAdEvent>();
|
|
@@ -82,6 +218,25 @@ export function createFakeAdsEngine(options: FakeEngineOptions = {}): FakeAdsEng
|
|
|
82
218
|
|
|
83
219
|
return {
|
|
84
220
|
calls,
|
|
221
|
+
consentCalls,
|
|
222
|
+
consent,
|
|
223
|
+
clock,
|
|
224
|
+
|
|
225
|
+
advance(ms) {
|
|
226
|
+
clock.advance(ms);
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
setConsentInfo(info) {
|
|
230
|
+
consentInfo = { ...consentInfo, ...info };
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
failConsentRequest(cause = new Error('consent request failed')) {
|
|
234
|
+
consentRequestFailure = cause;
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
failConsentForm(cause = new Error('consent form failed')) {
|
|
238
|
+
consentFormFailure = cause;
|
|
239
|
+
},
|
|
85
240
|
|
|
86
241
|
isAvailable() {
|
|
87
242
|
return options.available !== false;
|