social-kit / test / dom-toast.test.mjs
  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");
});

static mirror of HEAD · about · clone: git clone https://git.ardegazu.ro/social-kit.git