diff --git a/integration-test/playwright/breakage-reporting.spec.js b/integration-test/playwright/breakage-reporting.spec.js new file mode 100644 index 000000000..e9d953966 --- /dev/null +++ b/integration-test/playwright/breakage-reporting.spec.js @@ -0,0 +1,126 @@ +import { test, expect } from '@playwright/test' +import { readFileSync } from 'fs' +import { + mockWindowsMessaging, + readOutgoingMessages, simulateSubscriptionMessage, waitForCallCount, + wrapWindowsScripts +} from '@duckduckgo/messaging/lib/test-utils.mjs' +import { perPlatform } from './type-helpers.mjs' + +test('Breakage Reporting Feature', async ({ page }, testInfo) => { + const breakageFeature = BreakageReportingSpec.create(page, testInfo) + await breakageFeature.enabled() + await breakageFeature.navigate() + + await page.evaluate(simulateSubscriptionMessage, { + messagingContext: { + context: 'contentScopeScripts', + featureName: 'breakageReporting', + env: 'development' + }, + name: 'getBreakageReportValues', + payload: {}, + injectName: breakageFeature.build.name + }) + + await page.waitForFunction(waitForCallCount, { + method: 'breakageReportResult', + count: 1 + }, { timeout: 5000, polling: 100 }) + const calls = await page.evaluate(readOutgoingMessages) + expect(calls.length).toBe(1) + + const result = calls[0].payload.params + expect(result.jsPerformance.length).toBe(1) + expect(result.jsPerformance[0]).toBeGreaterThan(0) + expect(result.referrer).toBe('http://localhost:3220/breakage-reporting/index.html') +}) + +export class BreakageReportingSpec { + htmlPage = '/breakage-reporting/index.html' + config = './integration-test/test-pages/breakage-reporting/config/config.json' + /** + * @param {import("@playwright/test").Page} page + * @param {import("./type-helpers.mjs").Build} build + * @param {import("./type-helpers.mjs").PlatformInfo} platform + */ + constructor (page, build, platform) { + this.page = page + this.build = build + this.platform = platform + } + + async enabled () { + const config = JSON.parse(readFileSync(this.config, 'utf8')) + await this.setup({ config }) + } + + async navigate () { + await this.page.goto(this.htmlPage) + + await this.page.evaluate(() => { + window.location.href = '/breakage-reporting/pages/ref.html' + }) + await this.page.waitForURL('**/ref.html') + + // Wait for first paint event to ensure we can get the performance metrics + await this.page.evaluate(() => { + const response = new Promise((resolve) => { + const observer = new PerformanceObserver((list) => { + list.getEntries().forEach((entry) => { + if (entry.name === 'first-paint') { + observer.disconnect() + // @ts-expect-error - error TS2810: Expected 1 argument, but got 0. 'new Promise()' needs a JSDoc hint to produce a 'resolve' that can be called without arguments. + resolve() + } + }) + }) + + observer.observe({ type: 'paint', buffered: true }) + }) + return response + }) + } + + /** + * @param {object} params + * @param {Record} params.config + * @return {Promise} + */ + async setup (params) { + const { config } = params + + // read the built file from disk and do replacements + const injectedJS = wrapWindowsScripts(this.build.artifact, { + $CONTENT_SCOPE$: config, + $USER_UNPROTECTED_DOMAINS$: [], + $USER_PREFERENCES$: { + platform: { name: 'windows' }, + debug: true + } + }) + + await this.page.addInitScript(mockWindowsMessaging, { + messagingContext: { + env: 'development', + context: 'contentScopeScripts', + featureName: 'n/a' + }, + responses: {} + }) + + // attach the JS + await this.page.addInitScript(injectedJS) + } + + /** + * Helper for creating an instance per platform + * @param {import("@playwright/test").Page} page + * @param {import("@playwright/test").TestInfo} testInfo + */ + static create (page, testInfo) { + // Read the configuration object to determine which platform we're testing against + const { platformInfo, build } = perPlatform(testInfo.project.use) + return new BreakageReportingSpec(page, build, platformInfo) + } +} diff --git a/integration-test/test-pages/breakage-reporting/config/config.json b/integration-test/test-pages/breakage-reporting/config/config.json new file mode 100644 index 000000000..050d5b795 --- /dev/null +++ b/integration-test/test-pages/breakage-reporting/config/config.json @@ -0,0 +1,9 @@ +{ + "unprotectedTemporary": [], + "features": { + "breakageReporting": { + "state": "enabled", + "exceptions": [] + } + } +} diff --git a/integration-test/test-pages/breakage-reporting/index.html b/integration-test/test-pages/breakage-reporting/index.html new file mode 100644 index 000000000..2e6241264 --- /dev/null +++ b/integration-test/test-pages/breakage-reporting/index.html @@ -0,0 +1,11 @@ + + + + + + Breakage Reporting + + +

Breakage Reporting

+ + diff --git a/integration-test/test-pages/breakage-reporting/pages/ref.html b/integration-test/test-pages/breakage-reporting/pages/ref.html new file mode 100644 index 000000000..d9fb62787 --- /dev/null +++ b/integration-test/test-pages/breakage-reporting/pages/ref.html @@ -0,0 +1,11 @@ + + + + + + Breakage Reporting with Referrer + + +

Breakage Reporting with Referrer

+ + diff --git a/playwright.config.js b/playwright.config.js index 7bc1696e3..a72927594 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -9,7 +9,8 @@ export default defineConfig({ 'integration-test/playwright/duckplayer-remote-config.spec.js', 'integration-test/playwright/harmful-apis.spec.js', 'integration-test/playwright/windows-permissions.spec.js', - 'integration-test/playwright/broker-protection.spec.js' + 'integration-test/playwright/broker-protection.spec.js', + 'integration-test/playwright/breakage-reporting.spec.js' ], use: { injectName: 'windows', platform: 'windows' } }, diff --git a/src/features.js b/src/features.js index 3394fa015..dccd74291 100644 --- a/src/features.js +++ b/src/features.js @@ -22,7 +22,8 @@ const otherFeatures = /** @type {const} */([ 'webCompat', 'windowsPermissionUsage', 'brokerProtection', - 'performanceMetrics' + 'performanceMetrics', + 'breakageReporting' ]) /** @typedef {baseFeatures[number]|otherFeatures[number]} FeatureName */ @@ -48,7 +49,8 @@ export const platformSupport = { ...baseFeatures, 'windowsPermissionUsage', 'duckPlayer', - 'brokerProtection' + 'brokerProtection', + 'breakageReporting' ], firefox: [ 'cookie', diff --git a/src/features/breakage-reporting.js b/src/features/breakage-reporting.js new file mode 100644 index 000000000..5b592fc2c --- /dev/null +++ b/src/features/breakage-reporting.js @@ -0,0 +1,16 @@ +import ContentFeature from '../content-feature' +import { getJsPerformanceMetrics } from './breakage-reporting/utils.js' + +export default class BreakageReporting extends ContentFeature { + init () { + this.messaging.subscribe('getBreakageReportValues', () => { + const jsPerformance = getJsPerformanceMetrics() + const referrer = document.referrer + + this.messaging.notify('breakageReportResult', { + jsPerformance, + referrer + }) + }) + } +} diff --git a/src/features/breakage-reporting/utils.js b/src/features/breakage-reporting/utils.js new file mode 100644 index 000000000..fee549f3b --- /dev/null +++ b/src/features/breakage-reporting/utils.js @@ -0,0 +1,8 @@ +/** + * @returns array of performance metrics + */ +export function getJsPerformanceMetrics () { + const paintResources = performance.getEntriesByType('paint') + const firstPaint = paintResources.find((entry) => entry.name === 'first-contentful-paint') + return firstPaint ? [firstPaint.startTime] : [] +} diff --git a/src/features/performance-metrics.js b/src/features/performance-metrics.js index 78988c317..865096392 100644 --- a/src/features/performance-metrics.js +++ b/src/features/performance-metrics.js @@ -1,11 +1,10 @@ import ContentFeature from '../content-feature' +import { getJsPerformanceMetrics } from './breakage-reporting/utils.js' export default class PerformanceMetrics extends ContentFeature { init () { this.messaging.subscribe('getVitals', () => { - const paintResources = performance.getEntriesByType('paint') - const firstPaint = paintResources.find((entry) => entry.name === 'first-contentful-paint') - const vitals = firstPaint ? [firstPaint.startTime] : [] + const vitals = getJsPerformanceMetrics() this.messaging.notify('vitalsResult', { vitals }) }) }