155 lines
6.1 KiB
JavaScript
155 lines
6.1 KiB
JavaScript
const { expect, Page } = require("@playwright/test");
|
|
|
|
// Since pageleave events in the Plausible script are throttled to 500ms, we
|
|
// often need to wait for an artificial timeout before navigating in tests.
|
|
exports.pageleaveCooldown = async function(page) {
|
|
return page.waitForTimeout(600)
|
|
}
|
|
|
|
// Mocks an HTTP request call with the given path. Returns a Promise that resolves to the request
|
|
// data. If the request is not made, resolves to null after 3 seconds.
|
|
const mockRequest = function (page, path) {
|
|
return new Promise((resolve, _reject) => {
|
|
const requestTimeoutTimer = setTimeout(() => resolve(null), 3000)
|
|
|
|
page.route(path, (route, request) => {
|
|
clearTimeout(requestTimeoutTimer)
|
|
resolve(request)
|
|
return route.fulfill({ status: 202, contentType: 'text/plain', body: 'ok' })
|
|
})
|
|
})
|
|
}
|
|
|
|
exports.mockRequest = mockRequest
|
|
|
|
exports.metaKey = function() {
|
|
if (process.platform === 'darwin') {
|
|
return 'Meta'
|
|
} else {
|
|
return 'Control'
|
|
}
|
|
}
|
|
|
|
// Mocks a specified number of HTTP requests with given path. Returns a promise that resolves to a
|
|
// list of requests as soon as the specified number of requests is made, or 3 seconds has passed.
|
|
const mockManyRequests = function({ page, path, numberOfRequests, responseDelay }) {
|
|
return new Promise((resolve, _reject) => {
|
|
let requestList = []
|
|
const requestTimeoutTimer = setTimeout(() => resolve(requestList), 3000)
|
|
|
|
page.route(path, async (route, request) => {
|
|
requestList.push(request)
|
|
if (responseDelay) {
|
|
await delay(responseDelay)
|
|
}
|
|
if (requestList.length === numberOfRequests) {
|
|
clearTimeout(requestTimeoutTimer)
|
|
resolve(requestList)
|
|
}
|
|
return route.fulfill({ status: 202, contentType: 'text/plain', body: 'ok' })
|
|
})
|
|
})
|
|
}
|
|
|
|
exports.mockManyRequests = mockManyRequests
|
|
|
|
/**
|
|
* A powerful utility function that makes it easy to assert on the event
|
|
* requests that should or should not have been made after doing a page
|
|
* action (e.g. navigating to the page, clicking a page element, etc).
|
|
*
|
|
* @param {Page} page - The Playwright Page object.
|
|
* @param {Object} args - The object configuring the action and related expectations.
|
|
* @param {Function} args.action - A function that returns a promise. The function is called
|
|
* without arguments, and is `await`ed. This is the action that should or should not trigger
|
|
* Plausible requests on the page.
|
|
* @param {Array} [args.expectedRequests] - A list of partial JSON payloads that get matched
|
|
* against the bodies of event requests made. An `expectedRequest` is considered as having
|
|
* occurred if all of its key-value pairs are found from the JSON body of an event request
|
|
* that was made. The default value is `[]`
|
|
* @param {Array} [args.refutedRequests] - Same as `expectedRequests` but the opposite. The
|
|
* expectation passes if none of the made requests match with these partial payloads. Note
|
|
* that the condition on which a partial payload matches an event request payload is exactly
|
|
* the same as it is for `expectedRequests`. The default value is `[]`
|
|
* @param {number} [args.awaitedRequestCount] - Sometimes we might want to wait for more events
|
|
* to happen, just to make sure they didn't. By default, the number of requests we wait for
|
|
* is `expectedRequests.length + refutedRequests.length`.
|
|
* @param {number} [args.expectedRequestCount] - When provided, expects the total amount of
|
|
* event requests made to match this number.
|
|
* @param {number} [args.responseDelay] - When provided, delays the response from the Plausible
|
|
* API by the given number of milliseconds.
|
|
*/
|
|
exports.expectPlausibleInAction = async function (page, {
|
|
action,
|
|
expectedRequests = [],
|
|
refutedRequests = [],
|
|
awaitedRequestCount,
|
|
expectedRequestCount,
|
|
responseDelay
|
|
}) {
|
|
const requestsToExpect = expectedRequestCount ? expectedRequestCount : expectedRequests.length
|
|
const requestsToAwait = awaitedRequestCount ? awaitedRequestCount : requestsToExpect + refutedRequests.length
|
|
|
|
const plausibleRequestMockList = mockManyRequests({
|
|
page,
|
|
path: '/api/event',
|
|
numberOfRequests: requestsToAwait,
|
|
responseDelay: responseDelay
|
|
})
|
|
await action()
|
|
const requestBodies = (await plausibleRequestMockList).map(r => r.postDataJSON())
|
|
|
|
const expectedButNotFoundBodySubsets = []
|
|
|
|
expectedRequests.forEach((bodySubset) => {
|
|
const wasFound = requestBodies.some((requestBody) => {
|
|
return includesSubset(requestBody, bodySubset)
|
|
})
|
|
|
|
if (!wasFound) {expectedButNotFoundBodySubsets.push(bodySubset)}
|
|
})
|
|
|
|
const refutedButFoundRequestBodies = []
|
|
|
|
refutedRequests.forEach((bodySubset) => {
|
|
const found = requestBodies.find((requestBody) => {
|
|
return includesSubset(requestBody, bodySubset)
|
|
})
|
|
|
|
if (found) {refutedButFoundRequestBodies.push(found)}
|
|
})
|
|
|
|
const expectedBodySubsetsErrorMessage = `The following body subsets were not found from the requests that were made:\n\n${JSON.stringify(expectedButNotFoundBodySubsets, null, 4)}\n\nReceived requests with the following bodies:\n\n${JSON.stringify(requestBodies, null, 4)}`
|
|
expect(expectedButNotFoundBodySubsets, expectedBodySubsetsErrorMessage).toHaveLength(0)
|
|
|
|
const refutedBodySubsetsErrorMessage = `The following requests were made, but were not expected:\n\n${JSON.stringify(refutedButFoundRequestBodies, null, 4)}`
|
|
expect(refutedButFoundRequestBodies, refutedBodySubsetsErrorMessage).toHaveLength(0)
|
|
|
|
expect(requestBodies.length).toBe(requestsToExpect)
|
|
}
|
|
|
|
function includesSubset(body, subset) {
|
|
return Object.keys(subset).every((key) => {
|
|
if (typeof subset[key] === 'object') {
|
|
return typeof body[key] === 'object' && areFlatObjectsEqual(body[key], subset[key])
|
|
} else {
|
|
return body[key] === subset[key]
|
|
}
|
|
})
|
|
}
|
|
|
|
// For comparing custom props - all key-value pairs
|
|
// must match but the order is not important.
|
|
function areFlatObjectsEqual(obj1, obj2) {
|
|
const keys1 = Object.keys(obj1)
|
|
const keys2 = Object.keys(obj2)
|
|
|
|
if (keys1.length !== keys2.length) return false;
|
|
|
|
return keys1.every(key => obj2[key] === obj1[key])
|
|
}
|
|
|
|
function delay(ms) {
|
|
return new Promise(resolve => setTimeout(resolve, ms))
|
|
}
|