Track HiveGrid Cloud planning reference
This commit is contained in:
1 parent
b3d5ea6764
commit
e341c5938a
5784 files changed
+1104929
-3
No files matched your search
@@ -0,0 +1,56 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync, statSync } from "node:fs";
|
||||
import { gzipSync } from "node:zlib";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const sdkPath = join(here, "..", "public", "embed", "marketing.js");
|
||||
|
||||
describe("marketing.js SDK", () => {
|
||||
it("is checked into apps/web/public/embed/marketing.js", () => {
|
||||
expect(statSync(sdkPath).isFile()).toBe(true);
|
||||
});
|
||||
|
||||
it("stays under the 3KB gzipped budget", () => {
|
||||
const raw = readFileSync(sdkPath);
|
||||
const gz = gzipSync(raw);
|
||||
expect(gz.byteLength).toBeLessThan(3 * 1024);
|
||||
});
|
||||
|
||||
it("stays under the 5KB raw budget", () => {
|
||||
const raw = readFileSync(sdkPath);
|
||||
expect(raw.byteLength).toBeLessThan(5 * 1024);
|
||||
});
|
||||
|
||||
it("references the capture endpoint path", () => {
|
||||
const text = readFileSync(sdkPath, "utf8");
|
||||
expect(text).toContain("/api/leads/capture");
|
||||
});
|
||||
|
||||
it("exposes the toolctrlMarketing global with the documented surface", () => {
|
||||
const text = readFileSync(sdkPath, "utf8");
|
||||
expect(text).toContain("toolctrlMarketing");
|
||||
expect(text).toContain("captureLead");
|
||||
expect(text).toContain("bindFormById");
|
||||
expect(text).toContain("getAttribution");
|
||||
});
|
||||
|
||||
it("auto-captures the documented UTM/click-id parameters", () => {
|
||||
const text = readFileSync(sdkPath, "utf8");
|
||||
for (const k of ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content", "gclid", "fbclid"]) {
|
||||
expect(text).toContain(k);
|
||||
}
|
||||
});
|
||||
|
||||
it("persists first-touch attribution in localStorage", () => {
|
||||
const text = readFileSync(sdkPath, "utf8");
|
||||
expect(text).toContain("localStorage");
|
||||
expect(text).toContain("__tcm_attr");
|
||||
});
|
||||
|
||||
it("includes a honeypot field name compatible with the capture endpoint", () => {
|
||||
const text = readFileSync(sdkPath, "utf8");
|
||||
expect(text).toContain("website_url");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync, statSync } from "node:fs";
|
||||
import { gzipSync } from "node:zlib";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const widgetPath = join(here, "..", "public", "embed", "widget.js");
|
||||
|
||||
describe("widget.js bootstrap loader", () => {
|
||||
it("is checked into apps/web/public/embed/widget.js", () => {
|
||||
expect(statSync(widgetPath).isFile()).toBe(true);
|
||||
});
|
||||
|
||||
it("stays under the 3KB gzipped budget", () => {
|
||||
const raw = readFileSync(widgetPath);
|
||||
const gz = gzipSync(raw);
|
||||
// 3 KB = 3 * 1024 = 3072 bytes
|
||||
expect(gz.byteLength).toBeLessThan(3072);
|
||||
});
|
||||
|
||||
it("stays under the 5KB raw budget", () => {
|
||||
const raw = readFileSync(widgetPath);
|
||||
expect(raw.byteLength).toBeLessThan(5 * 1024);
|
||||
});
|
||||
|
||||
it("references the embed forms route path", () => {
|
||||
const text = readFileSync(widgetPath, "utf8");
|
||||
expect(text).toContain("/embed/forms/");
|
||||
});
|
||||
|
||||
it("uses a sandboxed iframe", () => {
|
||||
const text = readFileSync(widgetPath, "utf8");
|
||||
expect(text).toContain("sandbox");
|
||||
expect(text).toContain("allow-scripts");
|
||||
});
|
||||
|
||||
it("listens for postMessage resize / submitted / error events", () => {
|
||||
const text = readFileSync(widgetPath, "utf8");
|
||||
expect(text).toContain("tcform:resize");
|
||||
expect(text).toContain("tcform:submitted");
|
||||
expect(text).toContain("tcform:error");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Form-Widget bootstrap loader (Phase 05 Task #58).
|
||||
*
|
||||
* Tenants embed this on their site with:
|
||||
*
|
||||
* <script src="https://<host>/embed/widget.js" data-form-key="abc123"
|
||||
* data-host="https://forms.example.com"
|
||||
* async></script>
|
||||
*
|
||||
* The loader:
|
||||
* 1. Reads `data-form-key` and optional `data-host` from its own <script> tag.
|
||||
* 2. Inserts a sandboxed <iframe> pointing at `<host>/embed/forms/<formKey>`.
|
||||
* 3. Listens for postMessage events from the iframe:
|
||||
* - `tcform:resize` -> updates iframe height for dynamic content
|
||||
* - `tcform:submitted` / `tcform:error` -> re-emits as
|
||||
* CustomEvents on document for parent-site code to listen on.
|
||||
*
|
||||
* Build-target: hand-crafted minified ES5-compatible JS in
|
||||
* `apps/web/public/embed/widget.js`. This file is the readable source
|
||||
* kept for documentation + tests. Keep both in sync.
|
||||
*/
|
||||
|
||||
(function () {
|
||||
if (typeof window === "undefined" || typeof document === "undefined") return;
|
||||
const script = (document.currentScript as HTMLScriptElement | null) ?? findSelf();
|
||||
if (!script) return;
|
||||
|
||||
const formKey = script.getAttribute("data-form-key");
|
||||
if (!formKey) return;
|
||||
|
||||
const explicitHost = script.getAttribute("data-host");
|
||||
const defaultHost = new URL(script.src).origin;
|
||||
const host = explicitHost ?? defaultHost;
|
||||
|
||||
const iframe = document.createElement("iframe");
|
||||
iframe.src = host + "/embed/forms/" + encodeURIComponent(formKey) + "?origin=" + encodeURIComponent(location.origin);
|
||||
iframe.setAttribute("title", "Form");
|
||||
iframe.setAttribute("loading", "lazy");
|
||||
iframe.setAttribute("sandbox", "allow-scripts allow-forms allow-same-origin");
|
||||
iframe.style.cssText = "border:0;width:100%;min-height:200px;max-width:100%;display:block";
|
||||
|
||||
const parent = script.parentNode;
|
||||
if (!parent) return;
|
||||
parent.insertBefore(iframe, script.nextSibling);
|
||||
|
||||
window.addEventListener("message", (event) => {
|
||||
if (event.source !== iframe.contentWindow) return;
|
||||
const data = event.data;
|
||||
if (!data || typeof data !== "object" || typeof data.type !== "string") return;
|
||||
if (data.type === "tcform:resize" && typeof data.height === "number") {
|
||||
iframe.style.height = Math.max(100, Math.min(8000, data.height)) + "px";
|
||||
return;
|
||||
}
|
||||
if (data.type === "tcform:submitted" || data.type === "tcform:error") {
|
||||
document.dispatchEvent(new CustomEvent(data.type, { detail: data }));
|
||||
}
|
||||
});
|
||||
|
||||
function findSelf(): HTMLScriptElement | null {
|
||||
const scripts = document.getElementsByTagName("script");
|
||||
for (let i = scripts.length - 1; i >= 0; i--) {
|
||||
const s = scripts[i];
|
||||
if (s.src && s.src.indexOf("/embed/widget.js") !== -1) return s;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
Reference in new issue
Block a user