mirror of
https://github.com/plausible/analytics.git
synced 2024-12-22 17:11:36 +03:00
Average Scroll Depth Metric: extracted tracker changes (#4826)
* (cherry-pick) implement scroll depth tracking under pageleave variant * drop unnecessary vars * remove unused require * add scroll depth tests * improve error messages in test util * reevaluate currentDocumentHeight on page load * account for dynamically loaded content when initializing documnent height * remove all semicolons from tracker specs * allow window and document globals in tracker eslint * tweak global tracker dir eslint rules * update comment * reevaluate document height on scroll * add test * remove unneccessary timeout
This commit is contained in:
parent
4d9ea15e9e
commit
2a7d02b6f0
@ -2,12 +2,35 @@
|
||||
"parserOptions": { "ecmaVersion": "latest" },
|
||||
"env": { "node": true, "es6": true },
|
||||
"extends": ["eslint:recommended", "plugin:playwright/playwright-test"],
|
||||
"globals": {
|
||||
"window": "readonly",
|
||||
"document": "readonly"
|
||||
},
|
||||
"rules": {
|
||||
"max-len": [0, {"code": 120}],
|
||||
"max-classes-per-file": [0],
|
||||
"no-unused-expressions": [1, { "allowShortCircuit": true }],
|
||||
"no-unused-vars": [2, { "varsIgnorePattern": "^_", "argsIgnorePattern": "^_" }],
|
||||
"no-prototype-builtins": [0],
|
||||
"playwright/no-conditional-in-test": [0]
|
||||
}
|
||||
"playwright/no-conditional-in-test": [0],
|
||||
"playwright/no-wait-for-timeout": "off",
|
||||
"playwright/expect-expect": [
|
||||
"error",
|
||||
{
|
||||
"assertFunctionNames": [
|
||||
"expect",
|
||||
"clickPageElementAndExpectEventRequests",
|
||||
"expectCustomEvent"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.spec.js"],
|
||||
"rules": {
|
||||
"semi": ["warn", "never"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -48,6 +48,49 @@
|
||||
// flag prevents sending multiple pageleaves in those cases.
|
||||
var pageLeaveSending = false
|
||||
|
||||
function getDocumentHeight() {
|
||||
return Math.max(
|
||||
document.body.scrollHeight || 0,
|
||||
document.body.offsetHeight || 0,
|
||||
document.body.clientHeight || 0,
|
||||
document.documentElement.scrollHeight || 0,
|
||||
document.documentElement.offsetHeight || 0,
|
||||
document.documentElement.clientHeight || 0
|
||||
)
|
||||
}
|
||||
|
||||
function getCurrentScrollDepthPx() {
|
||||
var viewportHeight = window.innerHeight || document.documentElement.clientHeight || 0
|
||||
var scrollTop = window.scrollY || document.documentElement.scrollTop || document.body.scrollTop || 0
|
||||
|
||||
return currentDocumentHeight <= viewportHeight ? currentDocumentHeight : scrollTop + viewportHeight
|
||||
}
|
||||
|
||||
var currentDocumentHeight = getDocumentHeight()
|
||||
var maxScrollDepthPx = getCurrentScrollDepthPx()
|
||||
|
||||
window.addEventListener('load', function () {
|
||||
currentDocumentHeight = getDocumentHeight()
|
||||
|
||||
// Update the document height again after every 200ms during the
|
||||
// next 3 seconds. This makes sure dynamically loaded content is
|
||||
// also accounted for.
|
||||
var count = 0
|
||||
var interval = setInterval(function () {
|
||||
currentDocumentHeight = getDocumentHeight()
|
||||
if (++count === 15) {clearInterval(interval)}
|
||||
}, 200)
|
||||
})
|
||||
|
||||
document.addEventListener('scroll', function() {
|
||||
currentDocumentHeight = getDocumentHeight()
|
||||
var currentScrollDepthPx = getCurrentScrollDepthPx()
|
||||
|
||||
if (currentScrollDepthPx > maxScrollDepthPx) {
|
||||
maxScrollDepthPx = currentScrollDepthPx
|
||||
}
|
||||
})
|
||||
|
||||
function triggerPageLeave() {
|
||||
if (pageLeaveSending) {return}
|
||||
pageLeaveSending = true
|
||||
@ -55,6 +98,7 @@
|
||||
|
||||
var payload = {
|
||||
n: 'pageleave',
|
||||
sd: Math.round((maxScrollDepthPx / currentDocumentHeight) * 100),
|
||||
d: dataDomain,
|
||||
u: currentPageLeaveURL,
|
||||
}
|
||||
@ -202,6 +246,8 @@
|
||||
if (isSPANavigation && listeningPageLeave) {
|
||||
triggerPageLeave();
|
||||
currentPageLeaveURL = location.href;
|
||||
currentDocumentHeight = getDocumentHeight()
|
||||
maxScrollDepthPx = getCurrentScrollDepthPx()
|
||||
}
|
||||
{{/if}}
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
const { mockRequest, mockManyRequests, expectCustomEvent } = require('./support/test-utils');
|
||||
const { expect, test } = require('@playwright/test');
|
||||
const { LOCAL_SERVER_ADDR } = require('./support/server');
|
||||
|
||||
const { mockRequest, mockManyRequests, expectCustomEvent } = require('./support/test-utils')
|
||||
const { expect, test } = require('@playwright/test')
|
||||
const { LOCAL_SERVER_ADDR } = require('./support/server')
|
||||
|
||||
test.describe('script.file-downloads.outbound-links.tagged-events.js', () => {
|
||||
test('sends only outbound link event when clicked link is both download and outbound', async ({ page }) => {
|
||||
@ -14,7 +13,7 @@ test.describe('script.file-downloads.outbound-links.tagged-events.js', () => {
|
||||
const requests = await plausibleRequestMockList
|
||||
expect(requests.length).toBe(1)
|
||||
expectCustomEvent(requests[0], 'Outbound Link: Click', {url: downloadURL})
|
||||
});
|
||||
})
|
||||
|
||||
test('sends file download event when local download link clicked', async ({ page }) => {
|
||||
await page.goto('/custom-event-edge-case.html')
|
||||
@ -24,7 +23,7 @@ test.describe('script.file-downloads.outbound-links.tagged-events.js', () => {
|
||||
await page.click('#local-download')
|
||||
|
||||
expectCustomEvent(await plausibleRequestMock, 'File Download', {url: downloadURL})
|
||||
});
|
||||
})
|
||||
|
||||
test('sends only tagged event when clicked link is tagged + outbound + download', async ({ page }) => {
|
||||
await page.goto('/custom-event-edge-case.html')
|
||||
@ -35,5 +34,5 @@ test.describe('script.file-downloads.outbound-links.tagged-events.js', () => {
|
||||
const requests = await plausibleRequestMockList
|
||||
expect(requests.length).toBe(1)
|
||||
expectCustomEvent(requests[0], 'Foo', {})
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
||||
|
@ -1,7 +1,6 @@
|
||||
const { mockRequest, expectCustomEvent, mockManyRequests, metaKey } = require('./support/test-utils');
|
||||
const { expect, test } = require('@playwright/test');
|
||||
const { LOCAL_SERVER_ADDR } = require('./support/server');
|
||||
|
||||
const { mockRequest, expectCustomEvent, mockManyRequests, metaKey } = require('./support/test-utils')
|
||||
const { expect, test } = require('@playwright/test')
|
||||
const { LOCAL_SERVER_ADDR } = require('./support/server')
|
||||
|
||||
test.describe('file-downloads extension', () => {
|
||||
test('sends event and does not start download when link opens in new tab', async ({ page }) => {
|
||||
@ -14,7 +13,7 @@ test.describe('file-downloads extension', () => {
|
||||
|
||||
expectCustomEvent(await plausibleRequestMock, 'File Download', { url: downloadURL })
|
||||
expect(await downloadRequestMock, "should not make download request").toBeNull()
|
||||
});
|
||||
})
|
||||
|
||||
test('sends event and starts download when link child is clicked', async ({ page }) => {
|
||||
await page.goto('/file-download.html')
|
||||
@ -26,7 +25,7 @@ test.describe('file-downloads extension', () => {
|
||||
|
||||
expectCustomEvent(await plausibleRequestMock, 'File Download', { url: downloadURL })
|
||||
expect((await downloadRequestMock).url()).toContain(downloadURL)
|
||||
});
|
||||
})
|
||||
|
||||
test('sends File Download event with query-stripped url property', async ({ page }) => {
|
||||
await page.goto('/file-download.html')
|
||||
@ -37,7 +36,7 @@ test.describe('file-downloads extension', () => {
|
||||
|
||||
const expectedURL = downloadURL.split("?")[0]
|
||||
expectCustomEvent(await plausibleRequestMock, 'File Download', { url: expectedURL })
|
||||
});
|
||||
})
|
||||
|
||||
test('starts download only once', async ({ page }) => {
|
||||
await page.goto('/file-download.html')
|
||||
@ -47,5 +46,5 @@ test.describe('file-downloads extension', () => {
|
||||
await page.click('#local-download')
|
||||
|
||||
expect((await downloadRequestMockList).length).toBe(1)
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
||||
|
BIN
tracker/test/fixtures/img/black3x3000.png
vendored
Normal file
BIN
tracker/test/fixtures/img/black3x3000.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 109 B |
35
tracker/test/fixtures/scroll-depth-content-onscroll.html
vendored
Normal file
35
tracker/test/fixtures/scroll-depth-content-onscroll.html
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
<!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>
|
||||
<div id="main" style="height: 3000px; background-color: lightblue;">
|
||||
<a id="navigate-away" href="/manual.html">Navigate away</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const mainDiv = document.getElementById("main")
|
||||
let loadedMore = false
|
||||
|
||||
window.addEventListener("scroll", () => {
|
||||
if (!loadedMore && window.innerHeight + window.scrollY >= document.body.offsetHeight) {
|
||||
loadedMore = true
|
||||
|
||||
const newDiv = document.createElement("div")
|
||||
newDiv.setAttribute("id", "more-content")
|
||||
newDiv.setAttribute("style", "height: 2000px; background-color: lightcoral;")
|
||||
document.body.appendChild(newDiv);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
25
tracker/test/fixtures/scroll-depth-dynamic-content-load.html
vendored
Normal file
25
tracker/test/fixtures/scroll-depth-dynamic-content-load.html
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Document</title>
|
||||
<script defer src="/tracker/js/plausible.local.pageleave.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<br>
|
||||
<div id="content"></div>
|
||||
<script>
|
||||
setTimeout(() => {
|
||||
document.getElementById('content').innerHTML =
|
||||
`
|
||||
<div style="height: 5000px; background-color: gray;">
|
||||
<a id="navigate-away" href="/manual.html">
|
||||
Navigate away
|
||||
</a>
|
||||
</div>
|
||||
`
|
||||
}, 500)
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
37
tracker/test/fixtures/scroll-depth-hash.html
vendored
Normal file
37
tracker/test/fixtures/scroll-depth-hash.html
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
<!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>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav>
|
||||
<a id="home-link" href="#home">Home</a>
|
||||
<a id="about-link" href="#about">About</a>
|
||||
</nav>
|
||||
|
||||
<div id="content"></div>
|
||||
|
||||
<script>
|
||||
const routes = {
|
||||
'#home': '<h1>Home</h1><p>Welcome to the Home page!</p>',
|
||||
'#about': '<h1>About</h1><p style="height: 2000px;">Learn more about us here</p>',
|
||||
};
|
||||
|
||||
function loadContent() {
|
||||
const hash = window.location.hash || '#home';
|
||||
const content = routes[hash]
|
||||
document.getElementById('content').innerHTML = content;
|
||||
}
|
||||
|
||||
window.addEventListener('hashchange', loadContent);
|
||||
window.addEventListener('DOMContentLoaded', loadContent);
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
16
tracker/test/fixtures/scroll-depth-slow-window-load.html
vendored
Normal file
16
tracker/test/fixtures/scroll-depth-slow-window-load.html
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Document</title>
|
||||
<script defer src="/tracker/js/plausible.local.pageleave.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<a id="navigate-away" href="/manual.html">
|
||||
Navigate away
|
||||
</a>
|
||||
<br>
|
||||
<img id="slow-image" src="/img/slow-image" alt="slow image">
|
||||
</body>
|
||||
</html>
|
14
tracker/test/fixtures/scroll-depth.html
vendored
Normal file
14
tracker/test/fixtures/scroll-depth.html
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<script defer src="/tracker/js/plausible.local.pageleave.js"></script>
|
||||
<title>Document</title>
|
||||
</head>
|
||||
<body>
|
||||
<div style="height: 5000px; background: repeating-linear-gradient(white, gray 500px);">
|
||||
<a id="navigate-away" href="/manual.html">Navigate away</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -1,12 +1,12 @@
|
||||
const { mockRequest } = require('./support/test-utils')
|
||||
const { expect, test } = require('@playwright/test');
|
||||
const { expect, test } = require('@playwright/test')
|
||||
|
||||
test.describe('combination of hash and exclusions script extensions', () => {
|
||||
test('excludes by hash part of the URL', async ({ page }) => {
|
||||
const plausibleRequestMock = mockRequest(page, '/api/event')
|
||||
|
||||
await page.goto('/hash-exclusions.html#this/hash/should/be/ignored');
|
||||
await page.goto('/hash-exclusions.html#this/hash/should/be/ignored')
|
||||
|
||||
expect(await plausibleRequestMock, "should not have sent event").toBeNull()
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
||||
|
@ -1,11 +1,10 @@
|
||||
/* eslint-disable playwright/expect-expect */
|
||||
const { clickPageElementAndExpectEventRequests } = require('./support/test-utils')
|
||||
const { test } = require('@playwright/test');
|
||||
const { LOCAL_SERVER_ADDR } = require('./support/server');
|
||||
const { test } = require('@playwright/test')
|
||||
const { LOCAL_SERVER_ADDR } = require('./support/server')
|
||||
|
||||
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 }) => {
|
||||
await page.goto('/manual.html');
|
||||
await page.goto('/manual.html')
|
||||
|
||||
await clickPageElementAndExpectEventRequests(page, '#pageview-trigger', [
|
||||
{n: 'pageview', u: `${LOCAL_SERVER_ADDR}/manual.html`}
|
||||
@ -16,10 +15,10 @@ test.describe('manual extension', () => {
|
||||
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 }) => {
|
||||
await page.goto('/manual.html');
|
||||
await page.goto('/manual.html')
|
||||
|
||||
await clickPageElementAndExpectEventRequests(page, '#pageview-trigger-custom-url', [
|
||||
{n: 'pageview', u: `https://example.com/custom/location`}
|
||||
@ -30,5 +29,5 @@ test.describe('manual extension', () => {
|
||||
await clickPageElementAndExpectEventRequests(page, '#custom-event-trigger-custom-url', [
|
||||
{n: 'CustomEvent', u: `https://example.com/custom/location`}
|
||||
])
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
||||
|
@ -1,5 +1,5 @@
|
||||
const { mockRequest, expectCustomEvent, metaKey } = require('./support/test-utils')
|
||||
const { expect, test } = require('@playwright/test');
|
||||
const { expect, test } = require('@playwright/test')
|
||||
|
||||
test.describe('outbound-links extension', () => {
|
||||
|
||||
@ -14,7 +14,7 @@ test.describe('outbound-links extension', () => {
|
||||
|
||||
expectCustomEvent(await plausibleRequestMock, 'Outbound Link: Click', { url: outboundURL })
|
||||
expect(await navigationRequestMock, "should not have made navigation request").toBeNull()
|
||||
});
|
||||
})
|
||||
|
||||
test('sends event and navigates to target when link child is clicked', async ({ page }) => {
|
||||
await page.goto('/outbound-link.html')
|
||||
@ -28,7 +28,7 @@ test.describe('outbound-links extension', () => {
|
||||
const navigationRequest = await navigationRequestMock
|
||||
expectCustomEvent(await plausibleRequestMock, 'Outbound Link: Click', { url: outboundURL })
|
||||
expect(navigationRequest.url()).toContain(outboundURL)
|
||||
});
|
||||
})
|
||||
|
||||
test('sends event and does not navigate if default externally prevented', async ({ page }) => {
|
||||
await page.goto('/outbound-link.html')
|
||||
@ -41,5 +41,5 @@ test.describe('outbound-links extension', () => {
|
||||
|
||||
expectCustomEvent(await plausibleRequestMock, 'Outbound Link: Click', { url: outboundURL })
|
||||
expect(await navigationRequestMock, "should not have made navigation request").toBeNull()
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
||||
|
@ -1,46 +1,45 @@
|
||||
/* 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');
|
||||
const { test } = require('@playwright/test')
|
||||
const { LOCAL_SERVER_ADDR } = require('./support/server')
|
||||
|
||||
test.describe('pageleave extension', () => {
|
||||
test.skip(({browserName}) => browserName === 'webkit', 'Not testable on Webkit');
|
||||
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 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 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 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 page.goto('/pageleave-manual.html')
|
||||
|
||||
await clickPageElementAndExpectEventRequests(page, '#pageview-trigger-custom-url', [
|
||||
{n: 'pageview', u: 'https://example.com/custom/location'}
|
||||
@ -49,22 +48,22 @@ test.describe('pageleave extension', () => {
|
||||
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 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;
|
||||
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
|
||||
@ -91,5 +90,5 @@ test.describe('pageleave extension', () => {
|
||||
{n: 'pageview', u: `${pageBaseURL}#hash2`, h: 1}
|
||||
],
|
||||
)
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
@ -1,13 +1,13 @@
|
||||
const { mockRequest } = require('./support/test-utils')
|
||||
const { expect, test } = require('@playwright/test');
|
||||
const { expect, test } = require('@playwright/test')
|
||||
|
||||
test.describe('Basic installation', () => {
|
||||
test('Sends pageview automatically', async ({ page }) => {
|
||||
const plausibleRequestMock = mockRequest(page, '/api/event')
|
||||
await page.goto('/simple.html');
|
||||
await page.goto('/simple.html')
|
||||
|
||||
const plausibleRequest = await plausibleRequestMock;
|
||||
const plausibleRequest = await plausibleRequestMock
|
||||
expect(plausibleRequest.url()).toContain('/api/event')
|
||||
expect(plausibleRequest.postDataJSON().n).toEqual('pageview')
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
||||
|
@ -1,22 +1,22 @@
|
||||
const { mockRequest, expectCustomEvent } = require('./support/test-utils');
|
||||
const { expect, test } = require('@playwright/test');
|
||||
const { mockRequest, expectCustomEvent } = require('./support/test-utils')
|
||||
const { expect, test } = require('@playwright/test')
|
||||
|
||||
test.describe('with revenue script extension', () => {
|
||||
test('sends revenue currency and amount in manual mode', async ({ page }) => {
|
||||
const plausibleRequestMock = mockRequest(page, '/api/event')
|
||||
await page.goto('/revenue.html');
|
||||
await page.goto('/revenue.html')
|
||||
await page.click('#manual-purchase')
|
||||
|
||||
const plausibleRequest = await plausibleRequestMock
|
||||
expect(plausibleRequest.postDataJSON()["$"]).toEqual({amount: 15.99, currency: "USD"})
|
||||
});
|
||||
})
|
||||
|
||||
test('sends revenue currency and amount with tagged class name', async ({ page }) => {
|
||||
const plausibleRequestMock = mockRequest(page, '/api/event')
|
||||
await page.goto('/revenue.html');
|
||||
await page.goto('/revenue.html')
|
||||
await page.click('#tagged-purchase')
|
||||
|
||||
const plausibleRequest = await plausibleRequestMock
|
||||
expect(plausibleRequest.postDataJSON()["$"]).toEqual({amount: "13.32", currency: "EUR"})
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
||||
|
89
tracker/test/scroll-depth.spec.js
Normal file
89
tracker/test/scroll-depth.spec.js
Normal file
@ -0,0 +1,89 @@
|
||||
/* eslint-disable playwright/no-skipped-test */
|
||||
const { clickPageElementAndExpectEventRequests, mockRequest } = require('./support/test-utils')
|
||||
const { test } = require('@playwright/test')
|
||||
const { LOCAL_SERVER_ADDR } = require('./support/server')
|
||||
|
||||
test.describe('scroll depth', () => {
|
||||
test.skip(({browserName}) => browserName === 'webkit', 'Not testable on Webkit')
|
||||
|
||||
test('sends scroll_depth in the pageleave payload when navigating to the next page', async ({ page }) => {
|
||||
const pageviewRequestMock = mockRequest(page, '/api/event')
|
||||
await page.goto('/scroll-depth.html')
|
||||
await pageviewRequestMock
|
||||
|
||||
await page.evaluate(() => window.scrollBy(0, 300))
|
||||
await page.evaluate(() => window.scrollBy(0, 0))
|
||||
|
||||
await clickPageElementAndExpectEventRequests(page, '#navigate-away', [
|
||||
{n: 'pageleave', u: `${LOCAL_SERVER_ADDR}/scroll-depth.html`, sd: 20}
|
||||
])
|
||||
})
|
||||
|
||||
test('sends scroll depth on hash navigation', async ({ page }) => {
|
||||
const pageviewRequestMock = mockRequest(page, '/api/event')
|
||||
await page.goto('/scroll-depth-hash.html')
|
||||
await pageviewRequestMock
|
||||
|
||||
await clickPageElementAndExpectEventRequests(page, '#about-link', [
|
||||
{n: 'pageleave', u: `${LOCAL_SERVER_ADDR}/scroll-depth-hash.html`, sd: 100},
|
||||
{n: 'pageview', u: `${LOCAL_SERVER_ADDR}/scroll-depth-hash.html#about`}
|
||||
])
|
||||
|
||||
// Wait 600ms before navigating again because pageleave events are throttled to 500ms.
|
||||
await page.waitForTimeout(600)
|
||||
|
||||
await clickPageElementAndExpectEventRequests(page, '#home-link', [
|
||||
{n: 'pageleave', u: `${LOCAL_SERVER_ADDR}/scroll-depth-hash.html#about`, sd: 34},
|
||||
{n: 'pageview', u: `${LOCAL_SERVER_ADDR}/scroll-depth-hash.html#home`}
|
||||
])
|
||||
})
|
||||
|
||||
test('document height gets reevaluated after window load', async ({ page }) => {
|
||||
const pageviewRequestMock = mockRequest(page, '/api/event')
|
||||
await page.goto('/scroll-depth-slow-window-load.html')
|
||||
await pageviewRequestMock
|
||||
|
||||
// Wait for the image to be loaded
|
||||
await page.waitForFunction(() => {
|
||||
return document.getElementById('slow-image').complete
|
||||
})
|
||||
|
||||
await clickPageElementAndExpectEventRequests(page, '#navigate-away', [
|
||||
{n: 'pageleave', u: `${LOCAL_SERVER_ADDR}/scroll-depth-slow-window-load.html`, sd: 24}
|
||||
])
|
||||
})
|
||||
|
||||
test('dynamically loaded content affects documentHeight', async ({ page }) => {
|
||||
const pageviewRequestMock = mockRequest(page, '/api/event')
|
||||
await page.goto('/scroll-depth-dynamic-content-load.html')
|
||||
await pageviewRequestMock
|
||||
|
||||
// The link appears dynamically after 500ms.
|
||||
await clickPageElementAndExpectEventRequests(page, '#navigate-away', [
|
||||
{n: 'pageleave', u: `${LOCAL_SERVER_ADDR}/scroll-depth-dynamic-content-load.html`, sd: 14}
|
||||
])
|
||||
})
|
||||
|
||||
test('document height gets reevaluated on scroll', async ({ page }) => {
|
||||
const pageviewRequestMock = mockRequest(page, '/api/event')
|
||||
await page.goto('/scroll-depth-content-onscroll.html')
|
||||
await pageviewRequestMock
|
||||
|
||||
// During the first 3 seconds, the script periodically updates document height
|
||||
// to account for dynamically loaded content. Since we want to test document
|
||||
// height also getting updated on scroll, we need to just wait for 3 seconds.
|
||||
await page.waitForTimeout(3100)
|
||||
|
||||
// scroll to the bottom of the page
|
||||
await page.evaluate(() => window.scrollBy(0, document.body.scrollHeight))
|
||||
|
||||
// Wait until documentHeight gets increased by the fixture JS
|
||||
await page.waitForSelector('#more-content')
|
||||
|
||||
await page.evaluate(() => window.scrollBy(0, 1000))
|
||||
|
||||
await clickPageElementAndExpectEventRequests(page, '#navigate-away', [
|
||||
{n: 'pageleave', u: `${LOCAL_SERVER_ADDR}/scroll-depth-content-onscroll.html`, sd: 80}
|
||||
])
|
||||
})
|
||||
})
|
@ -11,6 +11,13 @@ exports.runLocalFileServer = function () {
|
||||
app.use(express.static(FIXTURES_PATH));
|
||||
app.use('/tracker', express.static(TRACKERS_PATH));
|
||||
|
||||
// A test utility - serve an image with an artificial delay
|
||||
app.get('/img/slow-image', (_req, res) => {
|
||||
setTimeout(() => {
|
||||
res.sendFile(path.join(FIXTURES_PATH, '/img/black3x3000.png'));
|
||||
}, 100);
|
||||
});
|
||||
|
||||
app.listen(LOCAL_SERVER_PORT, function () {
|
||||
console.log(`Local server listening on ${LOCAL_SERVER_ADDR}`)
|
||||
});
|
||||
|
@ -54,33 +54,56 @@ exports.expectCustomEvent = function (request, eventName, eventProps) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A powerful utility function that makes it easy to assert on the event
|
||||
* requests that should or should not have been made after clicking a page
|
||||
* element.
|
||||
*
|
||||
* This function accepts subsets of request bodies (the JSON payloads) as
|
||||
* arguments, and compares them with the bodies of the requests that were
|
||||
* actually made. For a body subset to match a request, all the key-value
|
||||
* pairs present in the subset should also appear in the request body.
|
||||
*/
|
||||
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
|
||||
const requestBodies = (await plausibleRequestMockList).map(r => r.postDataJSON())
|
||||
|
||||
expect(requests.length).toBe(requestsToExpect)
|
||||
const expectedButNotFoundBodySubsets = []
|
||||
|
||||
expectedBodySubsets.forEach((bodySubset) => {
|
||||
expect(requests.some((request) => {
|
||||
return hasExpectedBodyParams(request, bodySubset)
|
||||
})).toBe(true)
|
||||
const wasFound = requestBodies.some((requestBody) => {
|
||||
return includesSubset(requestBody, bodySubset)
|
||||
})
|
||||
|
||||
if (!wasFound) {expectedButNotFoundBodySubsets.push(bodySubset)}
|
||||
})
|
||||
|
||||
const refutedButFoundRequestBodies = []
|
||||
|
||||
refutedBodySubsets.forEach((bodySubset) => {
|
||||
expect(requests.every((request) => {
|
||||
return !hasExpectedBodyParams(request, bodySubset)
|
||||
})).toBe(true)
|
||||
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 hasExpectedBodyParams(request, expectedBodyParams) {
|
||||
const body = request.postDataJSON()
|
||||
|
||||
return Object.keys(expectedBodyParams).every((key) => {
|
||||
return body[key] === expectedBodyParams[key]
|
||||
function includesSubset(body, subset) {
|
||||
return Object.keys(subset).every((key) => {
|
||||
return body[key] === subset[key]
|
||||
})
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ test.describe('tagged-events extension', () => {
|
||||
const plausibleRequestMock = mockRequest(page, '/api/event')
|
||||
await page.click('#link')
|
||||
expectCustomEvent(await plausibleRequestMock, 'Payment Complete', { amount: '100', method: "Credit Card", url: linkURL })
|
||||
});
|
||||
})
|
||||
|
||||
test('tracks a tagged form submit with custom props when submitting by pressing enter', async ({ page }) => {
|
||||
await page.goto('/tagged-event.html')
|
||||
@ -21,7 +21,7 @@ test.describe('tagged-events extension', () => {
|
||||
await inputLocator.press('Enter')
|
||||
|
||||
expectCustomEvent(await plausibleRequestMock, 'Signup', { type: "Newsletter" })
|
||||
});
|
||||
})
|
||||
|
||||
test('tracks submit on a form with a tagged parent when submit button is clicked', async ({ page }) => {
|
||||
await page.goto('/tagged-event.html')
|
||||
@ -34,7 +34,7 @@ test.describe('tagged-events extension', () => {
|
||||
|
||||
expect(requests.length).toBe(1)
|
||||
expectCustomEvent(requests[0], "Form Submit", {})
|
||||
});
|
||||
})
|
||||
|
||||
test('tracks click and auxclick on any tagged HTML element', async ({ page }) => {
|
||||
await page.goto('/tagged-event.html')
|
||||
@ -48,7 +48,7 @@ test.describe('tagged-events extension', () => {
|
||||
const requests = await plausibleRequestMockList
|
||||
expect(requests.length).toBe(3)
|
||||
requests.forEach(request => expectCustomEvent(request, 'Custom Event', { foo: "bar" }))
|
||||
});
|
||||
})
|
||||
|
||||
test('does not track elements without plausible-event-name class + link elements navigate', async ({ page }) => {
|
||||
await page.goto('/tagged-event.html')
|
||||
@ -65,7 +65,7 @@ test.describe('tagged-events extension', () => {
|
||||
|
||||
expect(await plausibleRequestMock, "should not have made Plausible request").toBeNull()
|
||||
expect((await navigationRequestMock).url()).toContain(linkURL)
|
||||
});
|
||||
})
|
||||
|
||||
test('tracks tagged HTML elements when their child element is clicked', async ({ page }) => {
|
||||
await page.goto('/tagged-event.html')
|
||||
@ -78,7 +78,7 @@ test.describe('tagged-events extension', () => {
|
||||
const requests = await plausibleRequestMockList
|
||||
expect(requests.length).toBe(2)
|
||||
requests.forEach(request => expectCustomEvent(request, 'Custom Event', { foo: "bar" }))
|
||||
});
|
||||
})
|
||||
|
||||
test('tracks tagged element that is dynamically added to the DOM', async ({ page }) => {
|
||||
await page.goto('/tagged-event.html')
|
||||
@ -92,7 +92,7 @@ test.describe('tagged-events extension', () => {
|
||||
await buttonLocator.click()
|
||||
|
||||
expectCustomEvent(await plausibleRequestMock, 'Custom Event', {})
|
||||
});
|
||||
})
|
||||
|
||||
test('does not track clicks inside a tagged form, except submit click', async ({ page }) => {
|
||||
await page.goto('/tagged-event.html')
|
||||
@ -104,5 +104,5 @@ test.describe('tagged-events extension', () => {
|
||||
await page.click('#form-div')
|
||||
|
||||
expect(await plausibleRequestMock, "should not have made Plausible request").toBeNull()
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user