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 | /**
* PROPOSED: inviteToast behavior not covered by test/dom.test.mjs —
* the mkT fallback chain (t returns the key / "" / a non-string), the
* what-line precedence ladder (label beats come-to beats come-play),
* default accept navigation, timeoutMs=0 (?? vs || parity), the clipboard
* success/failure texts, the navigator.share path, and single CSS injection.
*/
import test from "node:test";
import assert from "node:assert/strict";
import { installFakeDom } from "./helpers/fake-dom.mjs";
const env = installFakeDom();
const { inviteToast, enText } = await import("../dist/index.js");
const tick = (ms = 0) => new Promise((r) => setTimeout(r, ms));
const baseInv = (over = {}) => ({
id: "e",
from: { pub: "P", x: "X", xs: "XS", pet: "", name: "", state: "friend" },
app: "game2.ardegazu.ro",
url: "https://game2.ardegazu.ro/#secret",
label: "",
msg: undefined,
ts: Date.now(),
...over,
});
const show = (inv, opts) => {
inviteToast(inv, { label: "vecinul", timeoutMs: 30, ...opts });
return env.document.querySelector(".soc-toast");
};
const closeAll = () => {
for (const c of env.document.body.querySelectorAll(".soc-toast")) c.remove();
};
test("mkT fallback chain: key-echo, empty string and non-strings all fall back to EN", () => {
// t returns the key itself -> EN
let card = show(baseInv(), { t: (k) => k });
assert.equal(card.querySelector(".go").textContent, enText("social.toast.join"), "key-echo falls back");
closeAll();
// t returns "" -> EN
card = show(baseInv(), { t: () => "" });
assert.equal(card.querySelector(".later").textContent, enText("social.toast.not-now"), "empty string falls back");
closeAll();
// t returns a non-string -> EN
card = show(baseInv(), { t: () => 42 });
assert.equal(card.querySelector(".grab").textContent, enText("social.toast.copy"), "non-string falls back");
closeAll();
// t hits one key, misses the rest
card = show(baseInv(), { t: (k) => (k === "social.toast.join" ? "hai" : undefined) });
assert.equal(card.querySelector(".go").textContent, "hai", "translated key used");
assert.equal(card.querySelector(".grab").textContent, enText("social.toast.copy"), "missed key falls back");
closeAll();
});
test("what-line precedence: non-join msg keeps legacy label; then come-to; then come-play", () => {
// a msg code that is NOT "join" does not use the join line -> label wins
let card = show(baseInv({ msg: { k: "other" }, label: "legacy prose" }));
assert.equal(card.querySelector(".what").textContent, "legacy prose");
closeAll();
// no label, app present -> come to {app-short}
card = show(baseInv());
assert.equal(card.querySelector(".what").textContent, enText("social.toast.come-to", { app: "game2" }));
closeAll();
// no label, no app -> come play (and join-code without app also lands here)
card = show(baseInv({ app: "", msg: { k: "join" } }));
assert.equal(card.querySelector(".what").textContent, enText("social.toast.come-play"));
closeAll();
});
test("default accept navigates to the capability URL", () => {
const card = show(baseInv());
card.querySelector(".go").dispatch("click");
assert.equal(env.location.href, "https://game2.ardegazu.ro/#secret");
assert.equal(env.document.querySelector(".soc-toast"), null, "closed on accept");
});
test("timeoutMs: 0 is honored (nullish semantics, not falsy-default)", async () => {
show(baseInv(), { timeoutMs: 0 });
await tick(10);
assert.equal(env.document.querySelector(".soc-toast"), null, "0 means close now, not 45s");
});
test("grab: clipboard success and failure texts", async () => {
env.navigator.clipboard = { writeText: () => Promise.resolve() };
let card = show(baseInv());
let grab = card.querySelector(".grab");
grab.dispatch("click");
await tick(5);
assert.equal(grab.textContent, enText("social.toast.copied"), "copied tick on success");
closeAll();
env.navigator.clipboard = { writeText: () => Promise.reject(new Error("no")) };
card = show(baseInv());
grab = card.querySelector(".grab");
grab.dispatch("click");
await tick(5);
assert.equal(grab.textContent, baseInv().url.slice(0, 24) + "…", "URL stub shown on failure");
closeAll();
delete env.navigator.clipboard;
});
test("grab: navigator.share path (canShare captured at render time)", async () => {
let shared = null;
env.navigator.share = (o) => ((shared = o), Promise.resolve());
const card = show(baseInv());
const grab = card.querySelector(".grab");
assert.equal(grab.textContent, enText("social.toast.share"), "share label when share exists");
grab.dispatch("click");
await tick(5);
assert.deepEqual(shared, { title: "ardegazu invite", url: "https://game2.ardegazu.ro/#secret" });
closeAll();
delete env.navigator.share;
});
test("toast CSS is injected exactly once", () => {
const styles = env.document.head.querySelectorAll("style").filter((s) => s.textContent.includes(".soc-toast"));
assert.equal(styles.length, 1, "one style tag for many toasts");
assert.ok(styles[0].textContent.includes("var(--soc-accent, #93c1a5)"), "theme var with historical fallback");
});
|