Skip to the code
cf.

Online status

The tea shop's till runs in a browser, and staff need to know when the connection drops, because orders can't be sent. Build App: a <p role="status"> that says Online or Offline.

The answer comes from navigator.onLine. The browser fires an online or offline event on window when it changes. Treat the events only as a sign that something changed, and read navigator.onLine for the answer.

Use useSyncExternalStore. Subscribe to both events once, not again on every render, and stop listening when App leaves the page.

In the tests, the connection is faked by changing what navigator.onLine returns and then firing the event on window.

<App />
 
// renders, while the connection is down:
<p role="status">Offline</p>

What the tests check

  1. shows Online when the browser is online
    Object.defineProperty(navigator, "onLine", { configurable: true, get: () => true });
    render(<App />);
    expect(screen.getByRole("status").textContent).toBe("Online");
    
  2. shows Offline when the browser starts offline
    Object.defineProperty(navigator, "onLine", { configurable: true, get: () => false });
    render(<App />);
    expect(screen.getByRole("status").textContent).toBe("Offline");
    
  3. switches to Offline when the connection drops
    let online = true;
    Object.defineProperty(navigator, "onLine", { configurable: true, get: () => online });
    render(<App />);
    online = false;
    fireEvent(window, new Event("offline"));
    expect(screen.getByRole("status").textContent).toBe("Offline");
    
  4. switches back to Online when the connection returns
    let online = false;
    Object.defineProperty(navigator, "onLine", { configurable: true, get: () => online });
    render(<App />);
    online = true;
    fireEvent(window, new Event("online"));
    expect(screen.getByRole("status").textContent).toBe("Online");
    online = false;
    fireEvent(window, new Event("offline"));
    expect(screen.getByRole("status").textContent).toBe("Offline");
    
  5. reads navigator.onLine, not the event's name
    let online = false;
    Object.defineProperty(navigator, "onLine", { configurable: true, get: () => online });
    render(<App />);
    fireEvent(window, new Event("online"));
    expect(screen.getByRole("status").textContent).toBe("Offline");
    
  6. subscribes once, not on every render
    const added = [];
    const add = window.addEventListener;
    window.addEventListener = function (type, ...rest) {
      added.push(type);
      return add.call(this, type, ...rest);
    };
    const { rerender } = render(<App />);
    rerender(<App />);
    rerender(<App />);
    rerender(<App />);
    expect(added.filter((type) => type === "online")).toHaveLength(1);
    expect(added.filter((type) => type === "offline")).toHaveLength(1);
    
  7. stops listening when it leaves the page
    const live = { online: 0, offline: 0 };
    const add = window.addEventListener;
    const remove = window.removeEventListener;
    window.addEventListener = function (type, ...rest) {
      if (type in live) live[type]++;
      return add.call(this, type, ...rest);
    };
    window.removeEventListener = function (type, ...rest) {
      if (type in live) live[type]--;
      return remove.call(this, type, ...rest);
    };
    const { unmount } = render(<App />);
    expect(live).toEqual({ online: 1, offline: 1 });
    unmount();
    expect(live).toEqual({ online: 0, offline: 0 });
    

Learn more

App.jsxSaved in this browser

Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.

Ctrl + Enter runs the tests (⌘ + Enter on a Mac).