/* eslint-disable unicorn/no-null */ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; import { mockWindowHistory, mockWindowLocation, restoreWindowHistory, restoreWindowLocation, WindowHistoryMock } from './mockWindowLocation'; /** * Taken from https://en.wikipedia.org/wiki/Uniform_Resource_Identifier#Example_URIs * * userinfo host port * ┌──┴───┐ ┌──────┴──────┐ ┌┴┐ * https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top * └─┬─┘ └─────────────┬────────────┘└───────┬───────┘ └────────────┬────────────┘ └┬┘ * scheme authority path query fragment * */ const urlNetworking = 'https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top'; const urlComputing = 'https://john.doe@www.example.com:123/forum/answers/?tag=computing'; const urlMathematics = 'https://john.doe@www.example.com:123/forum/answers/?tag=mathematics'; const urlCryptography = 'https://john.doe@www.example.com:123/forum/answers/?tag=cryptography'; const urlWikipedia = 'https://en.wikipedia.org/wiki/Main_Page'; beforeEach(() => { mockWindowLocation(urlNetworking); mockWindowHistory(); }); afterEach(() => { restoreWindowLocation(); restoreWindowHistory(); }); describe('window.location', () => { test('mock and restore window.location', () => { expect(window.location.href).toBe(urlNetworking); expect(document.location.href).toBe('http://localhost:3000/'); // :-/ expect(document.URL).toBe('http://localhost:3000/'); // :-/ expect(document.documentURI).toBe('http://localhost:3000/'); // :-/ restoreWindowLocation(); expect(window.location.href).toBe('http://localhost:3000/'); expect(document.location.href).toBe('http://localhost:3000/'); expect(document.URL).toBe('http://localhost:3000/'); expect(document.documentURI).toBe('http://localhost:3000/'); }); test('getters', () => { expect(window.location.toString()).toBe(urlNetworking); expect(window.location.href).toBe(urlNetworking); expect(window.location.origin).toBe('https://www.example.com:123'); expect(window.location.protocol).toBe('https:'); expect(window.location.host).toBe('www.example.com:123'); expect(window.location.hostname).toBe('www.example.com'); expect(window.location.port).toBe('123'); expect(window.location.pathname).toBe('/forum/questions/'); expect(window.location.search).toBe('?tag=networking&order=newest'); expect(window.location.hash).toBe('#top'); }); test('set href', () => { expect(window.location.href).toBe(urlNetworking); window.location.href = urlWikipedia; expect(window.history.length).toBe(2); expect((window.history as WindowHistoryMock).sessionHistory).toEqual([ { state: null, url: urlNetworking }, { state: null, url: urlWikipedia } ]); expect(window.history.state).toBeNull(); expect(window.location.href).toBe(urlWikipedia); }); test('set protocol', () => { expect(window.location.protocol).toBe('https:'); window.location.protocol = 'http:'; expect(window.history.length).toBe(2); expect((window.history as WindowHistoryMock).sessionHistory).toEqual([ { state: null, url: urlNetworking }, { state: null, url: 'http://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top' } ]); expect(window.history.state).toBeNull(); expect(window.location.href).toBe( 'http://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top' ); }); test('set host', () => { expect(window.location.host).toBe('www.example.com:123'); window.location.host = 'en.wikipedia.org'; expect(window.history.length).toBe(2); expect((window.history as WindowHistoryMock).sessionHistory).toEqual([ { state: null, url: urlNetworking }, { state: null, url: 'https://john.doe@en.wikipedia.org:123/forum/questions/?tag=networking&order=newest#top' } ]); expect(window.history.state).toBeNull(); expect(window.location.href).toBe( 'https://john.doe@en.wikipedia.org:123/forum/questions/?tag=networking&order=newest#top' ); }); test('set hostname', () => { expect(window.location.hostname).toBe('www.example.com'); window.location.hostname = 'en.wikipedia.org'; expect(window.history.length).toBe(2); expect((window.history as WindowHistoryMock).sessionHistory).toEqual([ { state: null, url: urlNetworking }, { state: null, url: 'https://john.doe@en.wikipedia.org:123/forum/questions/?tag=networking&order=newest#top' } ]); expect(window.history.state).toBeNull(); expect(window.location.href).toBe( 'https://john.doe@en.wikipedia.org:123/forum/questions/?tag=networking&order=newest#top' ); }); test('set port', () => { expect(window.location.port).toBe('123'); window.location.port = '1234'; expect(window.history.length).toBe(2); expect((window.history as WindowHistoryMock).sessionHistory).toEqual([ { state: null, url: urlNetworking }, { state: null, url: 'https://john.doe@www.example.com:1234/forum/questions/?tag=networking&order=newest#top' } ]); expect(window.history.state).toBeNull(); expect(window.location.href).toBe( 'https://john.doe@www.example.com:1234/forum/questions/?tag=networking&order=newest#top' ); }); test('set pathname', () => { expect(window.location.pathname).toBe('/forum/questions/'); window.location.pathname = '/forum/answers/'; expect(window.history.length).toBe(2); expect((window.history as WindowHistoryMock).sessionHistory).toEqual([ { state: null, url: urlNetworking }, { state: null, url: 'https://john.doe@www.example.com:123/forum/answers/?tag=networking&order=newest#top' } ]); expect(window.history.state).toBeNull(); expect(window.location.href).toBe( 'https://john.doe@www.example.com:123/forum/answers/?tag=networking&order=newest#top' ); }); test('set search', () => { expect(window.location.search).toBe('?tag=networking&order=newest'); window.location.search = '?tag=networking&order=oldest'; expect(window.history.length).toBe(2); expect((window.history as WindowHistoryMock).sessionHistory).toEqual([ { state: null, url: urlNetworking }, { state: null, url: 'https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=oldest#top' } ]); expect(window.history.state).toBeNull(); expect(window.location.href).toBe( 'https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=oldest#top' ); }); test('set hash', () => { expect(window.location.hash).toBe('#top'); window.location.hash = '#bottom'; expect(window.history.length).toBe(2); expect((window.history as WindowHistoryMock).sessionHistory).toEqual([ { state: null, url: urlNetworking }, { state: null, url: 'https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#bottom' } ]); expect(window.history.state).toBeNull(); expect(window.location.href).toBe( 'https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#bottom' ); }); test('assign()', () => { window.location.assign(urlComputing); expect(window.history.length).toBe(2); expect((window.history as WindowHistoryMock).sessionHistory).toEqual([ { state: null, url: urlNetworking }, { state: null, url: urlComputing } ]); expect(window.history.state).toBeNull(); expect(window.location.href).toBe(urlComputing); }); test('replace()', () => { window.location.replace(urlComputing); expect(window.history.length).toBe(1); expect((window.history as WindowHistoryMock).sessionHistory).toEqual([ { state: null, url: urlComputing } ]); expect(window.history.state).toBeNull(); expect(window.location.href).toBe(urlComputing); }); test('reload()', () => { window.location.reload(); expect(window.history.length).toBe(1); expect((window.history as WindowHistoryMock).sessionHistory).toEqual([ { state: null, url: urlNetworking } ]); expect(window.history.state).toBeNull(); expect(window.location.href).toBe(urlNetworking); }); }); describe('window.history', () => { test('mock and restore window.history', () => { expect((window.history as WindowHistoryMock).sessionHistory).toEqual([ { state: null, url: urlNetworking } ]); restoreWindowHistory(); // Back to JSDOM window.history expect((window.history as WindowHistoryMock).sessionHistory).toBeUndefined(); }); test('go()', () => { expect(window.history.length).toBe(1); window.history.pushState('computing', 'unused', '/forum/answers/?tag=computing'); window.history.pushState('mathematics', 'unused', '/forum/answers/?tag=mathematics'); window.history.pushState('cryptography', 'unused', '/forum/answers/?tag=cryptography'); expect(window.history.length).toBe(4); expect(window.location.href).toBe(urlCryptography); window.history.go(-1); expect(window.location.href).toBe(urlMathematics); // Reload current page window.history.go(0); expect(window.location.href).toBe(urlMathematics); window.history.go(-2); expect(window.location.href).toBe(urlNetworking); // Do nothing window.history.go(-1); expect(window.location.href).toBe(urlNetworking); window.history.go(+1); expect(window.location.href).toBe(urlComputing); window.history.go(+2); expect(window.location.href).toBe(urlCryptography); // Do nothing window.history.go(+1); expect(window.location.href).toBe(urlCryptography); }); test('back()', () => { window.history.pushState('computing', 'unused', '/forum/answers/?tag=computing'); window.history.pushState('mathematics', 'unused', '/forum/answers/?tag=mathematics'); window.history.pushState('cryptography', 'unused', '/forum/answers/?tag=cryptography'); expect(window.location.href).toBe(urlCryptography); window.history.back(); expect(window.location.href).toBe(urlMathematics); window.history.back(); expect(window.location.href).toBe(urlComputing); window.history.back(); expect(window.location.href).toBe(urlNetworking); // Do nothing window.history.back(); expect(window.location.href).toBe(urlNetworking); }); test('forward()', () => { window.history.pushState('computing', 'unused', '/forum/answers/?tag=computing'); window.history.pushState('mathematics', 'unused', '/forum/answers/?tag=mathematics'); window.history.pushState('cryptography', 'unused', '/forum/answers/?tag=cryptography'); expect(window.location.href).toBe(urlCryptography); window.history.back(); window.history.back(); window.history.back(); expect(window.location.href).toBe(urlNetworking); window.history.forward(); expect(window.location.href).toBe(urlComputing); window.history.forward(); expect(window.location.href).toBe(urlMathematics); window.history.forward(); expect(window.location.href).toBe(urlCryptography); // Do nothing window.history.forward(); expect(window.location.href).toBe(urlCryptography); }); test('pushState()', () => { expect(window.history.length).toBe(1); expect((window.history as WindowHistoryMock).sessionHistory).toEqual([ { state: null, url: urlNetworking } ]); expect(window.history.state).toBeNull(); window.history.pushState('computing', 'unused', '/forum/answers/?tag=computing'); expect(window.history.length).toBe(2); expect((window.history as WindowHistoryMock).sessionHistory).toEqual([ { state: null, url: urlNetworking }, { state: 'computing', url: urlComputing } ]); expect(window.history.state).toBe('computing'); expect(window.location.href).toBe(urlComputing); window.history.pushState('mathematics', 'unused', '/forum/answers/?tag=mathematics'); expect(window.history.length).toBe(3); expect((window.history as WindowHistoryMock).sessionHistory).toEqual([ { state: null, url: urlNetworking }, { state: 'computing', url: urlComputing }, { state: 'mathematics', url: urlMathematics } ]); expect(window.history.state).toBe('mathematics'); expect(window.location.href).toBe(urlMathematics); }); test('replaceState()', () => { expect(window.history.length).toBe(1); expect((window.history as WindowHistoryMock).sessionHistory).toEqual([ { state: null, url: urlNetworking } ]); expect(window.history.state).toBeNull(); window.history.replaceState('computing', 'unused', '/forum/answers/?tag=computing'); expect(window.history.length).toBe(1); expect((window.history as WindowHistoryMock).sessionHistory).toEqual([ { state: 'computing', url: urlComputing } ]); expect(window.history.state).toBe('computing'); expect(window.location.href).toBe(urlComputing); window.history.pushState('mathematics', 'unused', '/forum/answers/?tag=mathematics'); expect(window.history.length).toBe(2); expect((window.history as WindowHistoryMock).sessionHistory).toEqual([ { state: 'computing', url: urlComputing }, { state: 'mathematics', url: urlMathematics } ]); expect(window.history.state).toBe('mathematics'); expect(window.location.href).toBe(urlMathematics); window.history.replaceState('cryptography', 'unused', '/forum/answers/?tag=cryptography'); expect(window.history.length).toBe(2); expect((window.history as WindowHistoryMock).sessionHistory).toEqual([ { state: 'computing', url: urlComputing }, { state: 'cryptography', url: urlCryptography } ]); expect(window.history.state).toBe('cryptography'); expect(window.location.href).toBe(urlCryptography); }); test('pushState() and replaceState() throw when pushing an URL with another origin', () => { expect(() => { window.history.pushState({}, 'unused', urlWikipedia); }).toThrow( "Failed to execute 'pushState' on 'History': A history state object with URL 'https://en.wikipedia.org/wiki/Main_Page' cannot be created in a document with origin 'https://www.example.com:123' and URL 'https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top'." ); expect(() => { window.history.replaceState({}, 'unused', urlWikipedia); }).toThrow( "Failed to execute 'replaceState' on 'History': A history state object with URL 'https://en.wikipedia.org/wiki/Main_Page' cannot be created in a document with origin 'https://www.example.com:123' and URL 'https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top'." ); }); test('popstate event - see window-history-playground.html', () => { const popstateSpy = vi.fn(); function popstateListener({ state }: PopStateEvent) { popstateSpy(state); } window.addEventListener('popstate', popstateListener); expect(popstateSpy).toHaveBeenCalledTimes(0); window.history.pushState('page=1', 'page=1', '?page=1'); window.history.pushState('page=2', 'page=2', '?page=2'); window.history.pushState('page=3', 'page=3', '?page=3'); window.history.replaceState('page=3-replace', 'page=3-replace', '?page=3-replace'); // popstate page=2 window.history.back(); expect(popstateSpy).toHaveBeenCalledTimes(1); expect(popstateSpy).toHaveBeenNthCalledWith(1, 'page=2'); // popstate page=1 window.history.back(); expect(popstateSpy).toHaveBeenCalledTimes(2); expect(popstateSpy).toHaveBeenNthCalledWith(2, 'page=1'); // popstate null (i.e. root page) window.history.back(); expect(popstateSpy).toHaveBeenCalledTimes(3); expect(popstateSpy).toHaveBeenNthCalledWith(3, null); // popstate page=2 window.history.go(2); expect(popstateSpy).toHaveBeenCalledTimes(4); expect(popstateSpy).toHaveBeenNthCalledWith(4, 'page=2'); // popstate page=1 window.history.go(-1); expect(popstateSpy).toHaveBeenCalledTimes(5); expect(popstateSpy).toHaveBeenNthCalledWith(5, 'page=1'); // load page=1 window.history.go(); expect(popstateSpy).toHaveBeenCalledTimes(5); window.removeEventListener('popstate', popstateListener); }); });