Last active
November 25, 2024 09:31
-
-
Save tkrotoff/52f4a29e919445d6e97f9a9e44ada449 to your computer and use it in GitHub Desktop.
Jest/Vitest mocks for JSDOM window.location & window.history
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* eslint-disable unicorn/no-null */ | |
/* | |
* Resetting window.location between tests is unfortunately a hard topic with JSDOM. | |
* | |
* https://gist.github.com/tkrotoff/52f4a29e919445d6e97f9a9e44ada449 | |
* | |
* FIXME JSDOM leaves the history in place after every test, so the history will be dirty. | |
* Also its implementations for window.location and window.history are lacking. | |
* - https://github.com/jsdom/jsdom/blob/22.1.0/lib/jsdom/living/window/Location-impl.js | |
* - https://github.com/jsdom/jsdom/blob/22.1.0/lib/jsdom/living/window/History-impl.js | |
* - https://github.com/jsdom/jsdom/blob/22.1.0/lib/jsdom/living/window/SessionHistory.js | |
* | |
* What about Happy DOM? Implementations are empty: | |
* - https://github.com/capricorn86/happy-dom/blob/v12.10.3/packages/happy-dom/src/location/Location.ts | |
* - https://github.com/capricorn86/happy-dom/blob/v12.10.3/packages/happy-dom/src/history/History.ts | |
* | |
* | |
* window.location and window.history should work together: | |
* window.history should update the location, and changing the location should push a new state in the history | |
* | |
* Solution: re-implement window.location and window.history | |
* The code is synchronous instead of asynchronous, yet it fires "popstate" events | |
* | |
* Inspired by: | |
* - https://github.com/jestjs/jest/issues/5124#issuecomment-792768806 | |
* - https://github.com/firefox-devtools/profiler/blob/f894531be77dee00bb641f49a657b072183ec1fa/src/test/fixtures/mocks/window-navigation.js | |
* | |
* | |
* Related issues: | |
* - https://github.com/jestjs/jest/issues/5987 | |
* - https://github.com/jestjs/jest/issues/890 | |
* - https://github.com/jestjs/jest/issues/5124 | |
* - https://stackoverflow.com/a/76424392 | |
* | |
* - Huge hope on jsdom.reconfigure() (tried by patching Vitest JSDOM env), doesn't work | |
* https://github.com/vitest-dev/vitest/discussions/2383 | |
* https://github.com/simon360/jest-environment-jsdom-global/blob/v4.0.0/environment.js | |
* https://github.com/simon360/jest-environment-jsdom-global/blob/v4.0.0/README.md#using-jsdom-in-your-test-suite | |
*/ | |
class WindowLocationMock implements Location { | |
private url: URL; | |
internalSetURLFromHistory(newURL: string | URL) { | |
this.url = new URL(newURL, this.url); | |
} | |
constructor(url: string) { | |
this.url = new URL(url); | |
} | |
toString() { | |
return this.url.toString(); | |
} | |
readonly ancestorOrigins = [] as unknown as DOMStringList; | |
get href() { | |
return this.url.toString(); | |
} | |
set href(newUrl) { | |
this.assign(newUrl); | |
} | |
get origin() { | |
return this.url.origin; | |
} | |
get protocol() { | |
return this.url.protocol; | |
} | |
set protocol(v) { | |
const newUrl = new URL(this.url); | |
newUrl.protocol = v; | |
this.assign(newUrl); | |
} | |
get host() { | |
return this.url.host; | |
} | |
set host(v) { | |
const newUrl = new URL(this.url); | |
newUrl.host = v; | |
this.assign(newUrl); | |
} | |
get hostname() { | |
return this.url.hostname; | |
} | |
set hostname(v) { | |
const newUrl = new URL(this.url); | |
newUrl.hostname = v; | |
this.assign(newUrl); | |
} | |
get port() { | |
return this.url.port; | |
} | |
set port(v) { | |
const newUrl = new URL(this.url); | |
newUrl.port = v; | |
this.assign(newUrl); | |
} | |
get pathname() { | |
return this.url.pathname; | |
} | |
set pathname(v) { | |
const newUrl = new URL(this.url); | |
newUrl.pathname = v; | |
this.assign(newUrl); | |
} | |
get search() { | |
return this.url.search; | |
} | |
set search(v) { | |
const newUrl = new URL(this.url); | |
newUrl.search = v; | |
this.assign(newUrl); | |
} | |
get hash() { | |
return this.url.hash; | |
} | |
set hash(v) { | |
const newUrl = new URL(this.url); | |
newUrl.hash = v; | |
this.assign(newUrl); | |
} | |
assign(newUrl: string | URL) { | |
window.history.pushState(null, 'origin:location', newUrl); | |
this.reload(); | |
} | |
replace(newUrl: string | URL) { | |
window.history.replaceState(null, 'origin:location', newUrl); | |
this.reload(); | |
} | |
// eslint-disable-next-line class-methods-use-this | |
reload() { | |
// Do nothing | |
} | |
} | |
const originalLocation = window.location; | |
export function mockWindowLocation(url: string) { | |
//window.location = new WindowLocationMock(url); | |
//document.location = window.location; | |
Object.defineProperty(window, 'location', { | |
writable: true, | |
value: new WindowLocationMock(url) | |
}); | |
} | |
export function restoreWindowLocation() { | |
//window.location = originalLocation; | |
Object.defineProperty(window, 'location', { | |
writable: true, | |
value: originalLocation | |
}); | |
} | |
function verifyOrigin(newURL: string | URL, method: 'pushState' | 'replaceState') { | |
const currentOrigin = window.location.origin; | |
if (new URL(newURL, currentOrigin).origin !== currentOrigin) { | |
// Same error message as Chrome 118 | |
throw new DOMException( | |
`Failed to execute '${method}' on 'History': A history state object with URL '${newURL.toString()}' cannot be created in a document with origin '${currentOrigin}' and URL '${ | |
window.location.href | |
}'.` | |
); | |
} | |
} | |
export class WindowHistoryMock implements History { | |
private index = 0; | |
// Should be private but making it public makes it really easy to verify everything is OK in some tests | |
public sessionHistory: [{ state: any; url: string }] = [ | |
{ state: null, url: window.location.href } | |
]; | |
get length() { | |
return this.sessionHistory.length; | |
} | |
scrollRestoration = 'auto' as const; | |
get state() { | |
// eslint-disable-next-line @typescript-eslint/no-unsafe-return | |
return this.sessionHistory[this.index].state; | |
} | |
back() { | |
this.go(-1); | |
} | |
forward() { | |
this.go(+1); | |
} | |
go(delta = 0) { | |
if (delta === 0) { | |
window.location.reload(); | |
} | |
const newIndex = this.index + delta; | |
if (newIndex < 0 || newIndex >= this.length) { | |
// Do nothing | |
} else if (newIndex === this.index) { | |
// Do nothing | |
} else { | |
this.index = newIndex; | |
(window.location as WindowLocationMock).internalSetURLFromHistory( | |
this.sessionHistory[this.index].url | |
); | |
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment | |
dispatchEvent(new PopStateEvent('popstate', { state: this.state })); | |
} | |
} | |
pushState(data: any, unused: string, url?: string | URL | null) { | |
if (url) { | |
if (unused !== 'origin:location') verifyOrigin(url, 'pushState'); | |
(window.location as WindowLocationMock).internalSetURLFromHistory(url); | |
} | |
this.sessionHistory.push({ | |
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment | |
state: structuredClone(data), | |
url: window.location.href | |
}); | |
this.index++; | |
} | |
replaceState(data: any, unused: string, url?: string | URL | null) { | |
if (url) { | |
if (unused !== 'origin:location') verifyOrigin(url, 'replaceState'); | |
(window.location as WindowLocationMock).internalSetURLFromHistory(url); | |
} | |
this.sessionHistory[this.index] = { | |
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment | |
state: structuredClone(data), | |
url: window.location.href | |
}; | |
} | |
} | |
const originalHistory = window.history; | |
export function mockWindowHistory() { | |
//window.history = new WindowHistoryMock(); | |
Object.defineProperty(window, 'history', { | |
writable: true, | |
value: new WindowHistoryMock() | |
}); | |
} | |
export function restoreWindowHistory() { | |
//window.history = originalHistory; | |
Object.defineProperty(window, 'history', { | |
writable: true, | |
value: originalHistory | |
}); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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://[email protected]:123/forum/questions/?tag=networking&order=newest#top | |
* └─┬─┘ └─────────────┬────────────┘└───────┬───────┘ └────────────┬────────────┘ └┬┘ | |
* scheme authority path query fragment | |
* | |
*/ | |
const urlNetworking = | |
'https://[email protected]:123/forum/questions/?tag=networking&order=newest#top'; | |
const urlComputing = 'https://[email protected]:123/forum/answers/?tag=computing'; | |
const urlMathematics = 'https://[email protected]:123/forum/answers/?tag=mathematics'; | |
const urlCryptography = 'https://[email protected]: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://[email protected]:123/forum/questions/?tag=networking&order=newest#top' | |
} | |
]); | |
expect(window.history.state).toBeNull(); | |
expect(window.location.href).toBe( | |
'http://[email protected]: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://[email protected]:123/forum/questions/?tag=networking&order=newest#top' | |
} | |
]); | |
expect(window.history.state).toBeNull(); | |
expect(window.location.href).toBe( | |
'https://[email protected]: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://[email protected]:123/forum/questions/?tag=networking&order=newest#top' | |
} | |
]); | |
expect(window.history.state).toBeNull(); | |
expect(window.location.href).toBe( | |
'https://[email protected]: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://[email protected]:1234/forum/questions/?tag=networking&order=newest#top' | |
} | |
]); | |
expect(window.history.state).toBeNull(); | |
expect(window.location.href).toBe( | |
'https://[email protected]: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://[email protected]:123/forum/answers/?tag=networking&order=newest#top' | |
} | |
]); | |
expect(window.history.state).toBeNull(); | |
expect(window.location.href).toBe( | |
'https://[email protected]: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://[email protected]:123/forum/questions/?tag=networking&order=oldest#top' | |
} | |
]); | |
expect(window.history.state).toBeNull(); | |
expect(window.location.href).toBe( | |
'https://[email protected]: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://[email protected]:123/forum/questions/?tag=networking&order=newest#bottom' | |
} | |
]); | |
expect(window.history.state).toBeNull(); | |
expect(window.location.href).toBe( | |
'https://[email protected]: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://[email protected]: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://[email protected]: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); | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* JSDOM is missing some important DOM APIs so we need to mock them. | |
*/ | |
import { afterEach, beforeEach } from 'vitest'; | |
import { | |
mockWindowHistory, | |
mockWindowLocation, | |
restoreWindowHistory, | |
restoreWindowLocation | |
} from './mockWindowLocation'; | |
beforeEach(() => { | |
mockWindowLocation('http://localhost:3000'); | |
mockWindowHistory(); | |
}); | |
afterEach(() => { | |
restoreWindowLocation(); | |
restoreWindowHistory(); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<title>window.history playground</title> | |
</head> | |
<body> | |
<script type="module"> | |
// See mockWindowLocation.test.ts "popstate event" test | |
window.addEventListener('load', event => { | |
console.info('load', location.href); | |
}); | |
window.addEventListener('popstate', ({ state }) => { | |
console.info('popstate', location.href, state); | |
}); | |
// window.location.assign("https://en.wikipedia.org/wiki/Main_Page"); | |
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(); | |
// popstate page=1 | |
//window.history.back(); | |
// popstate null (i.e. root page) | |
//window.history.back(); | |
// popstate page=2 | |
//window.history.go(+2); | |
// popstate page=1 | |
//window.history.go(-1); | |
//window.history.go(); | |
</script> | |
</body> | |
</html> |
Doesn't appear to work for me on Vitest with Svelte. I get
__vite_ssr_import_10__.replaceState is not a function
But this is more a problem with Svelte than anything else.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Could be worth doing an npm release for this?