@flightdev/ui 2.0.1 → 4.0.0
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/README.md +283 -68
- package/dist/{chunk-XTDK7ME5.js → chunk-S4DTUQII.js} +246 -19
- package/dist/chunk-S4DTUQII.js.map +1 -0
- package/dist/core/index.d.ts +423 -3
- package/dist/core/index.js +23 -2
- package/dist/core/index.js.map +1 -0
- package/dist/index.d.ts +2 -3
- package/dist/index.js +29 -5
- package/dist/index.js.map +1 -0
- package/package.json +7 -183
- package/.turbo/turbo-build.log +0 -81
- package/.turbo/turbo-lint.log +0 -40
- package/.turbo/turbo-typecheck.log +0 -4
- package/TESTING.md +0 -124
- package/dist/adapter-MMD-iHNx.d.ts +0 -424
- package/dist/adapters/tier-1/angular.d.ts +0 -60
- package/dist/adapters/tier-1/angular.js +0 -2
- package/dist/adapters/tier-1/index.d.ts +0 -7
- package/dist/adapters/tier-1/index.js +0 -7
- package/dist/adapters/tier-1/qwik.d.ts +0 -55
- package/dist/adapters/tier-1/qwik.js +0 -2
- package/dist/adapters/tier-1/react.d.ts +0 -67
- package/dist/adapters/tier-1/react.js +0 -2
- package/dist/adapters/tier-1/solid.d.ts +0 -45
- package/dist/adapters/tier-1/solid.js +0 -2
- package/dist/adapters/tier-1/svelte.d.ts +0 -48
- package/dist/adapters/tier-1/svelte.js +0 -2
- package/dist/adapters/tier-1/vue.d.ts +0 -47
- package/dist/adapters/tier-1/vue.js +0 -2
- package/dist/adapters/tier-2/index.d.ts +0 -7
- package/dist/adapters/tier-2/index.js +0 -7
- package/dist/adapters/tier-2/inferno.d.ts +0 -31
- package/dist/adapters/tier-2/inferno.js +0 -2
- package/dist/adapters/tier-2/lit.d.ts +0 -34
- package/dist/adapters/tier-2/lit.js +0 -2
- package/dist/adapters/tier-2/marko.d.ts +0 -59
- package/dist/adapters/tier-2/marko.js +0 -2
- package/dist/adapters/tier-2/mithril.d.ts +0 -31
- package/dist/adapters/tier-2/mithril.js +0 -2
- package/dist/adapters/tier-2/preact.d.ts +0 -33
- package/dist/adapters/tier-2/preact.js +0 -2
- package/dist/adapters/tier-2/stencil.d.ts +0 -52
- package/dist/adapters/tier-2/stencil.js +0 -2
- package/dist/adapters/tier-3/alpine.d.ts +0 -73
- package/dist/adapters/tier-3/alpine.js +0 -2
- package/dist/adapters/tier-3/hotwire.d.ts +0 -71
- package/dist/adapters/tier-3/hotwire.js +0 -2
- package/dist/adapters/tier-3/htmx.d.ts +0 -88
- package/dist/adapters/tier-3/htmx.js +0 -2
- package/dist/adapters/tier-3/index.d.ts +0 -7
- package/dist/adapters/tier-3/index.js +0 -7
- package/dist/adapters/tier-3/petite-vue.d.ts +0 -56
- package/dist/adapters/tier-3/petite-vue.js +0 -2
- package/dist/adapters/tier-3/stimulus.d.ts +0 -63
- package/dist/adapters/tier-3/stimulus.js +0 -2
- package/dist/adapters/tier-3/vanilla.d.ts +0 -63
- package/dist/adapters/tier-3/vanilla.js +0 -2
- package/dist/chunk-2SNQ6PTM.js +0 -217
- package/dist/chunk-3D4XMIZI.js +0 -136
- package/dist/chunk-3HU6GSQ4.js +0 -125
- package/dist/chunk-4PZDNFL7.js +0 -148
- package/dist/chunk-5IBLFTYL.js +0 -114
- package/dist/chunk-64JZJ7OK.js +0 -142
- package/dist/chunk-7ZJI3QU2.js +0 -132
- package/dist/chunk-CE4FJHQJ.js +0 -133
- package/dist/chunk-DTCAUBH5.js +0 -87
- package/dist/chunk-NTASPOHG.js +0 -106
- package/dist/chunk-OI2AMQLG.js +0 -152
- package/dist/chunk-Q7HUE44H.js +0 -106
- package/dist/chunk-QH3LOWXU.js +0 -155
- package/dist/chunk-QIVAK6BH.js +0 -103
- package/dist/chunk-V34XPVGK.js +0 -103
- package/dist/chunk-VK7ZPMO7.js +0 -221
- package/dist/chunk-X6CNUW6T.js +0 -136
- package/dist/chunk-YFGSHW5S.js +0 -121
- package/dist/chunk-ZAJVSE7J.js +0 -90
- package/docs/ADAPTERS.md +0 -946
- package/docs/PATTERNS.md +0 -836
- package/src/adapters/tier-1/angular.ts +0 -223
- package/src/adapters/tier-1/index.ts +0 -12
- package/src/adapters/tier-1/qwik.ts +0 -177
- package/src/adapters/tier-1/react.ts +0 -330
- package/src/adapters/tier-1/solid.ts +0 -222
- package/src/adapters/tier-1/svelte.ts +0 -211
- package/src/adapters/tier-1/vue.ts +0 -234
- package/src/adapters/tier-2/index.ts +0 -12
- package/src/adapters/tier-2/inferno.ts +0 -149
- package/src/adapters/tier-2/lit.ts +0 -191
- package/src/adapters/tier-2/marko.ts +0 -199
- package/src/adapters/tier-2/mithril.ts +0 -152
- package/src/adapters/tier-2/preact.ts +0 -133
- package/src/adapters/tier-2/stencil.ts +0 -214
- package/src/adapters/tier-3/alpine.ts +0 -218
- package/src/adapters/tier-3/hotwire.ts +0 -254
- package/src/adapters/tier-3/htmx.ts +0 -263
- package/src/adapters/tier-3/index.ts +0 -12
- package/src/adapters/tier-3/petite-vue.ts +0 -163
- package/src/adapters/tier-3/stimulus.ts +0 -233
- package/src/adapters/tier-3/vanilla.ts +0 -252
- package/src/ambient.d.ts +0 -310
- package/src/core/adapter.ts +0 -366
- package/src/core/index.ts +0 -56
- package/src/core/registry.ts +0 -518
- package/src/core/types.ts +0 -461
- package/src/htmx.ts +0 -134
- package/src/index.ts +0 -263
- package/test/__mocks__/stencil-core.ts +0 -19
- package/test/__mocks__/stencil-hydrate.ts +0 -15
- package/test/adapters/tier-1.test.ts +0 -206
- package/test/adapters/tier-2.test.ts +0 -175
- package/test/adapters/tier-3.test.ts +0 -284
- package/test/contracts/adapter.contract.ts +0 -293
- package/test/core/core.test.ts +0 -310
- package/test/errors/error-handling.test.ts +0 -454
- package/test/integration/htmx.integration.test.ts +0 -246
- package/test/integration/react.integration.test.ts +0 -271
- package/test/integration/registry.integration.test.ts +0 -308
- package/tsconfig.json +0 -22
- package/tsup.config.ts +0 -93
- package/vitest.config.ts +0 -101
|
@@ -1,454 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @flightdev/ui - Error Handling Tests
|
|
3
|
-
*
|
|
4
|
-
* Comprehensive error scenario tests for adapter resilience.
|
|
5
|
-
* Validates graceful degradation, error messages, and edge cases.
|
|
6
|
-
*
|
|
7
|
-
* @module test/errors/error-handling.test.ts
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
11
|
-
import { adapterRegistry, registerBuiltinAdapters } from '../../src/core/registry.js';
|
|
12
|
-
import type { Component, RenderResult, StreamingOptions, RenderContext } from '../../src/core/types.js';
|
|
13
|
-
|
|
14
|
-
// ============================================================================
|
|
15
|
-
// Error Handling Tests
|
|
16
|
-
// ============================================================================
|
|
17
|
-
|
|
18
|
-
describe('Error Handling Tests', () => {
|
|
19
|
-
beforeEach(() => {
|
|
20
|
-
registerBuiltinAdapters();
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
describe('Missing Framework Installation', () => {
|
|
24
|
-
it('should provide clear error when React is not installed', async () => {
|
|
25
|
-
const adapter = await adapterRegistry.get('react');
|
|
26
|
-
expect(adapter).toBeDefined();
|
|
27
|
-
|
|
28
|
-
// Attempting to render with a component that requires React
|
|
29
|
-
const component: Component = {
|
|
30
|
-
component: undefined as unknown as () => void,
|
|
31
|
-
props: {},
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
await expect(adapter!.renderToString(component)).rejects.toThrow();
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('should handle missing Vue gracefully', async () => {
|
|
38
|
-
const adapter = await adapterRegistry.get('vue');
|
|
39
|
-
expect(adapter).toBeDefined();
|
|
40
|
-
|
|
41
|
-
const component: Component = {
|
|
42
|
-
component: undefined as unknown as () => void,
|
|
43
|
-
props: {},
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
// Vue adapter handles undefined components gracefully
|
|
47
|
-
// It may return empty HTML or throw - both are valid
|
|
48
|
-
try {
|
|
49
|
-
const result = await adapter!.renderToString(component);
|
|
50
|
-
expect(result).toBeDefined();
|
|
51
|
-
} catch {
|
|
52
|
-
// Throwing is also acceptable behavior
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
describe('Component Render Errors', () => {
|
|
58
|
-
it('should handle component that throws during render', async () => {
|
|
59
|
-
const adapter = await adapterRegistry.get('react');
|
|
60
|
-
expect(adapter).toBeDefined();
|
|
61
|
-
|
|
62
|
-
const throwingComponent = () => {
|
|
63
|
-
throw new Error('Component crashed!');
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
const component: Component = {
|
|
67
|
-
component: throwingComponent,
|
|
68
|
-
props: {},
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
await expect(adapter!.renderToString(component)).rejects.toThrow();
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('should handle async component that rejects', async () => {
|
|
75
|
-
const adapter = await adapterRegistry.get('react');
|
|
76
|
-
expect(adapter).toBeDefined();
|
|
77
|
-
|
|
78
|
-
const asyncThrowingComponent = async () => {
|
|
79
|
-
throw new Error('Async component failed!');
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
const component: Component = {
|
|
83
|
-
component: asyncThrowingComponent,
|
|
84
|
-
props: {},
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
await expect(adapter!.renderToString(component)).rejects.toThrow();
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('should handle component returning invalid type', async () => {
|
|
91
|
-
const adapter = await adapterRegistry.get('react');
|
|
92
|
-
expect(adapter).toBeDefined();
|
|
93
|
-
|
|
94
|
-
const invalidComponent = () => {
|
|
95
|
-
return Symbol('invalid') as unknown;
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
const component: Component = {
|
|
99
|
-
component: invalidComponent,
|
|
100
|
-
props: {},
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
// Should throw or handle gracefully
|
|
104
|
-
try {
|
|
105
|
-
await adapter!.renderToString(component);
|
|
106
|
-
} catch (error) {
|
|
107
|
-
expect(error).toBeDefined();
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
describe('Invalid Component Type Errors', () => {
|
|
113
|
-
it('should reject null component', async () => {
|
|
114
|
-
const adapter = await adapterRegistry.get('react');
|
|
115
|
-
expect(adapter).toBeDefined();
|
|
116
|
-
|
|
117
|
-
const component: Component = {
|
|
118
|
-
component: null as unknown as () => void,
|
|
119
|
-
props: {},
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
await expect(adapter!.renderToString(component)).rejects.toThrow();
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('should handle string as component gracefully', async () => {
|
|
126
|
-
const adapter = await adapterRegistry.get('react');
|
|
127
|
-
expect(adapter).toBeDefined();
|
|
128
|
-
|
|
129
|
-
const component: Component = {
|
|
130
|
-
component: 'div' as unknown as () => void, // String component names are valid in React
|
|
131
|
-
props: {},
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
// React treats string components as HTML tags
|
|
135
|
-
try {
|
|
136
|
-
const result = await adapter!.renderToString(component);
|
|
137
|
-
expect(result).toBeDefined();
|
|
138
|
-
} catch {
|
|
139
|
-
// Throwing is also acceptable for invalid components
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('should reject number as component', async () => {
|
|
144
|
-
const adapter = await adapterRegistry.get('react');
|
|
145
|
-
expect(adapter).toBeDefined();
|
|
146
|
-
|
|
147
|
-
const component: Component = {
|
|
148
|
-
component: 42 as unknown as () => void,
|
|
149
|
-
props: {},
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
await expect(adapter!.renderToString(component)).rejects.toThrow();
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it('should reject object as component', async () => {
|
|
156
|
-
const adapter = await adapterRegistry.get('react');
|
|
157
|
-
expect(adapter).toBeDefined();
|
|
158
|
-
|
|
159
|
-
const component: Component = {
|
|
160
|
-
component: { render: () => 'html' } as unknown as () => void,
|
|
161
|
-
props: {},
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
await expect(adapter!.renderToString(component)).rejects.toThrow();
|
|
165
|
-
});
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
describe('Streaming Timeout Handling', () => {
|
|
169
|
-
it('should support abort signal in render context', async () => {
|
|
170
|
-
const adapter = await adapterRegistry.get('react');
|
|
171
|
-
expect(adapter).toBeDefined();
|
|
172
|
-
|
|
173
|
-
if (!adapter!.capabilities.streaming) {
|
|
174
|
-
return; // Skip for non-streaming adapters
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const component: Component = {
|
|
178
|
-
component: () => null,
|
|
179
|
-
props: {},
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
const abortController = new AbortController();
|
|
183
|
-
const context: RenderContext = {
|
|
184
|
-
signal: abortController.signal,
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
const result = adapter!.renderToStream!(component, context);
|
|
188
|
-
expect(result.stream).toBeDefined();
|
|
189
|
-
|
|
190
|
-
// Abort after starting stream
|
|
191
|
-
abortController.abort();
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it('should handle already-aborted signal', async () => {
|
|
195
|
-
const adapter = await adapterRegistry.get('react');
|
|
196
|
-
expect(adapter).toBeDefined();
|
|
197
|
-
|
|
198
|
-
if (!adapter!.capabilities.streaming) {
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const component: Component = {
|
|
203
|
-
component: () => null,
|
|
204
|
-
props: {},
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
const abortController = new AbortController();
|
|
208
|
-
abortController.abort(); // Abort before starting
|
|
209
|
-
|
|
210
|
-
const context: RenderContext = {
|
|
211
|
-
signal: abortController.signal,
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
// Should handle gracefully
|
|
215
|
-
const result = adapter!.renderToStream!(component, context);
|
|
216
|
-
expect(result.stream).toBeDefined();
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it('should support timeout option in streaming', async () => {
|
|
220
|
-
const adapter = await adapterRegistry.get('react');
|
|
221
|
-
expect(adapter).toBeDefined();
|
|
222
|
-
|
|
223
|
-
if (!adapter!.capabilities.streaming) {
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const component: Component = {
|
|
228
|
-
component: () => null,
|
|
229
|
-
props: {},
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
const options: StreamingOptions = {
|
|
233
|
-
timeout: 5000,
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
const result = adapter!.renderToStream!(component, undefined, options);
|
|
237
|
-
expect(result.stream).toBeDefined();
|
|
238
|
-
});
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
describe('Graceful Degradation', () => {
|
|
242
|
-
it('should return empty HTML string on render failure', async () => {
|
|
243
|
-
const adapter = await adapterRegistry.get('htmx');
|
|
244
|
-
expect(adapter).toBeDefined();
|
|
245
|
-
|
|
246
|
-
// HTMX adapter should handle empty template gracefully
|
|
247
|
-
const component: Component = {
|
|
248
|
-
component: () => '',
|
|
249
|
-
props: {},
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
const result = await adapter!.renderToString(component);
|
|
253
|
-
expect(typeof result.html).toBe('string');
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
it('should provide timing even on partial failure', async () => {
|
|
257
|
-
const adapter = await adapterRegistry.get('htmx');
|
|
258
|
-
expect(adapter).toBeDefined();
|
|
259
|
-
|
|
260
|
-
const component: Component = {
|
|
261
|
-
component: () => '<div>Test</div>',
|
|
262
|
-
props: {},
|
|
263
|
-
};
|
|
264
|
-
|
|
265
|
-
const result = await adapter!.renderToString(component);
|
|
266
|
-
expect(result.timing).toBeDefined();
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
it('should generate valid hydration script even with empty result', async () => {
|
|
270
|
-
const adapter = await adapterRegistry.get('react');
|
|
271
|
-
expect(adapter).toBeDefined();
|
|
272
|
-
|
|
273
|
-
const emptyResult: RenderResult = {
|
|
274
|
-
html: '',
|
|
275
|
-
};
|
|
276
|
-
|
|
277
|
-
const script = adapter!.getHydrationScript(emptyResult);
|
|
278
|
-
expect(typeof script).toBe('string');
|
|
279
|
-
});
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
describe('Props Validation', () => {
|
|
283
|
-
it('should handle circular reference in props', async () => {
|
|
284
|
-
const adapter = await adapterRegistry.get('htmx');
|
|
285
|
-
expect(adapter).toBeDefined();
|
|
286
|
-
|
|
287
|
-
const circularObj: Record<string, unknown> = { name: 'test' };
|
|
288
|
-
circularObj.self = circularObj; // Circular reference
|
|
289
|
-
|
|
290
|
-
const component: Component = {
|
|
291
|
-
component: () => '<div>Test</div>',
|
|
292
|
-
props: circularObj,
|
|
293
|
-
};
|
|
294
|
-
|
|
295
|
-
// Should not crash - either handles or throws meaningful error
|
|
296
|
-
try {
|
|
297
|
-
await adapter!.renderToString(component);
|
|
298
|
-
} catch (error) {
|
|
299
|
-
expect(error).toBeDefined();
|
|
300
|
-
}
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
it('should handle very large props object', async () => {
|
|
304
|
-
const adapter = await adapterRegistry.get('htmx');
|
|
305
|
-
expect(adapter).toBeDefined();
|
|
306
|
-
|
|
307
|
-
const largeProps: Record<string, unknown> = {};
|
|
308
|
-
for (let i = 0; i < 1000; i++) {
|
|
309
|
-
largeProps[`key_${i}`] = `value_${i}`.repeat(100);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
const component: Component = {
|
|
313
|
-
component: () => '<div>Test</div>',
|
|
314
|
-
props: largeProps,
|
|
315
|
-
};
|
|
316
|
-
|
|
317
|
-
// Should handle large props without crashing
|
|
318
|
-
const result = await adapter!.renderToString(component);
|
|
319
|
-
expect(result).toBeDefined();
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
it('should handle undefined values in props', async () => {
|
|
323
|
-
const adapter = await adapterRegistry.get('htmx');
|
|
324
|
-
expect(adapter).toBeDefined();
|
|
325
|
-
|
|
326
|
-
const component: Component = {
|
|
327
|
-
component: () => '<div>Test</div>',
|
|
328
|
-
props: {
|
|
329
|
-
defined: 'value',
|
|
330
|
-
undefinedProp: undefined,
|
|
331
|
-
nullProp: null,
|
|
332
|
-
},
|
|
333
|
-
};
|
|
334
|
-
|
|
335
|
-
const result = await adapter!.renderToString(component);
|
|
336
|
-
expect(result).toBeDefined();
|
|
337
|
-
});
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
describe('Edge Cases', () => {
|
|
341
|
-
it('should handle emoji and special characters in HTML', async () => {
|
|
342
|
-
const adapter = await adapterRegistry.get('htmx');
|
|
343
|
-
expect(adapter).toBeDefined();
|
|
344
|
-
|
|
345
|
-
const component: Component = {
|
|
346
|
-
component: () => '<div>Hello World! <special>&</special></div>',
|
|
347
|
-
props: {},
|
|
348
|
-
};
|
|
349
|
-
|
|
350
|
-
const result = await adapter!.renderToString(component);
|
|
351
|
-
expect(result.html).toContain('World');
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
it('should handle very long HTML output', async () => {
|
|
355
|
-
const adapter = await adapterRegistry.get('htmx');
|
|
356
|
-
expect(adapter).toBeDefined();
|
|
357
|
-
|
|
358
|
-
const longContent = '<p>Lorem ipsum</p>'.repeat(10000);
|
|
359
|
-
const component: Component = {
|
|
360
|
-
component: () => `<div>${longContent}</div>`,
|
|
361
|
-
props: {},
|
|
362
|
-
};
|
|
363
|
-
|
|
364
|
-
const result = await adapter!.renderToString(component);
|
|
365
|
-
expect(result.html.length).toBeGreaterThan(100000);
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
it('should handle malformed HTML gracefully', async () => {
|
|
369
|
-
const adapter = await adapterRegistry.get('htmx');
|
|
370
|
-
expect(adapter).toBeDefined();
|
|
371
|
-
|
|
372
|
-
const component: Component = {
|
|
373
|
-
component: () => '<div><span>Unclosed!',
|
|
374
|
-
props: {},
|
|
375
|
-
};
|
|
376
|
-
|
|
377
|
-
// Should not crash with malformed HTML
|
|
378
|
-
const result = await adapter!.renderToString(component);
|
|
379
|
-
expect(result).toBeDefined();
|
|
380
|
-
});
|
|
381
|
-
});
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
// ============================================================================
|
|
385
|
-
// Registry Error Handling
|
|
386
|
-
// ============================================================================
|
|
387
|
-
|
|
388
|
-
describe('Registry Error Handling', () => {
|
|
389
|
-
beforeEach(() => {
|
|
390
|
-
registerBuiltinAdapters();
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
it('should handle invalid adapter ID types', async () => {
|
|
394
|
-
const result1 = await adapterRegistry.get('' as string);
|
|
395
|
-
expect(result1).toBeUndefined();
|
|
396
|
-
|
|
397
|
-
const result2 = await adapterRegistry.get('123invalid');
|
|
398
|
-
expect(result2).toBeUndefined();
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
it('should return empty array for invalid tier', () => {
|
|
402
|
-
const result = adapterRegistry.listByTier('tier-99' as 'tier-1');
|
|
403
|
-
expect(result).toEqual([]);
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
it('should return empty array for invalid capability', () => {
|
|
407
|
-
const result = adapterRegistry.listByCapability('nonExistent' as 'streaming');
|
|
408
|
-
expect(result).toEqual([]);
|
|
409
|
-
});
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
// ============================================================================
|
|
413
|
-
// Concurrent Error Handling
|
|
414
|
-
// ============================================================================
|
|
415
|
-
|
|
416
|
-
describe('Concurrent Error Handling', () => {
|
|
417
|
-
beforeEach(() => {
|
|
418
|
-
registerBuiltinAdapters();
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
it('should handle multiple concurrent adapter loads', async () => {
|
|
422
|
-
const promises = [
|
|
423
|
-
adapterRegistry.get('react'),
|
|
424
|
-
adapterRegistry.get('vue'),
|
|
425
|
-
adapterRegistry.get('angular'),
|
|
426
|
-
adapterRegistry.get('svelte'),
|
|
427
|
-
adapterRegistry.get('solid'),
|
|
428
|
-
adapterRegistry.get('qwik'),
|
|
429
|
-
];
|
|
430
|
-
|
|
431
|
-
const results = await Promise.all(promises);
|
|
432
|
-
|
|
433
|
-
results.forEach((adapter, index) => {
|
|
434
|
-
expect(adapter, `Adapter ${index} should be defined`).toBeDefined();
|
|
435
|
-
});
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
it('should handle concurrent renders without interference', async () => {
|
|
439
|
-
const adapter = await adapterRegistry.get('htmx');
|
|
440
|
-
expect(adapter).toBeDefined();
|
|
441
|
-
|
|
442
|
-
const components = Array.from({ length: 10 }, (_, i) => ({
|
|
443
|
-
component: () => `<div>Component ${i}</div>`,
|
|
444
|
-
props: { id: i },
|
|
445
|
-
}));
|
|
446
|
-
|
|
447
|
-
const promises = components.map((c) => adapter!.renderToString(c));
|
|
448
|
-
const results = await Promise.all(promises);
|
|
449
|
-
|
|
450
|
-
results.forEach((result, i) => {
|
|
451
|
-
expect(result.html).toContain(`Component ${i}`);
|
|
452
|
-
});
|
|
453
|
-
});
|
|
454
|
-
});
|
|
@@ -1,246 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @flightdev/ui - HTMX Integration Tests
|
|
3
|
-
*
|
|
4
|
-
* Tests for HTMX adapter with HTML-first rendering scenarios.
|
|
5
|
-
* Validates hx-* attribute generation and template rendering.
|
|
6
|
-
*
|
|
7
|
-
* @module test/integration/htmx.integration.test.ts
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
11
|
-
import { adapterRegistry, registerBuiltinAdapters } from '../../src/core/registry.js';
|
|
12
|
-
import type { Component, RenderResult } from '../../src/core/types.js';
|
|
13
|
-
|
|
14
|
-
// ============================================================================
|
|
15
|
-
// HTMX Integration Tests
|
|
16
|
-
// ============================================================================
|
|
17
|
-
|
|
18
|
-
describe('HTMX Integration Tests', () => {
|
|
19
|
-
beforeEach(() => {
|
|
20
|
-
registerBuiltinAdapters();
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
describe('Adapter Retrieval', () => {
|
|
24
|
-
it('should load HTMX adapter from registry', async () => {
|
|
25
|
-
const adapter = await adapterRegistry.get('htmx');
|
|
26
|
-
|
|
27
|
-
expect(adapter).toBeDefined();
|
|
28
|
-
expect(adapter?.id).toBe('htmx');
|
|
29
|
-
expect(adapter?.tier).toBe('tier-3');
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('should have correct HTMX capabilities', async () => {
|
|
33
|
-
const adapter = await adapterRegistry.get('htmx');
|
|
34
|
-
|
|
35
|
-
// HTMX is HTML-first, so no client-side JS hydration
|
|
36
|
-
expect(adapter?.capabilities.streaming).toBe(false);
|
|
37
|
-
expect(adapter?.capabilities.islands).toBe(false);
|
|
38
|
-
expect(adapter?.capabilities.resumable).toBe(false);
|
|
39
|
-
expect(adapter?.capabilities.ssg).toBe(true);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('should be classified as tier-3 (HTML-first)', async () => {
|
|
43
|
-
const tier3Adapters = adapterRegistry.listByTier('tier-3');
|
|
44
|
-
|
|
45
|
-
expect(tier3Adapters).toContain('htmx');
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
describe('HTML Rendering', () => {
|
|
50
|
-
it('should render template to HTML string', async () => {
|
|
51
|
-
const adapter = await adapterRegistry.get('htmx');
|
|
52
|
-
expect(adapter).toBeDefined();
|
|
53
|
-
|
|
54
|
-
const component: Component = {
|
|
55
|
-
component: () => '<div>Hello HTMX!</div>',
|
|
56
|
-
props: {},
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const result = await adapter!.renderToString(component);
|
|
60
|
-
|
|
61
|
-
expect(result).toBeDefined();
|
|
62
|
-
expect(typeof result.html).toBe('string');
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('should include timing in render result', async () => {
|
|
66
|
-
const adapter = await adapterRegistry.get('htmx');
|
|
67
|
-
expect(adapter).toBeDefined();
|
|
68
|
-
|
|
69
|
-
const component: Component = {
|
|
70
|
-
component: () => '<span>Test</span>',
|
|
71
|
-
props: {},
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
const result = await adapter!.renderToString(component);
|
|
75
|
-
|
|
76
|
-
expect(result.timing).toBeDefined();
|
|
77
|
-
expect(typeof result.timing?.total).toBe('number');
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
describe('Hydration Scripts', () => {
|
|
82
|
-
it('should generate script with HTMX CDN reference', async () => {
|
|
83
|
-
const adapter = await adapterRegistry.get('htmx');
|
|
84
|
-
expect(adapter).toBeDefined();
|
|
85
|
-
|
|
86
|
-
const mockResult: RenderResult = {
|
|
87
|
-
html: '<div hx-get="/api/data">Load</div>',
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
const script = adapter!.getHydrationScript(mockResult);
|
|
91
|
-
|
|
92
|
-
expect(typeof script).toBe('string');
|
|
93
|
-
// HTMX hydration should include CDN script or htmx reference
|
|
94
|
-
expect(script).toMatch(/htmx|unpkg|cdn|script/i);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('should include HTMX configuration in script', async () => {
|
|
98
|
-
const adapter = await adapterRegistry.get('htmx');
|
|
99
|
-
expect(adapter).toBeDefined();
|
|
100
|
-
|
|
101
|
-
const mockResult: RenderResult = {
|
|
102
|
-
html: '<button hx-post="/api/submit">Submit</button>',
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
const script = adapter!.getHydrationScript(mockResult);
|
|
106
|
-
|
|
107
|
-
// Should contain HTMX initialization or config
|
|
108
|
-
expect(script.length).toBeGreaterThan(0);
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
describe('Client Entry', () => {
|
|
113
|
-
it('should provide minimal client entry for HTMX', async () => {
|
|
114
|
-
const adapter = await adapterRegistry.get('htmx');
|
|
115
|
-
expect(adapter).toBeDefined();
|
|
116
|
-
|
|
117
|
-
const entry = adapter!.getClientEntry();
|
|
118
|
-
|
|
119
|
-
expect(typeof entry).toBe('string');
|
|
120
|
-
// HTMX is HTML-first, so client entry should be minimal
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
describe('HX Attribute Patterns', () => {
|
|
125
|
-
it('should support hx-get pattern in templates', async () => {
|
|
126
|
-
const adapter = await adapterRegistry.get('htmx');
|
|
127
|
-
expect(adapter).toBeDefined();
|
|
128
|
-
|
|
129
|
-
// Template functions can generate hx-* attributes
|
|
130
|
-
const template = `<div hx-get="/api/users" hx-target="#results">Load Users</div>`;
|
|
131
|
-
|
|
132
|
-
const component: Component = {
|
|
133
|
-
component: () => template,
|
|
134
|
-
props: {},
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
const result = await adapter!.renderToString(component);
|
|
138
|
-
expect(result.html).toContain('hx-get');
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it('should support hx-post pattern in templates', async () => {
|
|
142
|
-
const adapter = await adapterRegistry.get('htmx');
|
|
143
|
-
expect(adapter).toBeDefined();
|
|
144
|
-
|
|
145
|
-
const template = `<form hx-post="/api/submit" hx-swap="innerHTML">
|
|
146
|
-
<input name="email" type="email" />
|
|
147
|
-
<button type="submit">Submit</button>
|
|
148
|
-
</form>`;
|
|
149
|
-
|
|
150
|
-
const component: Component = {
|
|
151
|
-
component: () => template,
|
|
152
|
-
props: {},
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
const result = await adapter!.renderToString(component);
|
|
156
|
-
expect(result.html).toContain('hx-post');
|
|
157
|
-
expect(result.html).toContain('hx-swap');
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it('should support hx-trigger pattern in templates', async () => {
|
|
161
|
-
const adapter = await adapterRegistry.get('htmx');
|
|
162
|
-
expect(adapter).toBeDefined();
|
|
163
|
-
|
|
164
|
-
const template = `<div hx-get="/updates" hx-trigger="every 5s">Live Updates</div>`;
|
|
165
|
-
|
|
166
|
-
const component: Component = {
|
|
167
|
-
component: () => template,
|
|
168
|
-
props: {},
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
const result = await adapter!.renderToString(component);
|
|
172
|
-
expect(result.html).toContain('hx-trigger');
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
describe('Template Composition', () => {
|
|
177
|
-
it('should render nested templates', async () => {
|
|
178
|
-
const adapter = await adapterRegistry.get('htmx');
|
|
179
|
-
expect(adapter).toBeDefined();
|
|
180
|
-
|
|
181
|
-
const template = `
|
|
182
|
-
<main>
|
|
183
|
-
<header hx-get="/header" hx-trigger="load">Loading...</header>
|
|
184
|
-
<section id="content">
|
|
185
|
-
<article hx-get="/article/1">Loading article...</article>
|
|
186
|
-
</section>
|
|
187
|
-
<footer>© 2026</footer>
|
|
188
|
-
</main>
|
|
189
|
-
`;
|
|
190
|
-
|
|
191
|
-
const component: Component = {
|
|
192
|
-
component: () => template,
|
|
193
|
-
props: {},
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
const result = await adapter!.renderToString(component);
|
|
197
|
-
expect(result.html).toContain('<main>');
|
|
198
|
-
expect(result.html).toContain('<header');
|
|
199
|
-
expect(result.html).toContain('<footer>');
|
|
200
|
-
});
|
|
201
|
-
});
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
// ============================================================================
|
|
205
|
-
// Alpine.js Integration Tests (Tier 3 comparison)
|
|
206
|
-
// ============================================================================
|
|
207
|
-
|
|
208
|
-
describe('Alpine.js Integration Tests', () => {
|
|
209
|
-
beforeEach(() => {
|
|
210
|
-
registerBuiltinAdapters();
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
it('should load Alpine adapter from registry', async () => {
|
|
214
|
-
const adapter = await adapterRegistry.get('alpine');
|
|
215
|
-
|
|
216
|
-
expect(adapter).toBeDefined();
|
|
217
|
-
expect(adapter?.id).toBe('alpine');
|
|
218
|
-
expect(adapter?.tier).toBe('tier-3');
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
it('should be listed alongside HTMX in tier-3', () => {
|
|
222
|
-
const tier3 = adapterRegistry.listByTier('tier-3');
|
|
223
|
-
|
|
224
|
-
expect(tier3).toContain('htmx');
|
|
225
|
-
expect(tier3).toContain('alpine');
|
|
226
|
-
expect(tier3).toContain('hotwire');
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it('should render x-data templates', async () => {
|
|
230
|
-
const adapter = await adapterRegistry.get('alpine');
|
|
231
|
-
expect(adapter).toBeDefined();
|
|
232
|
-
|
|
233
|
-
const template = `<div x-data="{ count: 0 }">
|
|
234
|
-
<button @click="count++">Increment</button>
|
|
235
|
-
<span x-text="count"></span>
|
|
236
|
-
</div>`;
|
|
237
|
-
|
|
238
|
-
const component: Component = {
|
|
239
|
-
component: () => template,
|
|
240
|
-
props: {},
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
const result = await adapter!.renderToString(component);
|
|
244
|
-
expect(result.html).toContain('x-data');
|
|
245
|
-
});
|
|
246
|
-
});
|