Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
5597084
feat: Add live updates context and section to HomePage
Lotfi-Arif Feb 25, 2026
12e6136
feat: Add Playwright tests for live updates behavior
Lotfi-Arif Feb 25, 2026
d6c3136
feat: Add e2e tests for common and user-specific variants
Lotfi-Arif Feb 25, 2026
3277026
refactor: inline LiveUpdatesSection props for readability
Lotfi-Arif Feb 26, 2026
66fa63e
feat: Add optimization-web-preview-panel package and export getter for
Lotfi-Arif Feb 26, 2026
3b3ffc5
feat: Add preview panel integration and toggle functionality
Lotfi-Arif Feb 26, 2026
77b7be0
feat: update tests to use data-testid for identify/reset buttons
Lotfi-Arif Feb 26, 2026
81425f9
refactor: personalizeEntry call to use updated SDK method
Lotfi-Arif Feb 26, 2026
02eb68b
feat: Add LiveUpdatesExampleEntry component for live update demo
Lotfi-Arif Feb 26, 2026
02a0cac
refactor: live updates section to use LiveUpdatesExampleEntry
Lotfi-Arif Feb 26, 2026
e511683
feat: Add Page Two personalized content and conversion demo
Lotfi-Arif Feb 27, 2026
586d5e7
refactor: Page Two entry IDs to use explicit constants
Lotfi-Arif Feb 27, 2026
0ae890b
docs: Add TODOs for preview panel open-state API and tree-shaking
Lotfi-Arif Feb 27, 2026
aa048aa
feat: show page URL in analytics event display and update test
Lotfi-Arif Feb 27, 2026
732aaa7
feat: Add env flag to enable preview panel for local demos
Lotfi-Arif Feb 27, 2026
2149e7e
feat: Make e2e tests conditional on preview panel env flag
Lotfi-Arif Feb 27, 2026
456cb4b
feat: rename preview panel env var to ENABLE_PREVIEW_PANEL
Lotfi-Arif Feb 27, 2026
d6db512
feat: Move LiveUpdatesProvider to main and lift globalLiveUpdates state
Lotfi-Arif Feb 27, 2026
dc8d373
feat: add e2e tests for offline queue and recovery scenarios
Lotfi-Arif Feb 27, 2026
d6e903a
refactor: format code for readability in e2e tests and App component
Lotfi-Arif Feb 27, 2026
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
1 change: 1 addition & 0 deletions implementations/web-react/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ PUBLIC_CONTENTFUL_SPACE_ID="mock-space-id"

PUBLIC_CONTENTFUL_CDA_HOST="localhost:8000"
PUBLIC_CONTENTFUL_BASE_PATH="contentful"
PUBLIC_OPTIMIZATION_ENABLE_PREVIEW_PANEL="false"
6 changes: 6 additions & 0 deletions implementations/web-react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ demonstrating:
- SPA navigation tracking with React Router v7
- Offline queue/recovery handling

The live updates section demonstrates the same parity scenarios directly in-page (default, forced
on, and locked), while keeping the main entry rendering flow customer-oriented.

## Prerequisites

- Node.js >= 16.20.0
Expand Down Expand Up @@ -97,6 +100,9 @@ See `.env.example` for available configuration options. The implementation reads
env wiring. To use local mock Contentful endpoints, set `PUBLIC_CONTENTFUL_CDA_HOST=localhost:8000`
and `PUBLIC_CONTENTFUL_BASE_PATH=contentful`.

Preview panel attachment is gated behind `PUBLIC_OPTIMIZATION_ENABLE_PREVIEW_PANEL`. Set it to
`true` for development demos that need preview panel behavior.

## Project Structure

```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,75 @@ test.describe('identified user', () => {
await page.waitForLoadState('domcontentloaded')
await expect(page.getByRole('heading', { name: 'Utilities' })).toBeVisible()

await page.getByRole('button', { name: 'Identify' }).click()
await expect(page.getByRole('button', { name: 'Reset Profile' })).toBeVisible()
await page.getByTestId('live-updates-identify-button').click()
await expect(page.getByTestId('live-updates-reset-button')).toBeVisible()

await page.reload()
await page.waitForLoadState('domcontentloaded')
await expect(page.getByRole('heading', { name: 'Utilities' })).toBeVisible()
await expect(page.getByRole('button', { name: 'Reset Profile' })).toBeVisible()
await expect(page.getByTestId('live-updates-reset-button')).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('renders identified variants', async ({ page }) => {
await expect(page.getByText('This is a level 0 nested variant entry.')).toBeVisible()
await expect(page.getByText('This is a level 1 nested variant entry.')).toBeVisible()
await expect(page.getByText('This is a level 2 nested variant entry.')).toBeVisible()

await expect(
page
.getByTestId('entry-text-2Z2WLOx07InSewC3LUB3eX')
.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 variant content entry for visitors with a custom event.'),
).toBeVisible()
await expect(
page.getByText('This is a variant content entry for identified users.'),
).toBeVisible()
await expect(page.getByText('This is a level 0 nested variant entry.')).toBeVisible()
})

test('reset persists unidentified state across reload', async ({ page }) => {
await page.getByRole('button', { name: 'Reset Profile' }).click()
await expect(page.getByRole('button', { name: 'Identify' })).toBeVisible()
await page.getByTestId('live-updates-reset-button').click()
await expect(page.getByTestId('live-updates-identify-button')).toBeVisible()

await page.reload()
await page.waitForLoadState('domcontentloaded')
await expect(page.getByRole('heading', { name: 'Utilities' })).toBeVisible()

await expect(page.getByRole('button', { name: 'Identify' })).toBeVisible()
await expect(page.getByTestId('live-updates-identify-button')).toBeVisible()
await expect(
page.getByText('This is a baseline content entry for all identified or unidentified users.'),
).toBeVisible()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,61 @@ test.describe('unidentified user', () => {
await expect(page.getByRole('heading', { name: 'Utilities' })).toBeVisible()
})

test('renders utility panel and entries', async ({ page }) => {
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 Entries' })).toBeVisible()
await expect(page.getByRole('heading', { name: 'Manually Observed Entries' })).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 merge tag content entry that displays the visitor\'s continent "EU" embedded within the text.',
'This is a baseline content entry for all visitors with or without a custom event.',
),
).toBeVisible()

await expect(page.getByText('This is a level 0 nested baseline entry.')).toBeVisible()
await expect(page.getByText('This is a variant content entry for new visitors.')).toBeVisible()
await expect(
page.getByText('This is a baseline content entry for all identified or unidentified users.'),
).toBeVisible()
})
})
137 changes: 137 additions & 0 deletions implementations/web-react/e2e/live-updates.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { type Locator, type Page, expect, test } from '@playwright/test'

const isPreviewPanelEnabled = process.env.PUBLIC_OPTIMIZATION_ENABLE_PREVIEW_PANEL === 'true'

async function getEntryId(locator: Locator): Promise<string> {
const text = await locator.innerText()
return text.replace('Entry: ', '').trim()
}

async function identify(page: Page): Promise<void> {
await page.getByTestId('live-updates-identify-button').click()
await expect(page.getByTestId('identified-status')).toHaveText('Yes')
}

test.describe('live updates behavior', () => {
test.beforeEach(async ({ page }) => {
await page.context().clearCookies()
await page.goto('/')
await page.waitForLoadState('domcontentloaded')
await expect(page.getByRole('heading', { name: 'Utilities' })).toBeVisible()
await expect(page.getByTestId('preview-panel-status')).toHaveText('Closed')
await expect(page.getByTestId('global-live-updates-status')).toHaveText('OFF')
await expect(page.getByTestId('identified-status')).toHaveText('No')
if (isPreviewPanelEnabled) {
await expect
.poll(async () => await page.locator('ctfl-opt-preview-panel').count())
.toBeGreaterThan(0)
} else {
await expect(page.locator('ctfl-opt-preview-panel')).toHaveCount(0)
}
await expect(page.getByTestId('live-updates-examples')).toBeVisible()
await expect
.poll(async () => {
const text = await page.getByTestId('personalizations-count').innerText()
const value = Number.parseInt(text.replace('Personalizations: ', ''), 10)
return Number.isNaN(value) ? 0 : value
})
.toBeGreaterThan(0)
})

test('default behavior locks to first value when global live updates is OFF', async ({
page,
}) => {
const initialDefaultEntryId = await getEntryId(page.getByTestId('entry-id-live-default'))

await identify(page)

await expect(page.getByTestId('entry-id-live-default')).toHaveText(
`Entry: ${initialDefaultEntryId}`,
)
})

test('global live updates ON updates default component while locked component stays fixed', async ({
page,
}) => {
await page.getByTestId('toggle-global-live-updates-button').click()
await expect(page.getByTestId('global-live-updates-status')).toHaveText('ON')

const initialDefaultEntryId = await getEntryId(page.getByTestId('entry-id-live-default'))
const initialLockedEntryId = await getEntryId(page.getByTestId('entry-id-live-locked'))

await identify(page)

await expect
.poll(async () => await getEntryId(page.getByTestId('entry-id-live-default')))
.not.toBe(initialDefaultEntryId)
await expect(page.getByTestId('entry-id-live-locked')).toHaveText(
`Entry: ${initialLockedEntryId}`,
)
})

test('per-component liveUpdates=true updates even when global live updates is OFF', async ({
page,
}) => {
const initialLiveEntryId = await getEntryId(page.getByTestId('entry-id-live-enabled'))

await identify(page)

await expect
.poll(async () => await getEntryId(page.getByTestId('entry-id-live-enabled')))
.not.toBe(initialLiveEntryId)
})

test('preview-panel override enables updates for locked components', async ({ page }) => {
test.skip(!isPreviewPanelEnabled, 'Preview panel is disabled for this build.')
const initialLockedEntryId = await getEntryId(page.getByTestId('entry-id-live-locked'))

await page.getByTestId('simulate-preview-panel-button').click()
await expect(page.getByTestId('preview-panel-status')).toHaveText('Open')

await identify(page)

await expect
.poll(async () => await getEntryId(page.getByTestId('entry-id-live-locked')))
.not.toBe(initialLockedEntryId)
})

test('screen controls toggle global live updates and preview panel', async ({ page }) => {
test.skip(!isPreviewPanelEnabled, 'Preview panel is disabled for this build.')
await expect(page.getByTestId('global-live-updates-status')).toHaveText('OFF')
await page.getByTestId('toggle-global-live-updates-button').click()
await expect(page.getByTestId('global-live-updates-status')).toHaveText('ON')
await page.getByTestId('toggle-global-live-updates-button').click()
await expect(page.getByTestId('global-live-updates-status')).toHaveText('OFF')

await expect(page.getByTestId('preview-panel-status')).toHaveText('Closed')
await page.getByTestId('simulate-preview-panel-button').click()
await expect(page.getByTestId('preview-panel-status')).toHaveText('Open')
await page.getByTestId('simulate-preview-panel-button').click()
await expect(page.getByTestId('preview-panel-status')).toHaveText('Closed')
})

test('screen controls identify and reset user', async ({ page }) => {
await expect(page.getByTestId('identified-status')).toHaveText('No')
await page.getByTestId('live-updates-identify-button').click()
await expect(page.getByTestId('identified-status')).toHaveText('Yes')
await page.getByTestId('live-updates-reset-button').click()
await expect(page.getByTestId('identified-status')).toHaveText('No')
})

test('renders default, enabled, and locked examples', async ({ page }) => {
await expect(page.getByTestId('live-updates-default')).toBeVisible()
await expect(page.getByTestId('live-updates-enabled')).toBeVisible()
await expect(page.getByTestId('live-updates-locked')).toBeVisible()

await expect(page.getByTestId('content-live-default')).toBeVisible()
await expect(page.getByTestId('content-live-enabled')).toBeVisible()
await expect(page.getByTestId('content-live-locked')).toBeVisible()

await expect(page.getByTestId('entry-text-live-default')).toBeVisible()
await expect(page.getByTestId('entry-text-live-enabled')).toBeVisible()
await expect(page.getByTestId('entry-text-live-locked')).toBeVisible()
await expect(page.getByTestId('entry-id-live-default')).toBeVisible()
await expect(page.getByTestId('entry-id-live-enabled')).toBeVisible()
await expect(page.getByTestId('entry-id-live-locked')).toBeVisible()
})
})
40 changes: 33 additions & 7 deletions implementations/web-react/e2e/navigation-page-events.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,23 @@
import { expect, test } from '@playwright/test'
import { type Page, expect, test } from '@playwright/test'

async function getRecentPageEventUrls(page: Page): Promise<string[]> {
const pageEvents = page.locator('[data-testid^="event-page-"]')
const count = await pageEvents.count()
const urls: string[] = []

for (let index = 0; index < count; index += 1) {
const text = await pageEvents.nth(index).innerText()
const marker = 'URL: '
const markerIndex = text.indexOf(marker)
if (markerIndex === -1) {
continue
}

urls.push(text.slice(markerIndex + marker.length).trim())
}

return urls
}

test.describe('navigation page events', () => {
test.beforeEach(async ({ page }) => {
Expand All @@ -7,11 +26,11 @@ test.describe('navigation page events', () => {
await expect(page.getByRole('heading', { name: 'Utilities' })).toBeVisible()
})

test('emits page events on route navigation', async ({ page }) => {
test('records ordered route sequence including revisits', async ({ page }) => {
const pageEventLocator = page.locator('[data-testid^="event-page-"]')
await expect(pageEventLocator.first()).toBeVisible()
const initialPageEventCount = await pageEventLocator.count()

const initialUrls = await getRecentPageEventUrls(page)
const initialPageEventCount = initialUrls.length
await page.getByTestId('link-page-two').click()
await expect(page).toHaveURL(/\/page-two$/)
await expect(page.getByTestId('page-two-view')).toBeVisible()
Expand All @@ -20,12 +39,19 @@ test.describe('navigation page events', () => {
.poll(async () => await pageEventLocator.count())
.toBeGreaterThan(initialPageEventCount)

const afterPageTwoCount = await pageEventLocator.count()

await page.getByTestId('link-back-home').click()
await expect(page).toHaveURL(/\/$/)
await expect(page.getByRole('heading', { name: 'Utilities' })).toBeVisible()

await expect.poll(async () => await pageEventLocator.count()).toBeGreaterThan(afterPageTwoCount)
await page.getByTestId('link-page-two').click()
await expect(page).toHaveURL(/\/page-two$/)
await expect(page.getByTestId('page-two-view')).toBeVisible()

await expect
.poll(async () => {
const urls = await getRecentPageEventUrls(page)
return urls.slice(0, 3)
})
.toEqual(['/page-two', '/', '/page-two'])
})
})
Loading