// 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: ``, }, ];