Files

638 lines
28 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect } from "vitest";
import {
START_TEXT,
HELP_TEXT,
TRIAL_TEXT,
TRIAL_RE,
COMMANDS,
findTrialEntry,
findFreeTwin,
extractSku,
TRIAL_INTENT_RE,
startSkuText,
parseTrialConsent,
ASK_USERNAME_TEXT,
confirmReuseText,
parseAccountDecision,
extractUsername,
ASK_NEW_ACCOUNT_USERNAME_TEXT,
trialSuccessText,
trialErrorText,
MEMORY_KEY_TRIAL_USERNAME,
resolveLanguageKey,
TRIAL_CB,
startSkuKeyboard,
confirmReuseKeyboard,
TRIAL_ACCT_PREFIX,
trialAcctCb,
chooseAccountText,
chooseAccountTooManyText,
chooseAccountKeyboard,
} from "../src/channels/telegram/commands/index.js";
describe("Telegram commands", () => {
it("registers start, help and trial", () => {
expect(COMMANDS.map((c) => c.command).sort()).toEqual(["help", "start", "trial"]);
});
it("has tri-lingual start, help and trial text", () => {
expect(START_TEXT).toContain("欢迎");
expect(START_TEXT).toContain("Welcome");
expect(START_TEXT).toContain("Selamat");
expect(HELP_TEXT).toContain("帮助");
expect(HELP_TEXT).toContain("Help");
});
describe("resolveLanguageKey (第4批: 单语模板的语言归一)", () => {
it("maps zh/en/ms prefixes, ignores case and treats id as ms", () => {
expect(resolveLanguageKey("zh")).toBe("zh");
expect(resolveLanguageKey("zh-cn")).toBe("zh");
expect(resolveLanguageKey("zh-Hant")).toBe("zh");
expect(resolveLanguageKey("en")).toBe("en");
expect(resolveLanguageKey("en-US")).toBe("en");
expect(resolveLanguageKey("ms")).toBe("ms");
expect(resolveLanguageKey("ms-MY")).toBe("ms");
expect(resolveLanguageKey("id")).toBe("ms");
expect(resolveLanguageKey("id-ID")).toBe("ms");
});
it("defaults to zh for unknown/undefined/empty codes", () => {
expect(resolveLanguageKey("fr")).toBe("zh");
expect(resolveLanguageKey("")).toBe("zh");
expect(resolveLanguageKey(undefined)).toBe("zh");
});
});
it("trial instructions are single-language per lang (第4批)", () => {
const zh = TRIAL_TEXT("zh");
expect(zh).toContain("FREECXM04");
expect(zh).toContain("免费试用");
expect(zh).not.toContain("Percubaan");
expect(zh).not.toContain("trial FREECXM04");
const en = TRIAL_TEXT("en");
expect(en).toContain("Free Trial");
expect(en).toContain("trial FREECXM04 username:abc123");
expect(en).not.toContain("免费");
const ms = TRIAL_TEXT("ms");
expect(ms).toContain("Percubaan Percuma");
expect(ms).toContain("percubaan FREECXM04 username:abc123");
expect(ms).not.toContain("Free Trial");
});
it("TRIAL_TEXT defaults to zh when lang omitted", () => {
expect(TRIAL_TEXT()).toContain("免费试用");
expect(TRIAL_TEXT()).not.toContain("Free Trial");
});
it("TRIAL_RE parses tri-lingual forms incl. ASCII username/password labels", () => {
// Chinese form
const zh = "免费试用 FREECXM04 用户名:abc123".match(TRIAL_RE);
expect(zh?.[1]).toBe("FREECXM04");
expect(zh?.[2]).toBe("abc123");
// English + Malay forms (broken before the fix: username: label was ignored)
const en = "trial FREECXM04 username:abc123".match(TRIAL_RE);
expect(en?.[1]).toBe("FREECXM04");
expect(en?.[2]).toBe("abc123");
const ms = "percubaan FREECXM04 username:abc123".match(TRIAL_RE);
expect(ms?.[1]).toBe("FREECXM04");
expect(ms?.[2]).toBe("abc123");
// Optional password under both labels
expect("trial FREECXM04 username:abc123 password:pw789".match(TRIAL_RE)?.[3]).toBe("pw789");
expect("免费试用 FREECXM04 用户名:abc123 密码:pw789".match(TRIAL_RE)?.[3]).toBe("pw789");
});
it("TRIAL_RE requires a username for provision", () => {
expect("trial FREECXM04".match(TRIAL_RE)?.[2]).toBeUndefined();
expect("trial FREECXM04 username:abc123".match(TRIAL_RE)?.[2]).toBe("abc123");
});
it("never advertises contact details proactively", () => {
expect(HELP_TEXT).not.toContain("012-");
expect(HELP_TEXT).not.toContain("WhatsApp");
expect(HELP_TEXT).not.toContain("@DigiKedai");
expect(HELP_TEXT).not.toContain("@MrFullStackDev");
expect(HELP_TEXT).not.toContain("t.me");
});
it("deep-link: resolves a FREE-prefixed trial SKU from the catalog", () => {
const e = findTrialEntry("FREECXM04");
expect(e).toBeDefined();
expect(e!.isTrial).toBe(true);
expect(e!.name).toContain("免费试看");
});
it("deep-link: lookup is case-insensitive, skips paid/unknown/empty payloads", () => {
expect(findTrialEntry("freecxm04 ")).toBeDefined();
expect(findTrialEntry("CXM04")).toBeUndefined(); // paid SKU, not a trial
expect(findTrialEntry("NOPE99")).toBeUndefined();
expect(findTrialEntry(" ")).toBeUndefined();
});
describe("findFreeTwin (改动1: 付费 SKU → 免费 twin 反查)", () => {
it("resolves a paid SKU to its FREE twin from the catalog", () => {
const twin = findFreeTwin("CZH01");
expect(twin).toBeDefined();
expect(twin!.isTrial).toBe(true);
expect(twin!.name).toContain("免费试看");
expect(twin!.sku).toBe("FREECZH01");
});
it("is idempotent: an already-FREE SKU resolves to itself", () => {
const twin = findFreeTwin("FREECZH01");
expect(twin?.sku).toBe("FREECZH01");
// case-insensitive
expect(findFreeTwin("freeczh01")?.sku).toBe("FREECZH01");
});
it("returns undefined for unknown SKUs and empty input", () => {
expect(findFreeTwin("NOPE99")).toBeUndefined();
expect(findFreeTwin(" ")).toBeUndefined();
expect(findFreeTwin("")).toBeUndefined();
});
it("returns undefined when the paid SKU has no free twin (not in catalog)", () => {
// CZH99 doesn't exist in the catalog at all → no twin
expect(findFreeTwin("CZH99")).toBeUndefined();
});
it("does not fabricate twins for paid SKUs that exist without a FREE variant", () => {
// CZP01 exists as a paid SKU (B 马来西亚名师课) with NO FREE twin —
// verified against catalog: only CZP01/CZP02 lack twins.
// findFreeTwin must NOT invent FREECZP01.
expect(findFreeTwin("CZP01")).toBeUndefined();
expect(findFreeTwin("CZP02")).toBeUndefined();
});
});
describe("extractSku (改动1: 从消息文本提取 SKU)", () => {
it("extracts the SKU from a digikedai.com product URL", () => {
expect(extractSku("digikedai.com/products/czh01")).toBe("CZH01");
expect(extractSku("https://www.digikedai.com/products/czh01/")).toBe("CZH01");
});
it("extracts a SKU embedded in a natural-language trial request", () => {
expect(extractSku("我要这个试用:\ndigikedai.com/products/czh01")).toBe("CZH01");
expect(extractSku("想试试 CZH01 的免费版")).toBe("CZH01");
});
it("extracts bare SKU tokens from casual text (case-insensitive)", () => {
expect(extractSku("czh01 有免费版吗")).toBe("CZH01");
expect(extractSku("tell me about freecxm04")).toBe("FREECXM04");
});
it("returns undefined when no known SKU appears", () => {
expect(extractSku("你好")).toBeUndefined();
expect(extractSku("这个产品多少钱")).toBeUndefined();
expect(extractSku("")).toBeUndefined();
expect(extractSku(" ")).toBeUndefined();
});
});
describe("TRIAL_INTENT_RE (试用意图检测)", () => {
it("matches zh/en/ms/id trial-intent phrases", () => {
// zh
expect(TRIAL_INTENT_RE.test("我要这个试用:")).toBe(true);
expect(TRIAL_INTENT_RE.test("免费版有吗")).toBe(true);
expect(TRIAL_INTENT_RE.test("想试试看")).toBe(true);
expect(TRIAL_INTENT_RE.test("免费试用")).toBe(true);
expect(TRIAL_INTENT_RE.test("看看这个")).toBe(true);
expect(TRIAL_INTENT_RE.test("预览一下")).toBe(true);
expect(TRIAL_INTENT_RE.test("试听一下")).toBe(true);
expect(TRIAL_INTENT_RE.test("体验一下")).toBe(true);
expect(TRIAL_INTENT_RE.test("试读看看")).toBe(true);
// en
expect(TRIAL_INTENT_RE.test("trial pls")).toBe(true);
expect(TRIAL_INTENT_RE.test("I want to try")).toBe(true);
expect(TRIAL_INTENT_RE.test("try this")).toBe(true);
expect(TRIAL_INTENT_RE.test("free trial")).toBe(true);
expect(TRIAL_INTENT_RE.test("preview")).toBe(true);
expect(TRIAL_INTENT_RE.test("sample")).toBe(true);
expect(TRIAL_INTENT_RE.test("demo")).toBe(true);
expect(TRIAL_INTENT_RE.test("check out")).toBe(true);
expect(TRIAL_INTENT_RE.test("take a look")).toBe(true);
expect(TRIAL_INTENT_RE.test("give it a try")).toBe(true);
// ms / id
expect(TRIAL_INTENT_RE.test("percubaan")).toBe(true);
expect(TRIAL_INTENT_RE.test("cuba")).toBe(true);
expect(TRIAL_INTENT_RE.test("nak cuba")).toBe(true);
expect(TRIAL_INTENT_RE.test("tengok dulu")).toBe(true);
expect(TRIAL_INTENT_RE.test("nak tengok")).toBe(true);
expect(TRIAL_INTENT_RE.test("lihat dulu")).toBe(true);
expect(TRIAL_INTENT_RE.test("coba")).toBe(true);
expect(TRIAL_INTENT_RE.test("gratis")).toBe(true);
});
it("does not match plain product/price questions", () => {
expect(TRIAL_INTENT_RE.test("这个产品多少钱")).toBe(false);
expect(TRIAL_INTENT_RE.test("CZH01 价格是多少")).toBe(false);
expect(TRIAL_INTENT_RE.test("how much")).toBe(false);
});
it("word boundaries avoid English false positives", () => {
expect(TRIAL_INTENT_RE.test("do you have chemistry courses?")).toBe(false);
expect(TRIAL_INTENT_RE.test("please retry")).toBe(false);
expect(TRIAL_INTENT_RE.test("latest update")).toBe(false);
expect(TRIAL_INTENT_RE.test("country list")).toBe(false);
});
});
it("deep-link guidance points to the claim button (第3批 一步一动作)", () => {
const e = findTrialEntry("FREECXM04")!;
const zh = startSkuText(e, "zh");
expect(zh).toContain("免费试看");
expect(zh).toContain("FREECXM04");
expect(zh).toContain("领取");
expect(zh).not.toContain("ok username:abc123");
expect(zh).not.toContain("同意 用户名:abc123");
expect(zh).not.toContain("setuju username:abc123");
const en = startSkuText(e, "en");
expect(en).toContain("claim");
expect(en).not.toContain("ok username:abc123");
expect(en).not.toContain("同意 用户名:abc123");
const ms = startSkuText(e, "ms");
expect(ms).toContain("menuntut");
expect(ms).not.toContain("setuju username:abc123");
});
it("parseTrialConsent: zh/en/ms consent words + label line all accepted", () => {
expect(parseTrialConsent("同意 abc123")).toEqual({ consented: true, username: "abc123" });
expect(parseTrialConsent("好的 abc123")).toEqual({ consented: true, username: "abc123" });
expect(parseTrialConsent("好 abc123")).toEqual({ consented: true, username: "abc123" });
expect(parseTrialConsent("ok abc123")).toEqual({ consented: true, username: "abc123" });
expect(parseTrialConsent("okay abc123")).toEqual({ consented: true, username: "abc123" });
expect(parseTrialConsent("yes abc123")).toEqual({ consented: true, username: "abc123" });
expect(parseTrialConsent("setuju abc123")).toEqual({ consented: true, username: "abc123" });
expect(parseTrialConsent("saya setuju abc123")).toEqual({ consented: true, username: "abc123" });
expect(parseTrialConsent("username:abc123")).toEqual({ consented: true, username: "abc123" });
expect(parseTrialConsent("用户名:abc123")).toEqual({ consented: true, username: "abc123" });
expect(parseTrialConsent("nama pengguna abc123")).toEqual({ consented: true, username: "abc123" });
});
it("parseTrialConsent: consent without username is consented but leaves it blank", () => {
expect(parseTrialConsent("同意")).toEqual({ consented: true, username: undefined });
expect(parseTrialConsent("ok")).toEqual({ consented: true, username: undefined });
expect(parseTrialConsent("setuju")).toEqual({ consented: true, username: undefined });
});
it("parseTrialConsent: ordinary chat is NOT a consent reply", () => {
expect(parseTrialConsent("这个产品多少钱")).toEqual({ consented: false });
expect(parseTrialConsent("如何获得账号")).toEqual({ consented: false });
expect(parseTrialConsent("谢谢")).toEqual({ consented: false });
expect(parseTrialConsent("")).toEqual({ consented: false });
});
it("ASK_USERNAME_TEXT asks only for the username (第3批 一步一动作)", () => {
const zh = ASK_USERNAME_TEXT("zh");
expect(zh).toContain("请回复你的用户名");
expect(zh).toContain("abc123");
expect(zh).not.toContain("ok username:abc123");
expect(zh).not.toContain("同意 用户名:abc123");
const en = ASK_USERNAME_TEXT("en");
expect(en).toContain("Please reply with your username");
expect(en).toContain("abc123");
expect(en).not.toContain("ok username:abc123");
const ms = ASK_USERNAME_TEXT("ms");
expect(ms).toContain("nama pengguna");
expect(ms).toContain("abc123");
expect(ms).not.toContain("setuju username:abc123");
});
it("trialSuccessText (第5批): drive login URL + bookmark + products up-sell, in order, single-language", () => {
const fresh = trialSuccessText(
{
ok: true,
username: "alice",
password: "pw12345678",
expiresAt: "2026-09-14",
productUrl: "https://www.digikedai.com/products/cxm04/",
message: "Free account ready: alice",
},
"zh",
);
// 全新账号:标题 + 凭据
expect(fresh).toContain("免费账号已开通");
expect(fresh).toContain("👤 用户名: <code>alice</code>");
expect(fresh).toContain("🔑 密码: <code>pw12345678</code>");
// 第2批:product URL 是原付费品页(不是 trial 页)
expect(fresh).toContain("https://www.digikedai.com/products/cxm04/");
expect(fresh).not.toContain("freecxm04");
// 第5批:登录网址独立成行(URL 单独一行,前后都是换行)
const lines = fresh.split("\n");
expect(lines).toContain("https://drive.digikedai.com");
const driveIdx = lines.indexOf("https://drive.digikedai.com");
expect(driveIdx).toBeGreaterThan(0);
// bookmark 提醒在 drive URL 之后
expect(lines[driveIdx + 1]).toContain("browser bookmark");
// products up-sell 在 drive/bookmark 之后
const productsIdx = lines.indexOf("https://www.digikedai.com/products");
expect(productsIdx).toBeGreaterThan(driveIdx);
expect(lines[productsIdx + 1]).toContain("免费试看");
// 单语:无英文残留
expect(fresh).not.toContain("Free account ready");
});
it("trialSuccessText: reused / added-product outcomes drop password, keep drive info", () => {
const reused = trialSuccessText(
{
ok: true,
existed: true,
username: "alice",
expiresAt: "2026-09-14",
productUrl: "https://www.digikedai.com/products/cxm04/",
message: "Added FREECXM04 to your existing account.",
},
"zh",
);
expect(reused).toContain("已有账号");
expect(reused).toContain("密码与之前一致");
expect(reused).not.toContain("🔑 密码: <code>");
expect(reused).toContain("https://drive.digikedai.com");
const added = trialSuccessText(
{
ok: true,
existed: true,
addedProduct: true,
username: "alice",
expiresAt: "2026-09-14",
productUrl: "https://www.digikedai.com/products/cxm04/",
message: "Added FREECXM04 to your existing account.",
},
"zh",
);
expect(added).toContain("新产品已加入你的账号");
expect(added).toContain("密码与之前一致");
expect(added).not.toContain("🔑 密码: <code>");
});
it("trialSuccessText: en and ms variants are single-language with same URL layout", () => {
const base = {
ok: true,
username: "alice",
password: "pw12345678",
expiresAt: "2026-09-14",
productUrl: "https://www.digikedai.com/products/cxm04/",
message: "Free account ready: alice",
};
const en = trialSuccessText(base, "en");
expect(en).toContain("Free account ready");
expect(en).toContain("https://drive.digikedai.com");
expect(en).toContain("browser bookmark");
expect(en).toContain("https://www.digikedai.com/products");
expect(en).not.toContain("免费账号已开通");
const ms = trialSuccessText(base, "ms");
expect(ms).toContain("Akaun percuma sedia");
expect(ms).toContain("https://drive.digikedai.com");
expect(ms).toContain("penanda buku");
expect(ms).not.toContain("Free account ready");
});
it("trialErrorText appends the /trial form reminder in one language", () => {
const zh = trialErrorText("Username taken", "zh");
expect(zh).toContain("⚠️ Username taken");
expect(zh).toContain("FREECXM04");
expect(zh).not.toContain("Percubaan Percuma");
const en = trialErrorText("Username taken", "en");
expect(en).toContain("Free Trial");
expect(en).not.toContain("免费试用");
});
it("user_memory key for trial username is stable", () => {
expect(MEMORY_KEY_TRIAL_USERNAME).toBe("trial_username");
});
it("四·补·三第3批: confirmReuseText 单语确认句包含既有账号名与两条路径", () => {
const zh = confirmReuseText("lover", "zh");
expect(zh).toContain("检测到你的账号");
expect(zh).toContain("<code>lover</code>");
expect(zh).toContain("可以");
expect(zh).toContain("新账号 用户名:abc123");
expect(zh).not.toContain("I found your existing account");
expect(zh).not.toContain("Akaun sedia ada");
const en = confirmReuseText("lover", "en");
expect(en).toContain("I found your existing account");
expect(en).toContain("<code>lover</code>");
expect(en).toContain("new account username:abc123");
expect(en).not.toContain("检测到你的账号");
const ms = confirmReuseText("lover", "ms");
expect(ms).toContain("Akaun sedia ada");
expect(ms).toContain("akaun baru username:abc123");
expect(ms).not.toContain("新账号 用户名");
});
it("四·补·三第3批: parseAccountDecision — 同意词 → reuse(加到既有账号)", () => {
expect(parseAccountDecision("可以")).toEqual({ action: "reuse" });
expect(parseAccountDecision("同意")).toEqual({ action: "reuse" });
expect(parseAccountDecision("好的")).toEqual({ action: "reuse" });
expect(parseAccountDecision("好")).toEqual({ action: "reuse" });
expect(parseAccountDecision("ok")).toEqual({ action: "reuse" });
expect(parseAccountDecision("okay")).toEqual({ action: "reuse" });
expect(parseAccountDecision("yes")).toEqual({ action: "reuse" });
expect(parseAccountDecision("ya")).toEqual({ action: "reuse" });
expect(parseAccountDecision("setuju")).toEqual({ action: "reuse" });
expect(parseAccountDecision("saya setuju")).toEqual({ action: "reuse" });
expect(parseAccountDecision("agree")).toEqual({ action: "reuse" });
expect(parseAccountDecision("可以,继续")).toEqual({ action: "reuse" });
});
it("四·补·三第3批: parseAccountDecision — 新账号意图 → new(可带 username", () => {
expect(parseAccountDecision("新账号 abc123")).toEqual({ action: "new", username: "abc123" });
expect(parseAccountDecision("新账号 用户名:abc123")).toEqual({ action: "new", username: "abc123" });
expect(parseAccountDecision("新账号 username:abc123")).toEqual({ action: "new", username: "abc123" });
expect(parseAccountDecision("new account abc123")).toEqual({ action: "new", username: "abc123" });
expect(parseAccountDecision("new account username:abc123")).toEqual({ action: "new", username: "abc123" });
expect(parseAccountDecision("akaun baru abc123")).toEqual({ action: "new", username: "abc123" });
expect(parseAccountDecision("akaun baru username:abc123")).toEqual({ action: "new", username: "abc123" });
// 只说要新账号、没给 username → bot 再追问
expect(parseAccountDecision("新账号")).toEqual({ action: "new", username: undefined });
expect(parseAccountDecision("new account")).toEqual({ action: "new", username: undefined });
expect(parseAccountDecision("akaun baru")).toEqual({ action: "new", username: undefined });
});
it("四·补·三第3批: parseAccountDecision — 无关消息返回 null(掉落普通管线)", () => {
expect(parseAccountDecision("我想问价格")).toBeNull();
expect(parseAccountDecision("谢谢")).toBeNull();
expect(parseAccountDecision("随便聊聊")).toBeNull();
expect(parseAccountDecision("")).toBeNull();
expect(parseAccountDecision(" ")).toBeNull();
});
it("四·补·五第3批: ASK_NEW_ACCOUNT_USERNAME_TEXT 只追问新用户名(一步一动作)", () => {
const zh = ASK_NEW_ACCOUNT_USERNAME_TEXT("zh");
expect(zh).toContain("开新账号");
expect(zh).toContain("abc123");
expect(zh).not.toContain("新账号 用户名:abc123");
const en = ASK_NEW_ACCOUNT_USERNAME_TEXT("en");
expect(en).toContain("new account");
expect(en).toContain("abc123");
expect(en).not.toContain("new account username:abc123");
const ms = ASK_NEW_ACCOUNT_USERNAME_TEXT("ms");
expect(ms).toContain("akaun baharu");
expect(ms).toContain("abc123");
expect(ms).not.toContain("akaun baru username:abc123");
});
it("四·补·三第3批: extractUsername 提取 label 行或裸用户名", () => {
expect(extractUsername("username:abc123")).toBe("abc123");
expect(extractUsername("用户名:abc123")).toBe("abc123");
expect(extractUsername("nama pengguna abc123")).toBe("abc123");
expect(extractUsername("abc123")).toBe("abc123");
expect(extractUsername("ABC123")).toBe("ABC123"); // 大小写原样,provision 层规范化
expect(extractUsername("随便聊聊 abc123")).toBe("abc123"); // label 行可带前缀语气词
expect(extractUsername("太好了")).toBeUndefined();
expect(extractUsername("")).toBeUndefined();
});
describe("第3批 — free 试看领取 inline 按钮(callback_data 契约)", () => {
it("TRIAL_CB 契约稳定且不与购买 buy:* 冲突", () => {
expect(TRIAL_CB.confirm).toBe("trial:confirm");
expect(TRIAL_CB.newUsername).toBe("trial:new-username");
expect(TRIAL_CB.reuse).toBe("trial:reuse");
for (const v of Object.values(TRIAL_CB)) {
expect(v.startsWith("trial:")).toBe(true);
expect(v.startsWith("buy:")).toBe(false);
}
});
it("startSkuKeyboard: 领取 + 换新用户名,三语跟随(第3批 一步一动作)", () => {
const zh = startSkuKeyboard("zh");
const zhRows = zh.inline_keyboard as { text: string; callback_data: string }[][];
expect(zhRows.length).toBe(2); // 两行按钮
expect(zhRows[0][0].text).toBe("✅ 领取");
expect(zhRows[0][0].callback_data).toBe(TRIAL_CB.confirm);
expect(zhRows[1][0].text).toBe("✏️ 换新用户名");
expect(zhRows[1][0].callback_data).toBe(TRIAL_CB.newUsername);
const en = startSkuKeyboard("en");
const enRows = en.inline_keyboard as { text: string; callback_data: string }[][];
expect(enRows[0][0].text).toBe("✅ Claim");
expect(enRows[1][0].text).toBe("✏️ New username");
const ms = startSkuKeyboard("ms");
const msRows = ms.inline_keyboard as { text: string; callback_data: string }[][];
expect(msRows[0][0].text).toBe("✅ Tuntut");
expect(msRows[1][0].text).toBe("✏️ Username baharu");
});
it("confirmReuseKeyboard: 加到现有账号 + 开新账号,三语跟随", () => {
const zh = confirmReuseKeyboard("zh");
const zhRows = zh.inline_keyboard as { text: string; callback_data: string }[][];
expect(zhRows.length).toBe(2);
expect(zhRows[0][0].text).toBe("♻️ 加到现有账号");
expect(zhRows[0][0].callback_data).toBe(TRIAL_CB.reuse);
expect(zhRows[1][0].text).toBe("✏️ 开新账号");
expect(zhRows[1][0].callback_data).toBe(TRIAL_CB.newUsername);
const en = confirmReuseKeyboard("en");
const enRows = en.inline_keyboard as { text: string; callback_data: string }[][];
expect(enRows[0][0].text).toBe("♻️ Add to existing account");
expect(enRows[1][0].text).toBe("✏️ Open new account");
const ms = confirmReuseKeyboard("ms");
const msRows = ms.inline_keyboard as { text: string; callback_data: string }[][];
expect(msRows[0][0].text).toBe("♻️ Tambah ke akaun sedia ada");
expect(msRows[1][0].text).toBe("✏️ Buka akaun baharu");
});
it("按钮文本中性词纪律:不出现平台/支付名", () => {
const noise = ["Shopee", "Lazada", "Add-On", "Touch 'n Go", "TnG", "bank-in", "银行转账"];
for (const lang of ["zh", "en", "ms"] as const) {
for (const kb of [startSkuKeyboard(lang), confirmReuseKeyboard(lang)]) {
const rows = kb.inline_keyboard as { text: string }[][];
for (const row of rows) {
for (const btn of row) {
for (const n of noise) expect(btn.text).not.toContain(n);
}
}
}
}
});
});
describe("四·补·五第2批 — 多账号选择(choose-account", () => {
it("TRIAL_ACCT_PREFIX 与 trialAcctCb 构造稳定,且不进 TRIAL_CB", () => {
expect(TRIAL_ACCT_PREFIX).toBe("trial:acct:");
expect(trialAcctCb(13)).toBe("trial:acct:13");
// 不进 TRIAL_CB:契约测试 Object.values 全为字符串
for (const v of Object.values(TRIAL_CB)) {
expect(v).not.toBe("trial:acct:");
}
});
it("chooseAccountText 三语单语,无其他语言残留", () => {
const accounts = [
{ id: 13, username: "lover" },
{ id: 14, username: "abc" },
];
const zh = chooseAccountText(accounts, "zh");
expect(zh).toContain("多个账号");
expect(zh).not.toContain("multiple accounts");
expect(zh).not.toContain("beberapa akaun");
const en = chooseAccountText(accounts, "en");
expect(en).toContain("multiple accounts");
expect(en).not.toContain("多个账号");
const ms = chooseAccountText(accounts, "ms");
expect(ms).toContain("beberapa akaun");
expect(ms).not.toContain("multiple accounts");
});
it("chooseAccountTooManyText 三语含完整编号清单", () => {
const accounts = [
{ id: 13, username: "lover" },
{ id: 14, username: "abc" },
];
const zh = chooseAccountTooManyText(accounts, "zh");
expect(zh).toContain("2 个账号");
expect(zh).toContain("1. <code>lover</code>");
expect(zh).toContain("2. <code>abc</code>");
const en = chooseAccountTooManyText(accounts, "en");
expect(en).toContain("2 accounts");
expect(en).toContain("1. <code>lover</code>");
expect(en).toContain("2. <code>abc</code>");
const ms = chooseAccountTooManyText(accounts, "ms");
expect(ms).toContain("2 akaun");
expect(ms).toContain("1. <code>lover</code>");
expect(ms).toContain("2. <code>abc</code>");
});
it("chooseAccountKeyboard: 每账号一行按钮 + 开新账号;cap 8", () => {
const accounts = [
{ id: 13, username: "lover" },
{ id: 14, username: "abc" },
];
const kb = chooseAccountKeyboard(accounts, "zh");
const rows = kb.inline_keyboard as { text: string; callback_data: string }[][];
expect(rows.length).toBe(3); // 2 账号 + 1 开新账号
expect(rows[0][0].text).toBe("👤 lover");
expect(rows[0][0].callback_data).toBe("trial:acct:13");
expect(rows[1][0].text).toBe("👤 abc");
expect(rows[1][0].callback_data).toBe("trial:acct:14");
expect(rows[2][0].text).toBe("✏️ 开新账号");
expect(rows[2][0].callback_data).toBe(TRIAL_CB.newUsername);
// cap 8:传 10 个账号只出 8 行 + 1 行开新账号
const ten = Array.from({ length: 10 }, (_, i) => ({
id: i + 1,
username: `user${i + 1}`,
}));
const kb2 = chooseAccountKeyboard(ten, "zh");
const rows2 = kb2.inline_keyboard as { text: string; callback_data: string }[][];
expect(rows2.length).toBe(9); // 8 账号 + 1 开新账号
expect(rows2[7][0].callback_data).toBe("trial:acct:8");
expect(rows2[8][0].text).toBe("✏️ 开新账号");
// 三语「开新账号」标签
const en = chooseAccountKeyboard(accounts, "en");
const enRows = en.inline_keyboard as { text: string }[][];
expect(enRows[2][0].text).toBe("✏️ Open new account");
const ms = chooseAccountKeyboard(accounts, "ms");
const msRows = ms.inline_keyboard as { text: string }[][];
expect(msRows[2][0].text).toBe("✏️ Buka akaun baharu");
});
});
});