// The seven widgets, each with the install snippet its vendor documents and a widget id the vendor itself runs on its
// own homepage (found with ids.ts on 2026-09-27; no accounts were made anywhere). Spookat is the production launcher from
// cdn.spookat.com with a key no site uses: before the click nothing but the cdn is involved, so the key doesn't matter.
// data-api points its socket, opened on the click, at the lab's mock (serve.ts), the one part that is not production.
//
// Five of the six rivals refuse to boot on a domain their owner didn't allow (checked 2026-09-27 on our test page:
// Crisp shows "Invalid website", Intercom gets a 403, LiveChat logs "not added to the allowed domains", Chatwoot's iframe
// is blocked by its frame-ancestors CSP, Tidio's is set to stay hidden). The ids we may use belong to the vendors' own
// sites, so every test runs twice where it can: on our test page (what loads before the refusal), and on the vendor's own
// homepage (home), where their widget boots fully; there we count only the widget's traffic (widgetUrls).
export type Widget = {
id: string;
name: string;
/** the vendor's page that runs this widget id (spookat.com runs none yet) */
home?: string;
/** host + path of what only the widget loads: on the vendor's homepage, requests matching this count as the widget's */
widgetUrls: RegExp;
/** and every request made from a frame whose url matches this (a widget iframe that shares a CDN with its host page) */
widgetFrame?: RegExp;
/** the script the snippet loads first: blocking it gives the same page without the widget (Lighthouse pattern) */
loader: string;
/** boots fully on our test page (the others refuse the domain, so their interactive tests run on home) */
boots: boolean;
/** what a visitor clicks to open the chat, and what shows once they can use it (any frame, open shadow roots too) */
launcher: string;
ready: string;
/** what closes the open chat, when a second click on the launcher's spot doesn't */
close?: string;
/** the widget's root in the page itself (axe runs on it and on every frame the widget adds) */
root: string;
snippet: string;
};
export const SPOOKAT_KEY = "site_live_0000000000000000";
export const WIDGETS: Widget[] = [
{
id: "spookat",
loader: "*cdn.spookat.com/s/*",
widgetUrls: /^cdn\.spookat\.com\//,
boots: true,
launcher: "#spookat button.L",
ready: "#spookat form.c input",
root: "#spookat",
name: "Spookat",
snippet: ``,
},
{
id: "tawk-to",
loader: "*embed.tawk.to/*",
home: "https://www.tawk.to/",
widgetUrls: /^(?!www\.)[\w-]+\.tawk\.to\//,
boots: true,
launcher: "button.tawk-button-circle.tawk-button-large",
ready: "textarea.tawk-chatinput-editor",
root: "div.widget-visible, iframe[title=\"Chat widget\"]",
name: "Tawk.to",
snippet: ``,
},
{
id: "crisp",
loader: "*client.crisp.chat/l.js*",
home: "https://crisp.chat/en/",
widgetUrls: /^(client|settings|image)\.crisp\.chat\/|\.relay(\.rescue)?\.crisp\.chat\//,
boots: false,
launcher: "[aria-label=\"Open chat\"]",
ready: "textarea[name=message]",
root: "#crisp-chatbox, .crisp-client",
name: "Crisp",
snippet: ``,
},
{
id: "intercom",
loader: "*widget.intercom.io/widget/*",
home: "https://www.intercom.com/",
widgetUrls: /^([\w-]+\.)?intercom\.io\/|^js\.intercomcdn\.com\//,
boots: false,
launcher: ".intercom-launcher",
ready: ".messenger-rte-content, button:has-text(\"Send us a message\")",
root: ".intercom-lightweight-app, #intercom-container, iframe[name^=intercom]",
name: "Intercom",
snippet: `
`,
},
{
id: "tidio",
loader: "*code.tidio.co/*",
home: "https://www.tidio.com/",
widgetUrls: /\.tidio\.co\//,
boots: true,
launcher: "#button-body",
ready: "button:has-text(\"Chat with us\"), textarea",
close: "button[aria-label=Minimize]",
root: "#tidio-chat",
name: "Tidio",
// Tidio's own widget is set to stay hidden on tidio.com (they open it from their own links). Tidio's documented
// tidioChatApi.show(), run by the page every 250 ms until the launcher is there (before any click), gives the launcher a default install shows.
snippet: `
`,
},
{
id: "livechat",
loader: "*cdn.livechatinc.com/tracking.js*",
home: "https://www.livechat.com/",
widgetUrls: /\.livechatinc\.com\/(?!partners\/)/, // partners/: livechat.com's own affiliate tracking
boots: false,
launcher: "#chat-widget-minimized",
ready: "textarea[aria-label=\"Write a messageā¦\"], input[aria-label=\"Write a messageā¦\"]", // on livechat.com the closed widget is itself a message box
root: "#chat-widget-container",
name: "LiveChat",
snippet: ``,
},
{
id: "chatwoot",
loader: "*app.chatwoot.com/packs/js/sdk.js*",
// the homepage booted the widget in 0 of 3 tries and the pricing page in 3 of 17; the help center did every time. It is
// itself a Chatwoot portal and shares some script files with the widget, which it loads first: those come from cache
// for the widget, so its transferred bytes here are a floor
home: "https://www.chatwoot.com/hc/user-guide/en",
// the loader, the widget's iframe, its api, socket and help-center articles; its files on CloudFront by frame (below)
widgetUrls: /^app\.chatwoot\.com\/(packs|widget|api\/v1\/widget|cable|hc\/[^/]+\/[^/]+\/articles\.json)/,
widgetFrame: /^https:\/\/app\.chatwoot\.com\/widget/,
boots: false,
launcher: "button.woot-widget-bubble",
ready: "button:has-text(\"Start Conversation\"), textarea",
root: ".woot-widget-holder, .woot--bubble-holder, button.woot-widget-bubble",
name: "Chatwoot",
snippet: ``,
},
];