1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119 | /**
* PROPOSED: attachSocial with FRIENDS present — none of this is covered by
* test/dom.test.mjs (which only exercises the cold start): the join toggle
* renders and toggles .on, sheet rows exist per friend and sort by label,
* offline rows say "drop" with an un-lit dot, the invite button walks the
* disabled -> "…" -> "parked ✓" ladder offline, re-opening replaces the
* sheet, a click on the box does NOT close it, and the share button falls
* back to the clipboard.
*/
import test from "node:test";
import assert from "node:assert/strict";
import { installFakeDom } from "./helpers/fake-dom.mjs";
const env = installFakeDom();
const { attachSocial, enText } = await import("../dist/index.js");
const { Identity } = await import("ardegazu-id-kit");
const { deriveSuiteXKeyPair } = await import("ardegazu-id-kit/xkey");
const tick = (ms = 0) => new Promise((r) => setTimeout(r, ms));
async function fakeFriend(name) {
const id = await Identity.fromSeed(Identity.newSeed());
const x = await deriveSuiteXKeyPair(Identity.newSeed());
return {
pub: id.publicKeyB64,
x: x.pubB64,
xs: "C".repeat(64),
pet: "",
name,
state: "friend",
addedTs: Date.now(),
petTs: 0,
lastSeenTs: 0,
lastApp: "",
};
}
test("attachSocial with friends: toggle, sorted rows, offline invite, sheet lifecycle", async () => {
const identity = await Identity.fromSeed(Identity.newSeed());
// deliberately out of label order: zeta first in the blob
const soc = { v: 1, friends: [await fakeFriend("zeta"), await fakeFriend("ana")], blocked: [], ts: Date.now() };
const agent = await attachSocial({
ns: "t-dom-friends",
app: "game1.ardegazu.ro",
identity,
seed: Identity.newSeed(),
myName: () => "eu",
pubsub: null,
soc,
roomLink: () => "https://game1.ardegazu.ro/#room",
});
assert.ok(agent, "agent started");
// the join toggle exists once friends exist, and toggles .on per click
const join = env.document.querySelector(".soc-chip.join");
assert.ok(join, "join toggle rendered with friends");
assert.equal(join.querySelector("span").textContent, enText("social.chip.join"));
assert.equal(join.title, enText("social.chip.join.title"));
assert.ok(!join.classList.contains("on"), "off by default (capability grant is opt-in)");
join.dispatch("click");
assert.ok(join.classList.contains("on"), "on after first click");
join.dispatch("click");
assert.ok(!join.classList.contains("on"), "off again after second click");
// open the sheet
const inv = env.document.querySelector(".soc-chip.inv");
inv.dispatch("click");
let sheet = env.document.querySelector(".soc-sheet");
assert.ok(sheet, "sheet opened");
const box = sheet.querySelector(".box");
// rows: sorted by label (ana before zeta), both offline
const rows = box.querySelectorAll(".row");
assert.equal(rows.length, 2, "one row per friend, no cold-start hint");
assert.equal(sheet.querySelector(".hint"), null, "no nofriends hint");
assert.equal(sheet.querySelector("a.hub"), null, "no hub pointer");
const labels = rows.map((r) => r.querySelector(".who").textContent);
assert.deepEqual(labels, ["ana", "zeta"], "tie-broken by label, stable across opens");
for (const r of rows) {
assert.ok(!r.querySelector(".dot").classList.contains("on"), "offline dot un-lit");
assert.equal(r.querySelector("small").textContent, enText("social.sheet.drop"), "offline copy");
assert.equal(r.querySelector("button").textContent, enText("social.sheet.invite"));
}
// the invite button ladder, fully offline: disabled -> "…" -> parked
const btn = rows[0].querySelector("button");
btn.dispatch("click");
assert.equal(btn.disabled, true, "disabled immediately");
assert.equal(btn.textContent, "…", "spinner text while sealing");
await tick(200);
assert.equal(btn.textContent, enText("social.sheet.parked"), "offline invite reports parked");
// share button: clipboard fallback copies the room link
let copied = null;
env.navigator.clipboard = { writeText: (s) => ((copied = s), Promise.resolve()) };
const share = box.querySelector("button.share");
share.dispatch("click");
await tick(5);
assert.equal(copied, "https://game1.ardegazu.ro/#room", "room link copied");
assert.equal(share.textContent, enText("social.toast.copied"), "share button confirms");
delete env.navigator.clipboard;
// a click on the box does NOT close the sheet (only the backdrop does)
sheet.dispatch("click", { target: box });
assert.ok(env.document.querySelector(".soc-sheet"), "box click keeps the sheet");
// re-opening replaces the previous sheet instead of stacking
inv.dispatch("click");
assert.equal(env.document.body.querySelectorAll(".soc-sheet").length, 1, "one sheet at a time");
sheet = env.document.querySelector(".soc-sheet");
sheet.dispatch("click");
assert.equal(env.document.querySelector(".soc-sheet"), null, "backdrop closes");
// chip CSS injected exactly once despite ensureCss on every sheet open
const styles = env.document.head.querySelectorAll("style").filter((s) => s.textContent.includes(".soc-chip"));
assert.equal(styles.length, 1, "CHIP_CSS injected once");
agent.stop();
});
|