diff --git a/.github/workflows/main-pipeline.yaml b/.github/workflows/main-pipeline.yaml index 36acd758..5d1aa046 100644 --- a/.github/workflows/main-pipeline.yaml +++ b/.github/workflows/main-pipeline.yaml @@ -25,6 +25,7 @@ jobs: e2e_node_sdk_web_sdk: ${{ steps.filter.outputs.e2e_node_sdk_web_sdk }} e2e_web_sdk: ${{ steps.filter.outputs.e2e_web_sdk }} e2e_web_sdk_react: ${{ steps.filter.outputs.e2e_web_sdk_react }} + e2e_web_sdk_angular: ${{ steps.filter.outputs.e2e_web_sdk_angular }} e2e_react_web_sdk: ${{ steps.filter.outputs.e2e_react_web_sdk }} e2e_react_native_android: ${{ steps.filter.outputs.e2e_react_native_android }} e2e_android: ${{ steps.filter.outputs.e2e_android }} @@ -74,6 +75,11 @@ jobs: - '{implementations/web-sdk_react/**,lib/**,packages/web/web-sdk/**,packages/web/preview-panel/**,packages/universal/core-sdk/**,packages/universal/api-client/**,packages/universal/api-schemas/**,package.json,pnpm-lock.yaml,.github/workflows/main-pipeline.yaml}' - '!**/*.@(md|mdx|markdown)' - '!{docs/**,documentation/**,**/docs/**,**/documentation/**}' + # Angular + Web SDK implementation E2E coverage scope. + e2e_web_sdk_angular: + - '{implementations/web-sdk_angular/**,lib/**,packages/web/web-sdk/**,packages/web/preview-panel/**,packages/universal/core-sdk/**,packages/universal/api-client/**,packages/universal/api-schemas/**,package.json,pnpm-lock.yaml,.github/workflows/main-pipeline.yaml}' + - '!**/*.@(md|mdx|markdown)' + - '!{docs/**,documentation/**,**/docs/**,**/documentation/**}' # React Web SDK (optimization-react-web) implementation E2E coverage scope. e2e_react_web_sdk: - '{implementations/react-web-sdk/**,lib/**,packages/web/frameworks/react-web-sdk/**,packages/web/web-sdk/**,packages/web/preview-panel/**,packages/universal/core-sdk/**,packages/universal/api-client/**,packages/universal/api-schemas/**,package.json,pnpm-lock.yaml,.github/workflows/main-pipeline.yaml}' @@ -544,8 +550,8 @@ jobs: path: pkgs - run: pnpm store prune - run: pnpm run implementation:web-sdk_react -- implementation:install -- --no-frozen-lockfile - - run: - pnpm run implementation:web-sdk_react -- implementation:playwright:install -- --with-deps + - run: pnpm --dir lib/e2e-web install --no-frozen-lockfile + - run: pnpm --dir lib/e2e-web run setup:e2e - run: pnpm run implementation:web-sdk_react -- implementation:test:e2e:run - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -553,8 +559,56 @@ jobs: with: name: ci-results-web-sdk_react path: | - ./implementations/web-sdk_react/playwright-report/ - ./implementations/web-sdk_react/test-results/ + ./lib/e2e-web/playwright-report/ + ./lib/e2e-web/test-results/ + retention-days: 1 + + e2e-web-sdk_angular: + name: 🅰️ E2E Angular + Web SDK + runs-on: namespace-profile-linux-8-vcpu-16-gb-ram-optimal + timeout-minutes: 15 + needs: [setup, changes, build] + if: needs.changes.outputs.e2e_web_sdk_angular == 'true' + steps: + - uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1 + + - name: Create .env from .env.example + run: cp implementations/web-sdk_angular/.env.example implementations/web-sdk_angular/.env + + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version-file: '.nvmrc' + package-manager-cache: false + + - uses: pnpm/action-setup@903f9c1a6ebcba6cf41d87230be49611ac97822e # v6.0.3 + + - name: Set up caches (Namespace) + uses: namespacelabs/nscloud-cache-action@15799a6b54e5765f85b2aac25b3f0df43ed571c0 # v1.4.3 + with: + cache: | + pnpm + playwright + apt + + - run: pnpm install --prefer-offline --frozen-lockfile + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: sdk-package-tarballs + path: pkgs + - run: pnpm store prune + - run: + pnpm run implementation:web-sdk_angular -- implementation:install -- --no-frozen-lockfile + - run: pnpm --dir lib/e2e-web install --no-frozen-lockfile + - run: pnpm --dir lib/e2e-web run setup:e2e + - run: pnpm run implementation:web-sdk_angular -- implementation:test:e2e:run + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: ${{ !cancelled() }} + with: + name: ci-results-web-sdk_angular + path: | + ./lib/e2e-web/playwright-report/ + ./lib/e2e-web/test-results/ retention-days: 1 e2e-react-web-sdk: diff --git a/implementations/web-sdk_angular/.env.example b/implementations/web-sdk_angular/.env.example index b5612b79..5441490f 100644 --- a/implementations/web-sdk_angular/.env.example +++ b/implementations/web-sdk_angular/.env.example @@ -12,4 +12,4 @@ PUBLIC_CONTENTFUL_SPACE_ID="mock-space-id" PUBLIC_CONTENTFUL_CDA_HOST="localhost:8000" PUBLIC_CONTENTFUL_BASE_PATH="contentful" -PUBLIC_OPTIMIZATION_ENABLE_PREVIEW_PANEL="true" +PUBLIC_OPTIMIZATION_ENABLE_PREVIEW_PANEL="false" diff --git a/implementations/web-sdk_angular/angular.json b/implementations/web-sdk_angular/angular.json index 92e1c3d0..2047e314 100644 --- a/implementations/web-sdk_angular/angular.json +++ b/implementations/web-sdk_angular/angular.json @@ -17,14 +17,23 @@ "browser": "src/main.ts", "tsConfig": "tsconfig.json", "assets": [], - "styles": ["src/styles.css"] + "styles": ["src/styles.css"], + "allowedCommonJsDependencies": [ + "lodash", + "contentful-sdk-core", + "qs", + "json-stringify-safe" + ], + "define": { + "import.meta.env": "{}" + } }, "configurations": { "production": { "budgets": [ { "type": "initial", - "maximumWarning": "500kB", + "maximumWarning": "600kB", "maximumError": "1MB" } ], diff --git a/implementations/web-sdk_angular/package.json b/implementations/web-sdk_angular/package.json index c41f6c0a..8564e2fa 100644 --- a/implementations/web-sdk_angular/package.json +++ b/implementations/web-sdk_angular/package.json @@ -8,8 +8,16 @@ "dev": "ng serve", "build": "ng build", "clean": "rimraf ./dist", + "serve": "pnpm serve:mocks && pnpm serve:app", "serve:mocks": "pm2 start --name web-sdk_angular-mocks \"pnpm --dir ../../lib/mocks serve\"", "serve:mocks:stop": "pm2 stop web-sdk_angular-mocks && pm2 delete web-sdk_angular-mocks", + "serve:app": "ng build && pm2 start --name web-sdk_angular-app \"http-server dist/web-sdk_angular/browser -p 3000 --silent\"", + "serve:app:stop": "pm2 stop web-sdk_angular-app && pm2 delete web-sdk_angular-app", + "serve:stop": "pnpm serve:app:stop && pnpm serve:mocks:stop", + "test:e2e": "pnpm serve:app && pnpm --dir ../../lib/e2e-web test; E2E_RESULT=$?; pnpm serve:app:stop; exit $E2E_RESULT", + "test:e2e:ui": "pnpm serve:app && pnpm --dir ../../lib/e2e-web test:ui; pnpm serve:app:stop", + "test:e2e:report": "pnpm --dir ../../lib/e2e-web test:report", + "implementation:setup:e2e": "pnpm --dir ../../lib/e2e-web setup:e2e", "test:unit": "echo \"No unit tests necessary\"", "typecheck": "tsc -p tsconfig.json --noEmit" }, @@ -31,6 +39,7 @@ "@angular/cli": "^22.0.0", "@angular/compiler-cli": "^22.0.0", "@types/node": "^24.0.13", + "http-server": "^14.1.1", "pm2": "^6.0.14", "rimraf": "^6.1.3", "typescript": "~6.0.3" diff --git a/implementations/web-sdk_angular/src/app/app.config.ts b/implementations/web-sdk_angular/src/app/app.config.ts index 416fcbba..0938fb0b 100644 --- a/implementations/web-sdk_angular/src/app/app.config.ts +++ b/implementations/web-sdk_angular/src/app/app.config.ts @@ -30,7 +30,7 @@ export const appConfig: ApplicationConfig = { cdaHost: import.meta.env.PUBLIC_CONTENTFUL_CDA_HOST ?? 'localhost:8000', basePath: import.meta.env.PUBLIC_CONTENTFUL_BASE_PATH ?? 'contentful', }, - ...(import.meta.env.PUBLIC_OPTIMIZATION_ENABLE_PREVIEW_PANEL !== 'false' + ...(import.meta.env.PUBLIC_OPTIMIZATION_ENABLE_PREVIEW_PANEL === 'true' ? { previewPanel: {} } : {}), }), diff --git a/implementations/web-sdk_angular/src/app/components/control-panel/index.html b/implementations/web-sdk_angular/src/app/components/control-panel/index.html index 48e887ce..adbcf8b0 100644 --- a/implementations/web-sdk_angular/src/app/components/control-panel/index.html +++ b/implementations/web-sdk_angular/src/app/components/control-panel/index.html @@ -1,14 +1,15 @@
-

SDK state

+

Utilities

Consent - {{ consent() ?? 'undefined' }} + {{ consent() ?? 'undefined' }} @if (consent() === true) { } Identified - {{ isIdentified() ? 'Yes' : 'No' }} + {{ isIdentified() ? 'Yes' : 'No' }} @if (isIdentified()) { - } @else { - } @@ -50,14 +60,14 @@

SDK state

data-tooltip="When ON, entries re-resolve and rerender on profile changes" >Live updates - {{ liveUpdatesService.globalLiveUpdates() ? 'ON' : 'OFF' }} SDK state Active optimizations - {{ optimizationCount() }} + {{ optimizationCount() }}
@@ -97,7 +113,7 @@

SDK state

diff --git a/implementations/web-sdk_angular/src/app/components/entry-card/index.ts b/implementations/web-sdk_angular/src/app/components/entry-card/index.ts index 2ff8322b..ef2866d0 100644 --- a/implementations/web-sdk_angular/src/app/components/entry-card/index.ts +++ b/implementations/web-sdk_angular/src/app/components/entry-card/index.ts @@ -115,6 +115,7 @@ export class EntryCard { readonly manualTracking = input(false) readonly clickScenario = input(undefined) readonly liveUpdates = input(undefined) + readonly testIdPrefix = input(undefined) private readonly sanitizer = inject(DomSanitizer) private readonly liveUpdatesService = inject(NgLiveUpdates) diff --git a/implementations/web-sdk_angular/src/app/components/tracking-log/index.html b/implementations/web-sdk_angular/src/app/components/tracking-log/index.html index ead12748..0dc4f0a8 100644 --- a/implementations/web-sdk_angular/src/app/components/tracking-log/index.html +++ b/implementations/web-sdk_angular/src/app/components/tracking-log/index.html @@ -1,11 +1,18 @@
-

Tracking

- @let events = displayEvents(); @if (events.length === 0) { +

Tracking

+ @let events = displayEvents(); +

Events: {{ events.length }}

+

Raw Events: {{ rawEventsDisplay() }}

+ @if (events.length === 0) {

No events tracked yet

} @else { @for (event of events; track event.key) { - + + } diff --git a/implementations/web-sdk_angular/src/app/components/tracking-log/index.scss b/implementations/web-sdk_angular/src/app/components/tracking-log/index.scss index 1e9c45fc..600d6c6c 100644 --- a/implementations/web-sdk_angular/src/app/components/tracking-log/index.scss +++ b/implementations/web-sdk_angular/src/app/components/tracking-log/index.scss @@ -78,6 +78,13 @@ max-width: 200px; } +.tracking-log__duration { + font-size: 0.7rem; + color: var(--color-text-muted); + white-space: nowrap; + opacity: 0.7; +} + .tracking-log__count { font-size: 0.7rem; font-weight: 600; diff --git a/implementations/web-sdk_angular/src/app/components/tracking-log/index.ts b/implementations/web-sdk_angular/src/app/components/tracking-log/index.ts index b663d16c..504e207d 100644 --- a/implementations/web-sdk_angular/src/app/components/tracking-log/index.ts +++ b/implementations/web-sdk_angular/src/app/components/tracking-log/index.ts @@ -10,6 +10,8 @@ interface AnalyticsEvent { key: string count: number firedAt: number + hoverDurationMs?: number + viewDurationMs?: number } const MS_PER_SECOND = 1000 @@ -34,9 +36,11 @@ export class TrackingLog { private readonly optimization = inject(NgContentfulOptimization) private readonly events = signal>(new Map()) + private readonly rawEventsCount = signal(0) private readonly tick = toSignal(interval(TICK_INTERVAL_SECONDS * MS_PER_SECOND), { initialValue: 0, }) + protected readonly rawEventsDisplay = this.rawEventsCount.asReadonly() protected readonly displayEvents = computed(() => { this.tick() const now = Date.now() @@ -46,27 +50,58 @@ export class TrackingLog { }) constructor() { + let pageSeq = 0 + let componentSeq = 0 const sub = this.optimization.sdk.states.eventStream.subscribe((raw) => { + if (raw != null) { + this.rawEventsCount.update((n) => n + 1) + } switch (raw?.type) { case 'page': { const { properties: { url }, } = raw - this.track({ type: 'page', value: url, key: `page-${url}` }) + pageSeq += 1 + const pathname = (() => { + try { + return new URL(url, window.location.origin).pathname + } catch { + return url + } + })() + this.track({ type: 'page', value: pathname, key: `page-${pageSeq}-${url}` }) break } case 'component': { - const { componentId, viewId } = raw - this.track({ - type: viewId ? 'view' : 'comp', - value: componentId, - key: `component-${componentId}`, - }) + const { componentId, viewId, viewDurationMs } = raw + if (viewId) { + this.track({ + type: 'view', + value: componentId, + key: `view-${viewId}`, + viewDurationMs: typeof viewDurationMs === 'number' ? viewDurationMs : undefined, + }) + } else { + componentSeq += 1 + this.track( + { type: 'comp', value: componentId, key: `component-${componentId}-${componentSeq}` }, + `event-component-${componentId}`, + ) + } break } case 'component_hover': { - const { componentId } = raw - this.track({ type: 'hover', value: componentId, key: `component_hover-${componentId}` }) + const { componentId, hoverId, hoverDurationMs } = raw + if (hoverId) { + this.track({ + type: 'hover', + value: componentId, + key: `component_hover-hover-${hoverId}`, + hoverDurationMs: typeof hoverDurationMs === 'number' ? hoverDurationMs : undefined, + }) + } else { + this.track({ type: 'hover', value: componentId, key: `component_hover-${componentId}` }) + } break } case 'component_click': { @@ -83,13 +118,16 @@ export class TrackingLog { }) } - private track(event: Omit): void { + private track( + event: Omit, + testId?: string, + ): void { const { key } = event this.events.update((map) => { const existing = map.get(key) return new Map(map).set(key, { ...event, - testId: `event-${key}`, + testId: testId ?? `event-${key}`, count: (existing?.count ?? 0) + 1, firedAt: Date.now(), }) diff --git a/implementations/web-sdk_angular/src/app/pages/home/index.html b/implementations/web-sdk_angular/src/app/pages/home/index.html index 23363f3e..85ae0424 100644 --- a/implementations/web-sdk_angular/src/app/pages/home/index.html +++ b/implementations/web-sdk_angular/src/app/pages/home/index.html @@ -13,10 +13,19 @@

Web SDK + Angular

Live updates

@let liveEntry = entryFor(liveUpdatesEntryId); @if (liveEntry) { -
- - - +
+
+

Default (inherits global setting)

+ +
+
+

Always On (liveUpdates=true)

+ +
+
+

Locked (liveUpdates=false)

+ +
} @@ -24,7 +33,7 @@

Live updates

-

Auto-observed

+

Auto Observed Entries

@for (id of autoIds; track id) { @let entry = entryFor(id); @if (entry) { @@ -35,7 +44,7 @@

Auto-observed

-

Manually-observed

+

Manually Observed Entries

@for (id of manualIds; track id) { @let entry = entryFor(id); @if (entry) { diff --git a/implementations/web-sdk_angular/src/app/pages/page-two/index.html b/implementations/web-sdk_angular/src/app/pages/page-two/index.html index bfc61be6..798f20fe 100644 --- a/implementations/web-sdk_angular/src/app/pages/page-two/index.html +++ b/implementations/web-sdk_angular/src/app/pages/page-two/index.html @@ -1,32 +1,35 @@ -@if (entries.isLoading()) { -

Loading entries…

-} @else { - +
+ - + -
- @if (autoEntry(); as entry) { -
-
-

Auto-observed

-
-
- -
-
- } @if (manualEntry(); as entry) { -
-
-

Manually-observed

-
-
- -
-
+ @if (entries.isLoading()) { +

Loading entries…

+ } @else { +
+ @if (autoEntry(); as entry) { +
+
+

Auto-observed

+
+
+ +
+
+ } @if (manualEntry(); as entry) { +
+
+

Manually-observed

+
+
+ +
+
+ } +
}
-} diff --git a/implementations/web-sdk_angular/src/app/pages/page-two/index.ts b/implementations/web-sdk_angular/src/app/pages/page-two/index.ts index a2563ab3..f44c3a45 100644 --- a/implementations/web-sdk_angular/src/app/pages/page-two/index.ts +++ b/implementations/web-sdk_angular/src/app/pages/page-two/index.ts @@ -1,4 +1,5 @@ import { Component, inject } from '@angular/core' +import { RouterLink } from '@angular/router' import { ControlPanel } from '../../components/control-panel' import { EntryCard } from '../../components/entry-card' import { FIXTURES } from '../../fixtures' @@ -10,7 +11,7 @@ const PAGE_TWO_COMPONENT_ID = 'page-two-conversion' @Component({ selector: 'app-page-two', - imports: [EntryCard, ControlPanel], + imports: [EntryCard, ControlPanel, RouterLink], templateUrl: './index.html', host: { style: 'display: contents' }, }) diff --git a/implementations/web-sdk_angular/src/app/services/entry.ts b/implementations/web-sdk_angular/src/app/services/entry.ts index 8d68a617..8fd86ecb 100644 --- a/implementations/web-sdk_angular/src/app/services/entry.ts +++ b/implementations/web-sdk_angular/src/app/services/entry.ts @@ -113,30 +113,22 @@ export function injectContentfulEntry({ return isLive() ? sig() : untracked(sig) } - const variant = computed(() => { - const raw = entry() - return { - raw, - resolved: optimization.sdk.resolveOptimizedEntry( - raw, - liveRead(optimization.selectedOptimizations), - ), - } - }) - const result = computed(() => { - const { raw, resolved } = variant() - const profile = liveRead(optimization.profile) + const raw = entry() let mergeTagResolved: boolean | undefined = undefined - const entry = resolveEntryMergeTags(resolved.entry, (target) => { - const value = profile ? optimization.sdk.getMergeTagValue(target, profile) : undefined - if (value !== undefined) mergeTagResolved = true - else mergeTagResolved ??= false - return value ?? target.fields.nt_fallback - }) + + const resolved = optimization.sdk.resolveOptimizedEntry( + raw, + liveRead(optimization.selectedOptimizations), + ) return { - entry, + entry: resolveEntryMergeTags(resolved.entry, (target) => { + const value = optimization.sdk.getMergeTagValue(target) + if (value !== undefined) mergeTagResolved = true + else mergeTagResolved ??= false + return value ?? target.fields.nt_fallback + }), baselineId: raw.sys.id, entryId: resolved.entry.sys.id, optimizationId: resolved.selectedOptimization?.experienceId, diff --git a/implementations/web-sdk_angular/src/app/services/live-updates.ts b/implementations/web-sdk_angular/src/app/services/live-updates.ts index 55a9aad1..b8dc4418 100644 --- a/implementations/web-sdk_angular/src/app/services/live-updates.ts +++ b/implementations/web-sdk_angular/src/app/services/live-updates.ts @@ -2,6 +2,12 @@ import { computed, inject, Injectable, signal } from '@angular/core' import { fromSdkState } from '../utils' import { NgContentfulOptimization } from './optimization' +function clickPreviewPanelToggle(): void { + const panel = document.querySelector('ctfl-opt-preview-panel') + const btn = panel?.shadowRoot?.querySelector('button.toggle-drawer') + btn?.click() +} + @Injectable({ providedIn: 'root' }) export class NgLiveUpdates { private readonly sdk = inject(NgContentfulOptimization).sdk @@ -20,4 +26,6 @@ export class NgLiveUpdates { toggle(): void { this.globalLiveUpdatesSignal.update((v) => !v) } + + readonly togglePreviewPanel = clickPreviewPanelToggle } diff --git a/implementations/web-sdk_react/package.json b/implementations/web-sdk_react/package.json index 0c57dc0e..5504cee5 100644 --- a/implementations/web-sdk_react/package.json +++ b/implementations/web-sdk_react/package.json @@ -11,18 +11,15 @@ "clean": "rimraf ./.rsdoctor ./.rslib ./dist ./coverage ./playwright-report ./test-results .tsbuildinfo", "preview": "pnpm serve:mocks && rsbuild preview", "serve": "pnpm serve:mocks && pnpm serve:app", - "serve:app": "pnpm build && pm2 start --name web-sdk_react-app \"pnpm preview\"", + "serve:app": "pnpm build && pm2 start --name web-sdk_react-app \"rsbuild preview\"", "serve:app:stop": "pm2 stop web-sdk_react-app && pm2 delete web-sdk_react-app", "serve:mocks": "pm2 start --name web-sdk_react-mocks \"pnpm --dir ../../lib/mocks serve\"", "serve:mocks:stop": "pm2 stop web-sdk_react-mocks && pm2 delete web-sdk_react-mocks", "serve:stop": "pnpm serve:app:stop && pnpm serve:mocks:stop", - "test:e2e": "pnpm serve && playwright test; E2E_RESULT=$?; pnpm serve:stop; exit $E2E_RESULT", - "test:e2e:codegen": "playwright codegen", - "test:e2e:report": "playwright show-report", - "test:e2e:ui": "playwright test --ui", - "implementation:playwright:install": "playwright install", - "implementation:playwright:install-deps": "playwright install-deps", - "implementation:setup:e2e": "pnpm implementation:playwright:install && pnpm implementation:playwright:install-deps", + "test:e2e": "pnpm serve:app && pnpm --dir ../../lib/e2e-web test; E2E_RESULT=$?; pnpm serve:app:stop; exit $E2E_RESULT", + "test:e2e:ui": "pnpm serve:app && pnpm --dir ../../lib/e2e-web test:ui; pnpm serve:app:stop", + "test:e2e:report": "pnpm --dir ../../lib/e2e-web test:report", + "implementation:setup:e2e": "pnpm --dir ../../lib/e2e-web setup:e2e", "test:unit": "echo \"No unit tests necessary\"", "typecheck": "tsc --noEmit" }, @@ -37,7 +34,6 @@ "react-router-dom": "7.14.1" }, "devDependencies": { - "@playwright/test": "1.58.2", "@rsbuild/core": "1.7.3", "@rsbuild/plugin-react": "1.4.5", "@rsdoctor/cli": "1.5.2", diff --git a/implementations/web-sdk_react/playwright.config.mjs b/implementations/web-sdk_react/playwright.config.mjs deleted file mode 100644 index 2bb70679..00000000 --- a/implementations/web-sdk_react/playwright.config.mjs +++ /dev/null @@ -1,83 +0,0 @@ -import { defineConfig, devices } from '@playwright/test' -import dotenv from 'dotenv' -import path from 'path' -import { fileURLToPath } from 'url' - -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) -dotenv.config({ path: path.resolve(__dirname, '.env') }) - -const isCI = Boolean(process.env.CI) - -/** - * See https://playwright.dev/docs/test-configuration. - */ -export default defineConfig({ - testDir: './e2e', - /* Run tests in files in parallel */ - fullyParallel: true, - /* Fail the build on CI if you accidentally left test.only in the source code. */ - forbidOnly: isCI, - /* Retry on CI only */ - retries: isCI ? 2 : 0, - /* Opt out of parallel tests on CI. */ - workers: isCI ? 1 : undefined, - /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: [['html', { open: 'never' }]], - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ - use: { - /* Base URL to use in actions like `await page.goto('/')`. */ - baseURL: 'http://localhost:3000', - - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', - - /* Record video only when retrying a test for the first time. */ - video: 'on-first-retry', - }, - - /* Configure projects for major browsers */ - projects: [ - { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, - }, - - { - name: 'firefox', - use: { ...devices['Desktop Firefox'] }, - }, - - { - name: 'webkit', - use: { ...devices['Desktop Safari'] }, - }, - - /* Test against mobile viewports. */ - // { - // name: 'Mobile Chrome', - // use: { ...devices['Pixel 5'] }, - // }, - // { - // name: 'Mobile Safari', - // use: { ...devices['iPhone 12'] }, - // }, - - /* Test against branded browsers. */ - // { - // name: 'Microsoft Edge', - // use: { ...devices['Desktop Edge'], channel: 'msedge' }, - // }, - // { - // name: 'Google Chrome', - // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, - // }, - ], - - /* Run your local dev server before starting the tests */ - // webServer: { - // command: 'pnpm --filter e2e serve', - // url: 'http://localhost', - // reuseExistingServer: !isCI, - // }, -}) diff --git a/implementations/web-sdk_react/src/components/AnalyticsEventDisplay.tsx b/implementations/web-sdk_react/src/components/AnalyticsEventDisplay.tsx index 1a18b1da..ef4a7e71 100644 --- a/implementations/web-sdk_react/src/components/AnalyticsEventDisplay.tsx +++ b/implementations/web-sdk_react/src/components/AnalyticsEventDisplay.tsx @@ -166,7 +166,12 @@ export function AnalyticsEventDisplay(): JSX.Element { : event.type return ( -
  • +
  • {label}
  • ) diff --git a/implementations/web-sdk_react/src/optimization/hooks/useOptimizationResolver.ts b/implementations/web-sdk_react/src/optimization/hooks/useOptimizationResolver.ts index c717ee0f..b509097e 100644 --- a/implementations/web-sdk_react/src/optimization/hooks/useOptimizationResolver.ts +++ b/implementations/web-sdk_react/src/optimization/hooks/useOptimizationResolver.ts @@ -6,6 +6,7 @@ import type { ResolvedData } from '@contentful/optimization-web/core-sdk' import type { Entry, EntrySkeletonType } from 'contentful' import { useMemo } from 'react' import { useOptimization } from './useOptimization' +import { useOptimizationState } from './useOptimizationState' export interface UseOptimizationResolverResult { resolveEntry: ( @@ -44,6 +45,10 @@ function toStringValue(value: unknown): string { export function useOptimizationResolver(): UseOptimizationResolverResult { const { sdk, isReady } = useOptimization() + // Subscribe to selectedOptimizations so resolveEntry gets a new identity when the + // Experience API responds. Without this, ContentEntry's useMemo would lock in the + // baseline on first render (signal still empty) and never re-resolve on slow browsers. + const { selectedOptimizations } = useOptimizationState(sdk?.states) return useMemo(() => { if (!isReady || sdk === undefined) { @@ -56,12 +61,15 @@ export function useOptimizationResolver(): UseOptimizationResolverResult { return { resolveEntry: ( baselineEntry: Entry, - selectedOptimizations?: SelectedOptimizationArray, + callerSelectedOptimizations?: SelectedOptimizationArray, ): ResolvedData => - sdk.resolveOptimizedEntry(baselineEntry, selectedOptimizations), + sdk.resolveOptimizedEntry( + baselineEntry, + callerSelectedOptimizations ?? selectedOptimizations, + ), getMergeTagValue: (mergeTagEntry: MergeTagEntry): string => toStringValue(sdk.getMergeTagValue(mergeTagEntry)), } - }, [isReady, sdk]) + }, [isReady, sdk, selectedOptimizations]) } diff --git a/implementations/web-sdk_react/src/pages/HomePage.tsx b/implementations/web-sdk_react/src/pages/HomePage.tsx index d05223de..8fe92130 100644 --- a/implementations/web-sdk_react/src/pages/HomePage.tsx +++ b/implementations/web-sdk_react/src/pages/HomePage.tsx @@ -102,7 +102,7 @@ export function HomePage({
    -

    Consent: {String(consent)}

    +

    {String(consent)}

    Selected Optimizations: {selectedOptimizationCount}

    diff --git a/implementations/web-sdk_react/src/pages/PageTwoPage.tsx b/implementations/web-sdk_react/src/pages/PageTwoPage.tsx index 596e86c8..20cbf50d 100644 --- a/implementations/web-sdk_react/src/pages/PageTwoPage.tsx +++ b/implementations/web-sdk_react/src/pages/PageTwoPage.tsx @@ -35,6 +35,9 @@ export function PageTwoPage({ consent, entriesById, isIdentified }: PageTwoPageP return (
    + + Back to Home +

    Page Two

    Demo route for SPA navigation, route context (/page-two), and conversion-style @@ -72,10 +75,6 @@ export function PageTwoPage({ consent, entriesById, isIdentified }: PageTwoPageP Trigger Page Two CTA Event

    - - - Back to Home -
    ) } diff --git a/implementations/web-sdk_react/e2e/displays-identified-user-variants.spec.ts b/lib/e2e-web/e2e/displays-identified-user-variants.spec.ts similarity index 98% rename from implementations/web-sdk_react/e2e/displays-identified-user-variants.spec.ts rename to lib/e2e-web/e2e/displays-identified-user-variants.spec.ts index da15b1f6..7a982205 100644 --- a/implementations/web-sdk_react/e2e/displays-identified-user-variants.spec.ts +++ b/lib/e2e-web/e2e/displays-identified-user-variants.spec.ts @@ -1,13 +1,15 @@ import { expect, test } from '@playwright/test' test.describe('identified user', () => { + test.use({ storageState: { cookies: [], origins: [] } }) + test.beforeEach(async ({ page }) => { await page.goto('/') await page.waitForLoadState('domcontentloaded') await expect(page.getByRole('heading', { name: 'Utilities' })).toBeVisible() await page.getByTestId('consent-button').click() - await expect(page.getByTestId('consent-status')).toHaveText('Consent: true') + await expect(page.getByTestId('consent-status')).toHaveText('true') await page.getByTestId('live-updates-identify-button').click() await expect(page.getByTestId('live-updates-reset-button')).toBeVisible() diff --git a/implementations/web-sdk_react/e2e/displays-unidentified-user-variants.spec.ts b/lib/e2e-web/e2e/displays-unidentified-user-variants.spec.ts similarity index 97% rename from implementations/web-sdk_react/e2e/displays-unidentified-user-variants.spec.ts rename to lib/e2e-web/e2e/displays-unidentified-user-variants.spec.ts index 85390937..bef5fb73 100644 --- a/implementations/web-sdk_react/e2e/displays-unidentified-user-variants.spec.ts +++ b/lib/e2e-web/e2e/displays-unidentified-user-variants.spec.ts @@ -1,6 +1,8 @@ import { expect, test } from '@playwright/test' test.describe('unidentified user', () => { + test.use({ storageState: { cookies: [], origins: [] } }) + test.beforeEach(async ({ page }) => { await page.goto('/') await page.waitForLoadState('domcontentloaded') diff --git a/implementations/web-sdk_react/e2e/entry-click-tracking.spec.ts b/lib/e2e-web/e2e/entry-click-tracking.spec.ts similarity index 100% rename from implementations/web-sdk_react/e2e/entry-click-tracking.spec.ts rename to lib/e2e-web/e2e/entry-click-tracking.spec.ts diff --git a/implementations/web-sdk_react/e2e/entry-hover-tracking.spec.ts b/lib/e2e-web/e2e/entry-hover-tracking.spec.ts similarity index 81% rename from implementations/web-sdk_react/e2e/entry-hover-tracking.spec.ts rename to lib/e2e-web/e2e/entry-hover-tracking.spec.ts index 83500179..06998b89 100644 --- a/implementations/web-sdk_react/e2e/entry-hover-tracking.spec.ts +++ b/lib/e2e-web/e2e/entry-hover-tracking.spec.ts @@ -18,20 +18,6 @@ const hoverScenarios: HoverScenario[] = [ }, ] -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 { await page.getByRole('heading', { name: 'Utilities' }).hover() } @@ -45,8 +31,10 @@ async function readResolvedEntryId(page: Page): Promise { } async function readHoverDurationMs(page: Page, hoverId: string): Promise { - const label = await page.getByTestId(`event-component_hover-hover-${hoverId}`).innerText() - return parseHoverDurationMs(label) + const value = await page + .locator(`[data-testid="event-component_hover-hover-${hoverId}"]`) + .getAttribute('data-hover-duration-ms') + return value !== null ? Number.parseInt(value, 10) : Number.NaN } test.describe('entry hover tracking', () => { @@ -97,7 +85,9 @@ test.describe('entry hover tracking', () => { }) .toBeGreaterThan(baselineCount) - await expect(hoverEvents.first()).toContainText(`Entry/Flag: ${resolvedEntryId}`) + await expect( + page.locator(`[data-testid^="event-component_hover-hover-${resolvedEntryId}"]`), + ).toBeVisible() await movePointerAwayFromEntries(page) } @@ -120,12 +110,12 @@ test.describe('entry hover tracking', () => { await target.scrollIntoViewIfNeeded() await target.hover() - const hoverEvent = page.locator('[data-testid^="event-component_hover-hover-"]').first() + const hoverEvent = page.locator( + `[data-testid^="event-component_hover-hover-${resolvedEntryId}"]`, + ) await expect(hoverEvent).toBeVisible() - await expect(hoverEvent).toContainText(`Entry/Flag: ${resolvedEntryId}`) - const hoverEventTestId = await hoverEvent.getAttribute('data-testid') - const hoverId = parseHoverId(hoverEventTestId) + const hoverId = resolvedEntryId expect(hoverId).toBeTruthy() if (!hoverId) return diff --git a/implementations/web-sdk_react/e2e/events-consent-gating.spec.ts b/lib/e2e-web/e2e/events-consent-gating.spec.ts similarity index 95% rename from implementations/web-sdk_react/e2e/events-consent-gating.spec.ts rename to lib/e2e-web/e2e/events-consent-gating.spec.ts index 80fb3e0c..d2857b10 100644 --- a/implementations/web-sdk_react/e2e/events-consent-gating.spec.ts +++ b/lib/e2e-web/e2e/events-consent-gating.spec.ts @@ -32,7 +32,7 @@ test.describe('consent gating', () => { await expect(pageEvents.first()).toBeVisible() - await page.getByRole('button', { name: 'Accept Consent' }).click() + await page.getByTestId('consent-button').click() await scrollThroughEntries(page) await expect.poll(async () => await viewEvents.count()).toBeGreaterThan(0) diff --git a/implementations/web-sdk_react/e2e/flag-view-tracking.spec.ts b/lib/e2e-web/e2e/flag-view-tracking.spec.ts similarity index 100% rename from implementations/web-sdk_react/e2e/flag-view-tracking.spec.ts rename to lib/e2e-web/e2e/flag-view-tracking.spec.ts diff --git a/implementations/web-sdk_react/e2e/live-updates.spec.ts b/lib/e2e-web/e2e/live-updates.spec.ts similarity index 100% rename from implementations/web-sdk_react/e2e/live-updates.spec.ts rename to lib/e2e-web/e2e/live-updates.spec.ts diff --git a/implementations/web-sdk_react/e2e/navigation-page-events.spec.ts b/lib/e2e-web/e2e/navigation-page-events.spec.ts similarity index 100% rename from implementations/web-sdk_react/e2e/navigation-page-events.spec.ts rename to lib/e2e-web/e2e/navigation-page-events.spec.ts diff --git a/implementations/web-sdk_react/e2e/offline-queue-recovery.spec.ts b/lib/e2e-web/e2e/offline-queue-recovery.spec.ts similarity index 100% rename from implementations/web-sdk_react/e2e/offline-queue-recovery.spec.ts rename to lib/e2e-web/e2e/offline-queue-recovery.spec.ts diff --git a/lib/e2e-web/package.json b/lib/e2e-web/package.json new file mode 100644 index 00000000..b6ddfe40 --- /dev/null +++ b/lib/e2e-web/package.json @@ -0,0 +1,20 @@ +{ + "name": "e2e-web", + "private": true, + "version": "0.0.0", + "description": "Shared Playwright E2E suite for CSR web SDK implementations.", + "license": "MIT", + "scripts": { + "test": "playwright test", + "test:ui": "playwright test --ui", + "test:report": "playwright show-report", + "playwright:install": "playwright install", + "playwright:install-deps": "playwright install-deps", + "setup:e2e": "pnpm playwright:install && pnpm playwright:install-deps", + "test:unit": "echo \"No unit tests necessary\"" + }, + "devDependencies": { + "@playwright/test": "catalog:", + "@types/node": "catalog:" + } +} diff --git a/lib/e2e-web/playwright.config.mjs b/lib/e2e-web/playwright.config.mjs new file mode 100644 index 00000000..70ea933b --- /dev/null +++ b/lib/e2e-web/playwright.config.mjs @@ -0,0 +1,43 @@ +import { defineConfig, devices } from '@playwright/test' + +const isCI = Boolean(process.env.CI) + +export default defineConfig({ + testDir: './e2e', + fullyParallel: true, + forbidOnly: isCI, + retries: isCI ? 2 : 0, + workers: 1, + timeout: 60000, + expect: { timeout: 10_000 }, + reporter: [['html', { open: 'never' }]], + use: { + baseURL: process.env.BASE_URL ?? 'http://localhost:3000', + // 'on-first-retry' never writes traces locally (0 retries) — the UI trace viewer + // hits 404/500 on every snapshot load. Always capture locally so the viewer works. + trace: isCI ? 'on-first-retry' : 'on', + video: 'on-first-retry', + }, + projects: [ + { name: 'chromium', use: { ...devices['Desktop Chrome'] } }, + { name: 'firefox', use: { ...devices['Desktop Firefox'] } }, + { name: 'webkit', use: { ...devices['Desktop Safari'] } }, + ], + webServer: [ + { + name: 'Mocks', + command: 'pnpm --dir ../mocks serve', + url: 'http://localhost:8000/health', + reuseExistingServer: false, + timeout: 20_000, + }, + { + name: 'App', + // No-op command — app is started externally. Required by Playwright to poll the url. + command: 'tail -f /dev/null', + url: process.env.BASE_URL ?? 'http://localhost:3000', + reuseExistingServer: true, + timeout: 60_000, + }, + ], +}) diff --git a/lib/e2e-web/tsconfig.json b/lib/e2e-web/tsconfig.json new file mode 100644 index 00000000..7dbee1af --- /dev/null +++ b/lib/e2e-web/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "dom"], + "module": "ES2022", + "moduleResolution": "bundler", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "types": ["node"] + }, + "include": ["e2e/**/*.ts"] +} diff --git a/lib/mocks/src/server.ts b/lib/mocks/src/server.ts index 2cbad460..f4002bc0 100644 --- a/lib/mocks/src/server.ts +++ b/lib/mocks/src/server.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-magic-numbers -- testing */ import { createServer } from '@mswjs/http-middleware' +import { HttpResponse, http } from 'msw' import { getHandlers as getContentfulHandlers } from './contentful-handlers' import { getHandlers as getExperienceHandlers } from './experience-handlers' import { getHandlers as getInsightsHandlers } from './insights-handlers' @@ -19,6 +20,7 @@ const EXPERIENCE_BASE_URL = const INSIGHTS_BASE_URL = process.env.INSIGHTS_BASE_URL ?? `${BASE_HOST}:${PORT}${INSIGHTS_PATH}` const app = createServer( + http.get('*/health', () => HttpResponse.text('ok')), ...getContentfulHandlers(`*${CONTENTFUL_PATH}`), ...getExperienceHandlers(`*${EXPERIENCE_PATH}`), ...getInsightsHandlers(`*${INSIGHTS_PATH}`), diff --git a/package.json b/package.json index 01477fe5..ff27dfbd 100644 --- a/package.json +++ b/package.json @@ -38,8 +38,8 @@ "notices:generate:npm": "bash ./scripts/generate-third-party-notices.sh npm", "notices:generate:swift": "bash ./scripts/generate-third-party-notices.sh swift", "pack:pkgs": "bash ./scripts/pack-pkgs.sh", - "playwright:install": "pnpm run implementation:run -- --all -- implementation:playwright:install", - "playwright:install-deps": "pnpm run implementation:run -- --all -- implementation:playwright:install-deps", + "playwright:install": "pnpm run implementation:run -- --all -- implementation:playwright:install && pnpm --dir lib/e2e-web run playwright:install", + "playwright:install-deps": "pnpm run implementation:run -- --all -- implementation:playwright:install-deps && pnpm --dir lib/e2e-web run playwright:install-deps", "pm2:delete:all": "pm2 delete all", "pm2:list": "pm2 list", "pm2:logs": "pm2 logs", @@ -52,6 +52,7 @@ "setup:e2e:node-sdk+web-sdk": "pnpm run build:pkgs && pnpm run implementation:run -- node-sdk+web-sdk implementation:install && pnpm run implementation:run -- node-sdk+web-sdk implementation:setup:e2e", "setup:e2e:react-native-sdk": "pnpm run build:pkgs && pnpm run implementation:run -- react-native-sdk implementation:install && pnpm run implementation:run -- react-native-sdk implementation:setup:e2e", "setup:e2e:react-web-sdk": "pnpm run build:pkgs && pnpm run implementation:run -- react-web-sdk implementation:install && pnpm run implementation:run -- react-web-sdk implementation:setup:e2e", + "setup:e2e:web-sdk_angular": "pnpm run build:pkgs && pnpm run implementation:run -- web-sdk_angular implementation:install && pnpm run implementation:run -- web-sdk_angular implementation:setup:e2e", "setup:e2e:web-sdk_react": "pnpm run build:pkgs && pnpm run implementation:run -- web-sdk_react implementation:install && pnpm run implementation:run -- web-sdk_react implementation:setup:e2e", "setup:e2e:web-sdk": "pnpm run build:pkgs && pnpm run implementation:run -- web-sdk implementation:install && pnpm run implementation:run -- web-sdk implementation:setup:e2e", "test:e2e": "pnpm run setup:e2e && pnpm run implementation:run -- --all -- implementation:test:e2e:run", @@ -60,6 +61,7 @@ "test:e2e:node-sdk+web-sdk": "pnpm run setup:e2e:node-sdk+web-sdk && pnpm run implementation:run -- node-sdk+web-sdk implementation:test:e2e:run", "test:e2e:react-native-sdk": "pnpm run setup:e2e:react-native-sdk && pnpm run implementation:run -- react-native-sdk implementation:test:e2e:run", "test:e2e:react-web-sdk": "pnpm run setup:e2e:react-web-sdk && pnpm run implementation:run -- react-web-sdk implementation:test:e2e:run", + "test:e2e:web-sdk_angular": "pnpm run setup:e2e:web-sdk_angular && pnpm run implementation:run -- web-sdk_angular implementation:test:e2e:run", "test:e2e:web-sdk_react": "pnpm run setup:e2e:web-sdk_react && pnpm run implementation:run -- web-sdk_react implementation:test:e2e:run", "test:e2e:web-sdk": "pnpm run setup:e2e:web-sdk && pnpm run implementation:run -- web-sdk implementation:test:e2e:run", "size:check": "pnpm --filter @contentful/* --stream size:check", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ea50e160..7b248c11 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -83,7 +83,7 @@ catalogs: version: 4.3.6 overrides: - '@playwright/test': ^1.57.0 + '@playwright/test': 1.61.0 '@types/node': ^24.0.13 typescript: ^5.8.3 vitest: ^3.2.4 @@ -180,6 +180,15 @@ importers: specifier: ^5.8.3 version: 5.9.3 + lib/e2e-web: + devDependencies: + '@playwright/test': + specifier: 1.61.0 + version: 1.61.0 + '@types/node': + specifier: ^24.0.13 + version: 24.10.13 + lib/mocks: dependencies: '@contentful/optimization-api-schemas': @@ -663,7 +672,7 @@ importers: version: 20.8.9 next: specifier: ^16.2.6 - version: 16.2.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + version: 16.2.6(@playwright/test@1.61.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) react-router-dom: specifier: ^7.14.2 version: 7.14.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -2089,6 +2098,11 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@playwright/test@1.61.0': + resolution: {integrity: sha512-cKA5B6lpFEMyMGjxF54QihfYpB4FkEGH+qZhtArDEG+wezQAJY8Pq6C7T1SjWz+FFzt3TbyoXBQYk/0292TdJA==} + engines: {node: '>=18'} + hasBin: true + '@pm2/agent@2.1.1': resolution: {integrity: sha512-0V9ckHWd/HSC8BgAbZSoq8KXUG81X97nSkAxmhKDhmF8vanyaoc1YXwc2KVkbWz82Rg4gjd2n9qiT3i7bdvGrQ==} @@ -4543,6 +4557,11 @@ packages: fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -5975,7 +5994,7 @@ packages: hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 - '@playwright/test': ^1.57.0 + '@playwright/test': 1.61.0 babel-plugin-react-compiler: '*' react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 @@ -6316,6 +6335,16 @@ packages: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} + playwright-core@1.61.0: + resolution: {integrity: sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.61.0: + resolution: {integrity: sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ==} + engines: {node: '>=18'} + hasBin: true + pm2-axon-rpc@0.7.1: resolution: {integrity: sha512-FbLvW60w+vEyvMjP/xom2UPhUN/2bVpdtLfKJeYM3gwzYhoTEEChCOICfFzxkxuoEleOlnpjie+n1nue91bDQw==} engines: {node: '>=5'} @@ -9111,6 +9140,10 @@ snapshots: '@pkgr/core@0.2.9': {} + '@playwright/test@1.61.0': + dependencies: + playwright: 1.61.0 + '@pm2/agent@2.1.1': dependencies: async: 3.2.6 @@ -12368,6 +12401,9 @@ snapshots: fs.realpath@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -14262,7 +14298,7 @@ snapshots: netmask@2.0.2: {} - next@16.2.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + next@16.2.6(@playwright/test@1.61.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: '@next/env': 16.2.6 '@swc/helpers': 0.5.15 @@ -14281,6 +14317,7 @@ snapshots: '@next/swc-linux-x64-musl': 16.2.6 '@next/swc-win32-arm64-msvc': 16.2.6 '@next/swc-win32-x64-msvc': 16.2.6 + '@playwright/test': 1.61.0 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' @@ -14593,6 +14630,14 @@ snapshots: dependencies: find-up: 4.1.0 + playwright-core@1.61.0: {} + + playwright@1.61.0: + dependencies: + playwright-core: 1.61.0 + optionalDependencies: + fsevents: 2.3.2 + pm2-axon-rpc@0.7.1: dependencies: debug: 4.4.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f355365f..eb31478b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -20,7 +20,7 @@ catalog: '@contentful/rich-text-html-renderer': ^17.1.6 '@contentful/rich-text-types': ^17.2.5 '@microsoft/api-extractor': ^7.57.7 - '@playwright/test': ^1.57.0 + '@playwright/test': 1.61.0 '@preact/signals-core': ^1.13.0 '@rsbuild/core': ^1.7.3 '@rstest/core': ^0.8.5
    {{ event.timeAgo }} Tracking > {{ event.value }} + @if (event.hoverDurationMs !== undefined) { {{ event.hoverDurationMs }}ms } @else if + (event.viewDurationMs !== undefined) { {{ event.viewDurationMs }}ms } + @if (event.count > 1) { Ă—{{ event.count }} }