Files
buzz/desktop/src/features/onboarding/ui/OnboardingRelayConnectionErrorCard.test.mjs
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 cdb09e8c15 fix(onboarding): use relay connection state for phase-3 success signal
ProfileStep was observing the sidebar's relayConnectivitySuccess store to
detect phase-3 reconnect success. That store is only ever produced by
useSidebarRelayConnectionCard, which never mounts during onboarding —
AppReady returns OnboardingFlow before the sidebar path renders.

Replace the dead cross-surface coupling with a local approach:
- Add useRelayConnection() to OnboardingRelayConnectionErrorCard
- Add hadActiveReconnectRef to gate markSuccess() on a genuine attempt
- Set the ref true when runConnectivityAction starts; clear it and call
  markSuccess() synchronously on phase-1 success, or leave it armed for
  the connection-state effect to fire on phase-3 connected transition
- Remove the two exports added to useSidebarRelayConnectionCard (no dead
  API surface)

Also fix the controller comment: the first comment incorrectly implied
cancelTimers() reached the subscription handle during sync emission. In
the assignment-order race, cancelTimers() ran before the handle was
assigned; cleanup happens in the if (resolved) block. Say that directly.

Add OnboardingRelayConnectionErrorCard.test.mjs: 5 tests covering the
guard contract — phase-3 async success, disconnected no-op, no-attempt
guard, phase-1 sync success idempotency, and double-connected no-op.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-07-02 15:07:52 -04:00

144 lines
4.6 KiB
JavaScript

/**
* Unit tests for the OnboardingRelayConnectionErrorCard phase-3 reconnect contract.
*
* Tests the guard logic that ties relay connection state to markSuccess():
* - phase-3 path (reconnect() returns false): card marks success when relay
* transitions to "connected" while a reconnect was attempted.
* - disconnected state never triggers success.
* - no prior reconnect attempt: connected state is ignored.
*
* These tests exercise the observable contract without React rendering by
* simulating the component's ref/effect logic as a pure state machine.
*/
import assert from "node:assert/strict";
import test from "node:test";
/**
* Minimal simulation of the OnboardingRelayConnectionErrorCard guard logic.
*
* Models the three-variable contract:
* hadActiveReconnect (ref) — set true when a reconnect is started
* relayConnectionState — driven by useRelayConnection
* hasSuccess (state) — becomes true when guard fires markSuccess
*/
function makeCard() {
let hadActiveReconnect = false;
let hasSuccess = false;
let markSuccessCallCount = 0;
const markSuccess = () => {
hasSuccess = true;
markSuccessCallCount++;
};
// Simulates what runConnectivityAction does: arm the ref, call reconnect,
// then either mark success synchronously (phase 1) or leave ref armed (phase 3).
const startReconnect = async (reconnectResult) => {
hadActiveReconnect = true;
const didReconnect = await reconnectResult;
if (didReconnect !== false) {
// Phase 1 sync success — clear ref and mark immediately.
hadActiveReconnect = false;
markSuccess();
}
// Phase 3: ref stays armed, waiting for connection-state effect.
};
// Simulates the useEffect(() => { if (connected && ref) markSuccess() })
const driveConnectionState = (state) => {
if (state === "connected" && hadActiveReconnect) {
hadActiveReconnect = false;
markSuccess();
}
};
return {
startReconnect,
driveConnectionState,
get hasSuccess() {
return hasSuccess;
},
get markSuccessCallCount() {
return markSuccessCallCount;
},
};
}
// ── Phase 3: async success via connection-state ───────────────────────────────
test("phase-3 path — reconnect() returns false, connected fires, markSuccess called once", async () => {
const card = makeCard();
// Simulate reconnect() entering phase 3 (returns false).
await card.startReconnect(Promise.resolve(false));
// Controller eventually drives relay to connected.
card.driveConnectionState("connected");
assert.equal(card.hasSuccess, true, "card marks success on connected");
assert.equal(card.markSuccessCallCount, 1, "markSuccess called exactly once");
});
test("phase-3 path — disconnected state does not mark success", async () => {
const card = makeCard();
await card.startReconnect(Promise.resolve(false));
// Drive through several non-connected states.
card.driveConnectionState("disconnected");
card.driveConnectionState("reconnecting");
card.driveConnectionState("stalled");
assert.equal(card.hasSuccess, false, "no success on non-connected states");
assert.equal(card.markSuccessCallCount, 0, "markSuccess never called");
});
test("no reconnect attempt — connected state does not mark success", async () => {
const card = makeCard();
// Drive to connected without any reconnect attempt.
card.driveConnectionState("connected");
assert.equal(
card.hasSuccess,
false,
"pre-existing connected state does not mark success",
);
assert.equal(card.markSuccessCallCount, 0, "markSuccess never called");
});
test("phase-1 sync success — markSuccess fires immediately, ref cleared, later connected is ignored", async () => {
const card = makeCard();
// Phase 1: reconnect() returns true (sync success).
await card.startReconnect(Promise.resolve(true));
assert.equal(card.hasSuccess, true, "success marked synchronously");
assert.equal(card.markSuccessCallCount, 1, "called once");
// A subsequent connected transition should NOT call markSuccess again.
card.driveConnectionState("connected");
assert.equal(
card.markSuccessCallCount,
1,
"no double-success on later connected",
);
});
test("phase-3 path — connected fires multiple times, markSuccess still called only once", async () => {
const card = makeCard();
await card.startReconnect(Promise.resolve(false));
card.driveConnectionState("connected");
card.driveConnectionState("connected"); // spurious second emission
assert.equal(
card.markSuccessCallCount,
1,
"ref cleared after first connected, no double-success",
);
});