2026-08-27 08:23:21 +01:00
|
|
|
import { getNavigationSource } from '@/lib/analytics';
|
|
|
|
|
|
|
|
|
|
/** jsdom's document.referrer is read-only; redefining it is the way in. */
|
|
|
|
|
function referrer(url: string) {
|
|
|
|
|
Object.defineProperty(document, 'referrer', { value: url, configurable: true });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ORIGIN = 'http://localhost';
|
|
|
|
|
|
|
|
|
|
describe('getNavigationSource', () => {
|
|
|
|
|
afterEach(() => referrer(''));
|
|
|
|
|
|
|
|
|
|
it('attributes a visit from a location page to the place layer', () => {
|
|
|
|
|
/*
|
|
|
|
|
* The one this was added for.
|
|
|
|
|
*
|
|
|
|
|
* W2 published ~3,900 location pages whose entire purpose is to funnel
|
|
|
|
|
* search traffic onto school pages. Before this case existed they fell
|
|
|
|
|
* through to 'direct' — so the location layer's contribution was not
|
|
|
|
|
* merely missing from the funnel, it was being counted in the bucket you
|
|
|
|
|
* read as "typed the URL". The measurement that decides whether W2 worked
|
|
|
|
|
* was confidently reporting the wrong answer.
|
|
|
|
|
*/
|
|
|
|
|
referrer(`${ORIGIN}/schools/barnet`);
|
|
|
|
|
expect(getNavigationSource()).toBe('place');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it.each([
|
|
|
|
|
['/schools/authority/kent', 'authority'],
|
|
|
|
|
['/schools/near/sw11', 'outcode'],
|
|
|
|
|
['/schools/brentwood/primary', 'phase variant'],
|
|
|
|
|
])('covers %s (%s)', (path) => {
|
|
|
|
|
referrer(`${ORIGIN}${path}`);
|
|
|
|
|
expect(getNavigationSource()).toBe('place');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('still calls a school page "detail", one character away', () => {
|
|
|
|
|
// /school/ and /schools/ differ by one letter and mean different things.
|
|
|
|
|
// A prefix test written in the wrong order silently merges them.
|
|
|
|
|
referrer(`${ORIGIN}/school/100010-brecknock-primary-school`);
|
|
|
|
|
expect(getNavigationSource()).toBe('detail');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it.each([
|
|
|
|
|
['/', 'search'],
|
|
|
|
|
['/rankings', 'rankings'],
|
|
|
|
|
['/compare?urns=1,2', 'compare'],
|
|
|
|
|
])('leaves %s attributed as %s', (path, expected) => {
|
|
|
|
|
referrer(`${ORIGIN}${path}`);
|
|
|
|
|
expect(getNavigationSource()).toBe(expected);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('treats an external referrer as direct', () => {
|
|
|
|
|
// Umami records the real referrer on the pageview; this field is only
|
|
|
|
|
// about internal navigation.
|
|
|
|
|
referrer('https://www.google.com/search?q=schools+in+barnet');
|
|
|
|
|
expect(getNavigationSource()).toBe('direct');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('treats no referrer as direct', () => {
|
|
|
|
|
referrer('');
|
|
|
|
|
expect(getNavigationSource()).toBe('direct');
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-08-27 09:22:37 +01:00
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* The defect the existing suite could not see.
|
|
|
|
|
*
|
|
|
|
|
* Every test above sets document.referrer, which the browser writes only when
|
|
|
|
|
* a *document* loads. Every internal navigation in this app is an App Router
|
|
|
|
|
* soft navigation — history.pushState, no new document — so document.referrer
|
|
|
|
|
* keeps naming whatever opened the tab for the whole session. Verified on
|
|
|
|
|
* staging: /schools/brentwood → click a school → URL changes to /school/…
|
|
|
|
|
* and document.referrer is still "".
|
|
|
|
|
*
|
|
|
|
|
* So `from` reported 'direct' for essentially every in-app journey, and the
|
|
|
|
|
* suite passed because it only ever exercised the full-page-load path.
|
|
|
|
|
*/
|
|
|
|
|
function freshAnalytics() {
|
|
|
|
|
let mod!: typeof import('@/lib/analytics');
|
|
|
|
|
jest.isolateModules(() => {
|
|
|
|
|
mod = require('@/lib/analytics');
|
|
|
|
|
});
|
|
|
|
|
return mod;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function at(path: string) {
|
|
|
|
|
window.history.pushState({}, '', path);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe('getNavigationSource across a soft navigation', () => {
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
referrer('');
|
|
|
|
|
at('/');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('attributes a school view to the place page the user actually came from', () => {
|
|
|
|
|
const { recordVisitedPath, getNavigationSource: source } = freshAnalytics();
|
|
|
|
|
at('/schools/brentwood');
|
|
|
|
|
recordVisitedPath('/schools/brentwood');
|
|
|
|
|
|
|
|
|
|
at('/school/115429-brentwood-school');
|
|
|
|
|
recordVisitedPath('/school/115429-brentwood-school');
|
|
|
|
|
|
|
|
|
|
expect(source()).toBe('place');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('does not depend on whether the new path was recorded first', () => {
|
|
|
|
|
// The trail is written by a layout-level effect and read by a page-level
|
|
|
|
|
// one. React orders those by tree position, which is not a contract worth
|
|
|
|
|
// resting a measurement on, so the answer must be the same either way.
|
|
|
|
|
const { recordVisitedPath, getNavigationSource: source } = freshAnalytics();
|
|
|
|
|
recordVisitedPath('/rankings');
|
|
|
|
|
at('/school/115429-brentwood-school');
|
|
|
|
|
|
|
|
|
|
expect(source()).toBe('rankings');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('names the previous page, not the current one, when both are schools', () => {
|
|
|
|
|
const { recordVisitedPath, getNavigationSource: source } = freshAnalytics();
|
|
|
|
|
at('/school/100010-brecknock-primary-school');
|
|
|
|
|
recordVisitedPath('/school/100010-brecknock-primary-school');
|
|
|
|
|
|
|
|
|
|
at('/school/115429-brentwood-school');
|
|
|
|
|
recordVisitedPath('/school/115429-brentwood-school');
|
|
|
|
|
|
|
|
|
|
expect(source()).toBe('detail');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('looks past a return visit to the page the user came back from', () => {
|
|
|
|
|
const { recordVisitedPath, getNavigationSource: source } = freshAnalytics();
|
|
|
|
|
for (const p of ['/schools/brentwood', '/school/115429-brentwood-school',
|
|
|
|
|
'/schools/brentwood']) {
|
|
|
|
|
at(p);
|
|
|
|
|
recordVisitedPath(p);
|
|
|
|
|
}
|
|
|
|
|
expect(source()).toBe('detail');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('falls back to the referrer on a real document load, where it is true', () => {
|
|
|
|
|
// A fresh module is a fresh document: nothing has been recorded, and
|
|
|
|
|
// document.referrer is meaningful again.
|
|
|
|
|
const { getNavigationSource: source } = freshAnalytics();
|
|
|
|
|
at('/school/115429-brentwood-school');
|
|
|
|
|
referrer(`${ORIGIN}/schools/barnet`);
|
|
|
|
|
|
|
|
|
|
expect(source()).toBe('place');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('still reads an arrival from outside as direct', () => {
|
|
|
|
|
const { recordVisitedPath, getNavigationSource: source } = freshAnalytics();
|
|
|
|
|
at('/schools/brentwood');
|
|
|
|
|
recordVisitedPath('/schools/brentwood');
|
|
|
|
|
referrer('https://www.google.com/search?q=schools+in+brentwood');
|
|
|
|
|
|
|
|
|
|
expect(source()).toBe('direct');
|
|
|
|
|
});
|
|
|
|
|
});
|