Skip to content

Commit 72b62db

Browse files
committed
Implement hasProvidedEmail functionality for Download CTA, update other components to use also
1 parent 9a3b85f commit 72b62db

13 files changed

Lines changed: 449 additions & 84 deletions

File tree

src/components/CallToAction/Download/__tests__/index.spec.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { beforeEach, describe, expect, test } from 'vitest'
22
import { experimental_AstroContainer as AstroContainer } from 'astro/container'
33
import { withJsdomEnvironment } from '@test/unit/helpers/litRuntime'
44

5+
const existingResource = 'performance-testing-load-models-benchmark-accuracy'
6+
57
describe('Download CallToAction (Astro)', () => {
68
let container: AstroContainer
79

@@ -21,7 +23,9 @@ describe('Download CallToAction (Astro)', () => {
2123
await withJsdomEnvironment(async ({ window }) => {
2224
window.document.body.innerHTML = renderedHtml
2325

26+
const host = window.document.querySelector('download-cta')
2427
const section = window.document.querySelector('section')
28+
expect(host).toBeTruthy()
2529
expect(section).toBeTruthy()
2630
expect(section?.getAttribute('aria-labelledby')).toBe('download-cta-title')
2731
expect(section?.getAttribute('aria-describedby')).toBe('download-cta-description')
@@ -38,6 +42,31 @@ describe('Download CallToAction (Astro)', () => {
3842
})
3943
})
4044

45+
test('renders direct and landing download URLs for the web component host', async () => {
46+
const Download = (await import('@components/CallToAction/Download/index.astro')).default
47+
48+
const renderedHtml = await container.renderToString(Download, {
49+
props: {
50+
resource: existingResource,
51+
},
52+
})
53+
54+
await withJsdomEnvironment(async ({ window }) => {
55+
window.document.body.innerHTML = renderedHtml
56+
57+
const host = window.document.querySelector('download-cta')
58+
const primaryLink = window.document.querySelector('[data-download-cta-primary]')
59+
60+
expect(host?.getAttribute('data-landing-url')).toBe(
61+
`/downloads/${existingResource}`
62+
)
63+
expect(host?.getAttribute('data-direct-download-url')).toBe(
64+
'/downloads/performance-testing-load-models-benchmark-accuracy.pdf'
65+
)
66+
expect(primaryLink?.getAttribute('href')).toBe(`/downloads/${existingResource}`)
67+
})
68+
})
69+
4170
test('supports a custom id base for aria relationships', async () => {
4271
const Download = (await import('@components/CallToAction/Download/index.astro')).default
4372

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
import Download from '@components/CallToAction/Download/index.astro'
3+
---
4+
5+
<Download resource="performance-testing-load-models-benchmark-accuracy" />
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { afterEach, beforeAll, describe, expect, it } from 'vitest'
2+
import {
3+
__resetEmailCollectionForTests,
4+
markEmailCollected,
5+
} from '@components/scripts/store/emailCollection'
6+
7+
let renderDownloadCta: typeof import('./testUtils').renderDownloadCta
8+
9+
beforeAll(async () => {
10+
;({ renderDownloadCta } = await import('./testUtils'))
11+
})
12+
13+
describe('download-cta web component', () => {
14+
afterEach(() => {
15+
if (typeof localStorage !== 'undefined') {
16+
localStorage.clear()
17+
}
18+
__resetEmailCollectionForTests()
19+
})
20+
21+
it('uses the download landing page link until an email has been collected', async () => {
22+
await renderDownloadCta(async ({ elements, urls }) => {
23+
expect(elements.host.dataset['emailState']).toBe('gated')
24+
expect(elements.primaryLink.getAttribute('href')).toBe(urls.landingUrl)
25+
})
26+
})
27+
28+
it('switches the primary link to the direct download after email collection', async () => {
29+
await renderDownloadCta(async ({ elements, urls }) => {
30+
expect(elements.primaryLink.getAttribute('href')).toBe(urls.landingUrl)
31+
32+
markEmailCollected('reader@example.com', 'newsletter_form')
33+
await Promise.resolve()
34+
35+
expect(elements.host.dataset['emailState']).toBe('ready')
36+
expect(elements.primaryLink.getAttribute('href')).toBe(urls.directDownloadUrl)
37+
})
38+
})
39+
})
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { expect } from 'vitest'
2+
import { experimental_AstroContainer as AstroContainer } from 'astro/container'
3+
import DownloadCtaFixture from './__fixtures__/downloadCta.fixture.astro'
4+
import type { DownloadCtaElement } from '@components/CallToAction/Download/client'
5+
import { TestError } from '@test/errors'
6+
import {
7+
getDownloadCtaHost,
8+
getDownloadCtaPrimaryLink,
9+
getDownloadCtaUrls,
10+
} from '@components/CallToAction/Download/client/selectors'
11+
import type { WebComponentModule } from '@components/scripts/@types/webComponentModule'
12+
import { executeRender } from '@test/unit/helpers/litRuntime'
13+
14+
export type DownloadCtaModule = WebComponentModule<DownloadCtaElement>
15+
16+
export interface DownloadCtaElements {
17+
host: HTMLElement
18+
primaryLink: HTMLAnchorElement
19+
}
20+
21+
export interface RenderDownloadCtaContext {
22+
element: DownloadCtaElement
23+
module: DownloadCtaModule
24+
window: Window & typeof globalThis
25+
elements: DownloadCtaElements
26+
urls: {
27+
landingUrl: string
28+
directDownloadUrl: string
29+
}
30+
}
31+
32+
export type RenderDownloadCtaAssertion = (
33+
_context: RenderDownloadCtaContext
34+
) => Promise<void> | void
35+
36+
export const renderDownloadCta = async (assertion: RenderDownloadCtaAssertion): Promise<void> => {
37+
const container = await AstroContainer.create()
38+
39+
await executeRender<DownloadCtaModule>({
40+
container,
41+
component: DownloadCtaFixture,
42+
moduleSpecifier: '@components/CallToAction/Download/client/index',
43+
selector: 'download-cta',
44+
waitForReady: async element => {
45+
element.initialize()
46+
await Promise.resolve()
47+
},
48+
assert: async ({ element, module, window, renderResult }) => {
49+
if (!window) {
50+
throw new TestError('Download CTA tests require a DOM-like window environment')
51+
}
52+
53+
const domWindow = window as Window & typeof globalThis
54+
const host = getDownloadCtaHost(domWindow.document)
55+
const primaryLink = getDownloadCtaPrimaryLink(domWindow.document)
56+
57+
expect(renderResult).toContain(`<${module.registeredName}`)
58+
59+
await assertion({
60+
element,
61+
module,
62+
window: domWindow,
63+
elements: {
64+
host,
65+
primaryLink,
66+
},
67+
urls: getDownloadCtaUrls(domWindow.document),
68+
})
69+
},
70+
})
71+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { LitElement } from 'lit'
2+
import {
3+
getEmailCollectionSnapshot,
4+
subscribeToEmailCollection,
5+
type EmailCollectionState,
6+
} from '@components/scripts/store'
7+
import { addScriptBreadcrumb } from '@components/scripts/errors'
8+
import { handleScriptError } from '@components/scripts/errors/handler'
9+
import { defineCustomElement } from '@components/scripts/utils'
10+
import type { WebComponentModule } from '@components/scripts/@types/webComponentModule'
11+
import { getDownloadCtaPrimaryLink, getDownloadCtaUrls } from './selectors'
12+
13+
const scriptName = 'DownloadCtaElement'
14+
15+
export class DownloadCtaElement extends LitElement {
16+
static registeredName = 'download-cta'
17+
18+
private primaryLink: HTMLAnchorElement | null = null
19+
private unsubscribeFromEmailCollection: (() => void) | null = null
20+
private isInitialized = false
21+
22+
protected override createRenderRoot(): HTMLElement {
23+
return this
24+
}
25+
26+
override connectedCallback(): void {
27+
super.connectedCallback()
28+
29+
queueMicrotask(() => {
30+
this.initialize()
31+
})
32+
}
33+
34+
override disconnectedCallback(): void {
35+
this.unsubscribeFromEmailCollection?.()
36+
this.unsubscribeFromEmailCollection = null
37+
this.isInitialized = false
38+
super.disconnectedCallback()
39+
}
40+
41+
public initialize(): void {
42+
const context = { scriptName, operation: 'initialize' }
43+
addScriptBreadcrumb(context)
44+
45+
try {
46+
if (this.isInitialized) {
47+
return
48+
}
49+
50+
this.primaryLink = getDownloadCtaPrimaryLink(this)
51+
const { landingUrl, directDownloadUrl } = getDownloadCtaUrls(this)
52+
53+
this.syncPrimaryLink(getEmailCollectionSnapshot(), {
54+
landingUrl,
55+
directDownloadUrl,
56+
})
57+
58+
this.unsubscribeFromEmailCollection = subscribeToEmailCollection(state => {
59+
this.syncPrimaryLink(state, {
60+
landingUrl,
61+
directDownloadUrl,
62+
})
63+
})
64+
65+
this.isInitialized = true
66+
} catch (error) {
67+
handleScriptError(error, context)
68+
}
69+
}
70+
71+
private syncPrimaryLink(
72+
state: EmailCollectionState,
73+
urls: { landingUrl: string; directDownloadUrl: string }
74+
): void {
75+
if (!this.primaryLink) {
76+
return
77+
}
78+
79+
const hasProvidedEmail = state.hasProvidedEmail === true
80+
const nextHref = hasProvidedEmail ? urls.directDownloadUrl : urls.landingUrl
81+
const nextState = hasProvidedEmail ? 'ready' : 'gated'
82+
83+
this.primaryLink.href = nextHref
84+
this.primaryLink.dataset['emailState'] = nextState
85+
this.dataset['emailState'] = nextState
86+
}
87+
}
88+
89+
export const registerDownloadCtaWebComponent = (tagName = DownloadCtaElement.registeredName) => {
90+
if (typeof window === 'undefined') {
91+
return
92+
}
93+
94+
defineCustomElement(tagName, DownloadCtaElement)
95+
}
96+
97+
export const registerWebComponent = registerDownloadCtaWebComponent
98+
99+
export const webComponentModule: WebComponentModule<DownloadCtaElement> = {
100+
registeredName: DownloadCtaElement.registeredName,
101+
componentCtor: DownloadCtaElement,
102+
registerWebComponent: registerDownloadCtaWebComponent,
103+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { isAnchorElement, isType1Element } from '@components/scripts/assertions/elements'
2+
import { ClientScriptError } from '@components/scripts/errors'
3+
4+
const HOST_TAG_NAME = 'DOWNLOAD-CTA'
5+
6+
export const SELECTORS = {
7+
host: 'download-cta',
8+
primaryLink: '[data-download-cta-primary]',
9+
} as const
10+
11+
type SelectorRoot = Document | DocumentFragment | Element
12+
13+
const resolveRoot = (root?: SelectorRoot): SelectorRoot => {
14+
return root ?? document
15+
}
16+
17+
const isDownloadCtaHost = (element: unknown): element is HTMLElement => {
18+
return isType1Element(element) && element.tagName === HOST_TAG_NAME
19+
}
20+
21+
export const getDownloadCtaHost = (root?: SelectorRoot): HTMLElement => {
22+
const resolvedRoot = resolveRoot(root)
23+
const host = isDownloadCtaHost(resolvedRoot)
24+
? resolvedRoot
25+
: resolvedRoot.querySelector(SELECTORS.host)
26+
27+
if (!isDownloadCtaHost(host)) {
28+
throw new ClientScriptError({
29+
message: 'Download CTA host element not found',
30+
})
31+
}
32+
33+
return host
34+
}
35+
36+
export const getDownloadCtaPrimaryLink = (root?: SelectorRoot): HTMLAnchorElement => {
37+
const primaryLink = getDownloadCtaHost(root).querySelector(SELECTORS.primaryLink)
38+
39+
if (!isAnchorElement(primaryLink)) {
40+
throw new ClientScriptError({
41+
message: 'Download CTA primary link not found',
42+
})
43+
}
44+
45+
return primaryLink
46+
}
47+
48+
export const getDownloadCtaUrls = (root?: SelectorRoot): {
49+
landingUrl: string
50+
directDownloadUrl: string
51+
} => {
52+
const host = getDownloadCtaHost(root)
53+
const landingUrl = host.dataset['landingUrl']?.trim() ?? ''
54+
const directDownloadUrl = host.dataset['directDownloadUrl']?.trim() ?? ''
55+
56+
if (!landingUrl || !directDownloadUrl) {
57+
throw new ClientScriptError({
58+
message: 'Download CTA URLs are missing required data attributes',
59+
})
60+
}
61+
62+
return {
63+
landingUrl,
64+
directDownloadUrl,
65+
}
66+
}

0 commit comments

Comments
 (0)