Skip to the code
cf.

Polling order status

Build OrderStatus, the default export: the "Where's my tea?" panel for one order. It takes an orderId prop (default 1), and asks /api/order/ followed by the id, like /api/order/5, right away. The answer is JSON like { status: "shipped" }, where the status is preparing, shipped, or delivered.

Show Loading… (with the single … character) until the first answer, then Status: shipped. Under it, show Last updated: followed by the time the latest answer arrived: read it with Date.now() and show it with new Date(time).toLocaleTimeString().

Ask again 2 seconds after each answer, until the status is delivered; then stop for good. Never ask again while a request is still waiting for its answer, however slow it is.

If a check fails (fetch rejects, or the response isn't ok), show Couldn't check the order., keep the last status on screen, and try again 2 seconds later. The next good answer hides the message.

When the panel leaves the page, stop: no timer left behind and no more requests. If orderId changes, start over for the new order.

<OrderStatus orderId={5} />
 
<p>Status: shipped</p>
<p>Last updated: 10:42:07</p>

What the tests check

  1. asks for the order right away and shows its status
    useFakeTimers();
    const fetch = mockFetch(() => ({ status: "preparing" }));
    render(<App orderId={5} />);
    expect(screen.getByText("Loading…")).toBeInTheDocument();
    await flush();
    expect(screen.getByText("Status: preparing")).toBeInTheDocument();
    expect(fetch.calls.map((call) => call.url)).toEqual(["/api/order/5"]);
    
  2. asks again every 2 seconds and shows the newest status
    const clock = useFakeTimers();
    const statuses = ["preparing", "preparing", "shipped"];
    const fetch = mockFetch(() => ({ status: statuses[Math.min(fetch.calls.length, statuses.length) - 1] }));
    render(<App orderId={5} />);
    await flush();
    await clock.advance(1999);
    expect(fetch.calls).toHaveLength(1);
    await clock.advance(1);
    expect(fetch.calls).toHaveLength(2);
    await clock.advance(2000);
    expect(fetch.calls).toHaveLength(3);
    expect(screen.getByText("Status: shipped")).toBeInTheDocument();
    expect(fetch.calls.every((call) => call.url === "/api/order/5")).toBe(true);
    
  3. stops asking once the order is delivered
    const clock = useFakeTimers();
    const fetch = mockFetch(() => ({ status: fetch.calls.length === 1 ? "shipped" : "delivered" }));
    render(<App orderId={5} />);
    await flush();
    await clock.advance(2000);
    expect(screen.getByText("Status: delivered")).toBeInTheDocument();
    await clock.advance(10000);
    expect(fetch.calls).toHaveLength(2);
    expect(clock.pending()).toBe(0);
    
  4. shows when it last heard back
    const clock = useFakeTimers();
    mockFetch(() => ({ status: "shipped" }));
    render(<App orderId={5} />);
    await flush();
    expect(screen.getByText("Last updated: " + new Date(clock.now()).toLocaleTimeString())).toBeInTheDocument();
    await clock.advance(2000);
    expect(screen.getByText("Last updated: " + new Date(clock.now()).toLocaleTimeString())).toBeInTheDocument();
    
  5. never asks while a request is waiting, then asks 2 seconds after its answer
    const clock = useFakeTimers();
    const slow = deferred();
    const fetch = mockFetch(() => (fetch.calls.length === 1 ? slow.promise : { status: "shipped" }));
    render(<App orderId={5} />);
    await clock.advance(5000);
    expect(fetch.calls).toHaveLength(1);
    slow.resolve({ status: "preparing" });
    await flush();
    expect(screen.getByText("Status: preparing")).toBeInTheDocument();
    await clock.advance(1999);
    expect(fetch.calls).toHaveLength(1);
    await clock.advance(1);
    expect(fetch.calls).toHaveLength(2);
    
  6. a failed check shows an error, keeps the status, and tries again
    const clock = useFakeTimers();
    const fetch = mockFetch(() => {
      const call = fetch.calls.length;
      if (call === 2) return fail(503);
      return { status: call === 1 ? "preparing" : "shipped" };
    });
    render(<App orderId={5} />);
    await flush();
    await clock.advance(2000);
    expect(screen.getByText("Couldn't check the order.")).toBeInTheDocument();
    expect(screen.getByText("Status: preparing")).toBeInTheDocument();
    await clock.advance(2000);
    expect(fetch.calls).toHaveLength(3);
    expect(screen.getByText("Status: shipped")).toBeInTheDocument();
    expect(screen.queryByText("Couldn't check the order.")).not.toBeInTheDocument();
    
  7. stops when the panel leaves the page, even mid-request
    const clock = useFakeTimers();
    const second = deferred();
    const fetch = mockFetch(() => (fetch.calls.length === 2 ? second.promise : { status: "preparing" }));
    const { unmount } = render(<App orderId={5} />);
    await flush();
    await clock.advance(2000);
    expect(fetch.calls).toHaveLength(2);
    unmount();
    second.resolve({ status: "shipped" });
    await flush();
    expect(clock.pending()).toBe(0);
    await clock.advance(10000);
    expect(fetch.calls).toHaveLength(2);
    expect(logs).toEqual([]);
    
  8. starts over when orderId changes
    const clock = useFakeTimers();
    const fetch = mockFetch((url) => ({ status: url === "/api/order/5" ? "preparing" : "shipped" }));
    const { rerender } = render(<App orderId={5} />);
    await flush();
    await clock.advance(1000);
    rerender(<App orderId={6} />);
    await flush();
    expect(screen.getByText("Status: shipped")).toBeInTheDocument();
    await clock.advance(2000);
    expect(fetch.calls.map((call) => call.url)).toEqual(["/api/order/5", "/api/order/6", "/api/order/6"]);
    

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).