mirror of
https://github.com/plausible/analytics.git
synced 2024-12-22 17:11:36 +03:00
Add tests for the pageleave script (#4744)
* move util function to util module * move playwright config file to tracker dir root * update Playwright + add gitignores * Playwright: enable reuseExistingServer (non-CI env) * store tracker src copies to avoid re-compilation in dev env * test pageleave on simple navigation * fix test util fn * rename local_test npm script * make test util able to expect multiple requests * test pageleaves in SPAs * test pageleave with manual URL * test pageleave not sent in manual when pageview not triggered * extend util fn to refute event requests * test pageleaves not sent in excluded hash pages * store hashes instead of file copies to detect /tracker/src changes * drop async * speed up test suite
This commit is contained in:
parent
1a6fd1dc15
commit
37116a2b12
10
.gitignore
vendored
10
.gitignore
vendored
@ -35,6 +35,16 @@ npm-debug.log
|
|||||||
/assets/node_modules/
|
/assets/node_modules/
|
||||||
/tracker/node_modules/
|
/tracker/node_modules/
|
||||||
|
|
||||||
|
# Files generated by Playwright when running tracker tests
|
||||||
|
/tracker/test-results/
|
||||||
|
/tracker/playwright-report/
|
||||||
|
/tracker/blob-report/
|
||||||
|
/tracker/playwright/.cache/
|
||||||
|
|
||||||
|
# Stored hash of source tracker files used in development environment
|
||||||
|
# to detect changes in /tracker/src and avoid unnecessary compilation.
|
||||||
|
/tracker/dev-compile/last-hash.txt
|
||||||
|
|
||||||
# test coverage directory
|
# test coverage directory
|
||||||
/assets/coverage
|
/assets/coverage
|
||||||
|
|
||||||
|
@ -3,6 +3,12 @@ const fs = require('fs')
|
|||||||
const path = require('path')
|
const path = require('path')
|
||||||
const Handlebars = require("handlebars");
|
const Handlebars = require("handlebars");
|
||||||
const g = require("generatorics");
|
const g = require("generatorics");
|
||||||
|
const { canSkipCompile } = require("./dev-compile/can-skip-compile");
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'dev' && canSkipCompile()) {
|
||||||
|
console.info('COMPILATION SKIPPED: No changes detected in tracker dependencies')
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
Handlebars.registerHelper('any', function (...args) {
|
Handlebars.registerHelper('any', function (...args) {
|
||||||
return args.slice(0, -1).some(Boolean)
|
return args.slice(0, -1).some(Boolean)
|
||||||
@ -35,4 +41,4 @@ compilefile(relPath('src/p.js'), relPath('../priv/tracker/js/p.js'))
|
|||||||
variants.map(variant => {
|
variants.map(variant => {
|
||||||
const options = variant.map(variant => variant.replace('-', '_')).reduce((acc, curr) => (acc[curr] = true, acc), {})
|
const options = variant.map(variant => variant.replace('-', '_')).reduce((acc, curr) => (acc[curr] = true, acc), {})
|
||||||
compilefile(relPath('src/plausible.js'), relPath(`../priv/tracker/js/plausible.${variant.join('.')}.js`), options)
|
compilefile(relPath('src/plausible.js'), relPath(`../priv/tracker/js/plausible.${variant.join('.')}.js`), options)
|
||||||
})
|
})
|
52
tracker/dev-compile/can-skip-compile.js
Normal file
52
tracker/dev-compile/can-skip-compile.js
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
const LAST_HASH_FILEPATH = path.join(__dirname, './last-hash.txt')
|
||||||
|
|
||||||
|
// Re-compilation is only required if any of these files have been changed.
|
||||||
|
const COMPILE_DEPENDENCIES = [
|
||||||
|
path.join(__dirname, '../compile.js'),
|
||||||
|
path.join(__dirname, '../src/plausible.js'),
|
||||||
|
path.join(__dirname, '../src/customEvents.js')
|
||||||
|
]
|
||||||
|
|
||||||
|
function currentHash() {
|
||||||
|
const combinedHash = crypto.createHash('sha256');
|
||||||
|
|
||||||
|
for (const filePath of COMPILE_DEPENDENCIES) {
|
||||||
|
try {
|
||||||
|
const fileContent = fs.readFileSync(filePath);
|
||||||
|
const fileHash = crypto.createHash('sha256').update(fileContent).digest();
|
||||||
|
combinedHash.update(fileHash);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to read or hash ${filePath}: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return combinedHash.digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
function lastHash() {
|
||||||
|
if (fs.existsSync(LAST_HASH_FILEPATH)) {
|
||||||
|
return fs.readFileSync(LAST_HASH_FILEPATH).toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a boolean indicating whether the tracker compilation can be skipped.
|
||||||
|
* Every time this function gets executed, the hash of the tracker dependencies
|
||||||
|
* will be updated. Compilation can be skipped if the hash hasn't changed since
|
||||||
|
* the last execution.
|
||||||
|
*/
|
||||||
|
exports.canSkipCompile = function() {
|
||||||
|
const current = currentHash()
|
||||||
|
const last = lastHash()
|
||||||
|
|
||||||
|
if (current === last) {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
fs.writeFileSync(LAST_HASH_FILEPATH, current)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
50
tracker/package-lock.json
generated
50
tracker/package-lock.json
generated
@ -6,13 +6,14 @@
|
|||||||
"": {
|
"": {
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/test": "^1.41.1",
|
|
||||||
"express": "^4.18.1",
|
"express": "^4.18.1",
|
||||||
"generatorics": "^1.1.0",
|
"generatorics": "^1.1.0",
|
||||||
"handlebars": "^4.7.8",
|
"handlebars": "^4.7.8",
|
||||||
"uglify-js": "^3.9.4"
|
"uglify-js": "^3.9.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.48.1",
|
||||||
|
"@types/node": "^22.7.7",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-plugin-playwright": "^0.20.0"
|
"eslint-plugin-playwright": "^0.20.0"
|
||||||
}
|
}
|
||||||
@ -151,17 +152,27 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@playwright/test": {
|
"node_modules/@playwright/test": {
|
||||||
"version": "1.41.1",
|
"version": "1.48.1",
|
||||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.1.tgz",
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.1.tgz",
|
||||||
"integrity": "sha512-9g8EWTjiQ9yFBXc6HjCWe41msLpxEX0KhmfmPl9RPLJdfzL4F0lg2BdJ91O9azFdl11y1pmpwdjBiSxvqc+btw==",
|
"integrity": "sha512-s9RtWoxkOLmRJdw3oFvhFbs9OJS0BzrLUc8Hf6l2UdCNd1rqeEyD4BhCJkvzeEoD1FsK4mirsWwGerhVmYKtZg==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright": "1.41.1"
|
"playwright": "1.48.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16"
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "22.7.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.7.tgz",
|
||||||
|
"integrity": "sha512-SRxCrrg9CL/y54aiMCG3edPKdprgMVGDXjA3gB8UmmBW5TcXzRUYAh8EWzTnSJFAd1rgImPELza+A3bJ+qxz8Q==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~6.19.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@ungap/structured-clone": {
|
"node_modules/@ungap/structured-clone": {
|
||||||
@ -855,6 +866,7 @@
|
|||||||
"version": "2.3.2",
|
"version": "2.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -1395,31 +1407,33 @@
|
|||||||
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
|
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
|
||||||
},
|
},
|
||||||
"node_modules/playwright": {
|
"node_modules/playwright": {
|
||||||
"version": "1.41.1",
|
"version": "1.48.1",
|
||||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.41.1.tgz",
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.1.tgz",
|
||||||
"integrity": "sha512-gdZAWG97oUnbBdRL3GuBvX3nDDmUOuqzV/D24dytqlKt+eI5KbwusluZRGljx1YoJKZ2NRPaeWiFTeGZO7SosQ==",
|
"integrity": "sha512-j8CiHW/V6HxmbntOfyB4+T/uk08tBy6ph0MpBXwuoofkSnLmlfdYNNkFTYD6ofzzlSqLA1fwH4vwvVFvJgLN0w==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.41.1"
|
"playwright-core": "1.48.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"fsevents": "2.3.2"
|
"fsevents": "2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/playwright-core": {
|
"node_modules/playwright-core": {
|
||||||
"version": "1.41.1",
|
"version": "1.48.1",
|
||||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.41.1.tgz",
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.1.tgz",
|
||||||
"integrity": "sha512-/KPO5DzXSMlxSX77wy+HihKGOunh3hqndhqeo/nMxfigiKzogn8kfL0ZBDu0L1RKgan5XHCPmn6zXd2NUJgjhg==",
|
"integrity": "sha512-Yw/t4VAFX/bBr1OzwCuOMZkY1Cnb4z/doAFSwf4huqAGWmf9eMNjmK7NiOljCdLmxeRYcGPPmcDgU0zOlzP0YA==",
|
||||||
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright-core": "cli.js"
|
"playwright-core": "cli.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prelude-ls": {
|
"node_modules/prelude-ls": {
|
||||||
@ -1796,6 +1810,12 @@
|
|||||||
"node": ">=0.8.0"
|
"node": ">=0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "6.19.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||||
|
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/unpipe": {
|
"node_modules/unpipe": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||||
|
@ -1,19 +1,21 @@
|
|||||||
{
|
{
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"deploy": "node compile.js",
|
"deploy": "node compile.js",
|
||||||
"test": "npm run deploy && npx playwright test --config=./test/support/playwright.config.js",
|
"test": "npm run deploy && npx playwright test",
|
||||||
|
"test:local": "NODE_ENV=dev npm run deploy && npx playwright test",
|
||||||
"report-sizes": "node report-sizes.js",
|
"report-sizes": "node report-sizes.js",
|
||||||
"start": "node test/support/server.js"
|
"start": "node test/support/server.js"
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/test": "^1.41.1",
|
|
||||||
"express": "^4.18.1",
|
"express": "^4.18.1",
|
||||||
"generatorics": "^1.1.0",
|
"generatorics": "^1.1.0",
|
||||||
"handlebars": "^4.7.8",
|
"handlebars": "^4.7.8",
|
||||||
"uglify-js": "^3.9.4"
|
"uglify-js": "^3.9.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.48.1",
|
||||||
|
"@types/node": "^22.7.7",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-plugin-playwright": "^0.20.0"
|
"eslint-plugin-playwright": "^0.20.0"
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
const { devices } = require('@playwright/test');
|
// @ts-check
|
||||||
|
const { defineConfig, devices } = require('@playwright/test');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see https://playwright.dev/docs/test-configuration
|
* @see https://playwright.dev/docs/test-configuration
|
||||||
*/
|
*/
|
||||||
module.exports = {
|
module.exports = defineConfig({
|
||||||
testDir: '../',
|
testDir: './test',
|
||||||
timeout: 60 * 1000,
|
timeout: 60 * 1000,
|
||||||
fullyParallel: true,
|
fullyParallel: true,
|
||||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||||
@ -33,5 +34,7 @@ module.exports = {
|
|||||||
webServer: {
|
webServer: {
|
||||||
command: 'npm run start',
|
command: 'npm run start',
|
||||||
port: 3000,
|
port: 3000,
|
||||||
|
reuseExistingServer: !process.env.CI
|
||||||
},
|
},
|
||||||
}
|
});
|
||||||
|
|
18
tracker/test/fixtures/pageleave-hash-exclusions.html
vendored
Normal file
18
tracker/test/fixtures/pageleave-hash-exclusions.html
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
|
<title>Plausible Playwright tests</title>
|
||||||
|
<script defer data-exclude='/*#*/hash/**/ignored' src="/tracker/js/plausible.exclusions.hash.local.pageleave.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<a id="ignored-hash-link" href="#this/hash/should/be/ignored">Ignored Hash Link</a>
|
||||||
|
<a id="hash-link-1" href="#hash1">Hash Link 1</a>
|
||||||
|
<a id="hash-link-2" href="#hash2">Hash Link 2</a>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
19
tracker/test/fixtures/pageleave-hash.html
vendored
Normal file
19
tracker/test/fixtures/pageleave-hash.html
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
|
<title>Plausible Playwright tests</title>
|
||||||
|
<script defer src="/tracker/js/plausible.hash.local.pageleave.js"></script>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<a id="hash-nav" href="#some-hash">Hash link</a>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
29
tracker/test/fixtures/pageleave-manual.html
vendored
Normal file
29
tracker/test/fixtures/pageleave-manual.html
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
|
<title>Plausible Playwright tests</title>
|
||||||
|
<script defer src="/tracker/js/plausible.local.manual.pageleave.js"></script>
|
||||||
|
<script>window.plausible = window.plausible || function() { (window.plausible.q = window.plausible.q || []).push(arguments) }</script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<a id="navigate-away" href="/manual.html">Navigate away</a>
|
||||||
|
|
||||||
|
<button id="pageview-trigger-custom-url">
|
||||||
|
Triggers a pageview with custom URL
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (e.target.id === 'pageview-trigger-custom-url') {
|
||||||
|
window.plausible('pageview', {u: 'https://example.com/custom/location'})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
24
tracker/test/fixtures/pageleave.html
vendored
Normal file
24
tracker/test/fixtures/pageleave.html
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
|
<title>Plausible Playwright tests</title>
|
||||||
|
<script defer src="/tracker/js/plausible.local.pageleave.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<a id="navigate-away" href="/manual.html">Navigate away</a>
|
||||||
|
|
||||||
|
<button id="history-nav">Navigate with history</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.getElementById('history-nav').addEventListener('click', () => {
|
||||||
|
history.pushState({ page: 2 }, 'Another Page', '/another-page');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
@ -1,35 +1,34 @@
|
|||||||
const { mockRequest } = require('./support/test-utils')
|
/* eslint-disable playwright/expect-expect */
|
||||||
const { expect, test } = require('@playwright/test');
|
const { clickPageElementAndExpectEventRequests } = require('./support/test-utils')
|
||||||
|
const { test } = require('@playwright/test');
|
||||||
const { LOCAL_SERVER_ADDR } = require('./support/server');
|
const { LOCAL_SERVER_ADDR } = require('./support/server');
|
||||||
|
|
||||||
async function clickPageElementAndExpectEventRequest(page, buttonId, expectedBodyParams) {
|
|
||||||
const plausibleRequestMock = mockRequest(page, '/api/event')
|
|
||||||
await page.click(buttonId)
|
|
||||||
const plausibleRequest = await plausibleRequestMock;
|
|
||||||
|
|
||||||
expect(plausibleRequest.url()).toContain('/api/event')
|
|
||||||
|
|
||||||
const body = plausibleRequest.postDataJSON()
|
|
||||||
|
|
||||||
Object.keys(expectedBodyParams).forEach((key) => {
|
|
||||||
expect(body[key]).toEqual(expectedBodyParams[key])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
test.describe('manual extension', () => {
|
test.describe('manual extension', () => {
|
||||||
test('can trigger custom events with and without a custom URL if pageview was sent with the default URL', async ({ page }) => {
|
test('can trigger custom events with and without a custom URL if pageview was sent with the default URL', async ({ page }) => {
|
||||||
await page.goto('/manual.html');
|
await page.goto('/manual.html');
|
||||||
|
|
||||||
await clickPageElementAndExpectEventRequest(page, '#pageview-trigger', {n: 'pageview', u: `${LOCAL_SERVER_ADDR}/manual.html`})
|
await clickPageElementAndExpectEventRequests(page, '#pageview-trigger', [
|
||||||
await clickPageElementAndExpectEventRequest(page, '#custom-event-trigger', {n: 'CustomEvent', u: `${LOCAL_SERVER_ADDR}/manual.html`})
|
{n: 'pageview', u: `${LOCAL_SERVER_ADDR}/manual.html`}
|
||||||
await clickPageElementAndExpectEventRequest(page, '#custom-event-trigger-custom-url', {n: 'CustomEvent', u: `https://example.com/custom/location`})
|
])
|
||||||
|
await clickPageElementAndExpectEventRequests(page, '#custom-event-trigger', [
|
||||||
|
{n: 'CustomEvent', u: `${LOCAL_SERVER_ADDR}/manual.html`}
|
||||||
|
])
|
||||||
|
await clickPageElementAndExpectEventRequests(page, '#custom-event-trigger-custom-url', [
|
||||||
|
{n: 'CustomEvent', u: `https://example.com/custom/location`}
|
||||||
|
])
|
||||||
});
|
});
|
||||||
|
|
||||||
test('can trigger custom events with and without a custom URL if pageview was sent with a custom URL', async ({ page }) => {
|
test('can trigger custom events with and without a custom URL if pageview was sent with a custom URL', async ({ page }) => {
|
||||||
await page.goto('/manual.html');
|
await page.goto('/manual.html');
|
||||||
|
|
||||||
await clickPageElementAndExpectEventRequest(page, '#pageview-trigger-custom-url', {n: 'pageview', u: `https://example.com/custom/location`})
|
await clickPageElementAndExpectEventRequests(page, '#pageview-trigger-custom-url', [
|
||||||
await clickPageElementAndExpectEventRequest(page, '#custom-event-trigger', {n: 'CustomEvent', u: `${LOCAL_SERVER_ADDR}/manual.html`})
|
{n: 'pageview', u: `https://example.com/custom/location`}
|
||||||
await clickPageElementAndExpectEventRequest(page, '#custom-event-trigger-custom-url', {n: 'CustomEvent', u: `https://example.com/custom/location`})
|
])
|
||||||
|
await clickPageElementAndExpectEventRequests(page, '#custom-event-trigger', [
|
||||||
|
{n: 'CustomEvent', u: `${LOCAL_SERVER_ADDR}/manual.html`}
|
||||||
|
])
|
||||||
|
await clickPageElementAndExpectEventRequests(page, '#custom-event-trigger-custom-url', [
|
||||||
|
{n: 'CustomEvent', u: `https://example.com/custom/location`}
|
||||||
|
])
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
95
tracker/test/pageleave.spec.js
Normal file
95
tracker/test/pageleave.spec.js
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
/* eslint-disable playwright/expect-expect */
|
||||||
|
/* eslint-disable playwright/no-skipped-test */
|
||||||
|
const { clickPageElementAndExpectEventRequests, mockRequest } = require('./support/test-utils')
|
||||||
|
const { test, expect } = require('@playwright/test');
|
||||||
|
const { LOCAL_SERVER_ADDR } = require('./support/server');
|
||||||
|
|
||||||
|
test.describe('pageleave extension', () => {
|
||||||
|
test.skip(({browserName}) => browserName === 'webkit', 'Not testable on Webkit');
|
||||||
|
|
||||||
|
test('sends a pageleave when navigating to the next page', async ({ page }) => {
|
||||||
|
const pageviewRequestMock = mockRequest(page, '/api/event')
|
||||||
|
await page.goto('/pageleave.html');
|
||||||
|
await pageviewRequestMock;
|
||||||
|
|
||||||
|
await clickPageElementAndExpectEventRequests(page, '#navigate-away', [
|
||||||
|
{n: 'pageleave', u: `${LOCAL_SERVER_ADDR}/pageleave.html`}
|
||||||
|
])
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sends pageleave and pageview on hash-based SPA navigation', async ({ page }) => {
|
||||||
|
const pageviewRequestMock = mockRequest(page, '/api/event')
|
||||||
|
await page.goto('/pageleave-hash.html');
|
||||||
|
await pageviewRequestMock;
|
||||||
|
|
||||||
|
await clickPageElementAndExpectEventRequests(page, '#hash-nav', [
|
||||||
|
{n: 'pageleave', u: `${LOCAL_SERVER_ADDR}/pageleave-hash.html`},
|
||||||
|
{n: 'pageview', u: `${LOCAL_SERVER_ADDR}/pageleave-hash.html#some-hash`}
|
||||||
|
])
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sends pageleave and pageview on history-based SPA navigation', async ({ page }) => {
|
||||||
|
const pageviewRequestMock = mockRequest(page, '/api/event')
|
||||||
|
await page.goto('/pageleave.html');
|
||||||
|
await pageviewRequestMock;
|
||||||
|
|
||||||
|
await clickPageElementAndExpectEventRequests(page, '#history-nav', [
|
||||||
|
{n: 'pageleave', u: `${LOCAL_SERVER_ADDR}/pageleave.html`},
|
||||||
|
{n: 'pageview', u: `${LOCAL_SERVER_ADDR}/another-page`}
|
||||||
|
])
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sends pageleave with the manually overridden URL', async ({ page }) => {
|
||||||
|
await page.goto('/pageleave-manual.html');
|
||||||
|
|
||||||
|
await clickPageElementAndExpectEventRequests(page, '#pageview-trigger-custom-url', [
|
||||||
|
{n: 'pageview', u: 'https://example.com/custom/location'}
|
||||||
|
])
|
||||||
|
|
||||||
|
await clickPageElementAndExpectEventRequests(page, '#navigate-away', [
|
||||||
|
{n: 'pageleave', u: 'https://example.com/custom/location'}
|
||||||
|
])
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not send pageleave when pageview was not sent in manual mode', async ({ page }) => {
|
||||||
|
await page.goto('/pageleave-manual.html');
|
||||||
|
|
||||||
|
await clickPageElementAndExpectEventRequests(page, '#navigate-away', [], [
|
||||||
|
{n: 'pageleave'}
|
||||||
|
])
|
||||||
|
});
|
||||||
|
|
||||||
|
test('script.exclusions.hash.pageleave.js sends pageleave only from URLs where a pageview was sent', async ({ page }) => {
|
||||||
|
const pageBaseURL = `${LOCAL_SERVER_ADDR}/pageleave-hash-exclusions.html`
|
||||||
|
|
||||||
|
const pageviewRequestMock = mockRequest(page, '/api/event')
|
||||||
|
await page.goto('/pageleave-hash-exclusions.html');
|
||||||
|
await pageviewRequestMock;
|
||||||
|
|
||||||
|
// After the initial pageview is sent, navigate to ignored page ->
|
||||||
|
// pageleave event is sent from the initial page URL
|
||||||
|
await clickPageElementAndExpectEventRequests(page, '#ignored-hash-link', [
|
||||||
|
{n: 'pageleave', u: pageBaseURL, h: 1}
|
||||||
|
])
|
||||||
|
|
||||||
|
// Navigate from ignored page to a tracked page ->
|
||||||
|
// no pageleave from the current page, pageview on the next page
|
||||||
|
await clickPageElementAndExpectEventRequests(
|
||||||
|
page,
|
||||||
|
'#hash-link-1',
|
||||||
|
[{n: 'pageview', u: `${pageBaseURL}#hash1`, h: 1}],
|
||||||
|
[{n: 'pageleave'}]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Navigate from a tracked page to another tracked page ->
|
||||||
|
// pageleave with the last page URL, pageview with the new URL
|
||||||
|
await clickPageElementAndExpectEventRequests(
|
||||||
|
page,
|
||||||
|
'#hash-link-2',
|
||||||
|
[
|
||||||
|
{n: 'pageleave', u: `${pageBaseURL}#hash1`, h: 1},
|
||||||
|
{n: 'pageview', u: `${pageBaseURL}#hash2`, h: 1}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
});
|
||||||
|
});
|
@ -1,10 +1,10 @@
|
|||||||
const { expect } = require("@playwright/test");
|
const { expect } = require("@playwright/test");
|
||||||
|
|
||||||
// Mocks an HTTP request call with the given path. Returns a Promise that resolves to the request
|
// 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 10 seconds.
|
// data. If the request is not made, resolves to null after 3 seconds.
|
||||||
exports.mockRequest = function (page, path) {
|
const mockRequest = function (page, path) {
|
||||||
return new Promise((resolve, _reject) => {
|
return new Promise((resolve, _reject) => {
|
||||||
const requestTimeoutTimer = setTimeout(() => resolve(null), 10000)
|
const requestTimeoutTimer = setTimeout(() => resolve(null), 3000)
|
||||||
|
|
||||||
page.route(path, (route, request) => {
|
page.route(path, (route, request) => {
|
||||||
clearTimeout(requestTimeoutTimer)
|
clearTimeout(requestTimeoutTimer)
|
||||||
@ -14,6 +14,8 @@ exports.mockRequest = function (page, path) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.mockRequest = mockRequest
|
||||||
|
|
||||||
exports.metaKey = function() {
|
exports.metaKey = function() {
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
return 'Meta'
|
return 'Meta'
|
||||||
@ -23,13 +25,13 @@ exports.metaKey = function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Mocks a specified number of HTTP requests with given path. Returns a promise that resolves to a
|
// 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 10 seconds has passed.
|
// list of requests as soon as the specified number of requests is made, or 3 seconds has passed.
|
||||||
exports.mockManyRequests = function(page, path, numberOfRequests) {
|
const mockManyRequests = function(page, path, numberOfRequests) {
|
||||||
return new Promise((resolve, _reject) => {
|
return new Promise((resolve, _reject) => {
|
||||||
let requestList = []
|
let requestList = []
|
||||||
const requestTimeoutTimer = setTimeout(() => resolve(requestList), 10000)
|
const requestTimeoutTimer = setTimeout(() => resolve(requestList), 3000)
|
||||||
|
|
||||||
page.route('/api/event', (route, request) => {
|
page.route(path, (route, request) => {
|
||||||
requestList.push(request)
|
requestList.push(request)
|
||||||
if (requestList.length === numberOfRequests) {
|
if (requestList.length === numberOfRequests) {
|
||||||
clearTimeout(requestTimeoutTimer)
|
clearTimeout(requestTimeoutTimer)
|
||||||
@ -40,6 +42,8 @@ exports.mockManyRequests = function(page, path, numberOfRequests) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.mockManyRequests = mockManyRequests
|
||||||
|
|
||||||
exports.expectCustomEvent = function (request, eventName, eventProps) {
|
exports.expectCustomEvent = function (request, eventName, eventProps) {
|
||||||
const payload = request.postDataJSON()
|
const payload = request.postDataJSON()
|
||||||
|
|
||||||
@ -49,3 +53,34 @@ exports.expectCustomEvent = function (request, eventName, eventProps) {
|
|||||||
expect(payload.p[key]).toEqual(value)
|
expect(payload.p[key]).toEqual(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.clickPageElementAndExpectEventRequests = async function (page, locatorToClick, expectedBodySubsets, refutedBodySubsets = []) {
|
||||||
|
const requestsToExpect = expectedBodySubsets.length
|
||||||
|
const requestsToAwait = requestsToExpect + refutedBodySubsets.length
|
||||||
|
|
||||||
|
const plausibleRequestMockList = mockManyRequests(page, '/api/event', requestsToAwait)
|
||||||
|
await page.click(locatorToClick)
|
||||||
|
const requests = await plausibleRequestMockList
|
||||||
|
|
||||||
|
expect(requests.length).toBe(requestsToExpect)
|
||||||
|
|
||||||
|
expectedBodySubsets.forEach((bodySubset) => {
|
||||||
|
expect(requests.some((request) => {
|
||||||
|
return hasExpectedBodyParams(request, bodySubset)
|
||||||
|
})).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
refutedBodySubsets.forEach((bodySubset) => {
|
||||||
|
expect(requests.every((request) => {
|
||||||
|
return !hasExpectedBodyParams(request, bodySubset)
|
||||||
|
})).toBe(true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasExpectedBodyParams(request, expectedBodyParams) {
|
||||||
|
const body = request.postDataJSON()
|
||||||
|
|
||||||
|
return Object.keys(expectedBodyParams).every((key) => {
|
||||||
|
return body[key] === expectedBodyParams[key]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user