@chipmobilesdk/rn-ads 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/CHANGELOG.md +46 -0
- package/README.md +372 -0
- package/package.json +49 -0
- package/skills/sdk-rn-ads/SKILL.md +98 -0
- package/src/config/resolveAdUnit.ts +85 -0
- package/src/config/types.ts +119 -0
- package/src/config/validate.ts +112 -0
- package/src/core/adsInstance.ts +1031 -0
- package/src/core/fullScreenLock.ts +54 -0
- package/src/core/initMachine.ts +108 -0
- package/src/core/placementMachine.ts +114 -0
- package/src/core/privacyGate.ts +85 -0
- package/src/core/rewardedSession.ts +84 -0
- package/src/core/timers.ts +109 -0
- package/src/diagnostics/correlation.ts +31 -0
- package/src/diagnostics/errors.ts +210 -0
- package/src/diagnostics/events.ts +179 -0
- package/src/engine/admobEngine.ts +231 -0
- package/src/engine/port.ts +72 -0
- package/src/engine/testAdUnits.ts +58 -0
- package/src/index.ts +69 -0
- package/src/react/AdsBanner.tsx +270 -0
- package/src/react/AdsProvider.tsx +57 -0
- package/src/react/hooks.ts +75 -0
- package/src/testing/index.ts +206 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic in-memory ads engine for tests (SC-012).
|
|
3
|
+
*
|
|
4
|
+
* This is a real implementation of the same `AdsEngine` port the AdMob adapter
|
|
5
|
+
* implements, not a parallel mock — so a contract change breaks it at compile
|
|
6
|
+
* time and it cannot drift. Nothing here touches a native module or a network.
|
|
7
|
+
*
|
|
8
|
+
* It exists to make the package's guarantees provable. The recorded call log is
|
|
9
|
+
* what SC-002 asserts against ("zero production identifiers reached the
|
|
10
|
+
* adapter") and what SC-003 asserts against ("zero calls before consent").
|
|
11
|
+
* Callback emission is fully caller-driven, which is what lets a test run the
|
|
12
|
+
* reward-before-dismiss and dismiss-before-reward orderings and demand the same
|
|
13
|
+
* outcome from both (SC-007).
|
|
14
|
+
*
|
|
15
|
+
* Exported from `@chipmobilesdk/rn-ads/testing`, never from the main entrypoint,
|
|
16
|
+
* so test doubles cannot reach a production bundle.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { FullScreenFormat, Reward, Unsubscribe } from '../config/types';
|
|
20
|
+
import type {
|
|
21
|
+
AdsEngine,
|
|
22
|
+
EngineAdEvent,
|
|
23
|
+
EngineAdHandle,
|
|
24
|
+
EnginePrivacy,
|
|
25
|
+
} from '../engine/port';
|
|
26
|
+
|
|
27
|
+
export type FakeEngineCall =
|
|
28
|
+
| { readonly kind: 'initialize'; readonly privacy: EnginePrivacy }
|
|
29
|
+
| {
|
|
30
|
+
readonly kind: 'createFullScreen';
|
|
31
|
+
readonly format: FullScreenFormat;
|
|
32
|
+
readonly adUnitId: string;
|
|
33
|
+
readonly handleId: string;
|
|
34
|
+
}
|
|
35
|
+
| { readonly kind: 'show'; readonly handleId: string }
|
|
36
|
+
| { readonly kind: 'dispose'; readonly handleId: string };
|
|
37
|
+
|
|
38
|
+
export interface FakeEngineOptions {
|
|
39
|
+
/** Simulates an unsupported platform or a missing bridge (FR-025). */
|
|
40
|
+
readonly available?: boolean;
|
|
41
|
+
/** Rejects `initialize`, for the failed-initialization path (FR-024). */
|
|
42
|
+
readonly failInitialize?: unknown;
|
|
43
|
+
/**
|
|
44
|
+
* Withholds the `loaded` callback so a load runs to its timeout. This is how
|
|
45
|
+
* FR-032's timeout behavior is tested without waiting on a real network.
|
|
46
|
+
*/
|
|
47
|
+
readonly neverCallBack?: boolean;
|
|
48
|
+
/** Rejects `show`, for the display-failure path (FR-044). */
|
|
49
|
+
readonly failShow?: unknown;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface FakeAdsEngine extends AdsEngine {
|
|
53
|
+
/** Every call the package made, in order. */
|
|
54
|
+
readonly calls: readonly FakeEngineCall[];
|
|
55
|
+
/** Ad unit ids passed to `createFullScreen`, in order — the SC-002 assertion. */
|
|
56
|
+
adUnitIds(): readonly string[];
|
|
57
|
+
/** Handle ids currently alive; non-empty after dispose means a leak. */
|
|
58
|
+
liveHandles(): readonly string[];
|
|
59
|
+
/** Emits a provider callback for one handle. The caller controls ordering. */
|
|
60
|
+
emit(handleId: string, event: EngineAdEvent): void;
|
|
61
|
+
/** Convenience for the common `loaded` case. */
|
|
62
|
+
completeLoad(handleId: string): void;
|
|
63
|
+
/** Emits a reward with a normalized payload. */
|
|
64
|
+
emitReward(handleId: string, reward?: Reward): void;
|
|
65
|
+
/** How many listeners are attached; must reach zero after cleanup (SC-008). */
|
|
66
|
+
listenerCount(): number;
|
|
67
|
+
/** Drops every recorded call, for tests that assert on a later window only. */
|
|
68
|
+
reset(): void;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function createFakeAdsEngine(options: FakeEngineOptions = {}): FakeAdsEngine {
|
|
72
|
+
const calls: FakeEngineCall[] = [];
|
|
73
|
+
const listeners = new Map<string, Set<(event: EngineAdEvent) => void>>();
|
|
74
|
+
/** Callbacks queued before anyone subscribed; see `subscribe`. */
|
|
75
|
+
const pending = new Map<string, EngineAdEvent>();
|
|
76
|
+
const live = new Set<string>();
|
|
77
|
+
let counter = 0;
|
|
78
|
+
|
|
79
|
+
function record(call: FakeEngineCall): void {
|
|
80
|
+
calls.push(call);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
calls,
|
|
85
|
+
|
|
86
|
+
isAvailable() {
|
|
87
|
+
return options.available !== false;
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
async initialize(privacy) {
|
|
91
|
+
record({ kind: 'initialize', privacy });
|
|
92
|
+
if (options.failInitialize !== undefined) {
|
|
93
|
+
throw options.failInitialize;
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
async createFullScreen(format, adUnitId) {
|
|
98
|
+
counter += 1;
|
|
99
|
+
const id = `fake-${counter}`;
|
|
100
|
+
record({ kind: 'createFullScreen', format, adUnitId, handleId: id });
|
|
101
|
+
live.add(id);
|
|
102
|
+
// A real provider delivers `loaded` to a listener attached after the
|
|
103
|
+
// ad object exists, so the callback is queued here and flushed when
|
|
104
|
+
// the caller subscribes. Emitting eagerly would race the caller's own
|
|
105
|
+
// subscribe and the event would be dropped — which is a property of
|
|
106
|
+
// this fake, not of the package, and would make the load look hung.
|
|
107
|
+
if (!options.neverCallBack) {
|
|
108
|
+
pending.set(id, { type: 'loaded' });
|
|
109
|
+
}
|
|
110
|
+
return { id } satisfies EngineAdHandle;
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
async show(handle) {
|
|
114
|
+
record({ kind: 'show', handleId: handle.id });
|
|
115
|
+
if (options.failShow !== undefined) {
|
|
116
|
+
throw options.failShow;
|
|
117
|
+
}
|
|
118
|
+
if (!options.neverCallBack) {
|
|
119
|
+
// `show` is called with a listener already attached, so this one
|
|
120
|
+
// can go out directly on the next microtask.
|
|
121
|
+
queueMicrotask(() => {
|
|
122
|
+
for (const listener of Array.from(listeners.get(handle.id) ?? [])) {
|
|
123
|
+
listener({ type: 'opened' });
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
subscribe(handle, listener): Unsubscribe {
|
|
130
|
+
let set = listeners.get(handle.id);
|
|
131
|
+
if (!set) {
|
|
132
|
+
set = new Set();
|
|
133
|
+
listeners.set(handle.id, set);
|
|
134
|
+
}
|
|
135
|
+
set.add(listener);
|
|
136
|
+
// Flush whatever the provider would have delivered between creating
|
|
137
|
+
// the ad and this subscription.
|
|
138
|
+
const queued = pending.get(handle.id);
|
|
139
|
+
if (queued) {
|
|
140
|
+
pending.delete(handle.id);
|
|
141
|
+
queueMicrotask(() => {
|
|
142
|
+
for (const target of Array.from(listeners.get(handle.id) ?? [])) {
|
|
143
|
+
target(queued);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
let active = true;
|
|
148
|
+
return () => {
|
|
149
|
+
if (!active) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
active = false;
|
|
153
|
+
set?.delete(listener);
|
|
154
|
+
if (set && set.size === 0) {
|
|
155
|
+
listeners.delete(handle.id);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
dispose(handle) {
|
|
161
|
+
record({ kind: 'dispose', handleId: handle.id });
|
|
162
|
+
live.delete(handle.id);
|
|
163
|
+
listeners.delete(handle.id);
|
|
164
|
+
pending.delete(handle.id);
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
adUnitIds() {
|
|
168
|
+
return calls
|
|
169
|
+
.filter(
|
|
170
|
+
(call): call is Extract<FakeEngineCall, { kind: 'createFullScreen' }> =>
|
|
171
|
+
call.kind === 'createFullScreen',
|
|
172
|
+
)
|
|
173
|
+
.map(call => call.adUnitId);
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
liveHandles() {
|
|
177
|
+
return Array.from(live);
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
emit(handleId, event) {
|
|
181
|
+
for (const listener of Array.from(listeners.get(handleId) ?? [])) {
|
|
182
|
+
listener(event);
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
completeLoad(handleId) {
|
|
187
|
+
this.emit(handleId, { type: 'loaded' });
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
emitReward(handleId, reward = { type: 'coins', amount: 10 }) {
|
|
191
|
+
this.emit(handleId, { type: 'rewarded', reward });
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
listenerCount() {
|
|
195
|
+
let total = 0;
|
|
196
|
+
for (const set of listeners.values()) {
|
|
197
|
+
total += set.size;
|
|
198
|
+
}
|
|
199
|
+
return total;
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
reset() {
|
|
203
|
+
calls.length = 0;
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
}
|