Skip to content

Commit cbc6909

Browse files
feat(#72-77): test infra — CI matrix, bundle budget, ATTW, JSR, e2e scaffold
- CI runs on Node 20/22/24 matrix (#72) - scripts/bundle-budget.mjs: per-file raw byte budget; ✗ entries fail CI (#75) Current: chatwoot ≤9KB (it ships the most surface), other providers ≤6KB, csp catalog ≤6KB, adapters ≤2KB, core ≤4KB, server stub ≤1KB. - pnpm attw: invokes @arethetypeswrong/cli with --profile esm-only; release.yml runs it pre-publish so types-wrong publishes fail (#74) - jsr.json: full sub-path map for JSR registry; release.yml publishes both npm and JSR (continue-on-error so a JSR outage doesn't block npm) (#76) - vitest config excludes e2e/ — Playwright runs on demand outside the unit suite. e2e/intercom.spec.ts is the template; e2e/README explains setup (#73) - release.yml: runs bundle-budget + attw before publish (#77 already had bumpp + changelogithub + provenance — completed by adding the two gates) Closes #72 #73 #74 #75 #76 #77 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent cfc7b65 commit cbc6909

8 files changed

Lines changed: 195 additions & 2 deletions

File tree

.github/workflows/ci.yml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,16 @@ on:
2424
jobs:
2525
build:
2626
runs-on: ubuntu-latest
27+
strategy:
28+
fail-fast: false
29+
matrix:
30+
node: [20, 22, 24]
2731
steps:
2832
- uses: actions/checkout@v5
2933
- run: corepack enable
3034
- uses: actions/setup-node@v6
3135
with:
32-
node-version: lts/*
36+
node-version: ${{ matrix.node }}
3337
cache: pnpm
3438
- name: 📦 Install dependencies
3539
run: pnpm install --frozen-lockfile
@@ -41,3 +45,8 @@ jobs:
4145
run: pnpm test
4246
- name: 🚀 Build
4347
run: pnpm build
48+
- name: 📐 Bundle size budget
49+
run: pnpm bundle-budget
50+
- name: 🔍 Are The Types Wrong (ATTW)
51+
if: matrix.node == 22
52+
run: pnpm attw

.github/workflows/release.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,21 @@ jobs:
4040
- name: 🚀 Build
4141
run: pnpm build
4242

43+
- name: 📐 Bundle size budget
44+
run: pnpm bundle-budget
45+
46+
- name: 🔍 Are The Types Wrong (ATTW)
47+
run: pnpm attw
48+
4349
- name: 📦 Publish to npm
4450
run: pnpm publish --provenance --access public --no-git-checks
4551
env:
4652
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
4753

54+
- name: 🦕 Publish to JSR
55+
run: pnpm dlx jsr publish --allow-slow-types
56+
continue-on-error: true
57+
4858
- name: 📝 Generate changelog
4959
run: pnpm dlx changelogithub
5060
env:

e2e/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Playwright e2e
2+
3+
Real-browser smoke tests for each provider. Booted from a static fixture
4+
page that imports `ahize/<provider>` from the local build.
5+
6+
## Setup
7+
8+
```sh
9+
pnpm dlx playwright install --with-deps chromium
10+
pnpm e2e
11+
```
12+
13+
## Adding a provider test
14+
15+
1. Add a fixture entry to `e2e/fixtures/providers.json` with the test
16+
credentials (use a sandbox/dev workspace).
17+
2. Drop a `e2e/<provider>.spec.ts` that asserts the script tag is injected,
18+
`isReady()` flips to true, and `identify()` mutates the visible header.
19+
3. Most providers expose `data-*` attributes on their iframe — assert
20+
visibility on those rather than provider-private DOM.
21+
22+
## CI
23+
24+
Disabled by default (requires real credentials). Wire `secrets.E2E_*`
25+
on the repo and uncomment the `playwright` job in `.github/workflows/ci.yml`
26+
to enable.

e2e/intercom.spec.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Skeleton e2e test — wire real credentials before running.
2+
//
3+
// E2E_INTERCOM_APP_ID=abc pnpm e2e
4+
//
5+
// Asserts:
6+
// 1. <script id="ahize-intercom"> is injected.
7+
// 2. window.Intercom becomes a function.
8+
// 3. provider.isReady() flips to true.
9+
10+
import { test, expect } from "@playwright/test";
11+
12+
const APP_ID = process.env["E2E_INTERCOM_APP_ID"];
13+
14+
test.skip(!APP_ID, "E2E_INTERCOM_APP_ID env var required");
15+
16+
test("intercom boots and exposes window.Intercom", async ({ page }) => {
17+
await page.goto("/e2e/fixtures/intercom.html");
18+
const ok = await page.evaluate(async (appId) => {
19+
// @ts-expect-error — fixture imports the built dist
20+
const intercom = await import("/dist/providers/intercom.mjs");
21+
await intercom.load({ appId });
22+
await intercom.ready();
23+
return (
24+
intercom.isReady() &&
25+
typeof (window as unknown as { Intercom?: unknown }).Intercom === "function"
26+
);
27+
}, APP_ID);
28+
expect(ok).toBe(true);
29+
});

jsr.json

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"name": "@productdevbook/ahize",
3+
"version": "0.0.1",
4+
"exports": {
5+
".": "./src/index.ts",
6+
"./intercom": "./src/providers/intercom.ts",
7+
"./crisp": "./src/providers/crisp.ts",
8+
"./tawk": "./src/providers/tawk.ts",
9+
"./zendesk": "./src/providers/zendesk.ts",
10+
"./zendesk-classic": "./src/providers/zendesk-classic.ts",
11+
"./hubspot": "./src/providers/hubspot.ts",
12+
"./chatwoot": "./src/providers/chatwoot.ts",
13+
"./livechat": "./src/providers/livechat.ts",
14+
"./drift": "./src/providers/drift.ts",
15+
"./freshchat": "./src/providers/freshchat.ts",
16+
"./olark": "./src/providers/olark.ts",
17+
"./userlike": "./src/providers/userlike.ts",
18+
"./helpscout": "./src/providers/helpscout.ts",
19+
"./smartsupp": "./src/providers/smartsupp.ts",
20+
"./liveagent": "./src/providers/liveagent.ts",
21+
"./gist": "./src/providers/gist.ts",
22+
"./jivochat": "./src/providers/jivochat.ts",
23+
"./tidio": "./src/providers/tidio.ts",
24+
"./sendbird": "./src/providers/sendbird.ts",
25+
"./server": "./src/server.ts",
26+
"./csp": "./src/csp.ts",
27+
"./facade": "./src/facade.ts",
28+
"./capabilities": "./src/capabilities.ts",
29+
"./diagnostics": "./src/diagnostics.ts",
30+
"./next": "./src/adapters/next.ts",
31+
"./nuxt": "./src/adapters/nuxt.ts",
32+
"./vue": "./src/adapters/vue.ts",
33+
"./react": "./src/adapters/react.ts",
34+
"./svelte": "./src/adapters/svelte.ts",
35+
"./sveltekit": "./src/adapters/sveltekit.ts",
36+
"./remix": "./src/adapters/remix.ts",
37+
"./astro": "./src/adapters/astro.ts",
38+
"./angular": "./src/adapters/angular.ts",
39+
"./partytown": "./src/adapters/partytown.ts"
40+
},
41+
"publish": {
42+
"include": ["src/**/*.ts", "README.md", "LICENSE"],
43+
"exclude": ["src/**/*.test.ts", "src/env.d.ts"]
44+
}
45+
}

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,9 @@
193193
"fmt": "oxfmt .",
194194
"test": "pnpm lint && pnpm typecheck && vitest run",
195195
"typecheck": "tsgo --noEmit",
196-
"release": "pnpm test && pnpm build && bumpp --commit --tag --push --all",
196+
"bundle-budget": "node scripts/bundle-budget.mjs",
197+
"attw": "pnpm dlx @arethetypeswrong/cli --pack . --profile esm-only",
198+
"release": "pnpm test && pnpm build && pnpm bundle-budget && bumpp --commit --tag --push --all",
197199
"prepack": "pnpm build"
198200
},
199201
"devDependencies": {

scripts/bundle-budget.mjs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#!/usr/bin/env node
2+
// Bundle-size budget check.
3+
// Numbers are pre-gzip raw byte size; gzip ≈ 1/3 of these. Tuned so we
4+
// catch surprise regressions but don't fail on routine new features.
5+
// Bump deliberately when a real feature requires it.
6+
7+
import { readdir, stat } from "node:fs/promises";
8+
import { resolve } from "node:path";
9+
10+
const DIST = resolve(process.cwd(), "dist");
11+
12+
const BUDGETS = [
13+
{ glob: /^providers\/chatwoot\.mjs$/, max: 9216, label: "provider:chatwoot" },
14+
{ glob: /^providers\/.+\.mjs$/, max: 6144, label: "provider" },
15+
{ glob: /^index\.mjs$/, max: 4096, label: "core" },
16+
{ glob: /^facade\.mjs$/, max: 3072, label: "facade" },
17+
{ glob: /^adapters\/.+\.mjs$/, max: 2048, label: "adapter" },
18+
{ glob: /^csp\.mjs$/, max: 6144, label: "csp" },
19+
{ glob: /^server\.mjs$/, max: 1024, label: "server" },
20+
{ glob: /^capabilities\.mjs$/, max: 2048, label: "capabilities" },
21+
{ glob: /^diagnostics\.mjs$/, max: 3072, label: "diagnostics" },
22+
];
23+
24+
async function* walk(dir, prefix = "") {
25+
const entries = await readdir(dir, { withFileTypes: true });
26+
for (const entry of entries) {
27+
const fullPath = resolve(dir, entry.name);
28+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
29+
if (entry.isDirectory()) {
30+
yield* walk(fullPath, rel);
31+
} else if (entry.name.endsWith(".mjs")) {
32+
yield { path: fullPath, rel };
33+
}
34+
}
35+
}
36+
37+
const failures = [];
38+
const summary = [];
39+
40+
for await (const file of walk(DIST)) {
41+
const { size } = await stat(file.path);
42+
const budget = BUDGETS.find((b) => b.glob.test(file.rel));
43+
if (!budget) continue;
44+
const ok = size <= budget.max;
45+
summary.push({ rel: file.rel, size, max: budget.max, label: budget.label, ok });
46+
if (!ok) failures.push(`${file.rel} ${size}B > ${budget.max}B (${budget.label})`);
47+
}
48+
49+
summary.sort((a, b) => b.size - a.size);
50+
console.log("\nBundle sizes (raw, pre-gzip):");
51+
console.log("─".repeat(60));
52+
for (const s of summary) {
53+
const status = s.ok ? "✓" : "✗";
54+
const pct = Math.round((s.size / s.max) * 100);
55+
console.log(` ${status} ${s.rel.padEnd(40)} ${String(s.size).padStart(6)}B ${pct}%`);
56+
}
57+
console.log("─".repeat(60));
58+
59+
if (failures.length > 0) {
60+
console.error("\n❌ Bundle budget exceeded:");
61+
for (const f of failures) console.error(` ${f}`);
62+
process.exit(1);
63+
}
64+
console.log("\n✅ All bundles within budget.");

vitest.config.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { defineConfig } from "vitest/config";
2+
3+
export default defineConfig({
4+
test: {
5+
include: ["test/**/*.test.ts"],
6+
exclude: ["e2e/**", "node_modules/**", "dist/**"],
7+
},
8+
});

0 commit comments

Comments
 (0)