Skip to content

Commit a06a71a

Browse files
tinchox5claude
andcommitted
test: add type-checking for types/snapdom.d.ts to the test pipeline
The .d.ts had zero automated verification — the earlier fix removing a phantom snapdom.toJpeg() was checked only by manual reading. Add `typescript` as a devDependency, a minimal tsconfig.json, and types/snapdom.test-d.ts exercising the public API (main callable, static namespace helpers, session, plugin shape, preCache) plus `@ts-expect-error` assertions for things that must NOT type-check (the removed toJpeg phantom, writing to the readonly `dirty` getter). Wired into `npm test`/`test:full` via a new `test:types` script. Not published — package.json "files" only whitelists types/snapdom.d.ts, verified with `npm pack --dry-run`. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent b15b418 commit a06a71a

3 files changed

Lines changed: 128 additions & 3 deletions

File tree

package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,11 @@
3333
"compile": "node esbuild.config.mjs",
3434
"lint": "eslint src __tests__ --ext .js",
3535
"lint:fix": "eslint src __tests__ --ext .js --fix",
36-
"test": "npm run lint:fix && npx vitest run --browser.headless --reporter=verbose",
36+
"test:types": "tsc --noEmit",
37+
"test": "npm run lint:fix && npm run test:types && npx vitest run --browser.headless --reporter=verbose",
3738
"test:firefox": "BROWSER=firefox npx vitest run --browser.headless --reporter=verbose",
3839
"test:webkit": "BROWSER=webkit npx vitest run --browser.headless --reporter=verbose",
39-
"test:full": "npm run lint:fix && BROWSER=all npx vitest run --browser.headless --reporter=verbose",
40+
"test:full": "npm run lint:fix && npm run test:types && BROWSER=all npx vitest run --browser.headless --reporter=verbose",
4041
"report:cross": "node scripts/cross-report.mjs",
4142
"test:coverage": "npx vitest run --browser.headless --coverage",
4243
"test:benchmark": "npx vitest bench --browser.headless --watch=false",
@@ -90,7 +91,8 @@
9091
"esbuild": "^0.28.1",
9192
"eslint": "^9.36.0",
9293
"globals": "^16.4.0",
93-
"playwright": "^1.52.0"
94+
"playwright": "^1.52.0",
95+
"typescript": "^7.0.2"
9496
},
9597
"overrides": {
9698
"vite": "^6"

tsconfig.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2020",
4+
"module": "ESNext",
5+
"moduleResolution": "Bundler",
6+
"lib": ["ES2020", "DOM"],
7+
"strict": true,
8+
"noEmit": true,
9+
"skipLibCheck": true,
10+
"types": []
11+
},
12+
"include": ["types/**/*.ts"]
13+
}

types/snapdom.test-d.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/**
2+
* Compile-only type tests for snapdom.d.ts. No runtime assertions — a clean
3+
* `tsc --noEmit` (via `npm run test:types`) IS the test. Not published (see
4+
* package.json "files"); not picked up by vitest (name doesn't match *.test.ts).
5+
*
6+
* `// @ts-expect-error` lines assert that the given usage must NOT type-check —
7+
* if the API starts accepting it, tsc fails because the expected error is gone.
8+
*/
9+
import { snapdom, preCache } from './snapdom'
10+
import type {
11+
SnapdomOptions,
12+
CaptureResult,
13+
CaptureSession,
14+
SnapdomPlugin,
15+
PluginFactory,
16+
ExportMap,
17+
CaptureContext,
18+
} from './snapdom'
19+
20+
declare const el: Element
21+
22+
async function mainCallable() {
23+
const options: SnapdomOptions = {
24+
scale: 2,
25+
dpr: 2,
26+
outerTransforms: false,
27+
outerShadows: true,
28+
clip: 'viewport',
29+
exclude: ['.skip'],
30+
filter: (node) => node.tagName !== 'SCRIPT',
31+
cache: 'soft',
32+
}
33+
const result: CaptureResult = await snapdom(el, options)
34+
35+
const raw: string = result.toRaw()
36+
const img: HTMLImageElement = await result.toSvg()
37+
const canvas: HTMLCanvasElement = await result.toCanvas({ scale: 1 })
38+
const blob: Blob = await result.toBlob({ type: 'webp' })
39+
const png: HTMLImageElement = await result.toPng()
40+
const jpeg: HTMLImageElement = await result.toJpeg()
41+
const jpg: HTMLImageElement = await result.toJpg()
42+
const webp: HTMLImageElement = await result.toWebp()
43+
await result.download({ filename: 'capture' })
44+
await result.to('png')
45+
void raw; void img; void canvas; void blob; void png; void jpeg; void jpg; void webp
46+
}
47+
48+
function clipOptionShapes() {
49+
const a: SnapdomOptions['clip'] = 'viewport'
50+
const b: SnapdomOptions['clip'] = { x: 0, y: 0, width: 100, height: 100 }
51+
const c: SnapdomOptions['clip'] = null
52+
void a; void b; void c
53+
}
54+
55+
async function staticNamespaceHelpers() {
56+
await snapdom.toRaw(el)
57+
await snapdom.toSvg(el)
58+
await snapdom.toCanvas(el)
59+
await snapdom.toBlob(el, { type: 'jpeg' })
60+
await snapdom.toPng(el)
61+
await snapdom.toJpg(el)
62+
await snapdom.toWebp(el)
63+
await snapdom.download(el, { filename: 'x' })
64+
65+
// toJpeg() only exists on CaptureResult (via dynamic sugar) — the static
66+
// namespace never defined it, only toJpg. See snapdom-two-branch-audit.
67+
// @ts-expect-error
68+
await snapdom.toJpeg(el)
69+
}
70+
71+
async function session() {
72+
const s: CaptureSession = snapdom.session(el, { scale: 1 })
73+
const dirty: boolean = s.dirty
74+
const result: CaptureResult = await s.capture()
75+
await s.capture({ scale: 2 })
76+
s.invalidate()
77+
s.dispose()
78+
void dirty; void result
79+
80+
// @ts-expect-error dirty is readonly
81+
s.dirty = true
82+
}
83+
84+
function pluginShape() {
85+
const plugin: SnapdomPlugin = {
86+
name: 'example',
87+
async beforeSnap(_ctx: CaptureContext) {},
88+
async beforeClone() {},
89+
resolveNode(_node, _ctx) {
90+
return undefined
91+
},
92+
defineExports(_ctx): ExportMap {
93+
return { pdf: async (_ctx2, _opts) => new Blob() }
94+
},
95+
}
96+
const factory: PluginFactory = (_options) => plugin
97+
snapdom.plugins(plugin, factory, [factory, { foo: 1 }], { plugin: factory, options: {} })
98+
}
99+
100+
async function preCacheHelper() {
101+
await preCache()
102+
await preCache(el, { embedFonts: true, iconFonts: [/custom-icons/i], cache: 'full' })
103+
}
104+
105+
void mainCallable
106+
void clipOptionShapes
107+
void staticNamespaceHelpers
108+
void session
109+
void pluginShape
110+
void preCacheHelper

0 commit comments

Comments
 (0)