Skip to content

Commit 567359f

Browse files
authored
Update signal integration tests (#1154)
1 parent 3f58366 commit 567359f

File tree

7 files changed

+261
-195
lines changed

7 files changed

+261
-195
lines changed

packages/signals/signals-integration-tests/src/helpers/base-page-object.ts

+7-81
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,19 @@
11
import { CDNSettingsBuilder } from '@internal/test-helpers'
2-
import { Page, Request, Route } from '@playwright/test'
2+
import { Page, Request } from '@playwright/test'
33
import { logConsole } from './log-console'
44
import { SegmentEvent } from '@segment/analytics-next'
55
import { Signal, SignalsPluginSettingsConfig } from '@segment/analytics-signals'
6-
7-
type FulfillOptions = Parameters<Route['fulfill']>['0']
6+
import { PageNetworkUtils, SignalAPIRequestBuffer } from './network-utils'
87

98
export class BasePage {
109
protected page!: Page
11-
static defaultTestApiURL = 'http://localhost:5432/api/foo'
12-
public lastSignalsApiReq!: Request
13-
public signalsApiReqs: SegmentEvent[] = []
10+
public signalsAPI = new SignalAPIRequestBuffer()
1411
public lastTrackingApiReq!: Request
1512
public trackingApiReqs: SegmentEvent[] = []
16-
1713
public url: string
1814
public edgeFnDownloadURL = 'https://cdn.edgefn.segment.com/MY-WRITEKEY/foo.js'
1915
public edgeFn!: string
16+
public network!: PageNetworkUtils
2017

2118
constructor(path: string) {
2219
this.url = 'http://localhost:5432/src/tests' + path
@@ -46,6 +43,7 @@ export class BasePage {
4643
) {
4744
logConsole(page)
4845
this.page = page
46+
this.network = new PageNetworkUtils(page)
4947
this.edgeFn = edgeFn
5048
await this.setupMockedRoutes()
5149
await this.page.goto(this.url)
@@ -89,10 +87,8 @@ export class BasePage {
8987

9088
private async setupMockedRoutes() {
9189
// clear any existing saved requests
92-
this.signalsApiReqs = []
9390
this.trackingApiReqs = []
94-
this.lastSignalsApiReq = undefined as any as Request
95-
this.lastTrackingApiReq = undefined as any as Request
91+
this.signalsAPI.clear()
9692

9793
await Promise.all([
9894
this.mockSignalsApi(),
@@ -126,8 +122,7 @@ export class BasePage {
126122
await this.page.route(
127123
'https://signals.segment.io/v1/*',
128124
(route, request) => {
129-
this.lastSignalsApiReq = request
130-
this.signalsApiReqs.push(request.postDataJSON())
125+
this.signalsAPI.addRequest(request)
131126
if (request.method().toLowerCase() !== 'post') {
132127
throw new Error(`Unexpected method: ${request.method()}`)
133128
}
@@ -196,75 +191,6 @@ export class BasePage {
196191
)
197192
}
198193

199-
async mockTestRoute(
200-
url = BasePage.defaultTestApiURL,
201-
response?: Partial<FulfillOptions>
202-
) {
203-
if (url.startsWith('/')) {
204-
url = new URL(url, this.page.url()).href
205-
}
206-
await this.page.route(url, (route) => {
207-
return route.fulfill({
208-
contentType: 'application/json',
209-
status: 200,
210-
body: JSON.stringify({ someResponse: 'yep' }),
211-
...response,
212-
})
213-
})
214-
}
215-
216-
async makeFetchCall(
217-
url = BasePage.defaultTestApiURL,
218-
request?: Partial<RequestInit>
219-
): Promise<void> {
220-
let normalizeUrl = url
221-
if (url.startsWith('/')) {
222-
normalizeUrl = new URL(url, this.page.url()).href
223-
}
224-
const req = this.page.waitForResponse(normalizeUrl ?? url)
225-
await this.page.evaluate(
226-
({ url, request }) => {
227-
return fetch(url, {
228-
method: 'POST',
229-
headers: {
230-
'Content-Type': 'application/json',
231-
},
232-
body: JSON.stringify({ foo: 'bar' }),
233-
...request,
234-
}).catch(console.error)
235-
},
236-
{ url, request }
237-
)
238-
await req
239-
}
240-
241-
async makeXHRCall(
242-
url = BasePage.defaultTestApiURL,
243-
request: Partial<{
244-
method: string
245-
body: any
246-
contentType: string
247-
responseType: XMLHttpRequestResponseType
248-
}> = {}
249-
): Promise<void> {
250-
let normalizeUrl = url
251-
if (url.startsWith('/')) {
252-
normalizeUrl = new URL(url, this.page.url()).href
253-
}
254-
const req = this.page.waitForResponse(normalizeUrl ?? url)
255-
await this.page.evaluate(
256-
({ url, body, contentType, method, responseType }) => {
257-
const xhr = new XMLHttpRequest()
258-
xhr.open(method ?? 'POST', url)
259-
xhr.responseType = responseType ?? 'json'
260-
xhr.setRequestHeader('Content-Type', contentType ?? 'application/json')
261-
xhr.send(body || JSON.stringify({ foo: 'bar' }))
262-
},
263-
{ url, ...request }
264-
)
265-
await req
266-
}
267-
268194
waitForSignalsApiFlush(timeout = 5000) {
269195
return this.page.waitForResponse('https://signals.segment.io/v1/*', {
270196
timeout,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import { Page, Route, Request } from '@playwright/test'
2+
import { SegmentEvent } from '@segment/analytics-next'
3+
4+
type FulfillOptions = Parameters<Route['fulfill']>['0']
5+
export interface XHRRequestOptions {
6+
method?: string
7+
body?: any
8+
contentType?: string
9+
responseType?: XMLHttpRequestResponseType
10+
responseLatency?: number
11+
}
12+
export class PageNetworkUtils {
13+
private defaultTestApiURL = 'http://localhost:5432/api/foo'
14+
private defaultResponseTimeout = 3000
15+
constructor(public page: Page) {}
16+
17+
async makeXHRCall(
18+
url = this.defaultTestApiURL,
19+
reqOptions: XHRRequestOptions = {}
20+
): Promise<void> {
21+
let normalizeUrl = url
22+
if (url.startsWith('/')) {
23+
normalizeUrl = new URL(url, this.page.url()).href
24+
}
25+
const req = this.page.waitForResponse(normalizeUrl ?? url, {
26+
timeout: this.defaultResponseTimeout,
27+
})
28+
await this.page.evaluate(
29+
(args) => {
30+
const xhr = new XMLHttpRequest()
31+
xhr.open(args.method ?? 'POST', args.url)
32+
xhr.responseType = args.responseType ?? 'json'
33+
xhr.setRequestHeader(
34+
'Content-Type',
35+
args.contentType ?? 'application/json'
36+
)
37+
if (typeof args.responseLatency === 'number') {
38+
xhr.setRequestHeader(
39+
'x-test-latency',
40+
args.responseLatency.toString()
41+
)
42+
}
43+
xhr.send(args.body || JSON.stringify({ foo: 'bar' }))
44+
},
45+
{ url, ...reqOptions }
46+
)
47+
await req
48+
}
49+
/**
50+
* Make a fetch call in the page context. By default it will POST a JSON object with {foo: 'bar'}
51+
*/
52+
async makeFetchCall(
53+
url = this.defaultTestApiURL,
54+
request: Partial<RequestInit> = {}
55+
): Promise<void> {
56+
let normalizeUrl = url
57+
if (url.startsWith('/')) {
58+
normalizeUrl = new URL(url, this.page.url()).href
59+
}
60+
const req = this.page.waitForResponse(normalizeUrl ?? url, {
61+
timeout: this.defaultResponseTimeout,
62+
})
63+
await this.page.evaluate(
64+
(args) => {
65+
return fetch(args.url, {
66+
method: 'POST',
67+
headers: {
68+
'Content-Type': 'application/json',
69+
},
70+
body: JSON.stringify({ foo: 'bar' }),
71+
...args.request,
72+
})
73+
.then(console.log)
74+
.catch(console.error)
75+
},
76+
{ url, request }
77+
)
78+
await req
79+
}
80+
81+
async mockTestRoute(
82+
url = this.defaultTestApiURL,
83+
response?: Partial<FulfillOptions>
84+
) {
85+
if (url.startsWith('/')) {
86+
url = new URL(url, this.page.url()).href
87+
}
88+
await this.page.route(url, async (route) => {
89+
const latency = this.extractLatency(route)
90+
91+
// if a custom latency is set in the request headers, use that instead
92+
93+
await new Promise((resolve) => setTimeout(resolve, latency))
94+
return route.fulfill({
95+
contentType: 'application/json',
96+
status: 200,
97+
body: JSON.stringify({ someResponse: 'yep' }),
98+
...response,
99+
})
100+
})
101+
}
102+
private extractLatency(route: Route) {
103+
let latency = 0
104+
if (route.request().headers()['x-test-latency']) {
105+
const customLatency = parseInt(
106+
route.request().headers()['x-test-latency']
107+
)
108+
if (customLatency) {
109+
latency = customLatency
110+
}
111+
}
112+
return latency
113+
}
114+
}
115+
116+
class SegmentAPIRequestBuffer {
117+
private requests: Request[] = []
118+
public lastEvent() {
119+
return this.getEvents()[this.getEvents.length - 1]
120+
}
121+
public getEvents(): SegmentEvent[] {
122+
return this.requests.flatMap((req) => req.postDataJSON().batch)
123+
}
124+
125+
clear() {
126+
this.requests = []
127+
}
128+
addRequest(request: Request) {
129+
if (request.method().toLowerCase() !== 'post') {
130+
throw new Error(
131+
`Unexpected method: ${request.method()}, Tracking API only accepts POST`
132+
)
133+
}
134+
this.requests.push(request)
135+
}
136+
}
137+
138+
export class SignalAPIRequestBuffer extends SegmentAPIRequestBuffer {
139+
/**
140+
* @example 'network', 'interaction', 'navigation', etc
141+
*/
142+
override getEvents(signalType?: string): SegmentEvent[] {
143+
if (signalType) {
144+
return this.getEvents().filter((e) => e.properties!.type === signalType)
145+
}
146+
return super.getEvents()
147+
}
148+
149+
override lastEvent(signalType?: string | undefined): SegmentEvent {
150+
if (signalType) {
151+
const res =
152+
this.getEvents(signalType)[this.getEvents(signalType).length - 1]
153+
if (!res) {
154+
throw new Error(`No signal of type ${signalType} found`)
155+
}
156+
return res
157+
}
158+
return super.lastEvent()
159+
}
160+
}

packages/signals/signals-integration-tests/src/tests/signals-vanilla/basic.test.ts

+14-38
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { test, expect } from '@playwright/test'
2-
import type { SegmentEvent } from '@segment/analytics-next'
32
import { IndexPage } from './index-page'
43

54
const indexPage = new IndexPage()
@@ -21,14 +20,10 @@ test('network signals', async () => {
2120
/**
2221
* Make a fetch call, see if it gets sent to the signals endpoint
2322
*/
24-
await indexPage.mockTestRoute()
25-
await indexPage.makeFetchCall()
23+
await indexPage.network.mockTestRoute()
24+
await indexPage.network.makeFetchCall()
2625
await indexPage.waitForSignalsApiFlush()
27-
const batch = indexPage.lastSignalsApiReq.postDataJSON()
28-
.batch as SegmentEvent[]
29-
const networkEvents = batch.filter(
30-
(el: SegmentEvent) => el.properties!.type === 'network'
31-
)
26+
const networkEvents = indexPage.signalsAPI.getEvents('network')
3227
const requests = networkEvents.filter(
3328
(el) => el.properties!.data.action === 'request'
3429
)
@@ -46,14 +41,10 @@ test('network signals xhr', async () => {
4641
/**
4742
* Make a fetch call, see if it gets sent to the signals endpoint
4843
*/
49-
await indexPage.mockTestRoute()
50-
await indexPage.makeXHRCall()
44+
await indexPage.network.mockTestRoute()
45+
await indexPage.network.makeXHRCall()
5146
await indexPage.waitForSignalsApiFlush()
52-
const batch = indexPage.lastSignalsApiReq.postDataJSON()
53-
.batch as SegmentEvent[]
54-
const networkEvents = batch.filter(
55-
(el: SegmentEvent) => el.properties!.type === 'network'
56-
)
47+
const networkEvents = indexPage.signalsAPI.getEvents('network')
5748
expect(networkEvents).toHaveLength(2)
5849
const requests = networkEvents.filter(
5950
(el) => el.properties!.data.action === 'request'
@@ -77,17 +68,14 @@ test('instrumentation signals', async () => {
7768
indexPage.waitForSignalsApiFlush(),
7869
])
7970

80-
const signalReqJSON = indexPage.lastSignalsApiReq.postDataJSON()
81-
8271
const isoDateRegEx = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
83-
const instrumentationEvents = signalReqJSON.batch.filter(
84-
(el: SegmentEvent) => el.properties!.type === 'instrumentation'
85-
)
72+
const instrumentationEvents =
73+
indexPage.signalsAPI.getEvents('instrumentation')
8674
expect(instrumentationEvents).toHaveLength(1)
8775
const ev = instrumentationEvents[0]
8876
expect(ev.event).toBe('Segment Signal Generated')
8977
expect(ev.type).toBe('track')
90-
const rawEvent = ev.properties.data.rawEvent
78+
const rawEvent = ev.properties!.data.rawEvent
9179
expect(rawEvent).toMatchObject({
9280
type: 'page',
9381
anonymousId: expect.any(String),
@@ -107,10 +95,7 @@ test('interaction signals', async () => {
10795
indexPage.waitForTrackingApiFlush(),
10896
])
10997

110-
const signalsReqJSON = indexPage.lastSignalsApiReq.postDataJSON()
111-
const interactionSignals = signalsReqJSON.batch.filter(
112-
(el: SegmentEvent) => el.properties!.type === 'interaction'
113-
)
98+
const interactionSignals = indexPage.signalsAPI.getEvents('interaction')
11499
expect(interactionSignals).toHaveLength(1)
115100
const data = {
116101
eventType: 'click',
@@ -163,12 +148,8 @@ test('navigation signals', async ({ page }) => {
163148
{
164149
// on page load, a navigation signal should be sent
165150
await indexPage.waitForSignalsApiFlush()
166-
const signalReqJSON = indexPage.lastSignalsApiReq.postDataJSON()
167-
const navigationEvents = signalReqJSON.batch.filter(
168-
(el: SegmentEvent) => el.properties!.type === 'navigation'
169-
)
170-
expect(navigationEvents).toHaveLength(1)
171-
const ev = navigationEvents[0]
151+
expect(indexPage.signalsAPI.getEvents()).toHaveLength(1)
152+
const ev = indexPage.signalsAPI.lastEvent('navigation')
172153
expect(ev.properties).toMatchObject({
173154
type: 'navigation',
174155
data: {
@@ -188,13 +169,8 @@ test('navigation signals', async ({ page }) => {
188169
window.location.hash = '#foo'
189170
})
190171
await indexPage.waitForSignalsApiFlush()
191-
const signalReqJSON = indexPage.lastSignalsApiReq.postDataJSON()
192-
193-
const navigationEvents = signalReqJSON.batch.filter(
194-
(el: SegmentEvent) => el.properties!.type === 'navigation'
195-
)
196-
expect(navigationEvents).toHaveLength(1)
197-
const ev = navigationEvents[0]
172+
expect(indexPage.signalsAPI.getEvents()).toHaveLength(2)
173+
const ev = indexPage.signalsAPI.lastEvent('navigation')
198174
expect(ev.properties).toMatchObject({
199175
index: expect.any(Number),
200176
type: 'navigation',

0 commit comments

Comments
 (0)