@admoai/sdk-scenarios 1.0.0 → 1.2.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.
@@ -0,0 +1,597 @@
1
+ # GENERATED from scenarios.json — do not edit; regenerate with: go run ./tools/sdkfeaturegen
2
+ #
3
+ # scenarios.json is what the iOS, Android and Flutter manifest runners execute: the live
4
+ # scenarios against a real decision-engine, the @fixture scenarios offline against the golden
5
+ # decision wire shipped in the package. This is its readable rendering: what every AdMoai SDK
6
+ # promises a publisher's app, in the same words for all three. CI regenerates it and fails on
7
+ # any difference, so it cannot drift from what the runners execute.
8
+ # How it is published and kept fresh: docs/guides/sdk-ci.md, Section 7.
9
+ # Manifest schemaVersion: 2
10
+
11
+ Feature: Cross-SDK decision & tracking contract
12
+ Shared cross-SDK E2E scenario manifest. ONE definition, executed by a thin interpreter in each
13
+ SDK, so the three suites cannot drift. Declarative only: given a request, expect an outcome.
14
+ Sponsored Pin tracking scenarios add `pinTracking`: the point-tracking calls an app makes on
15
+ the served pins, as data, and the beacons that must reach the engine. Procedural scenarios
16
+ (log capture, concurrency, retry, TTL waits) stay hand-written per SDK. Mirrored
17
+ byte-identically into all three repos; a checksum test fails the build if the copies diverge.
18
+ Every expected value here was verified against a live local decision-engine before being
19
+ written. `fixtureScenarios` (schemaVersion 2) run offline: each decodes one golden
20
+ decision-wire file (ui/platform/lib/ad-formats/golden-wire in adhub, shipped in this package
21
+ under fixtures/golden-wire/) after optional `edits`, and asserts what the publisher's app
22
+ reads from the decoded creative. `contentKeys` is the exact set of plain content keys (slides
23
+ and matched points are asserted through their own fields); a key in `contentKeysAbsent` must
24
+ decode as absent (null), never as an empty string or a placeholder; a null in `metadata` means
25
+ absent. A runner that does not execute fixtureScenarios must refuse schemaVersion 2.
26
+
27
+ @L @native
28
+ Scenario: L1 — a normal native ad serves with complete, well-formed metadata
29
+ Given the app requests placement "home"
30
+ Then an ad is served
31
+ And exactly 1 decision is returned
32
+ And the creative carries complete metadata: ad, creative, placement and template ids
33
+ And the creative's priority is one of standard, sponsorship, house
34
+ And the creative is not a Journey Ad
35
+ And the creative's format is "native"
36
+
37
+ @L @native
38
+ Scenario: L5 — a placement with no eligible ad returns a clean no-ad, not an error
39
+ Given the app requests placement "postroll"
40
+ Then no ad is served, and the placement answers with a clean no-fill
41
+ And exactly 1 decision is returned
42
+
43
+ @L @native
44
+ Scenario: L6 — the advertiser block is complete (name, legalName, logoUrl)
45
+ Given the app requests placement "home"
46
+ Then an ad is served
47
+ And the creative names its advertiser: name, legal name and logo
48
+
49
+ @L @native
50
+ Scenario: L7 — the template block carries a non-empty key
51
+ Given the app requests placement "home"
52
+ Then an ad is served
53
+ And the creative names its template
54
+
55
+ @L @native
56
+ Scenario: L8 — a served native ad exposes an impression URL, a click URL and non-empty contents
57
+ Given the app requests placement "home"
58
+ Then an ad is served
59
+ And the creative carries its content fields
60
+ And the creative exposes an impression URL
61
+ And the creative exposes at least 1 click URL
62
+
63
+ @M
64
+ Scenario: M1 — count=2 returns at most two creatives, and they are distinct
65
+ Given the app requests placement "promotions"
66
+ And placement "promotions" asks for 2 creatives
67
+ Then an ad is served
68
+ And exactly 1 decision is returned
69
+ And at most 2 creatives are returned
70
+ And the returned creatives are all different
71
+
72
+ @M
73
+ Scenario: M2 — three placements in one request each get their own correctly-keyed decision
74
+ Given the app requests placement "home"
75
+ And the app requests placement "promotions"
76
+ And the app requests placement "search"
77
+ Then an ad is served
78
+ And exactly 3 decisions are returned
79
+ And every requested placement comes back with its own decision
80
+
81
+ @M
82
+ Scenario: M3 — an advertiserId filter still serves that advertiser's ad
83
+ Given the app requests placement "promotions"
84
+ And placement "promotions" is restricted to advertiser "advertiser_01JEPGMJXARVYSJA0CAM9FQ79Y"
85
+ Then an ad is served
86
+ And the creative names its advertiser: name, legal name and logo
87
+
88
+ @M
89
+ Scenario: M5 — format=native on a mixed placement returns a native ad
90
+ Given the app requests placement "promotions"
91
+ And placement "promotions" asks for format "native"
92
+ Then an ad is served
93
+ And the creative's format is "native"
94
+
95
+ @M
96
+ Scenario: M6 — format=video on the same mixed placement returns a video ad instead
97
+ Given the app requests placement "promotions"
98
+ And placement "promotions" asks for format "video"
99
+ Then an ad is served
100
+ And the creative's format is "video"
101
+
102
+ @M
103
+ Scenario: M7 — omitting format still serves — the engine chooses
104
+ Given the app requests placement "promotions"
105
+ Then an ad is served
106
+
107
+ @M
108
+ Scenario: M8 — a journey-owned placement and a normal placement resolve independently in ONE request
109
+ Given the app requests placement "promotions"
110
+ And the app requests placement "home"
111
+ And the request carries a fresh session id
112
+ And the user has opted in to Journey Ads
113
+ Then an ad is served
114
+ And exactly 2 decisions are returned
115
+
116
+ @P @video
117
+ Scenario: P1 — a non-skippable JSON video exposes json delivery and five video events (no skip)
118
+ Given the app requests placement "json_none"
119
+ Then an ad is served
120
+ And the creative's format is "video"
121
+ And the creative is delivered as "json"
122
+ And the creative exposes exactly 5 video event URLs
123
+ And the video events include start, first_quartile, midpoint, third_quartile, complete
124
+
125
+ @P @video
126
+ Scenario: P2 — a vast_tag ad exposes its tag URL and surfaces NO engine-side beacons (no double count)
127
+ Given the app requests placement "vasttag_none"
128
+ Then an ad is served
129
+ And the creative is delivered as "vast_tag"
130
+ And the creative exposes no engine impression URL, because the VAST owns the impression
131
+ And the creative exposes exactly 0 video event URLs
132
+ And the creative exposes a VAST tag URL
133
+
134
+ @P @video
135
+ Scenario: P3 — a vast_xml ad exposes base64 that decodes to a VAST document, and no beacons
136
+ Given the app requests placement "vastxml_none"
137
+ Then an ad is served
138
+ And the creative is delivered as "vast_xml"
139
+ And the creative exposes no engine impression URL, because the VAST owns the impression
140
+ And the creative exposes exactly 0 video event URLs
141
+ And the creative's inline VAST XML decodes to a VAST document
142
+
143
+ @P @video
144
+ Scenario: P6 — a skippable video resolves skippability from ENGINE METADATA, plus a sixth skip event
145
+ Given the app requests placement "json_none_skippable"
146
+ Then an ad is served
147
+ And the creative exposes exactly 6 video event URLs
148
+ And the metadata says the video is skippable
149
+ And the metadata's skip offset is 5 seconds
150
+ And the SDK's skippable helper says the video is skippable
151
+ And the SDK's skip-offset helper returns "5"
152
+
153
+ @P @video
154
+ Scenario: P8 — an end-card video reports its configured endCardMode
155
+ Given the app requests placement "json_native_endcard"
156
+ Then an ad is served
157
+ And the end-card mode is "native_endcard"
158
+
159
+ @V @validation
160
+ Scenario: V1 — an unknown placement key raises a typed validation error, not a silent no-fill
161
+ Given the app requests placement "definitely_not_a_real_placement"
162
+ Then the request fails
163
+ And the SDK surfaces a typed validation error with HTTP status 422
164
+ And the engine's error code is 10017
165
+
166
+ @V @validation
167
+ Scenario: V3 — an unknown custom-targeting key is rejected by the engine
168
+ Given the app requests placement "home"
169
+ And the custom target "not_a_configured_preset" is "x"
170
+ Then the request fails
171
+ And the SDK surfaces a typed validation error with HTTP status 422
172
+ And the engine's error code is 10028
173
+
174
+ @V @validation
175
+ Scenario: V4 — the SDK deduplicates a repeated custom key, so the engine never sees the duplicate that would fail the whole request
176
+ Sent raw over HTTP this same payload returns 422 code 10031, and the engine aborts
177
+ validation for the ENTIRE request — every placement, not just the offending target. All
178
+ three SDKs dedupe by key (last write wins), so a publisher cannot reach that state through
179
+ the public API. This scenario is the live proof of that guard: it must SERVE, and both
180
+ placements must come back.
181
+
182
+ Given the app requests placement "home"
183
+ And the app requests placement "promotions"
184
+ And the custom target "category" is "sports"
185
+ And the custom target "category" is "news"
186
+ Then an ad is served
187
+ And exactly 2 decisions are returned
188
+ And every requested placement comes back with its own decision
189
+
190
+ @V @validation
191
+ Scenario: V5 — a single valid custom target does not break serving
192
+ Given the app requests placement "home"
193
+ And the custom target "category" is "sports"
194
+ Then an ad is served
195
+
196
+ @V @validation
197
+ Scenario: V6 — a count above the documented maximum is rejected
198
+ Given the app requests placement "home"
199
+ And placement "home" asks for 99 creatives
200
+ Then the request fails
201
+ And the SDK surfaces a typed validation error with HTTP status 422
202
+ And the engine's error code is 10016
203
+
204
+ @V @validation
205
+ Scenario: V9 — a request with no placements fails locally, before any network call
206
+ Given the app requests no placements
207
+ Then the request fails
208
+ And the SDK rejects the request itself, before any network call
209
+
210
+ @Z @legacy-version
211
+ Scenario: Z1 — API version 2025-01-01 still serves a native ad and never a journey block
212
+ Given the app sends X-Decision-Version "2025-01-01"
213
+ And the app requests placement "home"
214
+ Then an ad is served
215
+ And the creative carries complete metadata: ad, creative, placement and template ids
216
+ And the creative is not a Journey Ad
217
+
218
+ @Z @legacy-version
219
+ Scenario: Z2 — omitting the version header still serves (engine falls back to the oldest active version)
220
+ Given the app sends no X-Decision-Version header
221
+ And the app requests placement "home"
222
+ Then an ad is served
223
+ And the creative is not a Journey Ad
224
+
225
+ @SP @sponsored-pin
226
+ Scenario: SP1 — a radius search returns the campaign's own shops, nearest first
227
+ Given the app sends X-Decision-Version "2025-11-01"
228
+ And the app requests placement "poi"
229
+ And the app searches for pins within 20000 m of (-33.4175, -70.6065)
230
+ Then an ad is served
231
+ And the ad carries at least 2 pins
232
+ And the pins are ordered nearest first
233
+ And every pin is one of the advertiser's locations, with a name and a distance
234
+
235
+ @SP @sponsored-pin
236
+ Scenario: SP2 — a bounds search returns them too — the other shape a map can ask with
237
+ Given the app sends X-Decision-Version "2025-11-01"
238
+ And the app requests placement "poi"
239
+ And the app searches for pins inside the map area north -33.3, south -33.55, east -70.45, west -70.65, around (-33.4175, -70.6065)
240
+ Then an ad is served
241
+ And the ad carries at least 2 pins
242
+ And every pin is one of the advertiser's locations, with a name and a distance
243
+
244
+ @SP @sponsored-pin
245
+ Scenario: SP3 — every returned shop carries its own view, tap and click beacons
246
+ Given the app sends X-Decision-Version "2025-11-01"
247
+ And the app requests placement "poi"
248
+ And the app searches for pins within 20000 m of (-33.4175, -70.6065)
249
+ Then an ad is served
250
+ And every pin carries its own view, tap and click beacons
251
+
252
+ @SP @sponsored-pin
253
+ Scenario: SP4 — every returned shop carries a resolved destination, its own or the creative's
254
+ Given the app sends X-Decision-Version "2025-11-01"
255
+ And the app requests placement "poi"
256
+ And the app searches for pins within 20000 m of (-33.4175, -70.6065)
257
+ Then an ad is served
258
+ And every pin carries a resolved destination URL
259
+
260
+ @SP @sponsored-pin
261
+ Scenario: SP5 — the request's limit narrows how many shops come back
262
+ Given the app sends X-Decision-Version "2025-11-01"
263
+ And the app requests placement "poi"
264
+ And the app searches for pins within 20000 m of (-33.4175, -70.6065)
265
+ And the search asks for at most 2 pins
266
+ Then an ad is served
267
+ And the ad carries exactly 2 pins
268
+
269
+ @SP @sponsored-pin
270
+ Scenario: SP6 — a tight radius excludes the shops outside it
271
+ Given the app sends X-Decision-Version "2025-11-01"
272
+ And the app requests placement "poi"
273
+ And the app searches for pins within 1500 m of (-33.4175, -70.6065)
274
+ Then an ad is served
275
+ And the ad carries exactly 1 pin
276
+
277
+ @SP @sponsored-pin
278
+ Scenario: SP7 — without a distance search a Sponsored Pin campaign does not serve at all
279
+ Given the app sends X-Decision-Version "2025-11-01"
280
+ And the app requests placement "poi"
281
+ Then no ad is served, and the placement answers with a clean no-fill
282
+
283
+ @SP @sponsored-pin
284
+ Scenario: SP8 — on the older API version the ad still serves, silently carrying no pins — the failure an integrator must recognise
285
+ Given the app sends no X-Decision-Version header
286
+ And the app requests placement "poi"
287
+ And the app searches for pins within 20000 m of (-33.4175, -70.6065)
288
+ Then an ad is served
289
+ And the ad carries no pins
290
+
291
+ @SP @sponsored-pin
292
+ Scenario: SP9 — a map's first render sends every shop's view beacon exactly once
293
+ Given the app sends X-Decision-Version "2025-11-01"
294
+ And the app requests placement "poi"
295
+ And the app searches for pins within 20000 m of (-33.4175, -70.6065)
296
+ Then an ad is served
297
+ And the ad carries at least 1 pin
298
+ And every pin carries its own view, tap and click beacons
299
+ When the app shows every pin
300
+ Then each shown pin's view beacon reaches the engine exactly once per response, and no pin the app never showed sends one
301
+ And the engine accepts every beacon
302
+
303
+ @SP @sponsored-pin
304
+ Scenario: SP10 — re-rendering the same response sends no view beacon again
305
+ Given the app sends X-Decision-Version "2025-11-01"
306
+ And the app requests placement "poi"
307
+ And the app searches for pins within 20000 m of (-33.4175, -70.6065)
308
+ Then an ad is served
309
+ And the ad carries at least 1 pin
310
+ And every pin carries its own view, tap and click beacons
311
+ When the app shows every pin
312
+ And the app shows every pin
313
+ And the app shows every pin
314
+ Then each shown pin's view beacon reaches the engine exactly once per response, and no pin the app never showed sends one
315
+ And the engine accepts every beacon
316
+
317
+ @SP @sponsored-pin
318
+ Scenario: SP11 — a render that brings new shops into view sends views only for the new ones
319
+ Given the app sends X-Decision-Version "2025-11-01"
320
+ And the app requests placement "poi"
321
+ And the app searches for pins within 20000 m of (-33.4175, -70.6065)
322
+ Then an ad is served
323
+ And the ad carries at least 4 pins
324
+ And every pin carries its own view, tap and click beacons
325
+ When the app shows the first 2 pins
326
+ And the app shows pins 2 to 4
327
+ And the app shows the first 4 pins
328
+ Then each shown pin's view beacon reaches the engine exactly once per response, and no pin the app never showed sends one
329
+ And the engine accepts every beacon
330
+
331
+ @SP @sponsored-pin
332
+ Scenario: SP12 — a new decision response reports the same shops again, once
333
+ Given the app sends X-Decision-Version "2025-11-01"
334
+ And the app requests placement "poi"
335
+ And the app searches for pins within 20000 m of (-33.4175, -70.6065)
336
+ Then an ad is served
337
+ And the ad carries at least 1 pin
338
+ And every pin carries its own view, tap and click beacons
339
+ When the app shows every pin
340
+ And the app fetches a new decision response for the same search
341
+ And the app shows every pin
342
+ And the app shows every pin
343
+ Then each shown pin's view beacon reaches the engine exactly once per response, and no pin the app never showed sends one
344
+ And a new response carries new view beacons for the same pins
345
+ And the engine accepts every beacon
346
+
347
+ @SP @sponsored-pin
348
+ Scenario: SP13 — single-pin views follow the same once-per-response rule as the bulk call
349
+ Given the app sends X-Decision-Version "2025-11-01"
350
+ And the app requests placement "poi"
351
+ And the app searches for pins within 20000 m of (-33.4175, -70.6065)
352
+ Then an ad is served
353
+ And the ad carries at least 1 pin
354
+ And every pin carries its own view, tap and click beacons
355
+ When the app shows pin 1 on its own
356
+ And the app shows pin 1 on its own
357
+ And the app shows every pin
358
+ And the app shows pin 1 on its own
359
+ Then each shown pin's view beacon reaches the engine exactly once per response, and no pin the app never showed sends one
360
+ And the engine accepts every beacon
361
+
362
+ @SP @sponsored-pin
363
+ Scenario: SP14 — taps and clicks are sent on every call, never de-duplicated
364
+ Given the app sends X-Decision-Version "2025-11-01"
365
+ And the app requests placement "poi"
366
+ And the app searches for pins within 20000 m of (-33.4175, -70.6065)
367
+ Then an ad is served
368
+ And the ad carries at least 1 pin
369
+ And every pin carries its own view, tap and click beacons
370
+ When the app shows every pin
371
+ And the user taps pin 1
372
+ And the user taps pin 1
373
+ And the user clicks pin 1
374
+ And the user clicks pin 1
375
+ And the app shows every pin
376
+ Then each shown pin's view beacon reaches the engine exactly once per response, and no pin the app never showed sends one
377
+ And every tap and every click sends its beacon, on every call
378
+ And the engine accepts every beacon
379
+
380
+ @F @fixture
381
+ Scenario: F1 — a Native Display creative exposes its standard format, version and template source
382
+ Given the app decodes the golden card.full response
383
+ Then the response decodes
384
+ And the creative's effectiveStandardFormatKey is "card"
385
+ And the creative's effectiveStandardFormatVersion is 1
386
+ And the creative's templateSource is "admoai_standard"
387
+ And the template key is "card"
388
+ And the creative's contents are exactly "image", "brand_logo", "headline", "supporting_text", "cta_text", "destination_url"
389
+ And the click trackers are exactly "default"
390
+ And the creative has no custom event tracker
391
+
392
+ @F @fixture
393
+ Scenario: F2 — a Native Display creative leaves every optional field it was served without absent
394
+ Given the app decodes the golden card.minimal response
395
+ Then the response decodes
396
+ And the creative's effectiveStandardFormatKey is "card"
397
+ And the creative's effectiveStandardFormatVersion is 1
398
+ And the creative's templateSource is "admoai_standard"
399
+ And the template key is "card"
400
+ And the creative's contents are exactly "image", "destination_url"
401
+ And "brand_logo", "headline", "supporting_text", "cta_text" decode as absent, never as an empty string or a placeholder
402
+ And the click trackers are exactly "default"
403
+ And the creative has no custom event tracker
404
+
405
+ @F @fixture
406
+ Scenario: F3 — a template based on a standard keeps the standard's identity and exposes the operator's addition by key
407
+ Given the app decodes the golden card.derived response
408
+ Then the response decodes
409
+ And the creative's effectiveStandardFormatKey is "card"
410
+ And the creative's effectiveStandardFormatVersion is 1
411
+ And the creative's templateSource is "based_on_standard"
412
+ And the template key is "city_promo_display"
413
+ And the creative's contents are exactly "image", "headline", "destination_url", "promo_code"
414
+ And the click trackers are exactly "default"
415
+ And the custom event trackers are exactly "save_offer"
416
+
417
+ @F @fixture
418
+ Scenario: F4 — a Carousel exposes its slides in server order, each with its id, position, contents and own trackers, and no creative-level click
419
+ Given the app decodes the golden carousel.full response
420
+ Then the response decodes
421
+ And the creative's effectiveStandardFormatKey is "carousel"
422
+ And the creative's effectiveStandardFormatVersion is 1
423
+ And the creative's templateSource is "admoai_standard"
424
+ And the template key is "carousel"
425
+ And the creative's contents are exactly "brand_logo", "disclaimer"
426
+ And the creative has no click tracker
427
+ And the creative has no custom event tracker
428
+ And there are 3 slides in server order
429
+ And slide 1 is "slide_01JDY02N8MA2K2HKQ74989YS99" with contents "image", "headline", "supporting_text", "cta_text", "destination_url", view trackers "slide_view" and click trackers "default"
430
+ And slide 2 is "slide_01JD04H8GY801VF8Y0FHETP6XA" with contents "image", "headline", "supporting_text", "cta_text", "destination_url", view trackers "slide_view" and click trackers "default"
431
+ And slide 3 is "slide_01JDRGMFSTZXQ7ES1QBCF0DR2X" with contents "image", "headline", "supporting_text", "cta_text", "destination_url", view trackers "slide_view" and click trackers "default"
432
+
433
+ @F @fixture
434
+ Scenario: F5 — a minimal Carousel still exposes its slides and leaves the optional brand logo and disclaimer absent
435
+ Given the app decodes the golden carousel.minimal response
436
+ Then the response decodes
437
+ And the creative's effectiveStandardFormatKey is "carousel"
438
+ And the creative's effectiveStandardFormatVersion is 1
439
+ And the creative's templateSource is "admoai_standard"
440
+ And the template key is "carousel"
441
+ And the creative has no plain contents
442
+ And "brand_logo", "disclaimer" decode as absent, never as an empty string or a placeholder
443
+ And the creative has no click tracker
444
+ And the creative has no custom event tracker
445
+ And there are 2 slides in server order
446
+ And slide 1 is "slide_01JDZ725G8017BHW9RQJX7Y56S" with contents "image", "destination_url", view trackers "slide_view" and click trackers "default"
447
+ And slide 2 is "slide_01JD4GNH6TH0XPP5YWCMDKN029" with contents "image", "destination_url", view trackers "slide_view" and click trackers "default"
448
+
449
+ @F @fixture
450
+ Scenario: F6 — a Carousel based on a standard exposes its slides and the operator's addition
451
+ Given the app decodes the golden carousel.derived response
452
+ Then the response decodes
453
+ And the creative's effectiveStandardFormatKey is "carousel"
454
+ And the creative's effectiveStandardFormatVersion is 1
455
+ And the creative's templateSource is "based_on_standard"
456
+ And the template key is "city_carousel"
457
+ And the creative's contents are exactly "promo_code"
458
+ And the creative has no click tracker
459
+ And the creative has no custom event tracker
460
+ And there are 2 slides in server order
461
+ And slide 1 is "slide_01JDS1H8VEM0ABE2PQ0JY6S38W" with contents "image", "destination_url", view trackers "slide_view" and click trackers "default"
462
+ And slide 2 is "slide_01JDDGXJE0JDBY21Y9KEJDD54A" with contents "image", "destination_url", view trackers "slide_view" and click trackers "default"
463
+
464
+ @F @fixture
465
+ Scenario: F7 — a Tap-to-expand creative exposes expand and close as custom events, apart from its click
466
+ Given the app decodes the golden expandable_banner.full response
467
+ Then the response decodes
468
+ And the creative's effectiveStandardFormatKey is "expandable_banner"
469
+ And the creative's effectiveStandardFormatVersion is 1
470
+ And the creative's templateSource is "admoai_standard"
471
+ And the template key is "expandable_banner"
472
+ And the creative's contents are exactly "teaser_image", "teaser_headline", "expanded_image", "headline", "supporting_text", "cta_text", "destination_url", "disclaimer"
473
+ And the click trackers are exactly "default"
474
+ And the custom event trackers are exactly "expand", "close"
475
+
476
+ @F @fixture
477
+ Scenario: F8 — a minimal Tap-to-expand creative leaves its optional texts absent and keeps expand and close
478
+ Given the app decodes the golden expandable_banner.minimal response
479
+ Then the response decodes
480
+ And the creative's effectiveStandardFormatKey is "expandable_banner"
481
+ And the creative's effectiveStandardFormatVersion is 1
482
+ And the creative's templateSource is "admoai_standard"
483
+ And the template key is "expandable_banner"
484
+ And the creative's contents are exactly "teaser_image", "expanded_image", "destination_url"
485
+ And "teaser_headline", "headline", "supporting_text", "cta_text", "disclaimer" decode as absent, never as an empty string or a placeholder
486
+ And the click trackers are exactly "default"
487
+ And the custom event trackers are exactly "expand", "close"
488
+
489
+ @F @fixture
490
+ Scenario: F9 — a Branded Assets creative exposes asset tap as a custom event, apart from its click
491
+ Given the app decodes the golden branded_assets.full response
492
+ Then the response decodes
493
+ And the creative's effectiveStandardFormatKey is "branded_assets"
494
+ And the creative's effectiveStandardFormatVersion is 1
495
+ And the creative's templateSource is "admoai_standard"
496
+ And the template key is "branded_assets"
497
+ And the creative's contents are exactly "branded_image", "accessible_label", "destination_url"
498
+ And the click trackers are exactly "default"
499
+ And the custom event trackers are exactly "asset_tap"
500
+
501
+ @F @fixture
502
+ Scenario: F10 — a Branded Assets creative without a destination has no click, only the asset tap
503
+ Given the app decodes the golden branded_assets.minimal response
504
+ Then the response decodes
505
+ And the creative's effectiveStandardFormatKey is "branded_assets"
506
+ And the creative's effectiveStandardFormatVersion is 1
507
+ And the creative's templateSource is "admoai_standard"
508
+ And the template key is "branded_assets"
509
+ And the creative's contents are exactly "branded_image"
510
+ And "accessible_label", "destination_url" decode as absent, never as an empty string or a placeholder
511
+ And the creative has no click tracker
512
+ And the custom event trackers are exactly "asset_tap"
513
+
514
+ @F @fixture
515
+ Scenario: F11 — a Standard Video exposes its video events and click, and no skippable setting
516
+ Given the app decodes the golden standard_video.full response
517
+ Then the response decodes
518
+ And the creative's effectiveStandardFormatKey is "standard_video"
519
+ And the creative's effectiveStandardFormatVersion is 1
520
+ And the creative's templateSource is "admoai_standard"
521
+ And the template key is "standard_video"
522
+ And the creative's contents are exactly "video_asset", "poster_image", "brand_logo", "headline", "supporting_text", "cta_text", "destination_url", "disclaimer"
523
+ And the click trackers are exactly "default"
524
+ And the creative has no custom event tracker
525
+ And the video event trackers are exactly "start", "first_quartile", "midpoint", "third_quartile", "complete"
526
+ And the creative exposes no skippable setting
527
+
528
+ @F @fixture
529
+ Scenario: F12 — a Standard Video without a destination has no click action or click tracker
530
+ Given the app decodes the golden standard_video.minimal response
531
+ Then the response decodes
532
+ And the creative's effectiveStandardFormatKey is "standard_video"
533
+ And the creative's effectiveStandardFormatVersion is 1
534
+ And the creative's templateSource is "admoai_standard"
535
+ And the template key is "standard_video"
536
+ And the creative's contents are exactly "video_asset", "poster_image"
537
+ And "brand_logo", "headline", "supporting_text", "cta_text", "destination_url", "disclaimer" decode as absent, never as an empty string or a placeholder
538
+ And the creative has no click tracker
539
+ And the creative has no custom event tracker
540
+ And the video event trackers are exactly "start", "first_quartile", "midpoint", "third_quartile", "complete"
541
+ And the creative exposes no skippable setting
542
+
543
+ @F @fixture
544
+ Scenario: F13 — a Sponsored Pin creative keeps decoding its matched points as it does today
545
+ Given the app decodes the golden sponsored_pin_location.full response
546
+ Then the response decodes
547
+ And the creative's effectiveStandardFormatKey is "sponsored_pin_location"
548
+ And the creative's effectiveStandardFormatVersion is 1
549
+ And the creative's templateSource is "admoai_standard"
550
+ And the template key is "sponsored_pin_location"
551
+ And the creative's contents are exactly "pinImage", "image", "headline", "description", "ctaText", "defaultDestinationUrl"
552
+ And the click trackers are exactly "default"
553
+ And the creative has no custom event tracker
554
+ And the creative exposes 2 matched points
555
+
556
+ @F @fixture
557
+ Scenario: F14 — a creative served before the standards (gate off) decodes as today, with the three standard fields absent
558
+ The engine omits the three standard fields while is_standard_native_formats_enabled is off;
559
+ this is that wire. Absent means null, never an empty string or a placeholder (BR-SDK-01,
560
+ BR-SDK-09).
561
+
562
+ Given the app decodes the golden card.full response
563
+ And with the metadata field "effectiveStandardFormatKey" removed
564
+ And with the metadata field "effectiveStandardFormatVersion" removed
565
+ And with the metadata field "templateSource" removed
566
+ Then the response decodes
567
+ And the creative's effectiveStandardFormatKey is absent
568
+ And the creative's effectiveStandardFormatVersion is absent
569
+ And the creative's templateSource is absent
570
+ And the template key is "card"
571
+ And the creative's contents are exactly "image", "brand_logo", "headline", "supporting_text", "cta_text", "destination_url"
572
+ And the click trackers are exactly "default"
573
+ And the creative has no custom event tracker
574
+
575
+ @F @fixture
576
+ Scenario: F15 — fields the SDK does not know yet, at any level, never break decoding
577
+ An additive field the engine starts sending later must be ignored, not fail the response
578
+ (BR-SDK-01).
579
+
580
+ Given the app decodes the golden carousel.full response
581
+ And with an unknown response field "futureField" added
582
+ And with an unknown creative field "futureField" added
583
+ And with an unknown metadata field "futureField" added
584
+ And with an unknown content field "futureField" added on every content entry
585
+ And with an unknown slide field "futureField" added on every slide
586
+ Then the response decodes
587
+ And the creative's effectiveStandardFormatKey is "carousel"
588
+ And the creative's effectiveStandardFormatVersion is 1
589
+ And the creative's templateSource is "admoai_standard"
590
+ And the template key is "carousel"
591
+ And the creative's contents are exactly "brand_logo", "disclaimer"
592
+ And the creative has no click tracker
593
+ And the creative has no custom event tracker
594
+ And there are 3 slides in server order
595
+ And slide 1 is "slide_01JDY02N8MA2K2HKQ74989YS99" with contents "image", "headline", "supporting_text", "cta_text", "destination_url", view trackers "slide_view" and click trackers "default"
596
+ And slide 2 is "slide_01JD04H8GY801VF8Y0FHETP6XA" with contents "image", "headline", "supporting_text", "cta_text", "destination_url", view trackers "slide_view" and click trackers "default"
597
+ And slide 3 is "slide_01JDRGMFSTZXQ7ES1QBCF0DR2X" with contents "image", "headline", "supporting_text", "cta_text", "destination_url", view trackers "slide_view" and click trackers "default"