fix(ui): block chat while QR setup settles

This commit is contained in:
Jesse Merhi
2026-08-10 10:03:37 +10:00
committed by jesse-merhi
parent daa95941f1
commit 52271a9696
2 changed files with 37 additions and 15 deletions
@@ -638,7 +638,8 @@ describe("custodian QR wizard step", () => {
.mockResolvedValueOnce(pendingResult)
.mockResolvedValueOnce(pendingResult)
.mockResolvedValueOnce(terminalResult("Signal setup cancelled."));
const { page } = await mountPage(createContext(request).context);
const { context, emitGatewayEvent } = createContext(request);
const { page } = await mountPage(context, { onboarding: false });
await vi.advanceTimersByTimeAsync(0);
await vi.advanceTimersByTimeAsync(1_000);
@@ -647,6 +648,19 @@ describe("custodian QR wizard step", () => {
expect(page.textContent).toContain("This QR code expired.");
expect(page.store.wizardInputPending).toBe(false);
expect(page.store.wizardSettling).toBe(true);
expect(page.store.messages.some((message) => message.step?.qrDataUrl)).toBe(false);
emitGatewayEvent({
event: "health",
payload: { channels: { telegram: { configured: true, healthState: "stale-socket" } } },
});
await page.updateComplete;
const nudge = page.querySelector<HTMLButtonElement>(".custodian__nudge-action");
expect(nudge).not.toBeNull();
expect(nudge?.disabled).toBe(true);
nudge?.click();
await expect(page.store.send("check health now")).resolves.toBe("rejected");
expect(request).toHaveBeenCalledTimes(2);
await vi.advanceTimersByTimeAsync(1_000);
expect(request.mock.calls.filter((call) => call[1]?.pollStepId === "qr-step")).toHaveLength(2);
@@ -661,5 +675,6 @@ describe("custodian QR wizard step", () => {
wizardAnswer: { stepId: "qr-step", value: false },
});
await waitForFast(() => expect(page.textContent).toContain("Signal setup cancelled."));
expect(page.store.messages.some((message) => message.step?.qrDataUrl)).toBe(false);
});
});
@@ -173,7 +173,7 @@ export class CustodianSessionStore {
this.messages,
this.dismissedQuestions,
this.answeredQuestions,
this.wizardInputPending,
this.wizardInputPending || this.wizardSettling,
this.questionReplyUncertain,
);
}
@@ -209,7 +209,15 @@ export class CustodianSessionStore {
// Trim decides emptiness only; sensitive values may carry meaningful whitespace.
const message = this.sensitive ? text : text.trim();
const client = this.activeClient;
if (!message.trim() || !client || !this.chatAvailable || this.sending || this.setupRequired) {
if (
!message.trim() ||
!client ||
!this.chatAvailable ||
this.sending ||
this.setupRequired ||
this.wizardInputPending ||
this.wizardSettling
) {
this.emit();
return "rejected";
}
@@ -451,12 +459,7 @@ export class CustodianSessionStore {
this.answeredQuestions = retireCustodianQuestions(this.messages, this.answeredQuestions);
this.retryParams = null;
this.input = "";
this.wizardValue = undefined;
this.wizardSecretVisible = false;
this.sensitive = false;
this.wizardInputPending = false;
this.wizardSettling = false;
this.questionReplyUncertain = false;
this.resetWizardInputState();
this.error = null;
this.setupIssue = null;
this.earlierBoundaryAfterId = this.messages.at(-1)?.id ?? null;
@@ -616,6 +619,15 @@ export class CustodianSessionStore {
this.emit();
}
private resetWizardInputState(): void {
this.wizardValue = undefined;
this.wizardSecretVisible = false;
this.sensitive = false;
this.wizardInputPending = false;
this.wizardSettling = false;
this.questionReplyUncertain = false;
}
private clearConversation(): void {
this.qrScheduler.clear();
this.messages = [];
@@ -625,12 +637,7 @@ export class CustodianSessionStore {
this.error = null;
this.setupIssue = null;
this.input = "";
this.wizardValue = undefined;
this.wizardSecretVisible = false;
this.sensitive = false;
this.wizardInputPending = false;
this.wizardSettling = false;
this.questionReplyUncertain = false;
this.resetWizardInputState();
this.earlierBoundaryAfterId = null;
}