Jest:测试 window.location.reload

39

我该如何编写一个测试,以确保方法reloadFn实际上重新加载窗口?我找到了这个资源,但我不清楚在编写测试时如何期望窗口重新加载,尤其是当该窗口重新加载发生在给定函数中时。感谢您的帮助!

const reloadFn = () => {
  window.location.reload();
}

1
大多数断言库包括助手函数,帮助确定函数已被调用的次数,因此您的测试可以调用它,然后检查它被调用的适当次数。请参见: https://www.chaijs.com/plugins/chai-spies/ expect(spy).to.have.been.called.exactly(3); - Matt Pengelly
3
我认为单元测试只需要确保函数被调用并且期望 window.location.reload 函数正确运行。通常情况下,您不需要测试窗口是否实际重新加载,只需测试是否调用了重新加载函数即可。 - pushkin
有关TypeScript,请参见此答案 - Sachin Joseph
8个回答

48

更新的答案(2021年11月)

包: "jest": "^26.6.0" "@testing-library/jest-dom": "^5.11.4"

构建:create-react-app 4

describe("test window location's reload function", () => {
  const original = window.location;

  const reloadFn = () => {
    window.location.reload();
  };

  beforeAll(() => {
    Object.defineProperty(window, 'location', {
      configurable: true,
      value: { reload: jest.fn() },
    });
  });

  afterAll(() => {
    Object.defineProperty(window, 'location', { configurable: true, value: original });
  });

  it('mocks reload function', () => {
    expect(jest.isMockFunction(window.location.reload)).toBe(true);
  });

  it('calls reload function', () => {
    reloadFn(); // as defined above..
    expect(window.location.reload).toHaveBeenCalled();
  });
});


注意:更新答案,因为旧的答案不支持 CRA 中使用的最新 jest 版本。

旧答案

这是解决方案,但为了更好的组织进行了重构:

describe('test window location\'s reload function', () => {
  const { reload } = window.location;

  beforeAll(() => {
    Object.defineProperty(window.location, 'reload', {
      configurable: true,
    });
    window.location.reload = jest.fn();
  });

  afterAll(() => {
    window.location.reload = reload;
  });

  it('mocks reload function', () => {
    expect(jest.isMockFunction(window.location.reload)).toBe(true);
  });

  it('calls reload function', () => {
    reloadFn(); // as defined above..
    expect(window.location.reload).toHaveBeenCalled();
  });
});

谢谢 :)


5
有一个版本肯定发生了重大变化,因为现在会抛出“TypeError: Cannot assign to read only property 'reload' of object '[object Location]'”错误。我添加了一个找到的有效解决方案的答案(此线程中还有其他好的解决方案)。 - DJ House
在我的更新答案中修复了上述错误。 - Murtaza Hussain
在jest.fn的调用次数上,似乎在第29次时没有被清除/重置。 - Damian Green

19

如果您在使用Jest时使用了TypeScript:

思路

  1. 创建一个副本,然后删除windowlocation属性。
  2. 现在使用模拟的reload函数设置location属性。
  3. 测试完成后将原始值设置回去。

代码:TypeScript 3.x及以下版本

const location: Location = window.location;
delete window.location;
window.location = {
    ...location,
    reload: jest.fn()
};

// <code to test>
// <code to test>
// <code to test>

expect(window.location.reload).toHaveBeenCalledTimes(1);
jest.restoreAllMocks();
window.location = location;

代码: TypeScript 4+

TypeScript 4有更严格的检查(这是件好事),因此我不确定除了使用 @ts-ignore 或者 @ts-expect-error 来抑制错误以外是否还有其他方法。

警告: 抑制TypeScript验证可能会存在风险。

const location: Location = window.location;

// WARNING:
//     @ts-ignore and @ts-expect-error suppress TypeScript validations by ignoring errors.
//     Suppressing TypeScript validations can be dangerous.

// @ts-ignore
delete window.location;

window.location = {
    ...location,
    reload: jest.fn()
};

// <code to test>
// <code to test>
// <code to test>

expect(window.location.reload).toHaveBeenCalledTimes(1);
jest.restoreAllMocks();
window.location = location;

2
这是我找到的唯一可以与typescript一起使用的解决方案。干得好。 - Bojan Tomić
2
伙计,这应该是被接受的答案,因为window.location.reload是只读且不可配置的。太棒了! - karfus
2
在 TypeScript 中,删除 window.location 会导致错误 "The operand of a 'delete' operator must be optional"。 - Ken Roy
1
使用jest 26.4.2和typescript 3.7.2,这是我唯一有效的答案。目前被接受的答案无法将window.location.reload分配给写入。 - matt.kauffman23
@Ken 我已经更新了答案,添加了适用于 TypeScript 4 的代码,但恐怕我没有一个好的解决方案。 - Sachin Joseph

15

你也可以简化Murtaza Hussain的解决方案为

  describe('refreshPage', () => {
    const { reload } = window.location;

    beforeAll(() => {
      Object.defineProperty(window, 'location', {
        writable: true,
        value: { reload: jest.fn() },
      });
    });

    afterAll(() => {
      window.location.reload = reload;
    });

    it('reloads the window', () => {
      refreshPage();
      expect(window.location.reload).toHaveBeenCalled();
    });
  });

使用writable而不是configurable,对我来说比Murtaza的答案有效。 - Damian Green
这里没有refreshPage函数? - fredrivett
这里没有“refreshPage”函数? - fredrivett

6

您可以使用sessionStorage在每次重新加载页面时保存一个值。 只要浏览器不关闭,该值就会保留在sessionStorage中。 当页面重新加载时,该值将增加。使用此值验证新的重新加载。 通过将reloadFn()粘贴到控制台中进行测试。 控制台将显示“重新加载计数:1”,并随着每次重新加载而增加。

const reloadFn = () => {
  window.location.reload(true);
}

window.onload = function() {
    // get reloadCount from sessionStorage
    reloadCount = sessionStorage.getItem('reloadCount');

    // reloadCount will be null the first page load or a new value for each reload
    if (reloadCount) {
        // increment reloadCount
        reloadCount = parseInt(reloadCount) + 1;
        // save the new value to sessionStorage
        sessionStorage.setItem('reloadCount', reloadCount);
        console.log("Reload count: " + reloadCount);
    } else {
        // if reloadCount was null then set it to 1 and save to sessionStorage
        sessionStorage.setItem('reloadCount', 1);
        console.log("Page was loaded for the first time");
    }
}

3

不必使用Object.defineProperty的变通方法,您可以使用Jest的本地spyOn函数来实现以下功能:

test("reload test", () => {
  const { getByText } = renderComponentWithReloadButton()

  const reload = jest.fn()

  jest
    .spyOn(window, "location", "get")
    .mockImplementation(() => ({ reload } as unknown as Location))

  // Call an action that should trigger window.location.reload() function
  act(() => {
    getByText("Reload me").click()
  })

  // Test if `reload` function was really called
  expect(reload).toBeCalled()
})

同时,请确保在测试之后使用jest.clearAllMocks()函数清除模拟。


1
我遇到了一个运行时错误:“属性位置没有访问类型get”。 - Damian Green

3

在你的函数测试中,你应该使用你链接的模拟代码:reloadFn

Object.defineProperty(window.location, 'reload', {
    configurable: true,
}); // makes window.location.reload writable
window.location.reload = jest.fn(); // set up the mock
reloadFn(); // this should call your mock defined above
expect(window.location.reload).toHaveBeenCalled(); // assert the call
window.location.reload.mockRestore(); // restore window.location.reload to its original function

为了进行更加完善的测试,您可以使用

expect(window.location.reload).toHaveBeenCalledWith(true);

值得注意的是,这实际上并未验证窗口是否已重新加载,这超出了单元测试的范围。类似浏览器测试或集成测试将进行验证。

1
根据官方文档,mockRestore 仅适用于使用 spyOn 创建的模拟对象:https://jestjs.io/fr/docs/mock-function-api#mockfnmockrestore - Soullivaneuh

1
如果有人在2020年查找这个问题,那么我也遇到了同样的问题。
为什么有些人会出现这个问题而有些人不会呢?这主要取决于你所运行的 Chrome 版本。我编写了一个组件的测试,最终调用了 window.location.reload。下面是该组件代码的部分内容:
onConfirmChange() {
    const {data, id} = this.state;

    this.setState({showConfirmationModal: false}, () => {
        this.update(data, id)
          .then(() => window.location.reload());
    });
}

我的构建服务器使用的是chrome 71版本,最初测试失败了,但在我本地使用chrome 79版本时通过了测试。 今天我将chrome更新到了84版本,在我的本地环境中出现了问题。 删除window.local似乎不被支持。尝试了谷歌上找到的所有解决方案,但都没有奏效。

那么解决方案是什么呢?

实际上非常简单,对于react测试,我的系统使用enzyme,所以我将window.location.reload包装在一个实例方法中,并在测试中进行存根处理。

JSX代码:

reloadWindow() {
    window.location.reload();
}

onConfirmChange() {
    const {data, id} = this.state;
  
    this.setState({showConfirmationModal: false}, () => {
      this.update(data, id)
        .then(() => reloadWindow());
    });   
}

测试

it('check what happened', () => {
    render();
    const component = wrapper.instance();
    sandbox.stub(component, 'reloadWindow').callsFake();
});

0

更新的答案(2021年5月)

我在这个帖子中遇到了很多问题。我认为随着时间推移,底层库的版本变化导致了故障。

我的配置:

  • "typescript": "~4.1.5"
  • "jest": "^26.6.3"
  • "jest-environment-jsdom": "^26.6.2"

另外,我应该注意到,我的解决方案非常冗长。但是我的用例需要测试 window.location.replace() 和结果。所以我不能简单地模拟 window.location.replace。如果您只需要模拟其中一个函数并且不关心实际的 href 如何更改,则该线程中的某些解决方案将使用更少的代码非常好用。

可行版本

我发现完全填充 window.location 对象可以解决我所有的问题。

window.location 填充

使用此代码并将其放置在测试文件或设置文件中的任何位置:

export class MockWindowLocation {
  private _url: URL = new URL();

  get href (): string {
    return this._url.toString();
  }

  set href (url: string) {
    this._url = new URL(url);
  }

  get protocol (): string {
    return this._url.protocol;
  }

  get host (): string {
    return this._url.host;
  }

  get hostname (): string {
    return this._url.hostname;
  }

  get origin (): string {
    return this._url.origin;
  }

  get port (): string {
    return this._url.port;
  }

  get pathname (): string {
    return this._url.pathname;
  }

  get hash (): string {
    return this._url.hash;
  }

  get search (): string {
    return this._url.search;
  }

  replace = jest.fn().mockImplementation((url: string) => {
    this.href = url;
  });

  assign = jest.fn().mockImplementation((url: string) => {
    this.href = url;
  });

  reload = jest.fn();

  toString(): string {
    return this._url.toString();
  }
}

测试它

然后,您必须删除 window.location 并将其设置为新的 polyfill:

  it('should be able to test window.location', () => {
    delete window.location;
    Object.defineProperty(window, 'location', {
      value: new MockWindowLocation()
    });

    window.location.href = 'https://example.com/app/#/route/1';
    window.location.reload();

    expect(window.location.reload).toHaveBeenCalled();
    expect(window.location.href).toBe('https://example.com/app/#/route/1');
    expect(window.location.pathname).toBe('/app/');
    expect(window.location.hash).toBe('#/route/1');
  });

这对我非常有效,希望能帮助其他人。

其他答案更简单

再强调一遍,这个主题中还有其他完全可行的答案。我发现:

Object.defineProperty(window, 'location', {
  writable: true,
  value: { reload: jest.fn() },
});

而且:

const location: Location = window.location;
delete window.location;
window.location = {
  ...location,
  reload: jest.fn()
};

两者都很有帮助。但是像我说的,我需要窥探replace()并仍然保留window.location的标准功能。

希望这能帮助到某些人。干杯!


你是否遇到了在删除 window.location 时出现“操作符 'delete' 的操作数必须是可选的.ts(2790)”这样的问题? - Alexey Nikonov
@AlexeyNikonov,我认为我已经做了。但可能是新的TypeScript或Jest版本导致了这个问题。你使用的是哪些版本? - DJ House

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接