///
/**
* Multi-user pattern test for the CFC group chat demo.
*
* Unlike main.test.tsx (one runtime, two GroupChatDemo instances wired to
* separate cells), this runs ONE shared instance across two worker-isolated
* runtimes with distinct identities — so PerUser/PerSession scope
* partitioning and cross-runtime propagation are actually exercised.
*
* Each participant's steps run in order; cross-user ordering happens only at
* `{ label }` / `{ await }` markers. See the multi-user section of
* docs/common/patterns/multi-user-patterns.md.
*/
import {
action,
computed,
type Default,
multiUserTest,
pattern,
type Writable,
} from "commonfabric";
import {
messagesValue,
profilesValue,
type SharedMessagesCell,
type SharedProfilesCell,
TRUSTED_GROUP_CHAT_ADMIN_SURFACE,
TRUSTED_GROUP_CHAT_PROFILE_SURFACE,
TRUSTED_GROUP_CHAT_SAVE_PROFILE_ACTION,
TRUSTED_GROUP_CHAT_SEND_ACTION,
TRUSTED_GROUP_CHAT_SEND_SURFACE,
TRUSTED_GROUP_CHAT_SET_ADMIN_ACTION,
} from "./trusted.tsx";
import { GroupChatDemo, type GroupChatDemoOutput } from "./main.tsx";
// Renderer-trusted gestures for the protected writes; the runner sends these
// with trusted DOM provenance — the headless equivalent of clicking the
// reviewed surface (see main.test.tsx).
const profileGesture = {
surface: TRUSTED_GROUP_CHAT_PROFILE_SURFACE,
action: TRUSTED_GROUP_CHAT_SAVE_PROFILE_ACTION,
};
const sendGesture = {
surface: TRUSTED_GROUP_CHAT_SEND_SURFACE,
action: TRUSTED_GROUP_CHAT_SEND_ACTION,
};
const adminGesture = {
surface: TRUSTED_GROUP_CHAT_ADMIN_SURFACE,
action: TRUSTED_GROUP_CHAT_SET_ADMIN_ACTION,
};
type GroupChatDemoInputArg = Parameters[0];
interface Setup {
chat: GroupChatDemoOutput;
}
export const setup = pattern(() => ({
chat: GroupChatDemo({} as GroupChatDemoInputArg),
}));
const profileNames = (chat: GroupChatDemoOutput): string[] =>
profilesValue(chat.profiles as SharedProfilesCell)
.map((profile) => profile.get()?.name ?? "")
.filter((name) => name.length > 0)
.toSorted();
const messageBodies = (chat: GroupChatDemoOutput): string[] =>
messagesValue(chat.messages as SharedMessagesCell).map((m) => m.body);
export const alice = pattern<{ setup: Setup }>(({ setup }) => {
const chat = setup.chat;
const action_set_name = action(() => {
chat.setProfileDraft.send("Alice");
});
const action_set_message = action(() => {
chat.setMessageDraft.send("Hello from Alice");
});
const assert_named_alice = computed(() =>
chat.currentProfileName === "Alice"
);
const assert_sees_both_profiles = computed(() =>
profileNames(chat).join(",") === "Alice,Bob"
);
const assert_sees_bobs_message = computed(() =>
messageBodies(chat).includes("Hi from Bob")
);
const assert_sees_bobs_lockdown_message = computed(() =>
messageBodies(chat).includes("Bob posts after lockdown")
);
const assert_is_admin = computed(() => chat.currentUserIsAdmin === true);
return {
tests: [
{ action: action_set_name },
{ action: chat.saveProfile, trustedUi: profileGesture },
{ assertion: assert_named_alice },
{ label: "alice-saved" },
{ await: "bob-saved" },
// Bob saving must not clobber Alice's PerUser profile, and the shared
// registry must resolve BOTH names in Alice's runtime.
{ assertion: assert_named_alice },
{ assertion: assert_sees_both_profiles },
{ action: action_set_message },
{ action: chat.sendTrustedMessage, trustedUi: sendGesture },
{ label: "alice-posted" },
{ await: "bob-posted" },
{ assertion: assert_sees_bobs_message },
// Admin lockdown: Alice becomes the bootstrap admin; posting stays
// open for everyone (the original regression disabled it).
{
action: chat.toggleEveryoneAdmin,
event: { everyoneIsAdmin: false },
trustedUi: adminGesture,
},
{ assertion: assert_is_admin },
{ label: "alice-locked-down" },
{ await: "bob-posted-after-lockdown" },
{ assertion: assert_sees_bobs_lockdown_message },
],
};
});
export const bob = pattern<{ setup: Setup }>(({ setup }) => {
const chat = setup.chat;
const action_set_name = action(() => {
chat.setProfileDraft.send("Bob");
});
const action_set_message = action(() => {
chat.setMessageDraft.send("Hi from Bob");
});
const action_set_lockdown_message = action(() => {
chat.setMessageDraft.send("Bob posts after lockdown");
});
// PerUser draft: Alice's typing must never show up in Bob's runtime.
const assert_draft_empty = computed(() =>
((chat.profileDraft as Writable>).get() ?? "") === ""
);
const assert_unnamed = computed(() =>
chat.currentProfileName === "Name not set"
);
const assert_named_bob = computed(() => chat.currentProfileName === "Bob");
const assert_sees_alice_profile = computed(() =>
profileNames(chat).includes("Alice")
);
const assert_sees_alices_message = computed(() =>
messageBodies(chat).includes("Hello from Alice")
);
const assert_not_admin = computed(() => chat.currentUserIsAdmin === false);
return {
tests: [
{ await: "alice-saved" },
{ assertion: assert_draft_empty },
{ assertion: assert_unnamed },
{ assertion: assert_sees_alice_profile },
{ action: action_set_name },
{ action: chat.saveProfile, trustedUi: profileGesture },
{ assertion: assert_named_bob },
{ label: "bob-saved" },
{ await: "alice-posted" },
{ assertion: assert_sees_alices_message },
{ action: action_set_message },
{ action: chat.sendTrustedMessage, trustedUi: sendGesture },
{ label: "bob-posted" },
{ await: "alice-locked-down" },
{ assertion: assert_not_admin },
{ action: action_set_lockdown_message },
{ action: chat.sendTrustedMessage, trustedUi: sendGesture },
{ label: "bob-posted-after-lockdown" },
],
};
});
export default multiUserTest({ setup, participants: { alice, bob } });