Skip to content

Commit

Permalink
Update signal integration tests (#1154)
Browse files Browse the repository at this point in the history
  • Loading branch information
silesky authored Sep 25, 2024
1 parent 3f58366 commit 567359f
Show file tree
Hide file tree
Showing 7 changed files with 261 additions and 195 deletions.
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
import { CDNSettingsBuilder } from '@internal/test-helpers'
import { Page, Request, Route } from '@playwright/test'
import { Page, Request } from '@playwright/test'
import { logConsole } from './log-console'
import { SegmentEvent } from '@segment/analytics-next'
import { Signal, SignalsPluginSettingsConfig } from '@segment/analytics-signals'

type FulfillOptions = Parameters<Route['fulfill']>['0']
import { PageNetworkUtils, SignalAPIRequestBuffer } from './network-utils'

export class BasePage {
protected page!: Page
static defaultTestApiURL = 'http://localhost:5432/api/foo'
public lastSignalsApiReq!: Request
public signalsApiReqs: SegmentEvent[] = []
public signalsAPI = new SignalAPIRequestBuffer()
public lastTrackingApiReq!: Request
public trackingApiReqs: SegmentEvent[] = []

public url: string
public edgeFnDownloadURL = 'https://cdn.edgefn.segment.com/MY-WRITEKEY/foo.js'
public edgeFn!: string
public network!: PageNetworkUtils

constructor(path: string) {
this.url = 'http://localhost:5432/src/tests' + path
Expand Down Expand Up @@ -46,6 +43,7 @@ export class BasePage {
) {
logConsole(page)
this.page = page
this.network = new PageNetworkUtils(page)
this.edgeFn = edgeFn
await this.setupMockedRoutes()
await this.page.goto(this.url)
Expand Down Expand Up @@ -89,10 +87,8 @@ export class BasePage {

private async setupMockedRoutes() {
// clear any existing saved requests
this.signalsApiReqs = []
this.trackingApiReqs = []
this.lastSignalsApiReq = undefined as any as Request
this.lastTrackingApiReq = undefined as any as Request
this.signalsAPI.clear()

await Promise.all([
this.mockSignalsApi(),
Expand Down Expand Up @@ -126,8 +122,7 @@ export class BasePage {
await this.page.route(
'https://signals.segment.io/v1/*',
(route, request) => {
this.lastSignalsApiReq = request
this.signalsApiReqs.push(request.postDataJSON())
this.signalsAPI.addRequest(request)
if (request.method().toLowerCase() !== 'post') {
throw new Error(`Unexpected method: ${request.method()}`)
}
Expand Down Expand Up @@ -196,75 +191,6 @@ export class BasePage {
)
}

async mockTestRoute(
url = BasePage.defaultTestApiURL,
response?: Partial<FulfillOptions>
) {
if (url.startsWith('/')) {
url = new URL(url, this.page.url()).href
}
await this.page.route(url, (route) => {
return route.fulfill({
contentType: 'application/json',
status: 200,
body: JSON.stringify({ someResponse: 'yep' }),
...response,
})
})
}

async makeFetchCall(
url = BasePage.defaultTestApiURL,
request?: Partial<RequestInit>
): Promise<void> {
let normalizeUrl = url
if (url.startsWith('/')) {
normalizeUrl = new URL(url, this.page.url()).href
}
const req = this.page.waitForResponse(normalizeUrl ?? url)
await this.page.evaluate(
({ url, request }) => {
return fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ foo: 'bar' }),
...request,
}).catch(console.error)
},
{ url, request }
)
await req
}

async makeXHRCall(
url = BasePage.defaultTestApiURL,
request: Partial<{
method: string
body: any
contentType: string
responseType: XMLHttpRequestResponseType
}> = {}
): Promise<void> {
let normalizeUrl = url
if (url.startsWith('/')) {
normalizeUrl = new URL(url, this.page.url()).href
}
const req = this.page.waitForResponse(normalizeUrl ?? url)
await this.page.evaluate(
({ url, body, contentType, method, responseType }) => {
const xhr = new XMLHttpRequest()
xhr.open(method ?? 'POST', url)
xhr.responseType = responseType ?? 'json'
xhr.setRequestHeader('Content-Type', contentType ?? 'application/json')
xhr.send(body || JSON.stringify({ foo: 'bar' }))
},
{ url, ...request }
)
await req
}

waitForSignalsApiFlush(timeout = 5000) {
return this.page.waitForResponse('https://signals.segment.io/v1/*', {
timeout,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { Page, Route, Request } from '@playwright/test'
import { SegmentEvent } from '@segment/analytics-next'

type FulfillOptions = Parameters<Route['fulfill']>['0']
export interface XHRRequestOptions {
method?: string
body?: any
contentType?: string
responseType?: XMLHttpRequestResponseType
responseLatency?: number
}
export class PageNetworkUtils {
private defaultTestApiURL = 'http://localhost:5432/api/foo'
private defaultResponseTimeout = 3000
constructor(public page: Page) {}

async makeXHRCall(
url = this.defaultTestApiURL,
reqOptions: XHRRequestOptions = {}
): Promise<void> {
let normalizeUrl = url
if (url.startsWith('/')) {
normalizeUrl = new URL(url, this.page.url()).href
}
const req = this.page.waitForResponse(normalizeUrl ?? url, {
timeout: this.defaultResponseTimeout,
})
await this.page.evaluate(
(args) => {
const xhr = new XMLHttpRequest()
xhr.open(args.method ?? 'POST', args.url)
xhr.responseType = args.responseType ?? 'json'
xhr.setRequestHeader(
'Content-Type',
args.contentType ?? 'application/json'
)
if (typeof args.responseLatency === 'number') {
xhr.setRequestHeader(
'x-test-latency',
args.responseLatency.toString()
)
}
xhr.send(args.body || JSON.stringify({ foo: 'bar' }))
},
{ url, ...reqOptions }
)
await req
}
/**
* Make a fetch call in the page context. By default it will POST a JSON object with {foo: 'bar'}
*/
async makeFetchCall(
url = this.defaultTestApiURL,
request: Partial<RequestInit> = {}
): Promise<void> {
let normalizeUrl = url
if (url.startsWith('/')) {
normalizeUrl = new URL(url, this.page.url()).href
}
const req = this.page.waitForResponse(normalizeUrl ?? url, {
timeout: this.defaultResponseTimeout,
})
await this.page.evaluate(
(args) => {
return fetch(args.url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ foo: 'bar' }),
...args.request,
})
.then(console.log)
.catch(console.error)
},
{ url, request }
)
await req
}

async mockTestRoute(
url = this.defaultTestApiURL,
response?: Partial<FulfillOptions>
) {
if (url.startsWith('/')) {
url = new URL(url, this.page.url()).href
}
await this.page.route(url, async (route) => {
const latency = this.extractLatency(route)

// if a custom latency is set in the request headers, use that instead

await new Promise((resolve) => setTimeout(resolve, latency))
return route.fulfill({
contentType: 'application/json',
status: 200,
body: JSON.stringify({ someResponse: 'yep' }),
...response,
})
})
}
private extractLatency(route: Route) {
let latency = 0
if (route.request().headers()['x-test-latency']) {
const customLatency = parseInt(
route.request().headers()['x-test-latency']
)
if (customLatency) {
latency = customLatency
}
}
return latency
}
}

class SegmentAPIRequestBuffer {
private requests: Request[] = []
public lastEvent() {
return this.getEvents()[this.getEvents.length - 1]
}
public getEvents(): SegmentEvent[] {
return this.requests.flatMap((req) => req.postDataJSON().batch)
}

clear() {
this.requests = []
}
addRequest(request: Request) {
if (request.method().toLowerCase() !== 'post') {
throw new Error(
`Unexpected method: ${request.method()}, Tracking API only accepts POST`
)
}
this.requests.push(request)
}
}

export class SignalAPIRequestBuffer extends SegmentAPIRequestBuffer {
/**
* @example 'network', 'interaction', 'navigation', etc
*/
override getEvents(signalType?: string): SegmentEvent[] {
if (signalType) {
return this.getEvents().filter((e) => e.properties!.type === signalType)
}
return super.getEvents()
}

override lastEvent(signalType?: string | undefined): SegmentEvent {
if (signalType) {
const res =
this.getEvents(signalType)[this.getEvents(signalType).length - 1]
if (!res) {
throw new Error(`No signal of type ${signalType} found`)
}
return res
}
return super.lastEvent()
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { test, expect } from '@playwright/test'
import type { SegmentEvent } from '@segment/analytics-next'
import { IndexPage } from './index-page'

const indexPage = new IndexPage()
Expand All @@ -21,14 +20,10 @@ test('network signals', async () => {
/**
* Make a fetch call, see if it gets sent to the signals endpoint
*/
await indexPage.mockTestRoute()
await indexPage.makeFetchCall()
await indexPage.network.mockTestRoute()
await indexPage.network.makeFetchCall()
await indexPage.waitForSignalsApiFlush()
const batch = indexPage.lastSignalsApiReq.postDataJSON()
.batch as SegmentEvent[]
const networkEvents = batch.filter(
(el: SegmentEvent) => el.properties!.type === 'network'
)
const networkEvents = indexPage.signalsAPI.getEvents('network')
const requests = networkEvents.filter(
(el) => el.properties!.data.action === 'request'
)
Expand All @@ -46,14 +41,10 @@ test('network signals xhr', async () => {
/**
* Make a fetch call, see if it gets sent to the signals endpoint
*/
await indexPage.mockTestRoute()
await indexPage.makeXHRCall()
await indexPage.network.mockTestRoute()
await indexPage.network.makeXHRCall()
await indexPage.waitForSignalsApiFlush()
const batch = indexPage.lastSignalsApiReq.postDataJSON()
.batch as SegmentEvent[]
const networkEvents = batch.filter(
(el: SegmentEvent) => el.properties!.type === 'network'
)
const networkEvents = indexPage.signalsAPI.getEvents('network')
expect(networkEvents).toHaveLength(2)
const requests = networkEvents.filter(
(el) => el.properties!.data.action === 'request'
Expand All @@ -77,17 +68,14 @@ test('instrumentation signals', async () => {
indexPage.waitForSignalsApiFlush(),
])

const signalReqJSON = indexPage.lastSignalsApiReq.postDataJSON()

const isoDateRegEx = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
const instrumentationEvents = signalReqJSON.batch.filter(
(el: SegmentEvent) => el.properties!.type === 'instrumentation'
)
const instrumentationEvents =
indexPage.signalsAPI.getEvents('instrumentation')
expect(instrumentationEvents).toHaveLength(1)
const ev = instrumentationEvents[0]
expect(ev.event).toBe('Segment Signal Generated')
expect(ev.type).toBe('track')
const rawEvent = ev.properties.data.rawEvent
const rawEvent = ev.properties!.data.rawEvent
expect(rawEvent).toMatchObject({
type: 'page',
anonymousId: expect.any(String),
Expand All @@ -107,10 +95,7 @@ test('interaction signals', async () => {
indexPage.waitForTrackingApiFlush(),
])

const signalsReqJSON = indexPage.lastSignalsApiReq.postDataJSON()
const interactionSignals = signalsReqJSON.batch.filter(
(el: SegmentEvent) => el.properties!.type === 'interaction'
)
const interactionSignals = indexPage.signalsAPI.getEvents('interaction')
expect(interactionSignals).toHaveLength(1)
const data = {
eventType: 'click',
Expand Down Expand Up @@ -163,12 +148,8 @@ test('navigation signals', async ({ page }) => {
{
// on page load, a navigation signal should be sent
await indexPage.waitForSignalsApiFlush()
const signalReqJSON = indexPage.lastSignalsApiReq.postDataJSON()
const navigationEvents = signalReqJSON.batch.filter(
(el: SegmentEvent) => el.properties!.type === 'navigation'
)
expect(navigationEvents).toHaveLength(1)
const ev = navigationEvents[0]
expect(indexPage.signalsAPI.getEvents()).toHaveLength(1)
const ev = indexPage.signalsAPI.lastEvent('navigation')
expect(ev.properties).toMatchObject({
type: 'navigation',
data: {
Expand All @@ -188,13 +169,8 @@ test('navigation signals', async ({ page }) => {
window.location.hash = '#foo'
})
await indexPage.waitForSignalsApiFlush()
const signalReqJSON = indexPage.lastSignalsApiReq.postDataJSON()

const navigationEvents = signalReqJSON.batch.filter(
(el: SegmentEvent) => el.properties!.type === 'navigation'
)
expect(navigationEvents).toHaveLength(1)
const ev = navigationEvents[0]
expect(indexPage.signalsAPI.getEvents()).toHaveLength(2)
const ev = indexPage.signalsAPI.lastEvent('navigation')
expect(ev.properties).toMatchObject({
index: expect.any(Number),
type: 'navigation',
Expand Down
Loading

0 comments on commit 567359f

Please sign in to comment.