mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
fix(ui): surface rejected WhatsApp actions without stalled refreshes (#129800)
This commit is contained in:
committed by
GitHub
parent
cb6c508fe3
commit
c8382f9db9
@@ -137,6 +137,65 @@ suite.define(() => {
|
||||
);
|
||||
});
|
||||
|
||||
it("shows rejected WhatsApp login immediately without waiting for channel status", async () => {
|
||||
await suite.withPage(
|
||||
{ locale: "en-US", serviceWorkers: "block", viewport: { height: 900, width: 1280 } },
|
||||
async ({ page }) => {
|
||||
const gateway = await installMockGateway(page, {
|
||||
methodResponses: {
|
||||
"channels.status": {
|
||||
ts: Date.now(),
|
||||
channelOrder: ["whatsapp"],
|
||||
channelLabels: { whatsapp: "WhatsApp" },
|
||||
channels: {
|
||||
whatsapp: { configured: true, linked: true, running: true, connected: true },
|
||||
},
|
||||
channelAccounts: {},
|
||||
channelDefaultAccountId: {},
|
||||
},
|
||||
"channels.pairing.list": {
|
||||
accounts: [],
|
||||
requests: [],
|
||||
commandOwnerConfigured: true,
|
||||
limits: { pendingPerAccount: 3, ttlMs: 3_600_000 },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect((await page.goto(`${suite.server.baseUrl}settings/channels`))?.status()).toBe(200);
|
||||
await page.locator(".channels-item", { hasText: "WhatsApp" }).first().click();
|
||||
const detail = page.locator(".channels-detail");
|
||||
const relink = detail.getByRole("button", { name: "Relink" });
|
||||
await relink.waitFor();
|
||||
const statusReadsBefore = (await gateway.getRequests("channels.status")).length;
|
||||
await gateway.deferNext("channels.status", { probe: true });
|
||||
await gateway.deferNext("web.login.start");
|
||||
|
||||
await relink.click();
|
||||
await gateway.waitForRequest("web.login.start");
|
||||
await gateway.rejectDeferred("web.login.start", {
|
||||
code: "INVALID_REQUEST",
|
||||
message: "WhatsApp login rejected",
|
||||
});
|
||||
|
||||
if (captureUiProofEnabled) {
|
||||
await mkdir(uiProofArtifactDir, { recursive: true });
|
||||
await page.screenshot({
|
||||
animations: "disabled",
|
||||
fullPage: true,
|
||||
path: path.join(
|
||||
uiProofArtifactDir,
|
||||
`whatsapp-mutation-${process.env.OPENCLAW_UI_PROOF_LABEL ?? "rejected"}.png`,
|
||||
),
|
||||
});
|
||||
}
|
||||
await expect.poll(() => detail.textContent()).toContain("WhatsApp login rejected");
|
||||
await expect.poll(() => relink.isEnabled()).toBe(true);
|
||||
expect(await gateway.getRequests("channels.status")).toHaveLength(statusReadsBefore);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("confirms the explicit default account and preserves a no-op logout", async () => {
|
||||
await suite.withPage(
|
||||
{
|
||||
|
||||
@@ -329,11 +329,66 @@ describe("channels controller WhatsApp logout", () => {
|
||||
expect(channels.state.whatsappLoginMessage).toBe("credential cleanup failed");
|
||||
expect(channels.state.whatsappLoginQrDataUrl).toBe("data:image/png;base64,current-qr");
|
||||
expect(channels.state.whatsappLoginConnected).toBe(true);
|
||||
expect(request.mock.calls.filter(([method]) => method === "channels.status")).toHaveLength(1);
|
||||
expect(request.mock.calls.filter(([method]) => method === "channels.status")).toHaveLength(0);
|
||||
channels.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
describe("channels controller WhatsApp mutation failures", () => {
|
||||
it.each([
|
||||
{
|
||||
operation: "login",
|
||||
method: "web.login.start",
|
||||
invoke: (channels: ReturnType<typeof createChannelCapability>) =>
|
||||
channels.startWhatsApp(false),
|
||||
preservesQr: false,
|
||||
},
|
||||
{
|
||||
operation: "scan wait",
|
||||
method: "web.login.wait",
|
||||
invoke: (channels: ReturnType<typeof createChannelCapability>) => channels.waitWhatsApp(),
|
||||
preservesQr: true,
|
||||
},
|
||||
{
|
||||
operation: "logout",
|
||||
method: "channels.logout",
|
||||
invoke: (channels: ReturnType<typeof createChannelCapability>) => channels.logoutWhatsApp(),
|
||||
preservesQr: true,
|
||||
},
|
||||
])(
|
||||
"publishes a rejected $operation without probing channel status",
|
||||
async ({ method, invoke, preservesQr }) => {
|
||||
const request = vi.fn(async (requestedMethod: string) => {
|
||||
if (requestedMethod === method) {
|
||||
throw new Error("WhatsApp request rejected");
|
||||
}
|
||||
return createChannelsSnapshot("unexpected refresh");
|
||||
});
|
||||
const channels = createChannelCapability({
|
||||
snapshot: { client: { request }, phase: "connected" },
|
||||
subscribe: () => () => undefined,
|
||||
} as never);
|
||||
channels.state.whatsappLoginQrDataUrl = "data:image/png;base64,current-qr";
|
||||
channels.state.whatsappLoginConnected = true;
|
||||
const updates: Array<{ busy: boolean; message: string | null }> = [];
|
||||
channels.subscribe((state) => {
|
||||
updates.push({ busy: state.whatsappBusy, message: state.whatsappLoginMessage });
|
||||
});
|
||||
|
||||
await invoke(channels);
|
||||
|
||||
expect(
|
||||
request.mock.calls.filter(([requestedMethod]) => requestedMethod === "channels.status"),
|
||||
).toHaveLength(0);
|
||||
expect(updates.at(-1)).toEqual({ busy: false, message: "WhatsApp request rejected" });
|
||||
expect(channels.state.whatsappLoginQrDataUrl).toBe(
|
||||
preservesQr ? "data:image/png;base64,current-qr" : null,
|
||||
);
|
||||
channels.dispose();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("channels controller DM pairing", () => {
|
||||
const emptyPairing: ChannelsPairingListResult = {
|
||||
accounts: [],
|
||||
|
||||
@@ -469,12 +469,12 @@ async function startWhatsAppLogin(
|
||||
state.whatsappLoginQrDataUrl = res.qrDataUrl ?? null;
|
||||
state.whatsappLoginConnected = typeof res.connected === "boolean" ? res.connected : null;
|
||||
} catch (err) {
|
||||
if (!isCurrentWhatsAppOperation(state, operation)) {
|
||||
return false;
|
||||
if (isCurrentWhatsAppOperation(state, operation)) {
|
||||
state.whatsappLoginMessage = formatUiError(err);
|
||||
state.whatsappLoginQrDataUrl = null;
|
||||
state.whatsappLoginConnected = null;
|
||||
}
|
||||
state.whatsappLoginMessage = formatUiError(err);
|
||||
state.whatsappLoginQrDataUrl = null;
|
||||
state.whatsappLoginConnected = null;
|
||||
return false;
|
||||
} finally {
|
||||
if (isCurrentWhatsAppOperation(state, operation)) {
|
||||
state.whatsappBusy = false;
|
||||
@@ -510,11 +510,11 @@ async function waitWhatsAppLogin(state: ChannelsState, accountId?: string): Prom
|
||||
state.whatsappLoginQrDataUrl = null;
|
||||
}
|
||||
} catch (err) {
|
||||
if (!isCurrentWhatsAppOperation(state, operation)) {
|
||||
return false;
|
||||
if (isCurrentWhatsAppOperation(state, operation)) {
|
||||
state.whatsappLoginMessage = formatUiError(err);
|
||||
state.whatsappLoginConnected = null;
|
||||
}
|
||||
state.whatsappLoginMessage = formatUiError(err);
|
||||
state.whatsappLoginConnected = null;
|
||||
return false;
|
||||
} finally {
|
||||
if (isCurrentWhatsAppOperation(state, operation)) {
|
||||
state.whatsappBusy = false;
|
||||
@@ -544,10 +544,10 @@ async function logoutWhatsApp(state: ChannelsState, accountId?: string): Promise
|
||||
state.whatsappLoginMessage = t("channels.whatsapp.logoutNotCleared");
|
||||
}
|
||||
} catch (err) {
|
||||
if (!isCurrentWhatsAppOperation(state, operation)) {
|
||||
return false;
|
||||
if (isCurrentWhatsAppOperation(state, operation)) {
|
||||
state.whatsappLoginMessage = formatUiError(err);
|
||||
}
|
||||
state.whatsappLoginMessage = formatUiError(err);
|
||||
return false;
|
||||
} finally {
|
||||
if (isCurrentWhatsAppOperation(state, operation)) {
|
||||
state.whatsappBusy = false;
|
||||
|
||||
Reference in New Issue
Block a user