我通过重新实现window.location和window.history来解决了这个问题:
https://gist.github.com/tkrotoff/52f4a29e919445d6e97f9a9e44ada449
这主要受到
https://github.com/jestjs/jest/issues/5124#issuecomment-792768806(感谢sanek306)和
firefox-devtools window-navigation.js(感谢gregtatum和julienw)的启发。
它附带了单元测试,并且在我们的源代码库中运行良好。
然后,您可以像预期的那样在您的测试中使用window.location。
it('should ...', () => {
window.location.pathname = '/product/308072';
render(<MyComponent />);
const link = screen.getByRole<HTMLAnchorElement>('link', { name: 'Show more' });
expect(link.href).toBe('http://localhost:3000/product/308072/more');
});
it('should ...', () => {
const assignSpy = vi.spyOn(window.location, 'assign');
render(<MyComponent />);
const input = screen.getByRole<HTMLInputElement>('searchbox');
fireEvent.change(input, { target: { value: 'My Search Query' } });
fireEvent.submit(input);
expect(assignSpy).toHaveBeenCalledTimes(1);
expect(assignSpy).toHaveBeenNthCalledWith(1, '/search?query=My+Search+Query');
assignSpy.mockRestore();
});
这里是实现(从gist中复制粘贴,更多细节请参见gist):
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();
}
reload() {
}
}
const originalLocation = window.location;
export function mockWindowLocation(url: string) {
Object.defineProperty(window, 'location', {
writable: true,
value: new WindowLocationMock(url)
});
}
export function restoreWindowLocation() {
Object.defineProperty(window, 'location', {
writable: true,
value: originalLocation
});
}
function verifyOrigin(newURL: string | URL, method: 'pushState' | 'replaceState') {
const currentOrigin = new URL(window.location.href).origin;
if (new URL(newURL, currentOrigin).origin !== currentOrigin) {
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;
public sessionHistory: [{ state: any; url: string }] = [
{ state: null, url: window.location.href }
];
get length() {
return this.sessionHistory.length;
}
scrollRestoration = 'auto' as const;
get state() {
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) {
} else if (newIndex === this.index) {
} else {
this.index = newIndex;
(window.location as WindowLocationMock).internalSetURLFromHistory(
this.sessionHistory[this.index].url
);
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({
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] = {
state: structuredClone(data),
url: window.location.href
};
}
}
const originalHistory = window.history;
export function mockWindowHistory() {
Object.defineProperty(window, 'history', {
writable: true,
value: new WindowHistoryMock()
});
}
export function restoreWindowHistory() {
Object.defineProperty(window, 'history', {
writable: true,
value: originalHistory
});
}
window.location.assign(url)
函数实际上可以达到同样的效果,因此您可以使用jest.spyOn(window.location, 'assign').mockImplementation(() => {});
进行模拟。 - Brady Dowling