loopwind 0.25.6 → 0.25.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/.astro/types.d.ts +1 -0
- package/app/dist/_astro/callback.Ci5gaEfJ.css +1 -0
- package/app/dist/auth/callback/index.html +81 -0
- package/app/dist/device/index.html +70 -0
- package/app/dist/index.html +327 -0
- package/app/package-lock.json +9239 -0
- package/app/package.json +23 -0
- package/app/wrangler.toml +8 -0
- package/dist/cli.js +54 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/login.d.ts +5 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +60 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logout.d.ts +5 -0
- package/dist/commands/logout.d.ts.map +1 -0
- package/dist/commands/logout.js +15 -0
- package/dist/commands/logout.js.map +1 -0
- package/dist/commands/publish.d.ts +10 -0
- package/dist/commands/publish.d.ts.map +1 -0
- package/dist/commands/publish.js +155 -0
- package/dist/commands/publish.js.map +1 -0
- package/dist/commands/templates.d.ts +5 -0
- package/dist/commands/templates.d.ts.map +1 -0
- package/dist/commands/templates.js +60 -0
- package/dist/commands/templates.js.map +1 -0
- package/dist/commands/unpublish.d.ts +5 -0
- package/dist/commands/unpublish.d.ts.map +1 -0
- package/dist/commands/unpublish.js +54 -0
- package/dist/commands/unpublish.js.map +1 -0
- package/dist/commands/whoami.d.ts +5 -0
- package/dist/commands/whoami.d.ts.map +1 -0
- package/dist/commands/whoami.js +30 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/lib/api.d.ts +92 -0
- package/dist/lib/api.d.ts.map +1 -0
- package/dist/lib/api.js +149 -0
- package/dist/lib/api.js.map +1 -0
- package/dist/lib/auth.d.ts +41 -0
- package/dist/lib/auth.d.ts.map +1 -0
- package/dist/lib/auth.js +89 -0
- package/dist/lib/auth.js.map +1 -0
- package/dist/lib/bundler.d.ts +18 -0
- package/dist/lib/bundler.d.ts.map +1 -0
- package/dist/lib/bundler.js +105 -0
- package/dist/lib/bundler.js.map +1 -0
- package/dist/lib/helpers.d.ts +35 -2
- package/dist/lib/helpers.d.ts.map +1 -1
- package/dist/lib/helpers.js +91 -13
- package/dist/lib/helpers.js.map +1 -1
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +9 -0
- package/dist/lib/utils.js.map +1 -1
- package/dist/sdk/edge.d.ts +65 -0
- package/dist/sdk/edge.d.ts.map +1 -0
- package/dist/sdk/edge.js +329 -0
- package/dist/sdk/edge.js.map +1 -0
- package/dist/sdk/errors.d.ts +64 -0
- package/dist/sdk/errors.d.ts.map +1 -0
- package/dist/sdk/errors.js +94 -0
- package/dist/sdk/errors.js.map +1 -0
- package/dist/sdk/index.d.ts +29 -0
- package/dist/sdk/index.d.ts.map +1 -0
- package/dist/sdk/index.js +30 -0
- package/dist/sdk/index.js.map +1 -0
- package/dist/sdk/render.d.ts +52 -0
- package/dist/sdk/render.d.ts.map +1 -0
- package/dist/sdk/render.js +432 -0
- package/dist/sdk/render.js.map +1 -0
- package/dist/sdk/types.d.ts +185 -0
- package/dist/sdk/types.d.ts.map +1 -0
- package/dist/sdk/types.js +5 -0
- package/dist/sdk/types.js.map +1 -0
- package/dist/types/template.d.ts +18 -0
- package/dist/types/template.d.ts.map +1 -1
- package/package.json +26 -4
- package/plans/PLATFORM.md +1637 -237
- package/plans/PLATFORM_IMPLEMENTATION.md +1347 -530
- package/plans/SDK.md +797 -0
- package/platform/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/ebad93a0a7be9c5768c512f3e30740b64d2b6e575277a40d77044af5ae8fd3f2.sqlite +0 -0
- package/platform/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/ebad93a0a7be9c5768c512f3e30740b64d2b6e575277a40d77044af5ae8fd3f2.sqlite-shm +0 -0
- package/platform/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/ebad93a0a7be9c5768c512f3e30740b64d2b6e575277a40d77044af5ae8fd3f2.sqlite-wal +0 -0
- package/platform/migrations/0001_initial.sql +90 -0
- package/platform/package-lock.json +3104 -0
- package/platform/package.json +30 -0
- package/platform/wrangler.toml +43 -0
- package/tests-sdk/createRenderer.test.ts +251 -0
- package/tests-sdk/errors.test.ts +230 -0
- package/tests-sdk/render.test.ts +241 -0
- package/tests-sdk/tw.test.ts +277 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "loopwind-platform",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "wrangler dev",
|
|
8
|
+
"deploy": "wrangler deploy",
|
|
9
|
+
"db:migrate": "wrangler d1 migrations apply loopwind --local",
|
|
10
|
+
"db:migrate:prod": "wrangler d1 migrations apply loopwind --remote",
|
|
11
|
+
"typecheck": "tsc --noEmit"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@types/react": "^19.2.7",
|
|
15
|
+
"gifenc": "^1.0.3",
|
|
16
|
+
"h264-mp4-encoder": "^1.0.12",
|
|
17
|
+
"hono": "^4.6.0",
|
|
18
|
+
"jose": "^5.9.0",
|
|
19
|
+
"loopwind": "^0.25.6",
|
|
20
|
+
"nanoid": "^5.0.0",
|
|
21
|
+
"react": "^19.2.3",
|
|
22
|
+
"workers-og": "^0.0.27",
|
|
23
|
+
"zod": "^3.23.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@cloudflare/workers-types": "^4.20241230.0",
|
|
27
|
+
"typescript": "^5.3.3",
|
|
28
|
+
"wrangler": "^4.0.0"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
name = "loopwind-api"
|
|
2
|
+
main = "src/index.ts"
|
|
3
|
+
compatibility_date = "2024-12-01"
|
|
4
|
+
compatibility_flags = ["nodejs_compat"]
|
|
5
|
+
|
|
6
|
+
# D1 Database
|
|
7
|
+
[[d1_databases]]
|
|
8
|
+
binding = "DB"
|
|
9
|
+
database_name = "loopwind"
|
|
10
|
+
database_id = "c65f8a95-80eb-4fd1-af25-eb4bc2d7e565"
|
|
11
|
+
|
|
12
|
+
# R2 Storage
|
|
13
|
+
[[r2_buckets]]
|
|
14
|
+
binding = "ASSETS"
|
|
15
|
+
bucket_name = "loopwind-assets"
|
|
16
|
+
|
|
17
|
+
[[r2_buckets]]
|
|
18
|
+
binding = "RENDERS"
|
|
19
|
+
bucket_name = "loopwind-renders"
|
|
20
|
+
|
|
21
|
+
# KV Namespaces
|
|
22
|
+
[[kv_namespaces]]
|
|
23
|
+
binding = "SESSIONS"
|
|
24
|
+
id = "d8d639b6fdef451d9f775d42d49398b3"
|
|
25
|
+
|
|
26
|
+
[[kv_namespaces]]
|
|
27
|
+
binding = "RATE_LIMIT"
|
|
28
|
+
id = "41204264b9d44b85a96e89f052ad9221"
|
|
29
|
+
|
|
30
|
+
# Environment variables (set via wrangler secret)
|
|
31
|
+
# GITHUB_CLIENT_ID
|
|
32
|
+
# GITHUB_CLIENT_SECRET
|
|
33
|
+
# JWT_SECRET
|
|
34
|
+
|
|
35
|
+
[vars]
|
|
36
|
+
ENVIRONMENT = "production"
|
|
37
|
+
API_BASE_URL = "https://loopwind-api.loopwind.dev"
|
|
38
|
+
APP_BASE_URL = "https://app.loopwind.dev"
|
|
39
|
+
|
|
40
|
+
# Custom domain
|
|
41
|
+
[[routes]]
|
|
42
|
+
pattern = "loopwind-api.loopwind.dev"
|
|
43
|
+
custom_domain = true
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SDK createRenderer() Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { test, describe } from 'node:test';
|
|
6
|
+
import assert from 'node:assert';
|
|
7
|
+
import { createRenderer, ValidationError } from '../dist/sdk/index.js';
|
|
8
|
+
import React from 'react';
|
|
9
|
+
|
|
10
|
+
// Test templates
|
|
11
|
+
const OGTemplate = ({ title, tw }: { title: string; tw: (c: string) => any }) => {
|
|
12
|
+
return React.createElement(
|
|
13
|
+
'div',
|
|
14
|
+
{ style: tw('w-full h-full bg-blue-500 flex items-center justify-center') },
|
|
15
|
+
React.createElement('h1', { style: tw('text-white text-4xl') }, title)
|
|
16
|
+
);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const CardTemplate = ({
|
|
20
|
+
title,
|
|
21
|
+
description,
|
|
22
|
+
tw,
|
|
23
|
+
}: {
|
|
24
|
+
title: string;
|
|
25
|
+
description: string;
|
|
26
|
+
tw: (c: string) => any;
|
|
27
|
+
}) => {
|
|
28
|
+
return React.createElement('div', { style: tw('w-full h-full bg-white p-8 flex flex-col') }, [
|
|
29
|
+
React.createElement('h1', { key: 'title', style: tw('text-2xl font-bold text-gray-900') }, title),
|
|
30
|
+
React.createElement('p', { key: 'desc', style: tw('text-gray-600 mt-2') }, description),
|
|
31
|
+
]);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
describe('createRenderer()', () => {
|
|
35
|
+
test('creates a working renderer', async () => {
|
|
36
|
+
const render = createRenderer({});
|
|
37
|
+
|
|
38
|
+
const result = await render(OGTemplate, {
|
|
39
|
+
props: { title: 'Test' },
|
|
40
|
+
format: 'png',
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
assert.ok(result instanceof Buffer || result instanceof Uint8Array);
|
|
44
|
+
assert.ok(result.length > 0);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('uses default dimensions from config', async () => {
|
|
48
|
+
const render = createRenderer({
|
|
49
|
+
defaults: {
|
|
50
|
+
width: 800,
|
|
51
|
+
height: 400,
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const result = await render(OGTemplate, {
|
|
56
|
+
props: { title: 'Default Size' },
|
|
57
|
+
format: 'svg',
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const svgString = result.toString('utf-8');
|
|
61
|
+
assert.ok(svgString.includes('width="800"'), 'Should use default width from config');
|
|
62
|
+
assert.ok(svgString.includes('height="400"'), 'Should use default height from config');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('options override config defaults', async () => {
|
|
66
|
+
const render = createRenderer({
|
|
67
|
+
defaults: {
|
|
68
|
+
width: 800,
|
|
69
|
+
height: 400,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const result = await render(OGTemplate, {
|
|
74
|
+
props: { title: 'Override Size' },
|
|
75
|
+
width: 500,
|
|
76
|
+
height: 250,
|
|
77
|
+
format: 'svg',
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const svgString = result.toString('utf-8');
|
|
81
|
+
assert.ok(svgString.includes('width="500"'), 'Should use overridden width');
|
|
82
|
+
assert.ok(svgString.includes('height="250"'), 'Should use overridden height');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('uses default format from config', async () => {
|
|
86
|
+
const render = createRenderer({
|
|
87
|
+
defaults: {
|
|
88
|
+
format: 'svg',
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const result = await render(OGTemplate, {
|
|
93
|
+
props: { title: 'Default Format' },
|
|
94
|
+
width: 100,
|
|
95
|
+
height: 100,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const content = result.toString('utf-8');
|
|
99
|
+
assert.ok(content.includes('<svg'), 'Should output SVG by default');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('createRenderer() with template registry', () => {
|
|
104
|
+
test('renders registered template by name', async () => {
|
|
105
|
+
const render = createRenderer({
|
|
106
|
+
templates: {
|
|
107
|
+
'og-image': OGTemplate,
|
|
108
|
+
'card': CardTemplate,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const result = await render('og-image', {
|
|
113
|
+
props: { title: 'Named Template' },
|
|
114
|
+
format: 'svg',
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const svgString = result.toString('utf-8');
|
|
118
|
+
assert.ok(svgString.includes('<svg'), 'Should output valid SVG');
|
|
119
|
+
assert.ok(svgString.length > 100, 'SVG should have content');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('renders different templates by name', async () => {
|
|
123
|
+
const render = createRenderer({
|
|
124
|
+
templates: {
|
|
125
|
+
'og-image': OGTemplate,
|
|
126
|
+
'card': CardTemplate,
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const ogResult = await render('og-image', {
|
|
131
|
+
props: { title: 'OG Test' },
|
|
132
|
+
format: 'svg',
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const cardResult = await render('card', {
|
|
136
|
+
props: { title: 'Card Title', description: 'Card description' },
|
|
137
|
+
format: 'svg',
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Both should produce valid SVG
|
|
141
|
+
assert.ok(ogResult.toString('utf-8').includes('<svg'), 'OG template should output SVG');
|
|
142
|
+
assert.ok(cardResult.toString('utf-8').includes('<svg'), 'Card template should output SVG');
|
|
143
|
+
// They should be different SVGs
|
|
144
|
+
assert.notStrictEqual(ogResult.toString('utf-8'), cardResult.toString('utf-8'), 'Different templates should produce different output');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('throws ValidationError for unknown template name', async () => {
|
|
148
|
+
const render = createRenderer({
|
|
149
|
+
templates: {
|
|
150
|
+
'og-image': OGTemplate,
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
await assert.rejects(
|
|
155
|
+
async () => {
|
|
156
|
+
await render('unknown-template', {
|
|
157
|
+
props: { title: 'Test' },
|
|
158
|
+
format: 'png',
|
|
159
|
+
});
|
|
160
|
+
},
|
|
161
|
+
(error: any) => {
|
|
162
|
+
assert.ok(error instanceof ValidationError);
|
|
163
|
+
assert.strictEqual(error.field, 'template');
|
|
164
|
+
assert.ok(error.message.includes('unknown-template'));
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('allows passing template function even with registry', async () => {
|
|
171
|
+
const render = createRenderer({
|
|
172
|
+
templates: {
|
|
173
|
+
'og-image': OGTemplate,
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Should still work with direct template function
|
|
178
|
+
const result = await render(CardTemplate, {
|
|
179
|
+
props: { title: 'Direct', description: 'Function' },
|
|
180
|
+
format: 'svg',
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const svgString = result.toString('utf-8');
|
|
184
|
+
assert.ok(svgString.includes('<svg'), 'Should output valid SVG');
|
|
185
|
+
assert.ok(svgString.length > 100, 'SVG should have content');
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('createRenderer() with custom colors', () => {
|
|
190
|
+
test('applies custom colors via tw()', async () => {
|
|
191
|
+
const render = createRenderer({
|
|
192
|
+
colors: {
|
|
193
|
+
primary: '#ff0000',
|
|
194
|
+
secondary: '#00ff00',
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Use a simple single-child template to avoid flex requirement
|
|
199
|
+
const TemplateWithColors = ({ tw }: { tw: (c: string) => any }) => {
|
|
200
|
+
return React.createElement(
|
|
201
|
+
'div',
|
|
202
|
+
{ style: tw('w-full h-full bg-red-500 flex items-center justify-center') },
|
|
203
|
+
React.createElement('span', { style: tw('text-white text-xl') }, 'Colored')
|
|
204
|
+
);
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const result = await render(TemplateWithColors, {
|
|
208
|
+
props: {},
|
|
209
|
+
format: 'svg',
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// The SVG should render successfully
|
|
213
|
+
const svgString = result.toString('utf-8');
|
|
214
|
+
assert.ok(svgString.includes('<svg'), 'Should output valid SVG');
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe('createRenderer() reusability', () => {
|
|
219
|
+
test('can render multiple times with same renderer', async () => {
|
|
220
|
+
const render = createRenderer({
|
|
221
|
+
defaults: { width: 400, height: 200 },
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const results = await Promise.all([
|
|
225
|
+
render(OGTemplate, { props: { title: 'First' }, format: 'png' }),
|
|
226
|
+
render(OGTemplate, { props: { title: 'Second' }, format: 'png' }),
|
|
227
|
+
render(OGTemplate, { props: { title: 'Third' }, format: 'png' }),
|
|
228
|
+
]);
|
|
229
|
+
|
|
230
|
+
assert.strictEqual(results.length, 3);
|
|
231
|
+
results.forEach((result, i) => {
|
|
232
|
+
assert.ok(result.length > 0, `Result ${i + 1} should not be empty`);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('renderers are independent', async () => {
|
|
237
|
+
const render1 = createRenderer({
|
|
238
|
+
defaults: { width: 100, height: 100 },
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const render2 = createRenderer({
|
|
242
|
+
defaults: { width: 200, height: 200 },
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const result1 = await render1(OGTemplate, { props: { title: 'R1' }, format: 'svg' });
|
|
246
|
+
const result2 = await render2(OGTemplate, { props: { title: 'R2' }, format: 'svg' });
|
|
247
|
+
|
|
248
|
+
assert.ok(result1.toString('utf-8').includes('width="100"'));
|
|
249
|
+
assert.ok(result2.toString('utf-8').includes('width="200"'));
|
|
250
|
+
});
|
|
251
|
+
});
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SDK Error Classes Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { test, describe } from 'node:test';
|
|
6
|
+
import assert from 'node:assert';
|
|
7
|
+
import {
|
|
8
|
+
LoopwindError,
|
|
9
|
+
ValidationError,
|
|
10
|
+
FontError,
|
|
11
|
+
RenderError,
|
|
12
|
+
TimeoutError,
|
|
13
|
+
ImageFetchError,
|
|
14
|
+
} from '../dist/sdk/index.js';
|
|
15
|
+
|
|
16
|
+
describe('LoopwindError', () => {
|
|
17
|
+
test('creates error with message', () => {
|
|
18
|
+
const error = new LoopwindError('Test error message');
|
|
19
|
+
|
|
20
|
+
assert.ok(error instanceof Error);
|
|
21
|
+
assert.ok(error instanceof LoopwindError);
|
|
22
|
+
assert.strictEqual(error.name, 'LoopwindError');
|
|
23
|
+
assert.strictEqual(error.message, 'Test error message');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('has proper stack trace', () => {
|
|
27
|
+
const error = new LoopwindError('Stack test');
|
|
28
|
+
|
|
29
|
+
assert.ok(error.stack);
|
|
30
|
+
assert.ok(error.stack.includes('Stack test'));
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('ValidationError', () => {
|
|
35
|
+
test('creates error with all properties', () => {
|
|
36
|
+
const error = new ValidationError(
|
|
37
|
+
'Width must be positive',
|
|
38
|
+
'width',
|
|
39
|
+
'positive number',
|
|
40
|
+
-100
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
assert.ok(error instanceof Error);
|
|
44
|
+
assert.ok(error instanceof LoopwindError);
|
|
45
|
+
assert.ok(error instanceof ValidationError);
|
|
46
|
+
assert.strictEqual(error.name, 'ValidationError');
|
|
47
|
+
assert.strictEqual(error.message, 'Width must be positive');
|
|
48
|
+
assert.strictEqual(error.field, 'width');
|
|
49
|
+
assert.strictEqual(error.expected, 'positive number');
|
|
50
|
+
assert.strictEqual(error.received, -100);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('can be caught as ValidationError', () => {
|
|
54
|
+
try {
|
|
55
|
+
throw new ValidationError('Invalid format', 'format', 'png|jpg|svg', 'bmp');
|
|
56
|
+
} catch (e) {
|
|
57
|
+
assert.ok(e instanceof ValidationError);
|
|
58
|
+
if (e instanceof ValidationError) {
|
|
59
|
+
assert.strictEqual(e.field, 'format');
|
|
60
|
+
assert.strictEqual(e.expected, 'png|jpg|svg');
|
|
61
|
+
assert.strictEqual(e.received, 'bmp');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('FontError', () => {
|
|
68
|
+
test('creates error with font name', () => {
|
|
69
|
+
const error = new FontError('Font not found', 'Inter');
|
|
70
|
+
|
|
71
|
+
assert.ok(error instanceof FontError);
|
|
72
|
+
assert.strictEqual(error.name, 'FontError');
|
|
73
|
+
assert.strictEqual(error.message, 'Font not found');
|
|
74
|
+
assert.strictEqual(error.fontName, 'Inter');
|
|
75
|
+
assert.strictEqual(error.fontPath, undefined);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('creates error with font path', () => {
|
|
79
|
+
const error = new FontError(
|
|
80
|
+
'Failed to load font',
|
|
81
|
+
'CustomFont',
|
|
82
|
+
'/fonts/custom.woff2'
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
assert.strictEqual(error.fontName, 'CustomFont');
|
|
86
|
+
assert.strictEqual(error.fontPath, '/fonts/custom.woff2');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('RenderError', () => {
|
|
91
|
+
test('creates error with stage', () => {
|
|
92
|
+
const error = new RenderError('JSX rendering failed', 'jsx');
|
|
93
|
+
|
|
94
|
+
assert.ok(error instanceof RenderError);
|
|
95
|
+
assert.strictEqual(error.name, 'RenderError');
|
|
96
|
+
assert.strictEqual(error.message, 'JSX rendering failed');
|
|
97
|
+
assert.strictEqual(error.stage, 'jsx');
|
|
98
|
+
assert.strictEqual(error.cause, undefined);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('creates error with cause', () => {
|
|
102
|
+
const originalError = new Error('Original error');
|
|
103
|
+
const error = new RenderError('SVG failed', 'svg', originalError);
|
|
104
|
+
|
|
105
|
+
assert.strictEqual(error.stage, 'svg');
|
|
106
|
+
assert.strictEqual(error.cause, originalError);
|
|
107
|
+
assert.strictEqual(error.cause?.message, 'Original error');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('supports all stages', () => {
|
|
111
|
+
const stages = ['jsx', 'svg', 'rasterize', 'encode'] as const;
|
|
112
|
+
|
|
113
|
+
stages.forEach(stage => {
|
|
114
|
+
const error = new RenderError(`Failed at ${stage}`, stage);
|
|
115
|
+
assert.strictEqual(error.stage, stage);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('TimeoutError', () => {
|
|
121
|
+
test('creates error with timeout values', () => {
|
|
122
|
+
const error = new TimeoutError('Render timed out', 5000, 5123);
|
|
123
|
+
|
|
124
|
+
assert.ok(error instanceof TimeoutError);
|
|
125
|
+
assert.strictEqual(error.name, 'TimeoutError');
|
|
126
|
+
assert.strictEqual(error.message, 'Render timed out');
|
|
127
|
+
assert.strictEqual(error.timeout, 5000);
|
|
128
|
+
assert.strictEqual(error.elapsed, 5123);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('captures accurate timing info', () => {
|
|
132
|
+
const error = new TimeoutError(
|
|
133
|
+
'Operation exceeded time limit',
|
|
134
|
+
30000,
|
|
135
|
+
30456
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
assert.ok(error.elapsed >= error.timeout);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('ImageFetchError', () => {
|
|
143
|
+
test('creates error with URL', () => {
|
|
144
|
+
const error = new ImageFetchError(
|
|
145
|
+
'Failed to fetch image',
|
|
146
|
+
'https://example.com/image.png'
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
assert.ok(error instanceof ImageFetchError);
|
|
150
|
+
assert.strictEqual(error.name, 'ImageFetchError');
|
|
151
|
+
assert.strictEqual(error.url, 'https://example.com/image.png');
|
|
152
|
+
assert.strictEqual(error.status, undefined);
|
|
153
|
+
assert.strictEqual(error.cause, undefined);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('creates error with HTTP status', () => {
|
|
157
|
+
const error = new ImageFetchError(
|
|
158
|
+
'Image not found',
|
|
159
|
+
'https://example.com/missing.png',
|
|
160
|
+
404
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
assert.strictEqual(error.status, 404);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('creates error with cause', () => {
|
|
167
|
+
const networkError = new Error('Network error');
|
|
168
|
+
const error = new ImageFetchError(
|
|
169
|
+
'Connection failed',
|
|
170
|
+
'https://example.com/image.png',
|
|
171
|
+
undefined,
|
|
172
|
+
networkError
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
assert.strictEqual(error.cause, networkError);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('creates error with all properties', () => {
|
|
179
|
+
const cause = new Error('SSL certificate error');
|
|
180
|
+
const error = new ImageFetchError(
|
|
181
|
+
'SSL error while fetching',
|
|
182
|
+
'https://example.com/secure.png',
|
|
183
|
+
0,
|
|
184
|
+
cause
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
assert.strictEqual(error.url, 'https://example.com/secure.png');
|
|
188
|
+
assert.strictEqual(error.status, 0);
|
|
189
|
+
assert.strictEqual(error.cause, cause);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe('Error inheritance', () => {
|
|
194
|
+
test('all errors are instances of Error', () => {
|
|
195
|
+
const errors = [
|
|
196
|
+
new LoopwindError('test'),
|
|
197
|
+
new ValidationError('test', 'field', 'expected', 'received'),
|
|
198
|
+
new FontError('test', 'font'),
|
|
199
|
+
new RenderError('test', 'jsx'),
|
|
200
|
+
new TimeoutError('test', 1000, 1000),
|
|
201
|
+
new ImageFetchError('test', 'url'),
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
errors.forEach(error => {
|
|
205
|
+
assert.ok(error instanceof Error, `${error.name} should be an Error`);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('all errors are instances of LoopwindError', () => {
|
|
210
|
+
const errors = [
|
|
211
|
+
new ValidationError('test', 'field', 'expected', 'received'),
|
|
212
|
+
new FontError('test', 'font'),
|
|
213
|
+
new RenderError('test', 'jsx'),
|
|
214
|
+
new TimeoutError('test', 1000, 1000),
|
|
215
|
+
new ImageFetchError('test', 'url'),
|
|
216
|
+
];
|
|
217
|
+
|
|
218
|
+
errors.forEach(error => {
|
|
219
|
+
assert.ok(error instanceof LoopwindError, `${error.name} should be a LoopwindError`);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('specific errors are not instances of each other', () => {
|
|
224
|
+
const validationError = new ValidationError('test', 'field', 'expected', 'received');
|
|
225
|
+
const fontError = new FontError('test', 'font');
|
|
226
|
+
|
|
227
|
+
assert.ok(!(validationError instanceof FontError));
|
|
228
|
+
assert.ok(!(fontError instanceof ValidationError));
|
|
229
|
+
});
|
|
230
|
+
});
|