File size: 8,301 Bytes
6778ee0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 | import { expect } from '@playwright/test'
import packageJson from '../../package.json' with { type: 'json' }
import { mockManyRequests } from './mock-many-requests'
export const tracker_script_version = packageJson.tracker_script_version
/**
* 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 {string} [args.pathToMock] - The path to create the route at and mock the response for.
* @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 {Array|Function} [args.shouldIgnoreRequest] - When provided, ignores certain requests
* @param {number} [args.responseDelay] - When provided, delays the response from the Plausible
* API by the given number of milliseconds.
* @param {number} [args.mockRequestTimeout] - How long to wait for the requests to be made
*/
export const expectPlausibleInAction = async function (
page,
{
action,
expectedRequests = [],
refutedRequests = [],
pathToMock = '/api/event',
awaitedRequestCount,
expectedRequestCount,
responseDelay,
shouldIgnoreRequest,
mockRequestTimeout = 3000
}
) {
const requestsToExpect = expectedRequestCount
? expectedRequestCount
: expectedRequests.length
const requestsToAwait = awaitedRequestCount
? awaitedRequestCount
: requestsToExpect + refutedRequests.length
const { getRequestList } = await mockManyRequests({
page,
path: pathToMock,
fulfill: { status: 202, contentType: 'text/plain', body: 'ok' },
responseDelay,
shouldIgnoreRequest,
awaitedRequestCount: requestsToAwait,
mockRequestTimeout
})
await action()
const requestBodies = await getRequestList()
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)
const unexpectedRequestBodiesErrorMessage = `Expected ${requestsToExpect} requests, but received ${requestBodies.length}:\n\n${JSON.stringify(requestBodies, null, 4)}`
expect(requestBodies.length, unexpectedRequestBodiesErrorMessage).toBe(
requestsToExpect
)
return requestBodies
}
export const isPageviewEvent = function (requestPostData) {
return requestPostData.n === 'pageview'
}
export const isEngagementEvent = function (requestPostData) {
return requestPostData.n === 'engagement'
}
async function toggleTabVisibility(page, hide) {
await page.evaluate((hide) => {
Object.defineProperty(document, 'visibilityState', {
value: hide ? 'hidden' : 'visible',
writable: true
})
Object.defineProperty(document, 'hidden', { value: hide, writable: true })
document.dispatchEvent(new Event('visibilitychange'))
}, hide)
}
export const hideCurrentTab = async function (page) {
return toggleTabVisibility(page, true)
}
export const showCurrentTab = async function (page) {
return toggleTabVisibility(page, false)
}
async function setFocus(page, focus) {
await page.evaluate((focus) => {
Object.defineProperty(document, 'hasFocus', {
value: () => focus,
writable: true
})
const eventName = focus ? 'focus' : 'blur'
window.dispatchEvent(new Event(eventName))
}, focus)
}
export const focus = async function (page) {
return setFocus(page, true)
}
export const blur = async function (page) {
return setFocus(page, false)
}
export const hideAndShowCurrentTab = async function (page, options = {}) {
await hideCurrentTab(page)
if (options.delay > 0) {
await delay(options.delay)
}
await showCurrentTab(page)
}
export const blurAndFocusPage = async function (page, options = {}) {
await blur(page)
if (options.delay > 0) {
await delay(options.delay)
}
await focus(page)
}
// Custom assertion methods for checking plausible request bodies
export const e = {
stringContaining: (value) => ({
expected: value,
__expectation__: (actual) => actual.includes(value)
}),
toBeUndefined: () => ({
expected: undefined,
__expectation__: (actual) => actual === undefined
})
}
function includesSubset(body, subset) {
return Object.keys(subset).every((key) => {
if (typeof subset[key] === 'object' && !subset[key].__expectation__) {
return (
typeof body[key] === 'object' &&
areFlatObjectsEqual(body[key], subset[key])
)
} else {
return checkEqual(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) => checkEqual(obj2[key], obj1[key]))
}
function checkEqual(a, b) {
if (typeof b === 'object' && b.__expectation__) {
return b.__expectation__(a)
}
return a === b
}
export function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
export function switchByMode(cases, mode) {
switch (mode) {
case 'web':
return cases.web
case 'esm':
return cases.esm
case 'legacy':
return cases.legacy
default:
throw new Error(`Unimplemented mode: ${mode}`)
}
}
/**
* This function ensures that the tracker script has attached the event listener before test is run.
* Note that this race condition happens in the real world as well:
* Events from features like form submissions, file downloads, outbound links, tagged events
* that work with event handlers registered on the document
* will not be tracked if the event happens before the tracker script has attached the event listener.
*/
export function ensurePlausibleInitialized(page) {
return page.waitForFunction(() => window.plausible?.l === true)
}
|