feat(banner): per-campaign colour overrides and urgent alerts
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 1m8s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m50s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 1m8s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m50s
Non-default campaigns gain an optional colour override (background / text / link) in two sets — one for every theme, one for the dark theme only — and an "urgent" flag. - Colours ride profile.get as six trailing FlatBuffers strings on BannerCampaign (backward-compatible). The client resolves the cascade (dark <- dark ?? all, light <- all) per rendered theme and derives the strip border from the background in JS (no CSS color-mix, for the old Android WebView floor); AdBanner applies them as inline vars scoped to the strip. - Urgent is resolved entirely server-side: while any enabled, in-window urgent campaign exists, computeActiveSet returns only the urgent campaigns and bannerFor skips the eligibility gate — so a system notice reaches every viewer (paid / hint-holding / no_banner included) and preempts the ordinary feed. No wire field; it appears on each viewer's next profile.get. - Admin console (/_gm/banners): native colour pickers + a live light/dark preview of the strip, and an urgent toggle. The default campaign stays plain, enforced by the service and a DB CHECK. Migration 00009 is additive (nullable colour columns + a bool default + all-or-nothing / hex / default-plain CHECKs) — expand-contract, rollback-safe. Docs: ARCHITECTURE §10, UI_DESIGN, FUNCTIONAL (+ru). Tests: ads unit (urgent preempt + colour validation), codec + resolver unit, gateway transcode, and integration (colour round-trip + urgent bypass against real Postgres).
This commit is contained in:
@@ -61,6 +61,54 @@ func TestProfileGetEncodesBanner(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestProfileGetEncodesBannerColors verifies the gateway carries a campaign's
|
||||
// optional colour overrides into the FlatBuffers payload, and leaves an absent
|
||||
// colour (or an absent set) empty so the client falls back to the theme tokens.
|
||||
func TestProfileGetEncodesBannerColors(t *testing.T) {
|
||||
backend, cleanup := fakeBackend(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte(`{"user_id":"u-1","display_name":"Kaya","preferred_language":"en",` +
|
||||
`"banner":{"campaigns":[` +
|
||||
`{"weight":1,"messages":["promo-en"],` +
|
||||
`"override_bg":"#aa0000","override_fg":"#ffffff","override_link":"#ffdd00",` +
|
||||
`"override_bg_dark":"#330000","override_fg_dark":"#eeeeee","override_link_dark":"#ffcc00"},` +
|
||||
`{"weight":1,"messages":["plain-en"]}],` +
|
||||
`"timings":{"hold_ms":60000,"edge_pause_ms":5000,"scroll_px_per_sec":40,` +
|
||||
`"fade_out_ms":1000,"gap_ms":250,"fade_in_ms":1000}}}`))
|
||||
})
|
||||
defer cleanup()
|
||||
|
||||
reg := transcode.NewRegistry(backend, nil)
|
||||
op, _ := reg.Lookup(transcode.MsgProfileGet)
|
||||
payload, err := op.Handler(context.Background(), transcode.Request{UserID: "u-1"})
|
||||
if err != nil {
|
||||
t.Fatalf("handler: %v", err)
|
||||
}
|
||||
banner := fb.GetRootAsProfile(payload, 0).Banner(nil)
|
||||
if banner == nil {
|
||||
t.Fatal("profile carries no banner block")
|
||||
}
|
||||
var c fb.BannerCampaign
|
||||
banner.Campaigns(&c, 0)
|
||||
if got := string(c.OverrideBg()); got != "#aa0000" {
|
||||
t.Errorf("override_bg = %q, want #aa0000", got)
|
||||
}
|
||||
if got := string(c.OverrideLink()); got != "#ffdd00" {
|
||||
t.Errorf("override_link = %q, want #ffdd00", got)
|
||||
}
|
||||
if got := string(c.OverrideBgDark()); got != "#330000" {
|
||||
t.Errorf("override_bg_dark = %q, want #330000", got)
|
||||
}
|
||||
if got := string(c.OverrideLinkDark()); got != "#ffcc00" {
|
||||
t.Errorf("override_link_dark = %q, want #ffcc00", got)
|
||||
}
|
||||
// The second campaign has no override: every colour field must be absent (nil).
|
||||
banner.Campaigns(&c, 1)
|
||||
if c.OverrideBg() != nil || c.OverrideFg() != nil || c.OverrideLink() != nil ||
|
||||
c.OverrideBgDark() != nil || c.OverrideFgDark() != nil || c.OverrideLinkDark() != nil {
|
||||
t.Error("plain campaign unexpectedly carries a colour override")
|
||||
}
|
||||
}
|
||||
|
||||
// TestProfileGetNoBanner verifies an ineligible viewer's profile carries no
|
||||
// banner block (the backend omits it).
|
||||
func TestProfileGetNoBanner(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user