Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion implementations/web-sdk_angular/angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"browser": "src/main.ts",
"tsConfig": "tsconfig.json",
"assets": [],
"styles": ["src/styles.css"]
"styles": ["src/styles.css"],
"allowedCommonJsDependencies": ["lodash"]
},
"configurations": {
"production": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { expect, test } from '@playwright/test'

test.describe('identified user', () => {
test.beforeEach(async ({ page }) => {
await test.step('load home page', async () => {
await page.goto('/')
await page.waitForLoadState('domcontentloaded')
await expect(
page.getByRole('heading', { name: 'Utilities' }),
'Utilities heading should be visible after load',
).toBeVisible()
})

await test.step('grant consent', async () => {
await page.getByTestId('consent-button').click()
await expect(
page.getByTestId('consent-status'),
'consent-status should show true after granting',
).toHaveText('Consent: true')
})

await test.step('identify user', async () => {
await page.getByTestId('live-updates-identify-button').click()
await expect(
page.getByTestId('live-updates-reset-button'),
'reset button should appear after identify',
).toBeVisible()
})

await test.step('reload and verify identified state persists', async () => {
await page.reload()
await page.waitForLoadState('domcontentloaded')
await expect(
page.getByRole('heading', { name: 'Utilities' }),
'Utilities heading should be visible after reload',
).toBeVisible()
await expect(
page.getByTestId('live-updates-reset-button'),
'reset button should still be visible after reload — identified state was not persisted',
).toBeVisible()
})
})

test('displays common variants', async ({ page }) => {
await expect(
page.getByText(
'This is a merge tag content entry that displays the visitor\'s continent "EU" embedded within the text.',
),
).toBeVisible()

const continentEntry = page.getByTestId('entry-text-4ib0hsHWoSOnCVdDkizE8d')
await expect(
continentEntry
.getByText('This is a variant content entry for visitors from Europe.')
.or(
continentEntry.getByText(
'This is a baseline content entry for visitors from any continent.',
),
),
).toBeVisible()

const deviceEntry = page.getByTestId('entry-text-xFwgG3oNaOcjzWiGe4vXo')
await expect(
deviceEntry
.getByText('This is a variant content entry for visitors using a desktop browser.')
.or(
deviceEntry.getByText(
'This is a baseline content entry for all visitors using any device.',
),
),
).toBeVisible()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { expect, test } from '@playwright/test'

test.describe('unidentified user', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/')
await page.waitForLoadState('domcontentloaded')
await expect(page.getByRole('heading', { name: 'Utilities' })).toBeVisible()
})

test('displays common variants', async ({ page }) => {
await expect(
page.getByText(
'This is a merge tag content entry that displays the visitor\'s continent "EU" embedded within the text.',
),
).toBeVisible()

const continentEntry = page.getByTestId('entry-text-4ib0hsHWoSOnCVdDkizE8d')
await expect(
continentEntry
.getByText('This is a variant content entry for visitors from Europe.')
.or(
continentEntry.getByText(
'This is a baseline content entry for visitors from any continent.',
),
),
).toBeVisible()

const deviceEntry = page.getByTestId('entry-text-xFwgG3oNaOcjzWiGe4vXo')
await expect(
deviceEntry
.getByText('This is a variant content entry for visitors using a desktop browser.')
.or(
deviceEntry.getByText(
'This is a baseline content entry for all visitors using any device.',
),
),
).toBeVisible()
})

test('displays unidentified user variants', async ({ page }) => {
await expect(page.getByRole('heading', { name: 'Utilities' })).toBeVisible()
await expect(page.getByRole('heading', { name: 'Auto-observed' })).toBeVisible()
await expect(page.getByRole('heading', { name: 'Manually-observed' })).toBeVisible()

await expect(page.getByText('This is a level 0 nested baseline entry.')).toBeVisible()
await expect(page.getByText('This is a level 1 nested baseline entry.')).toBeVisible()
await expect(page.getByText('This is a level 2 nested baseline entry.')).toBeVisible()

const visitorVariant = page.getByTestId('entry-text-2Z2WLOx07InSewC3LUB3eX')
await expect(
visitorVariant
.getByText('This is a variant content entry for new visitors.')
.or(visitorVariant.getByText('This is a variant content entry for return visitors.')),
).toBeVisible()
await expect(
page.getByText('This is a variant content entry for an A/B/C experiment: B'),
).toBeVisible()
await expect(
page.getByText(
'This is a baseline content entry for all visitors with or without a custom event.',
),
).toBeVisible()
await expect(
page.getByText('This is a baseline content entry for all identified or unidentified users.'),
).toBeVisible()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { type Page, expect, test } from '@playwright/test'

interface ClickScenario {
name: string
entryTestId: string
clickTargetTestId: string
}

const clickScenarios: ClickScenario[] = [
{
name: 'direct entry click target',
entryTestId: 'content-4ib0hsHWoSOnCVdDkizE8d',
clickTargetTestId: 'content-4ib0hsHWoSOnCVdDkizE8d',
},
{
name: 'clickable descendant button',
entryTestId: 'content-xFwgG3oNaOcjzWiGe4vXo',
clickTargetTestId: 'entry-click-descendant-button',
},
{
name: 'clickable ancestor wrapper',
entryTestId: 'content-2Z2WLOx07InSewC3LUB3eX',
clickTargetTestId: 'entry-click-ancestor-wrapper',
},
]

async function readResolvedEntryId(page: Page, entryTestId: string): Promise<string> {
const entryId = await page.getByTestId(entryTestId).getAttribute('data-ctfl-entry-id')

return entryId ?? ''
}

test.describe('entry click tracking', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/')
await page.waitForLoadState('domcontentloaded')
await expect(page.getByRole('heading', { name: 'Utilities' })).toBeVisible()
})

test('does not emit component_click events without consent', async ({ page }) => {
for (const scenario of clickScenarios) {
const target = page.getByTestId(scenario.clickTargetTestId)
await expect(target, `${scenario.name}: click target should render`).toBeVisible()

await target.scrollIntoViewIfNeeded()
await target.click()
}

await expect(page.locator('[data-testid^="event-component_click-"]')).toHaveCount(0)
})

test('emits component_click events for direct, descendant, and ancestor clickables after consent', async ({
page,
}) => {
await page.getByTestId('consent-button').click()
const clickEvents = page.locator('[data-testid^="event-component_click-"]')

for (const scenario of clickScenarios) {
const entryLocator = page.getByTestId(scenario.entryTestId)
await expect(entryLocator, `${scenario.name}: entry should render`).toBeVisible()

await expect
.poll(async () => await readResolvedEntryId(page, scenario.entryTestId), {
message: `${scenario.name}: resolved entry id should be available`,
})
.not.toEqual('')
const resolvedEntryId = await readResolvedEntryId(page, scenario.entryTestId)

const target = page.getByTestId(scenario.clickTargetTestId)
await target.scrollIntoViewIfNeeded()
await target.click()

await expect(page.getByTestId(`event-component_click-${resolvedEntryId}`)).toBeVisible()
}

await expect.poll(async () => await clickEvents.count()).toBe(3)
})
})
152 changes: 152 additions & 0 deletions implementations/web-sdk_angular/e2e/entry-hover-tracking.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { type Page, expect, test } from '@playwright/test'

interface HoverScenario {
name: string
hoverTargetTestId: string
}

const HOVER_ENTRY_BASELINE_ID = '4ib0hsHWoSOnCVdDkizE8d'

const hoverScenarios: HoverScenario[] = [
{
name: 'entry container',
hoverTargetTestId: `content-${HOVER_ENTRY_BASELINE_ID}`,
},
{
name: 'descendant content node',
hoverTargetTestId: `entry-text-${HOVER_ENTRY_BASELINE_ID}`,
},
]

function parseHoverDurationMs(label: string): number {
const match = /Hover Duration:\s*(\d+)ms/.exec(label)
if (!match?.[1]) return Number.NaN

return Number.parseInt(match[1], 10)
}

function parseHoverId(testId: string | null): string | undefined {
if (!testId) return undefined

const prefix = 'event-component_hover-hover-'
return testId.startsWith(prefix) ? testId.slice(prefix.length) : undefined
}

async function movePointerAwayFromEntries(page: Page): Promise<void> {
await page.getByRole('heading', { name: 'Utilities' }).hover()
}

async function readResolvedEntryId(page: Page): Promise<string> {
const entryId = await page
.getByTestId(`content-${HOVER_ENTRY_BASELINE_ID}`)
.getAttribute('data-ctfl-entry-id')

return entryId ?? ''
}

async function readHoverDurationMs(page: Page, hoverId: string): Promise<number> {
const label = await page.getByTestId(`event-component_hover-hover-${hoverId}`).innerText()
return parseHoverDurationMs(label)
}

test.describe('entry hover tracking', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/')
await page.waitForLoadState('domcontentloaded')
await expect(page.getByRole('heading', { name: 'Utilities' })).toBeVisible()
})

test('does not emit entry hover events without consent', async ({ page }) => {
for (const scenario of hoverScenarios) {
const target = page.getByTestId(scenario.hoverTargetTestId)
await expect(target, `${scenario.name}: hover target should render`).toBeVisible()

await target.scrollIntoViewIfNeeded()
await target.hover()
await page.waitForTimeout(1200)
await movePointerAwayFromEntries(page)
}

await expect(page.locator('[data-testid^="event-component_hover-hover-"]')).toHaveCount(0)
})

test('emits entry hover events for entry container and descendant hovers after consent', async ({
page,
}) => {
await page.getByTestId('consent-button').click()

await expect
.poll(async () => await readResolvedEntryId(page), {
message: 'resolved entry id should be available',
})
.not.toEqual('')
const resolvedEntryId = await readResolvedEntryId(page)

const hoverEvents = page.locator('[data-testid^="event-component_hover-hover-"]')

for (const scenario of hoverScenarios) {
const target = page.getByTestId(scenario.hoverTargetTestId)
const baselineCount = await hoverEvents.count()

await target.scrollIntoViewIfNeeded()
await target.hover()

await expect
.poll(async () => await hoverEvents.count(), {
message: `${scenario.name}: hover event count should increase`,
})
.toBeGreaterThan(baselineCount)

await expect(hoverEvents.first()).toContainText(`Entry/Flag: ${resolvedEntryId}`)
await movePointerAwayFromEntries(page)
}

await expect.poll(async () => await hoverEvents.count()).toBeGreaterThanOrEqual(2)
})

test('updates hover duration while hovered and emits a final update when hover ends', async ({
page,
}) => {
await page.getByTestId('consent-button').click()

await expect
.poll(async () => await readResolvedEntryId(page), {
message: 'resolved entry id should be available',
})
.not.toEqual('')
const resolvedEntryId = await readResolvedEntryId(page)

const target = page.getByTestId(`content-${HOVER_ENTRY_BASELINE_ID}`)
await target.scrollIntoViewIfNeeded()
await target.hover()

const hoverEvent = page.locator('[data-testid^="event-component_hover-hover-"]').first()
await expect(hoverEvent).toBeVisible()
await expect(hoverEvent).toContainText(`Entry/Flag: ${resolvedEntryId}`)

const hoverEventTestId = await hoverEvent.getAttribute('data-testid')
const hoverId = parseHoverId(hoverEventTestId)
expect(hoverId).toBeTruthy()
if (!hoverId) return

await expect
.poll(async () => await readHoverDurationMs(page, hoverId))
.toBeGreaterThanOrEqual(1000)
const firstHoverDurationMs = await readHoverDurationMs(page, hoverId)

await expect
.poll(async () => await readHoverDurationMs(page, hoverId))
.toBeGreaterThan(firstHoverDurationMs)
const updatedHoverDurationMs = await readHoverDurationMs(page, hoverId)

await page.waitForTimeout(300)
await movePointerAwayFromEntries(page)

await expect
.poll(async () => await readHoverDurationMs(page, hoverId))
.toBeGreaterThan(updatedHoverDurationMs)
const finalHoverDurationMs = await readHoverDurationMs(page, hoverId)

expect(finalHoverDurationMs).toBeGreaterThan(firstHoverDurationMs)
})
})
Loading