7697673a3b
Co-Authored-By: Claude Code <noreply@anthropic.com>
213 lines
10 KiB
HTML
213 lines
10 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Widget Test — Invite Card + Theme Color + Assertions</title>
|
|
<!-- 断言面板依赖 window.ChatTheme(与 widget.js 内联拷贝做一致性比对) -->
|
|
<script src="/static/js/theme.js"></script>
|
|
<style>
|
|
body {
|
|
font-family: Arial, sans-serif;
|
|
padding: 40px;
|
|
background: #f5f5f5;
|
|
}
|
|
.info {
|
|
background: white;
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
margin-bottom: 20px;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
}
|
|
h1 { color: #333; margin-bottom: 10px; }
|
|
h2 { color: #333; margin-bottom: 10px; }
|
|
p { color: #666; line-height: 1.6; }
|
|
ul { color: #666; }
|
|
.test-list li { margin-bottom: 8px; }
|
|
.test-list li::before { content: "☐ "; color: #066fc0; font-weight: bold; }
|
|
#assertions div { padding: 4px 0; border-bottom: 1px solid #eee; font-family: Consolas, monospace; font-size: 13px; }
|
|
#assertions div:last-child { border-bottom: none; }
|
|
#colorPicker { width: 48px; height: 32px; padding: 0; border: 1px solid #ccc; border-radius: 4px; vertical-align: middle; cursor: pointer; }
|
|
.btn { padding: 8px 16px; background: #066fc0; color: white; border: none; border-radius: 4px; cursor: pointer; margin-right: 6px; }
|
|
.btn:hover { opacity: 0.9; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="info">
|
|
<h1>Widget Invite Card Test Page</h1>
|
|
<p>This page tests the auto-popup invite card feature.</p>
|
|
<p><strong>Expected behavior:</strong></p>
|
|
<ul class="test-list">
|
|
<li>Invite card appears after 5 seconds</li>
|
|
<li>Invite card has brand-color gradient header with robot emoji</li>
|
|
<li>Topic buttons come from the tenant brand config (<code>/api/public/brand</code> → topics)</li>
|
|
<li>Action buttons: "Start Chat" and "No Thanks"</li>
|
|
<li>Click a topic button → invite closes → chat opens → message auto-sends</li>
|
|
<li>Click "Start Chat" → invite closes → chat opens</li>
|
|
<li>Click "No Thanks" → invite permanently closes</li>
|
|
<li>Click "No Thanks" or X → refresh → invite stays hidden AND the old card area stays clickable (no invisible overlay; regression 2026-09-01)</li>
|
|
<li>Click toggle button (while invite visible) → invite closes → chat opens</li>
|
|
<li>Refresh page → invite appears again after 5s (unless "No Thanks" was clicked)</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="info">
|
|
<h2>Popular Topics in Chat Panel</h2>
|
|
<ul class="test-list">
|
|
<li>Chat panel shows "POPULAR TOPICS" section with the tenant's topic buttons</li>
|
|
<li>Click topic → message auto-sends → topics section hides</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="info">
|
|
<h2>Theme Color Preview</h2>
|
|
<p>Pick a color and Apply, or use a preset. Each click tears down the old widget (host + script + invite dismissal state) and re-embeds with <code>data-color</code>. "Test Broken Avatar" points <code>data-avatar</code> at a missing image to verify the 🤖 emoji fallback.</p>
|
|
<p>
|
|
<input type="color" id="colorPicker" value="#066fc0">
|
|
<button class="btn" onclick="applyColor()">Apply</button>
|
|
<button class="btn" onclick="applyColor('#066fc0')">Brand Blue</button>
|
|
<button class="btn" onclick="applyColor('#1a73e8')">Blue</button>
|
|
<button class="btn" onclick="applyColor('#16a34a')">Green</button>
|
|
<button class="btn" onclick="applyColor('#7c3aed')">Purple</button>
|
|
<button class="btn" onclick="rebuildWidget({'data-mobile':'false','data-color':document.getElementById('colorPicker').value})">Desktop Pill</button>
|
|
<button class="btn" onclick="applyBrokenAvatar()">Test Broken Avatar</button>
|
|
</p>
|
|
</div>
|
|
|
|
<div class="info">
|
|
<h2>Assertion Panel</h2>
|
|
<p>Runs on every widget (re)embed. Checks: derived default palette vs current hardcoded values (max per-channel Δ ≤ 8), derive(default) === DEFAULT_PALETTE, inline widget.js copy === theme.js copy, invalid input falls back to DEFAULT_PALETTE.</p>
|
|
<div id="assertions"></div>
|
|
</div>
|
|
|
|
<div class="info">
|
|
<p><strong>Current Page:</strong> <span id="pathname"></span></p>
|
|
<p><strong>Storage Key:</strong> widget_invite_dismissed_<slug>_<span id="storage-key"></span> (Task 10: key 按租户 slug 命名空间化)</p>
|
|
<p><button class="btn" onclick="clearStorage()">Clear Storage & Refresh</button></p>
|
|
</div>
|
|
|
|
<script>
|
|
document.getElementById('pathname').textContent = window.location.pathname;
|
|
document.getElementById('storage-key').textContent = window.location.pathname;
|
|
|
|
function clearStorage() {
|
|
// Task 10: key 已租户化(widget_invite_dismissed_<slug>_<path>)——按前缀清扫全部
|
|
try {
|
|
Object.keys(sessionStorage).forEach(function (k) {
|
|
if (k.indexOf('widget_invite_dismissed_') === 0) sessionStorage.removeItem(k);
|
|
});
|
|
} catch (e) {}
|
|
location.reload();
|
|
}
|
|
|
|
/* ===== Teardown / 重建(spec §7.1 三个陷阱:旧 host、旧 script、sessionStorage) ===== */
|
|
var BASE_ATTRS = {
|
|
"data-position": "bottom-right",
|
|
"data-theme": "light",
|
|
"data-mobile": "true",
|
|
"data-debug": "true",
|
|
"data-whatsapp": "https://wa.me/8613800138000",
|
|
"data-email": "mailto:demo@example.com"
|
|
};
|
|
|
|
var rebuildLoading = false, rebuildPending = null;
|
|
function removeWidgetHosts() {
|
|
// Task 10: host id 已租户化(cc-widget-host-<slug>),按前缀清扫
|
|
var hosts = document.querySelectorAll('[id^="cc-widget-host-"]');
|
|
for (var i = 0; i < hosts.length; i++) hosts[i].remove();
|
|
// 复审: 清掉旧挂件的 debug 钩子——waitForWidget 轮询它,旧钩子残留
|
|
// 会让重建中的断言误读"新挂件已挂载"
|
|
try { delete window.__chatWidgetDebug; } catch (e) {}
|
|
}
|
|
function rebuildWidget(extra) {
|
|
if (rebuildLoading) { rebuildPending = extra || {}; return; } // 快速连点:只记最新意图,当前加载完接力重建
|
|
rebuildLoading = true;
|
|
document.getElementById("assertions").innerHTML = "<div>…rebuilding…</div>";
|
|
removeWidgetHosts();
|
|
document.querySelectorAll("script[src*='widget.js']").forEach(function (s) { s.remove(); });
|
|
try {
|
|
Object.keys(sessionStorage).forEach(function (k) {
|
|
if (k.indexOf("widget_invite_dismissed_") === 0) sessionStorage.removeItem(k);
|
|
});
|
|
} catch (e) {}
|
|
var s = document.createElement("script");
|
|
s.src = "/static/widget.js?" + Date.now();
|
|
var attrs = Object.assign({}, BASE_ATTRS, extra || {});
|
|
Object.keys(attrs).forEach(function (k) { s.setAttribute(k, attrs[k]); });
|
|
s.onload = function () {
|
|
rebuildLoading = false;
|
|
// Task 10: 挂件改等 brand 端点落定后才挂载——脚本 onload 早于挂载,
|
|
// 轮询 __chatWidgetDebug(boot 里同步设置,观察到场即 init 已完成)
|
|
// 就绪再清竞态残留并跑断言
|
|
waitForWidget(function () {
|
|
var hosts = document.querySelectorAll('[id^="cc-widget-host-"]');
|
|
for (var i = 0; i < hosts.length - 1; i++) hosts[i].remove(); // 竞态兜底:只保留最后挂载
|
|
runAssertions(); // 挂件未就绪(超时)时其内部 !d 分支自行报红
|
|
});
|
|
var next = rebuildPending; rebuildPending = null;
|
|
if (next) rebuildWidget(next);
|
|
};
|
|
s.onerror = function () {
|
|
rebuildLoading = false;
|
|
document.getElementById("assertions").innerHTML = "<div>❌ widget.js 加载失败</div>";
|
|
var next = rebuildPending; rebuildPending = null;
|
|
if (next) rebuildWidget(next);
|
|
};
|
|
document.body.appendChild(s);
|
|
}
|
|
function waitForWidget(cb) {
|
|
var started = Date.now();
|
|
(function poll() {
|
|
if (window.__chatWidgetDebug) return cb(true);
|
|
if (Date.now() - started > 5000) return cb(false); // brand 挂起/失败兜底
|
|
setTimeout(poll, 100);
|
|
})();
|
|
}
|
|
function applyColor(c) {
|
|
rebuildWidget({ "data-color": c || document.getElementById("colorPicker").value });
|
|
}
|
|
function applyBrokenAvatar() {
|
|
rebuildWidget({ "data-color": "#1a73e8", "data-avatar": "/static/nope.png" });
|
|
}
|
|
|
|
/* ===== 断言面板(spec §8.2) ===== */
|
|
var CURRENT_HARDCODED = { // 现状代表值(HESUPIPE 品牌蓝 #066fc0 派生),Δ ≤ 8/通道
|
|
primary: "#066fc0", gradientEnd: "#0666bb", deepText: "#14517a",
|
|
captionText: "#2174ab", softBg: "#f3f9fe", borderSoft: "#bbdcf2"
|
|
};
|
|
function hexToRgb6(h) {
|
|
h = String(h).replace(/^#/, "");
|
|
return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
|
|
}
|
|
function channelDelta(a, b) {
|
|
var ra = hexToRgb6(a), rb = hexToRgb6(b);
|
|
return Math.max(Math.abs(ra[0] - rb[0]), Math.abs(ra[1] - rb[1]), Math.abs(ra[2] - rb[2]));
|
|
}
|
|
function runAssertions() {
|
|
var rows = [];
|
|
function check(name, ok, detail) { rows.push((ok ? "✅ " : "❌ ") + name + (detail ? " — " + detail : "")); }
|
|
var t = window.ChatTheme, d = window.__chatWidgetDebug;
|
|
if (!t || !d) {
|
|
rows.push("❌ theme.js / widget debug 钩子未就绪");
|
|
} else {
|
|
var p = t.derivePalette("#066fc0");
|
|
Object.keys(CURRENT_HARDCODED).forEach(function (k) {
|
|
var delta = channelDelta(p[k], CURRENT_HARDCODED[k]);
|
|
check("default " + k + " Δ≤8", delta <= 8, p[k] + " vs " + CURRENT_HARDCODED[k] + ", Δ=" + delta);
|
|
});
|
|
check("derive(default) === DEFAULT_PALETTE",
|
|
JSON.stringify(p) === JSON.stringify(t.DEFAULT_PALETTE));
|
|
check("inline copy === theme.js copy",
|
|
d.derivePalette.toString().replace(/\s+/g, "") === t.derivePalette.toString().replace(/\s+/g, ""));
|
|
check("invalid color → DEFAULT_PALETTE",
|
|
JSON.stringify(t.derivePalette("not-a-color")) === JSON.stringify(t.DEFAULT_PALETTE));
|
|
}
|
|
document.getElementById("assertions").innerHTML = rows.map(function (r) { return "<div>" + r + "</div>"; }).join("");
|
|
}
|
|
|
|
/* 初始嵌入:打开页面即有挂件 + 断言跑一轮 */
|
|
document.addEventListener("DOMContentLoaded", function () { rebuildWidget({}); });
|
|
</script>
|
|
</body>
|
|
</html>
|