|
1 | | -import { describe, expect, test } from 'bun:test' |
| 1 | +import { beforeAll, describe, expect, test } from 'bun:test' |
| 2 | +import { createTestRenderer } from '@opentui/core/testing' |
| 3 | +import { createRoot, flushSync } from '@opentui/react' |
| 4 | +import React from 'react' |
2 | 5 |
|
3 | | -import { getAdDisplayLabel, getInlineAdLayout } from '../ad-banner' |
| 6 | +import { |
| 7 | + AD_CARD_HEIGHT, |
| 8 | + AdCard, |
| 9 | + getAdDisplayLabel, |
| 10 | + getCardAdLayout, |
| 11 | + getInlineAdLayout, |
| 12 | +} from '../ad-banner' |
| 13 | +import { initializeThemeStore } from '../../hooks/use-theme' |
| 14 | + |
| 15 | +beforeAll(() => { |
| 16 | + initializeThemeStore() |
| 17 | +}) |
| 18 | + |
| 19 | +describe('card ad layout', () => { |
| 20 | + const ad = { |
| 21 | + adText: |
| 22 | + 'Automate mobile UI testing with plain-English test steps and AI-powered execution.', |
| 23 | + title: 'Test every release before you ship', |
| 24 | + cta: 'Try free', |
| 25 | + url: 'https://www.drizz.dev/ios', |
| 26 | + } |
| 27 | + |
| 28 | + test('renders the headline for the creative the console asks for', () => { |
| 29 | + // The regression this whole function exists for: with a CTA and a landing |
| 30 | + // URL both set, the headline used to reach neither the CTA fallback nor |
| 31 | + // the destination label, so it rendered nowhere at all. |
| 32 | + const layout = getCardAdLayout(ad, 78) |
| 33 | + |
| 34 | + expect(layout.headline).toBe('Test every release before you ship') |
| 35 | + expect(layout.ctaText).toBe('Try free') |
| 36 | + expect(layout.labelText).toBe('drizz.dev') |
| 37 | + }) |
| 38 | + |
| 39 | + test('gives up a description line to make room for the headline', () => { |
| 40 | + expect(getCardAdLayout(ad, 78).descriptionLines).toBe(1) |
| 41 | + }) |
| 42 | + |
| 43 | + test('keeps both description lines when the ad has no headline', () => { |
| 44 | + const layout = getCardAdLayout({ ...ad, title: '' }, 78) |
| 45 | + |
| 46 | + expect(layout.headline).toBe('') |
| 47 | + expect(layout.descriptionLines).toBe(2) |
| 48 | + }) |
| 49 | + |
| 50 | + test('the interior always sums to the reserved card height', () => { |
| 51 | + // The landing screen subtracts AD_CARD_HEIGHT from the model picker's |
| 52 | + // budget, so a layout that needs a row it was not given clips silently. |
| 53 | + for (const title of ['A headline', '']) { |
| 54 | + const layout = getCardAdLayout({ ...ad, title }, 78) |
| 55 | + const headlineRows = layout.headline ? 1 : 0 |
| 56 | + const borderRows = 2 |
| 57 | + const ctaRow = 1 |
| 58 | + |
| 59 | + expect(borderRows + headlineRows + layout.descriptionLines + ctaRow).toBe( |
| 60 | + AD_CARD_HEIGHT, |
| 61 | + ) |
| 62 | + } |
| 63 | + }) |
| 64 | + |
| 65 | + test('falls back to Learn more rather than reprinting the headline', () => { |
| 66 | + // `ad.cta || ad.title` put the same string in two rows of a five-row card. |
| 67 | + expect(getCardAdLayout({ ...ad, cta: '' }, 78).ctaText).toBe('Learn more') |
| 68 | + }) |
| 69 | + |
| 70 | + test('drops the destination label when it would repeat the headline', () => { |
| 71 | + // With no URL the label falls back to the title, which now has its own row. |
| 72 | + const layout = getCardAdLayout({ ...ad, url: '' }, 78) |
| 73 | + |
| 74 | + expect(layout.headline).toBe('Test every release before you ship') |
| 75 | + expect(layout.labelText).toBe('') |
| 76 | + }) |
| 77 | + |
| 78 | + test('keeps the Sponsored label when there is no headline and no URL', () => { |
| 79 | + const layout = getCardAdLayout({ ...ad, title: '', url: '' }, 78) |
| 80 | + |
| 81 | + expect(layout.labelText).toBe('Sponsored') |
| 82 | + }) |
| 83 | + |
| 84 | + test('survives a provider that omits fields the type says are required', () => { |
| 85 | + // `AdResponse` types these as required strings, but the Gravity provider |
| 86 | + // casts `response.json()` instead of parsing it and copies `cta: raw.cta` |
| 87 | + // with no default — while Carbon beside it writes `?? 'Learn more'`. A |
| 88 | + // throw here is a throw inside AdCard's render on the landing screen. |
| 89 | + const layout = getCardAdLayout( |
| 90 | + {} as Parameters<typeof getCardAdLayout>[0], |
| 91 | + 78, |
| 92 | + ) |
| 93 | + |
| 94 | + expect(layout.headline).toBe('') |
| 95 | + expect(layout.description).toBe('') |
| 96 | + expect(layout.ctaText).toBe('Learn more') |
| 97 | + expect(layout.labelText).toBe('Sponsored') |
| 98 | + expect(layout.descriptionLines).toBe(2) |
| 99 | + }) |
| 100 | + |
| 101 | + test('falls back to Learn more when only the CTA is missing', () => { |
| 102 | + const layout = getCardAdLayout( |
| 103 | + { ...ad, cta: undefined } as unknown as Parameters< |
| 104 | + typeof getCardAdLayout |
| 105 | + >[0], |
| 106 | + 78, |
| 107 | + ) |
| 108 | + |
| 109 | + expect(layout.ctaText).toBe('Learn more') |
| 110 | + expect(layout.headline).toBe('Test every release before you ship') |
| 111 | + }) |
| 112 | + |
| 113 | + test('truncates the headline to the interior width', () => { |
| 114 | + const layout = getCardAdLayout( |
| 115 | + { |
| 116 | + ...ad, |
| 117 | + title: |
| 118 | + 'A headline considerably longer than this narrow card could ever hold', |
| 119 | + }, |
| 120 | + 60, |
| 121 | + ) |
| 122 | + |
| 123 | + expect(layout.headline.length).toBeLessThanOrEqual(60 - 8) |
| 124 | + expect(layout.headline.endsWith('…')).toBe(true) |
| 125 | + }) |
| 126 | +}) |
| 127 | + |
| 128 | +describe('card ad render', () => { |
| 129 | + // getCardAdLayout being correct proves nothing on its own: the bug it fixes |
| 130 | + // was that the JSX never referenced the title at all. This renders a real |
| 131 | + // character frame, so a refactor that drops the headline row goes red here |
| 132 | + // rather than shipping an advertiser a field that renders nowhere. |
| 133 | + const ad = { |
| 134 | + adText: 'Automate mobile UI testing with plain-English test steps.', |
| 135 | + title: 'Test every release before you ship', |
| 136 | + cta: 'Try free', |
| 137 | + url: 'https://www.drizz.dev/ios', |
| 138 | + favicon: '', |
| 139 | + clickUrl: 'https://www.drizz.dev/ios?click=1', |
| 140 | + impUrl: 'imp-1', |
| 141 | + } |
| 142 | + |
| 143 | + const renderCard = async ( |
| 144 | + overrides: Partial<typeof ad>, |
| 145 | + width = 78, |
| 146 | + ): Promise<string> => { |
| 147 | + const setup = await createTestRenderer({ width, height: AD_CARD_HEIGHT }) |
| 148 | + const root = createRoot(setup.renderer) |
| 149 | + flushSync(() => { |
| 150 | + root.render(<AdCard ad={{ ...ad, ...overrides }} width={width} />) |
| 151 | + }) |
| 152 | + try { |
| 153 | + await setup.renderOnce() |
| 154 | + return setup.captureCharFrame() |
| 155 | + } finally { |
| 156 | + flushSync(() => root.unmount()) |
| 157 | + setup.renderer.destroy() |
| 158 | + } |
| 159 | + } |
| 160 | + |
| 161 | + test('draws the headline, the body, the CTA and the destination', async () => { |
| 162 | + const frame = await renderCard({}) |
| 163 | + |
| 164 | + expect(frame).toContain('Test every release before you ship') |
| 165 | + expect(frame).toContain('Automate mobile UI testing') |
| 166 | + expect(frame).toContain('Try free') |
| 167 | + expect(frame).toContain('drizz.dev') |
| 168 | + }) |
| 169 | + |
| 170 | + test('still discloses itself as an ad', async () => { |
| 171 | + expect(await renderCard({})).toContain('Ad') |
| 172 | + }) |
| 173 | + |
| 174 | + test('does not print the headline twice when there is no CTA', async () => { |
| 175 | + const frame = await renderCard({ cta: '' }) |
| 176 | + |
| 177 | + expect(frame).toContain('Test every release before you ship') |
| 178 | + expect(frame).toContain('Learn more') |
| 179 | + }) |
| 180 | + |
| 181 | + test('renders rather than throwing when the provider omits a CTA', async () => { |
| 182 | + // The regression this guards: `ad.cta.trim()` threw inside render, and |
| 183 | + // cli/src/components/error-boundary.tsx does not catch render errors, so |
| 184 | + // one malformed Gravity creative took down the landing screen. |
| 185 | + const frame = await renderCard({ |
| 186 | + cta: undefined, |
| 187 | + } as Partial<typeof ad>) |
| 188 | + |
| 189 | + expect(frame).toContain('Learn more') |
| 190 | + expect(frame).toContain('Test every release before you ship') |
| 191 | + }) |
| 192 | + |
| 193 | + test('renders a title-less ad without a blank first row', async () => { |
| 194 | + const frame = await renderCard({ title: '' }) |
| 195 | + |
| 196 | + expect(frame).toContain('Automate mobile UI testing') |
| 197 | + expect(frame).toContain('Ad') |
| 198 | + }) |
| 199 | +}) |
4 | 200 |
|
5 | 201 | describe('ad banner display label', () => { |
6 | 202 | test('uses the display domain when the ad has a URL', () => { |
@@ -69,7 +265,8 @@ describe('inline ad layout', () => { |
69 | 265 | test('uses the full detail row when no destination domain is available', () => { |
70 | 266 | const layout = getInlineAdLayout( |
71 | 267 | { |
72 | | - adText: 'A Carbon ad whose tracked destination is intentionally hidden.', |
| 268 | + adText: |
| 269 | + 'A Carbon ad whose tracked destination is intentionally hidden.', |
73 | 270 | title: 'Example Sponsor', |
74 | 271 | url: '', |
75 | 272 | }, |
|
0 commit comments