Skip to content

Commit

Permalink
chore: replace karma with playwright
Browse files Browse the repository at this point in the history
  • Loading branch information
Joozty committed Nov 14, 2024
1 parent 58acac3 commit 622c53c
Show file tree
Hide file tree
Showing 15 changed files with 214 additions and 217 deletions.
2 changes: 2 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/integration-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@types/ejs": "^3.1.5",
"@types/express": "^5.0.0",
"@types/node": "^22.9.0",
"cors": "^2.8.5",
"ejs": "^3.1.10",
"express": "^4.21.1"
},
Expand Down
24 changes: 17 additions & 7 deletions packages/integration-tests/src/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { RENDER_AGENT_TEMPLATE } from './render-agent'
import { render } from 'ejs'
import { setServerTimingHeader } from './server-timing'
import * as fs from 'node:fs'
import cors from 'cors'

const GLOBAL_TEST_BUFFER_TIMEOUT = 20

Expand All @@ -29,20 +30,33 @@ const app = express()
app.set('view engine', 'ejs')
app.set('views', path.join(__dirname, '../tests'))

app.use(cors())
app.use(express.json())
app.use(express.static(path.join(__dirname, '../../../web/dist')))
app.use(async (req, res, next) => {
if (typeof req.query.delay === 'string') {
const timeout = parseInt(req.query.delay)
await new Promise((resolve) => setTimeout(resolve, timeout))
}

if (typeof req.query.serverTiming === 'string') {
res.setHeader('Server-Timing', req.query.serverTiming)
} else {
setServerTimingHeader(res)
}

next()
})

app.post('/api/v2/spans', (req, res) => {
res.send('')
})

app.post('/echo', (req, res) => {
console.log('echooooo', req.body)
res.send(req.body)
})

app.get('/some-data', (req, response) => {
setServerTimingHeader(response)
response.send({
key1: 'value1',
key2: 'value2',
Expand All @@ -59,14 +73,10 @@ app.get('/', (_, res) => {
})

app.get('*', (req, res) => {
if (typeof req.query.serverTiming === 'string') {
res.setHeader('Server-Timing', req.query.serverTiming)
}

const beaconUrl = new URL(`http://${req.headers.host}/api/v2/spans`)
const defaultFile = '/artifacts/splunk-otel-web.js'

const filePath = path.join(__dirname, '../tests', req.path)
console.log('filePath', filePath)
if (fs.existsSync(filePath)) {
if (req.path.endsWith('.ejs')) {
res.render(req.path.slice(1), {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
body {
background-image: 'splunk-black.png';
font-family: splunkerz;
}

@font-face {
font-family: splunkerz;
src: url(splunk.woff);
src: url('splunk.woff');
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,24 @@
<%- renderAgent({
bufferTimeout: 200
}) %>
<link rel="stylesheet" href="/utils/devServer/assets/css-font-img.css">
<link rel="stylesheet" href="assets/css-font-img.css">
</head>
<body>
<p>Docload</p>
<script>
function makeHXR(url) {
var oReq = new XMLHttpRequest();
const oReq = new XMLHttpRequest();
oReq.open("GET", url);
oReq.setRequestHeader('Content-Type', 'text/plain');
oReq.send();
return oReq;
}
makeHXR('/some-data');
fetch('/some-data?t=1').then(response => response.json());
fetch('/some-data?delay=1').then(response => response.json());
</script>

<iframe src="iframe.ejs" frameborder="0"></iframe>
<img src="/utils/devServer/assets/splunk-black.png?t=300">
<iframe src="iframe.ejs" frameborder="0"></iframe>
<img src="assets/splunk-black.png?delay=300">
</body>
</html>
137 changes: 137 additions & 0 deletions packages/integration-tests/src/tests/docload/docload.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/**
*
* Copyright 2024 Splunk Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import { expect } from '@playwright/test'
import { test } from '../../utils/test'
import { timesMakeSense } from '../../utils/time-make-sense'

test.describe('docload', () => {
test('resources before load event are correctly captured', async ({ recordPage }) => {
await recordPage.goTo('/docload/docload-all.ejs')

await recordPage.waitForSpans((spans) => spans.filter((span) => span.name === 'documentLoad').length === 1)
const docLoadSpans = recordPage.receivedSpans.filter((span) => span.name === 'documentLoad')

expect(docLoadSpans).toHaveLength(1)

const resources = ['css-font-img.css', 'splunk-black.png?delay=300', 'iframe.ejs', 'splunk.woff']
for (const urlEnd of resources) {
const resourceSpans = recordPage.receivedSpans.filter(
(span) => span.tags['http.url'] && (span.tags['http.url'] as string).endsWith(urlEnd),
)

expect(docLoadSpans[0].traceId, `${urlEnd} has correct traceId`).toBe(resourceSpans[0].traceId)
}

const ignoredResources = ['/some-data', '/some-data?delay=1', '/api/v2/spans']
for (const urlEnd of ignoredResources) {
const resourceSpans = recordPage.receivedSpans.filter(
(span) =>
span.tags['component'] === 'document-load' &&
typeof span.tags['http.url'] === 'string' &&
span.tags['http.url'].endsWith(urlEnd),
)

expect(resourceSpans, `${urlEnd} is not captured`).toHaveLength(0)
}
})

test('documentFetch, resourceFetch, and documentLoad spans', async ({ recordPage }) => {
await recordPage.goTo('/docload/docload.ejs')

await recordPage.waitForSpans((spans) => spans.filter((span) => span.name === 'documentLoad').length === 1)
const docLoadSpans = recordPage.receivedSpans.filter((span) => span.name === 'documentLoad')
const docFetchSpans = recordPage.receivedSpans.filter((span) => span.name === 'documentFetch')
const scriptFetchSpans = recordPage.receivedSpans.filter(
(span) =>
span.name === 'resourceFetch' &&
typeof span.tags['http.url'] === 'string' &&
span.tags['http.url'].includes('splunk-otel-web.js'),
)
const brokenImageFetchSpans = recordPage.receivedSpans.filter(
(span) =>
span.name === 'resourceFetch' &&
typeof span.tags['http.url'] === 'string' &&
span.tags['http.url'].includes('/nosuchimage.jpg'),
)

expect(docFetchSpans).toHaveLength(1)
expect(docLoadSpans).toHaveLength(1)
expect(docLoadSpans[0].traceId.match(/[a-f0-9]+/), 'Checking sanity of traceId').toBeTruthy()
expect(docLoadSpans[0].id.match(/[a-f0-9]+/), 'Checking sanity of id').toBeTruthy()
expect(docFetchSpans[0].traceId).toBe(docLoadSpans[0].traceId)
expect(docFetchSpans[0].parentId).toBe(docLoadSpans[0].id)

expect(scriptFetchSpans).toHaveLength(1)
expect(scriptFetchSpans[0].traceId).toBe(docLoadSpans[0].traceId)
expect(scriptFetchSpans[0].parentId).toBe(docLoadSpans[0].id)
expect(scriptFetchSpans[0].tags['component']).toBe('document-load')

expect(brokenImageFetchSpans.length).toBeGreaterThanOrEqual(1)
expect(brokenImageFetchSpans[0].traceId).toBe(docLoadSpans[0].traceId)
expect(brokenImageFetchSpans[0].parentId).toBe(docLoadSpans[0].id)

expect(docFetchSpans[0].tags['component']).toBe('document-load')
expect(docLoadSpans[0].tags['location.href']).toBe('http://localhost:3000/docload/docload.ejs')

timesMakeSense(docFetchSpans[0].annotations, 'domainLookupStart', 'domainLookupEnd')
timesMakeSense(docFetchSpans[0].annotations, 'connectStart', 'connectEnd')
timesMakeSense(docFetchSpans[0].annotations, 'requestStart', 'responseStart')
timesMakeSense(docFetchSpans[0].annotations, 'responseStart', 'responseEnd')
timesMakeSense(docFetchSpans[0].annotations, 'fetchStart', 'responseEnd')

expect(docFetchSpans[0].tags['link.traceId']).toBeDefined()
expect(docFetchSpans[0].tags['link.spanId']).toBeDefined()
expect(parseInt(scriptFetchSpans[0].tags['http.response_content_length'] as string)).toBeGreaterThan(0)

expect(docLoadSpans[0].tags['component']).toBe('document-load')
expect(docLoadSpans[0].tags['location.href']).toBe('http://localhost:3000/docload/docload.ejs')
expect(
(docLoadSpans[0].tags['screen.xy'] as string).match(/[0-9]+x[0-9]+/),
'Checking sanity of screen.xy',
).toBeTruthy()

timesMakeSense(docLoadSpans[0].annotations, 'domContentLoadedEventStart', 'domContentLoadedEventEnd')
timesMakeSense(docLoadSpans[0].annotations, 'loadEventStart', 'loadEventEnd')
timesMakeSense(docLoadSpans[0].annotations, 'fetchStart', 'domInteractive')
timesMakeSense(docLoadSpans[0].annotations, 'fetchStart', 'domComplete')

expect(recordPage.receivedErrorSpans).toHaveLength(0)
})

test('ignoring resource URLs', async ({ recordPage }) => {
await recordPage.goTo('/docload/docload-ignored.ejs')

await recordPage.waitForSpans((spans) => spans.filter((span) => span.name === 'documentFetch').length === 1)
const ignoredResourceFetchSpans = recordPage.receivedSpans.filter(
(span) => span.tags['http.url'] === 'http://localhost:3000/non-impactful-resource.jpg',
)

expect(ignoredResourceFetchSpans).toHaveLength(0)
})

test('module can be disabled', async ({ recordPage }) => {
await recordPage.goTo('/docload/docload.ejs?disableInstrumentation=document')

await recordPage.waitForTimeoutAndFlushData(1000)
const SPAN_TYPES = ['documentFetch', 'documentLoad', 'resourceFetch']
const documentSpans = recordPage.receivedSpans.filter((span) => SPAN_TYPES.includes(span.name))

expect(documentSpans).toHaveLength(0)
expect(recordPage.receivedErrorSpans).toHaveLength(0)
})
})
33 changes: 1 addition & 32 deletions packages/integration-tests/src/tests/fetch/fetch.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,38 +17,7 @@
*/
import { expect } from '@playwright/test'
import { test } from '../../utils/test'

const timesMakeSense = (
annotations: Array<{ timestamp: number; value: string }>,
startName: string,
endName: string,
) => {
const annotationsObject: Record<string, number> = {}
annotations.forEach((event) => {
annotationsObject[event.value] = event.timestamp
})

expect(annotationsObject[startName]).toBeTruthy()
expect(annotationsObject[endName]).toBeTruthy()

expect(annotationsObject[startName]).toBeLessThanOrEqual(annotationsObject[endName])

const diff = annotationsObject[endName] - annotationsObject[startName]
const fiveMinutes = 5 * 60 * 1000 * 1000

// Sanity check for time difference
expect(diff).toBeLessThanOrEqual(fiveMinutes)

// Also looking for rough synchronization with reality (at least from our CI systems/laptops...)
const nowMicros = new Date().getTime() * 1000
let clockSkew = annotationsObject[startName] - nowMicros
if (clockSkew < 0) {
clockSkew = -clockSkew
}

// Sanity check for clock skew
expect(clockSkew).toBeLessThanOrEqual(fiveMinutes)
}
import { timesMakeSense } from '../../utils/time-make-sense'

test.describe('fetch', () => {
test('span created for fetch includes all properties', async ({ recordPage }) => {
Expand Down
50 changes: 50 additions & 0 deletions packages/integration-tests/src/utils/time-make-sense.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
*
* Copyright 2024 Splunk Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import { expect } from '@playwright/test'

export const timesMakeSense = (
annotations: Array<{ timestamp: number; value: string }>,
startName: string,
endName: string,
) => {
const annotationsObject: Record<string, number> = {}
annotations.forEach((event) => {
annotationsObject[event.value] = event.timestamp
})

expect(annotationsObject[startName]).toBeTruthy()
expect(annotationsObject[endName]).toBeTruthy()

expect(annotationsObject[startName]).toBeLessThanOrEqual(annotationsObject[endName])

const diff = annotationsObject[endName] - annotationsObject[startName]
const fiveMinutes = 5 * 60 * 1000 * 1000

// Sanity check for time difference
expect(diff).toBeLessThanOrEqual(fiveMinutes)

// Also looking for rough synchronization with reality (at least from our CI systems/laptops...)
const nowMicros = new Date().getTime() * 1000
let clockSkew = annotationsObject[startName] - nowMicros
if (clockSkew < 0) {
clockSkew = -clockSkew
}

// Sanity check for clock skew
expect(clockSkew).toBeLessThanOrEqual(fiveMinutes)
}
Loading

0 comments on commit 622c53c

Please sign in to comment.