Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 22 additions & 4 deletions src/client/hud/layers/WinModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ export class WinModal extends LitElement implements Controller {
// Polling starts once, at the end of the game.
private xpPolling = false;
private xpAbort: AbortController | null = null;
// A detach cut the end-of-game poll short; re-attaching restarts it (the
// Win update that started it is delivered only once).
private xpPollAborted = false;
// Whose XP the section shows: the Legend ceremony plays once per account.
private xpPublicId: string | null = null;
// The Legend ceremony, fetched only for a player who could become a Legend
Expand Down Expand Up @@ -504,6 +507,14 @@ export class WinModal extends LitElement implements Controller {
this.requestUpdate();
}

connectedCallback(): void {
super.connectedCallback();
if (this.xpPollAborted) {
this.xpPollAborted = false;
void this.updateXp(true);
}
}

disconnectedCallback(): void {
this.xpAbort?.abort();
super.disconnectedCallback();
Expand Down Expand Up @@ -551,10 +562,17 @@ export class WinModal extends LitElement implements Controller {
}
this.xpPolling = true;
this.xpView = { kind: "calculating" };
this.xpAbort = new AbortController();
const result = await pollGameXp(game.gameID(), {
signal: this.xpAbort.signal,
});
const abort = new AbortController();
this.xpAbort = abort;
const result = await pollGameXp(game.gameID(), { signal: abort.signal });
// Detached mid-poll: leave the section be and poll again on re-attach
// (right away if that happened while this poll was winding down).
if (abort.signal.aborted) {
this.xpPolling = false;
if (this.isConnected) void this.updateXp(true);
else this.xpPollAborted = true;
return;
}
if (result?.eligible && reachedLegendThisGame(result)) {
this.preloadLegendCeremony();
}
Expand Down
35 changes: 35 additions & 0 deletions tests/client/graphics/layers/WinModalXp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1120,6 +1120,41 @@ describe("WinModal XP section", () => {
expect(panel()!.getAttribute("aria-busy")).toBe("false");
});

it("polls again when the modal is detached mid-poll and re-attached", async () => {
const firstFetch = stubXpEndpoint([notFound]);
await mount(makeGame({ ended: true }));
expect(xpState()).toBe("calculating");
const view = () =>
(modal as unknown as { xpView: { kind: string } }).xpView.kind;

// Detaching aborts the poll; that is not a verdict on the XP.
modal.remove();
await settle(3_000);
expect(firstFetch).toHaveBeenCalledTimes(1);
expect(view()).toBe("calculating");

// The Win update came once and is spent: re-attaching alone polls afresh.
const fetchMock = stubXpEndpoint([() => json(eligible())]);
document.body.appendChild(modal);
await settle();
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(xpState()).toBe("result");
});

it("polls again when re-attached before the aborted poll has wound down", async () => {
stubXpEndpoint([notFound]);
await mount(makeGame({ ended: true }));
expect(xpState()).toBe("calculating");

// Moved in the DOM: detached and re-attached in the same task.
const fetchMock = stubXpEndpoint([() => json(eligible())]);
modal.remove();
document.body.appendChild(modal);
await settle();
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(xpState()).toBe("result");
});

it("skips from the keyboard with a real button", async () => {
stubXpEndpoint([() => json(eligible())]);
await mount(makeGame({ ended: true }));
Expand Down
Loading