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'); }); }); /* * 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'); }); });