@jskit-ai/google-rewarded-web 0.1.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.
@@ -0,0 +1,563 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import { createGoogleRewardedRuntime } from "../src/client/runtime/googleRewardedRuntime.js";
5
+
6
+ function waitForAsyncTurn() {
7
+ return new Promise((resolve) => {
8
+ setImmediate(resolve);
9
+ });
10
+ }
11
+
12
+ function createJsonResponse(data, status = 200) {
13
+ return {
14
+ ok: status >= 200 && status < 300,
15
+ status,
16
+ headers: {
17
+ get(name) {
18
+ return String(name || "").toLowerCase() === "content-type"
19
+ ? "application/json"
20
+ : "";
21
+ }
22
+ },
23
+ async json() {
24
+ return data;
25
+ }
26
+ };
27
+ }
28
+
29
+ function createFetchStub({
30
+ currentResponse,
31
+ startResponse,
32
+ grantResponse,
33
+ closeResponse
34
+ } = {}) {
35
+ const calls = [];
36
+
37
+ async function fetchStub(url, options = {}) {
38
+ const method = String(options.method || "GET").toUpperCase();
39
+ const serializedBody = typeof options.body === "string" ? options.body : "";
40
+ const parsedBody = serializedBody ? JSON.parse(serializedBody) : null;
41
+
42
+ calls.push({
43
+ url: String(url),
44
+ method,
45
+ body: parsedBody
46
+ });
47
+
48
+ if (String(url) === "/api/session") {
49
+ return createJsonResponse({
50
+ csrfToken: "csrf-token"
51
+ });
52
+ }
53
+
54
+ if (String(url).includes("/google-rewarded/current")) {
55
+ return createJsonResponse(currentResponse);
56
+ }
57
+ if (String(url).includes("/google-rewarded/start")) {
58
+ return createJsonResponse(startResponse);
59
+ }
60
+ if (String(url).includes("/google-rewarded/grant")) {
61
+ return createJsonResponse(grantResponse);
62
+ }
63
+ if (String(url).includes("/google-rewarded/close")) {
64
+ return createJsonResponse(closeResponse);
65
+ }
66
+
67
+ throw new Error(`Unexpected fetch call: ${method} ${String(url)}`);
68
+ }
69
+
70
+ return {
71
+ calls,
72
+ fetchStub
73
+ };
74
+ }
75
+
76
+ function installBrowserGlobals({ mode = "grant" } = {}) {
77
+ const originalWindow = globalThis.window;
78
+ const originalDocument = globalThis.document;
79
+ const originalFetch = globalThis.fetch;
80
+
81
+ const listenerMap = new Map();
82
+ const slot = {
83
+ addService() {
84
+ return this;
85
+ }
86
+ };
87
+ const pubads = {
88
+ addEventListener(name, handler) {
89
+ if (!listenerMap.has(name)) {
90
+ listenerMap.set(name, new Set());
91
+ }
92
+ listenerMap.get(name).add(handler);
93
+ },
94
+ removeEventListener(name, handler) {
95
+ listenerMap.get(name)?.delete(handler);
96
+ }
97
+ };
98
+
99
+ function emit(name, event) {
100
+ for (const handler of listenerMap.get(name) || []) {
101
+ handler(event);
102
+ }
103
+ }
104
+
105
+ const googletag = {
106
+ apiReady: true,
107
+ cmd: {
108
+ push(handler) {
109
+ handler();
110
+ }
111
+ },
112
+ enums: {
113
+ OutOfPageFormat: {
114
+ REWARDED: "REWARDED"
115
+ }
116
+ },
117
+ pubads() {
118
+ return pubads;
119
+ },
120
+ defineOutOfPageSlot() {
121
+ return mode === "unavailable" ? null : slot;
122
+ },
123
+ enableServices() {},
124
+ display() {
125
+ emit("rewardedSlotReady", {
126
+ slot,
127
+ makeRewardedVisible() {}
128
+ });
129
+
130
+ if (mode === "grant") {
131
+ emit("rewardedSlotGranted", {
132
+ slot
133
+ });
134
+ }
135
+
136
+ emit("rewardedSlotClosed", {
137
+ slot
138
+ });
139
+ },
140
+ destroySlots() {}
141
+ };
142
+
143
+ globalThis.window = {
144
+ setTimeout,
145
+ clearTimeout,
146
+ googletag
147
+ };
148
+ globalThis.document = {};
149
+
150
+ return {
151
+ restore() {
152
+ globalThis.window = originalWindow;
153
+ globalThis.document = originalDocument;
154
+ globalThis.fetch = originalFetch;
155
+ }
156
+ };
157
+ }
158
+
159
+ test("google rewarded runtime resolves immediately when the gate is already unlocked", async () => {
160
+ const originalFetch = globalThis.fetch;
161
+ const { calls, fetchStub } = createFetchStub({
162
+ currentResponse: {
163
+ gateKey: "progress-logging",
164
+ workspaceSlug: "alpha",
165
+ surface: "app",
166
+ enabled: true,
167
+ available: true,
168
+ blocked: false,
169
+ reason: "already-unlocked",
170
+ rule: null,
171
+ providerConfig: {
172
+ id: "21",
173
+ surface: "app",
174
+ enabled: true,
175
+ adUnitPath: "/123456/rewarded",
176
+ scriptMode: "gpt_rewarded"
177
+ },
178
+ unlock: {
179
+ id: "31",
180
+ gateKey: "progress-logging",
181
+ providerConfigId: "21",
182
+ watchSessionId: "41",
183
+ grantedAt: new Date().toISOString(),
184
+ unlockedUntil: new Date(Date.now() + 60_000).toISOString()
185
+ },
186
+ cooldownUntil: null,
187
+ dailyLimitRemaining: null
188
+ }
189
+ });
190
+ globalThis.fetch = fetchStub;
191
+
192
+ try {
193
+ const runtime = createGoogleRewardedRuntime();
194
+ const result = await runtime.requireUnlock({
195
+ gateKey: "progress-logging",
196
+ workspaceSlug: "alpha"
197
+ });
198
+
199
+ assert.equal(result.granted, true);
200
+ assert.equal(result.state.reason, "already-unlocked");
201
+ assert.equal(runtime.state.open, false);
202
+ assert.equal(calls.filter((entry) => entry.url.includes("/google-rewarded/current")).length, 1);
203
+ assert.doesNotMatch(
204
+ calls.find((entry) => entry.url.includes("/google-rewarded/current"))?.url || "",
205
+ /surface=/
206
+ );
207
+ } finally {
208
+ globalThis.fetch = originalFetch;
209
+ }
210
+ });
211
+
212
+ test("google rewarded runtime rejects when the current gate state is malformed", async () => {
213
+ const originalFetch = globalThis.fetch;
214
+ const { fetchStub } = createFetchStub({
215
+ currentResponse: {
216
+ gateKey: "progress-logging",
217
+ workspaceSlug: "alpha"
218
+ }
219
+ });
220
+ globalThis.fetch = fetchStub;
221
+
222
+ try {
223
+ const runtime = createGoogleRewardedRuntime();
224
+ await assert.rejects(
225
+ () => runtime.requireUnlock({
226
+ gateKey: "progress-logging",
227
+ workspaceSlug: "alpha"
228
+ }),
229
+ /invalid state/i
230
+ );
231
+ assert.equal(runtime.state.open, false);
232
+ } finally {
233
+ globalThis.fetch = originalFetch;
234
+ }
235
+ });
236
+
237
+ test("google rewarded runtime rejects reasonless non-blocking gate states", async () => {
238
+ const originalFetch = globalThis.fetch;
239
+ const { fetchStub } = createFetchStub({
240
+ currentResponse: {
241
+ gateKey: "progress-logging",
242
+ workspaceSlug: "alpha",
243
+ enabled: false,
244
+ blocked: false
245
+ }
246
+ });
247
+ globalThis.fetch = fetchStub;
248
+
249
+ try {
250
+ const runtime = createGoogleRewardedRuntime();
251
+ await assert.rejects(
252
+ () => runtime.requireUnlock({
253
+ gateKey: "progress-logging",
254
+ workspaceSlug: "alpha"
255
+ }),
256
+ /invalid state/i
257
+ );
258
+ assert.equal(runtime.state.open, false);
259
+ } finally {
260
+ globalThis.fetch = originalFetch;
261
+ }
262
+ });
263
+
264
+ test("google rewarded runtime completes a rewarded watch flow and grants unlock state", async () => {
265
+ const globals = installBrowserGlobals({
266
+ mode: "grant"
267
+ });
268
+ const { calls, fetchStub } = createFetchStub({
269
+ currentResponse: {
270
+ gateKey: "progress-logging",
271
+ workspaceSlug: "alpha",
272
+ surface: "app",
273
+ enabled: true,
274
+ available: true,
275
+ blocked: true,
276
+ reason: "reward-required",
277
+ rule: null,
278
+ providerConfig: {
279
+ id: "21",
280
+ surface: "app",
281
+ enabled: true,
282
+ adUnitPath: "/123456/rewarded",
283
+ scriptMode: "gpt_rewarded"
284
+ },
285
+ unlock: null,
286
+ cooldownUntil: null,
287
+ dailyLimitRemaining: null
288
+ },
289
+ startResponse: {
290
+ gateKey: "progress-logging",
291
+ workspaceSlug: "alpha",
292
+ surface: "app",
293
+ enabled: true,
294
+ available: true,
295
+ blocked: true,
296
+ reason: "reward-required",
297
+ rule: null,
298
+ providerConfig: {
299
+ id: "21",
300
+ surface: "app",
301
+ enabled: true,
302
+ adUnitPath: "/123456/rewarded",
303
+ scriptMode: "gpt_rewarded"
304
+ },
305
+ unlock: null,
306
+ cooldownUntil: null,
307
+ dailyLimitRemaining: null,
308
+ session: {
309
+ id: "41",
310
+ gateKey: "progress-logging",
311
+ providerConfigId: "21",
312
+ status: "started",
313
+ startedAt: new Date().toISOString(),
314
+ rewardedAt: null,
315
+ completedAt: null,
316
+ closedAt: null
317
+ }
318
+ },
319
+ grantResponse: {
320
+ unlocked: true,
321
+ workspaceSlug: "alpha",
322
+ gateKey: "progress-logging",
323
+ unlock: {
324
+ id: "51",
325
+ gateKey: "progress-logging",
326
+ providerConfigId: "21",
327
+ watchSessionId: "41",
328
+ grantedAt: new Date().toISOString(),
329
+ unlockedUntil: new Date(Date.now() + 30 * 60_000).toISOString()
330
+ },
331
+ session: {
332
+ id: "41",
333
+ gateKey: "progress-logging",
334
+ providerConfigId: "21",
335
+ status: "rewarded",
336
+ startedAt: new Date().toISOString(),
337
+ rewardedAt: new Date().toISOString(),
338
+ completedAt: new Date().toISOString(),
339
+ closedAt: null
340
+ }
341
+ }
342
+ });
343
+ globalThis.fetch = fetchStub;
344
+
345
+ try {
346
+ const runtime = createGoogleRewardedRuntime();
347
+ const unlockPromise = runtime.requireUnlock({
348
+ gateKey: "progress-logging",
349
+ workspaceSlug: "alpha"
350
+ });
351
+
352
+ await waitForAsyncTurn();
353
+ assert.equal(runtime.state.phase, "prompt");
354
+
355
+ await runtime.beginWatch();
356
+ const result = await unlockPromise;
357
+
358
+ assert.equal(result.granted, true);
359
+ assert.equal(result.state.unlock.watchSessionId, "41");
360
+ assert.equal(runtime.state.open, false);
361
+ assert.equal(calls.filter((entry) => entry.url.includes("/google-rewarded/start")).length, 1);
362
+ assert.equal(calls.filter((entry) => entry.url.includes("/google-rewarded/grant")).length, 1);
363
+ assert.equal(calls.filter((entry) => entry.url.includes("/google-rewarded/close")).length, 0);
364
+ assert.deepEqual(calls.find((entry) => entry.url.includes("/google-rewarded/start"))?.body, {
365
+ gateKey: "progress-logging"
366
+ });
367
+ } finally {
368
+ globals.restore();
369
+ }
370
+ });
371
+
372
+ test("google rewarded runtime closes a started session when the ad is dismissed without reward", async () => {
373
+ const globals = installBrowserGlobals({
374
+ mode: "close"
375
+ });
376
+ const { calls, fetchStub } = createFetchStub({
377
+ currentResponse: {
378
+ gateKey: "progress-logging",
379
+ workspaceSlug: "alpha",
380
+ surface: "app",
381
+ enabled: true,
382
+ available: true,
383
+ blocked: true,
384
+ reason: "reward-required",
385
+ rule: null,
386
+ providerConfig: {
387
+ id: "21",
388
+ surface: "app",
389
+ enabled: true,
390
+ adUnitPath: "/123456/rewarded",
391
+ scriptMode: "gpt_rewarded"
392
+ },
393
+ unlock: null,
394
+ cooldownUntil: null,
395
+ dailyLimitRemaining: null
396
+ },
397
+ startResponse: {
398
+ gateKey: "progress-logging",
399
+ workspaceSlug: "alpha",
400
+ surface: "app",
401
+ enabled: true,
402
+ available: true,
403
+ blocked: true,
404
+ reason: "reward-required",
405
+ rule: null,
406
+ providerConfig: {
407
+ id: "21",
408
+ surface: "app",
409
+ enabled: true,
410
+ adUnitPath: "/123456/rewarded",
411
+ scriptMode: "gpt_rewarded"
412
+ },
413
+ unlock: null,
414
+ cooldownUntil: null,
415
+ dailyLimitRemaining: null,
416
+ session: {
417
+ id: "41",
418
+ gateKey: "progress-logging",
419
+ providerConfigId: "21",
420
+ status: "started",
421
+ startedAt: new Date().toISOString(),
422
+ rewardedAt: null,
423
+ completedAt: null,
424
+ closedAt: null
425
+ }
426
+ },
427
+ closeResponse: {
428
+ closed: true,
429
+ workspaceSlug: "alpha",
430
+ gateKey: "progress-logging",
431
+ session: {
432
+ id: "41",
433
+ gateKey: "progress-logging",
434
+ providerConfigId: "21",
435
+ status: "closed",
436
+ startedAt: new Date().toISOString(),
437
+ rewardedAt: null,
438
+ completedAt: null,
439
+ closedAt: new Date().toISOString()
440
+ },
441
+ reason: null
442
+ }
443
+ });
444
+ globalThis.fetch = fetchStub;
445
+
446
+ try {
447
+ const runtime = createGoogleRewardedRuntime();
448
+ const unlockPromise = runtime.requireUnlock({
449
+ gateKey: "progress-logging",
450
+ workspaceSlug: "alpha"
451
+ });
452
+
453
+ await waitForAsyncTurn();
454
+ await runtime.beginWatch();
455
+ const result = await unlockPromise;
456
+
457
+ assert.equal(result.granted, false);
458
+ assert.equal(result.state.closed, true);
459
+ assert.equal(runtime.state.open, false);
460
+ assert.equal(calls.filter((entry) => entry.url.includes("/google-rewarded/grant")).length, 0);
461
+ assert.equal(calls.filter((entry) => entry.url.includes("/google-rewarded/close")).length, 1);
462
+ } finally {
463
+ globals.restore();
464
+ }
465
+ });
466
+
467
+ test("google rewarded runtime exposes an error state when no rewarded slot is available and cleans up on dismiss", async () => {
468
+ const globals = installBrowserGlobals({
469
+ mode: "unavailable"
470
+ });
471
+ const { calls, fetchStub } = createFetchStub({
472
+ currentResponse: {
473
+ gateKey: "progress-logging",
474
+ workspaceSlug: "alpha",
475
+ surface: "app",
476
+ enabled: true,
477
+ available: true,
478
+ blocked: true,
479
+ reason: "reward-required",
480
+ rule: null,
481
+ providerConfig: {
482
+ id: "21",
483
+ surface: "app",
484
+ enabled: true,
485
+ adUnitPath: "/123456/rewarded",
486
+ scriptMode: "gpt_rewarded"
487
+ },
488
+ unlock: null,
489
+ cooldownUntil: null,
490
+ dailyLimitRemaining: null
491
+ },
492
+ startResponse: {
493
+ gateKey: "progress-logging",
494
+ workspaceSlug: "alpha",
495
+ surface: "app",
496
+ enabled: true,
497
+ available: true,
498
+ blocked: true,
499
+ reason: "reward-required",
500
+ rule: null,
501
+ providerConfig: {
502
+ id: "21",
503
+ surface: "app",
504
+ enabled: true,
505
+ adUnitPath: "/123456/rewarded",
506
+ scriptMode: "gpt_rewarded"
507
+ },
508
+ unlock: null,
509
+ cooldownUntil: null,
510
+ dailyLimitRemaining: null,
511
+ session: {
512
+ id: "41",
513
+ gateKey: "progress-logging",
514
+ providerConfigId: "21",
515
+ status: "started",
516
+ startedAt: new Date().toISOString(),
517
+ rewardedAt: null,
518
+ completedAt: null,
519
+ closedAt: null
520
+ }
521
+ },
522
+ closeResponse: {
523
+ closed: true,
524
+ workspaceSlug: "alpha",
525
+ gateKey: "progress-logging",
526
+ session: {
527
+ id: "41",
528
+ gateKey: "progress-logging",
529
+ providerConfigId: "21",
530
+ status: "closed",
531
+ startedAt: new Date().toISOString(),
532
+ rewardedAt: null,
533
+ completedAt: null,
534
+ closedAt: new Date().toISOString()
535
+ },
536
+ reason: null
537
+ }
538
+ });
539
+ globalThis.fetch = fetchStub;
540
+
541
+ try {
542
+ const runtime = createGoogleRewardedRuntime();
543
+ const unlockPromise = runtime.requireUnlock({
544
+ gateKey: "progress-logging",
545
+ workspaceSlug: "alpha"
546
+ });
547
+
548
+ await waitForAsyncTurn();
549
+ await runtime.beginWatch();
550
+
551
+ assert.equal(runtime.state.phase, "error");
552
+ assert.match(runtime.state.errorMessage, /rewarded ad/i);
553
+
554
+ await runtime.dismissError();
555
+ const result = await unlockPromise;
556
+
557
+ assert.equal(result.granted, false);
558
+ assert.equal(runtime.state.open, false);
559
+ assert.equal(calls.filter((entry) => entry.url.includes("/google-rewarded/close")).length, 1);
560
+ } finally {
561
+ globals.restore();
562
+ }
563
+ });