create-elit 3.3.2 → 3.3.4
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/dist/templates/README.md +1 -0
- package/dist/templates/elit.config.ts +16 -0
- package/dist/templates/package.json +4 -1
- package/dist/templates/src/client.test.ts +292 -0
- package/dist/templates/src/components/Footer.test.ts +226 -0
- package/dist/templates/src/components/Header.test.ts +493 -0
- package/dist/templates/src/pages/ChatListPage.test.ts +603 -0
- package/dist/templates/src/pages/ChatPage.test.ts +530 -0
- package/dist/templates/src/pages/ForgotPasswordPage.test.ts +484 -0
- package/dist/templates/src/pages/HomePage.test.ts +601 -0
- package/dist/templates/src/pages/LoginPage.test.ts +619 -0
- package/dist/templates/src/pages/PrivateChatPage.test.ts +556 -0
- package/dist/templates/src/pages/ProfilePage.test.ts +628 -0
- package/dist/templates/src/pages/RegisterPage.test.ts +661 -0
- package/package.json +1 -1
package/dist/templates/README.md
CHANGED
|
@@ -52,5 +52,21 @@ export default {
|
|
|
52
52
|
root: './dist',
|
|
53
53
|
basePath: '',
|
|
54
54
|
index: './index.html'
|
|
55
|
+
},
|
|
56
|
+
test: {
|
|
57
|
+
include: ['**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
|
58
|
+
exclude: ['node_modules', 'dist', 'benchmark', 'docs', 'coverage'],
|
|
59
|
+
testTimeout: 5000,
|
|
60
|
+
bail: false,
|
|
61
|
+
globals: true,
|
|
62
|
+
watch: false,
|
|
63
|
+
reporter: 'verbose',
|
|
64
|
+
coverage: {
|
|
65
|
+
enabled: false,
|
|
66
|
+
provider: 'v8',
|
|
67
|
+
reporter: ['text', 'html', 'lcov', 'json', 'coverage-final.json', 'clover'],
|
|
68
|
+
include: ['**/*.ts'], // รวมทุกไฟล์ TypeScript ในโปรเจกต์
|
|
69
|
+
exclude: ['**/*.test.ts', '**/*.spec.ts', '**/node_modules/**', '**/dist/**', '**/coverage/**']
|
|
70
|
+
}
|
|
55
71
|
}
|
|
56
72
|
};
|
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "elit dev",
|
|
7
7
|
"build": "elit build",
|
|
8
|
-
"preview": "elit preview"
|
|
8
|
+
"preview": "elit preview",
|
|
9
|
+
"test": "elit test",
|
|
10
|
+
"test:watch": "elit test --watch",
|
|
11
|
+
"test:coverage": "elit test --coverage"
|
|
9
12
|
},
|
|
10
13
|
"dependencies": {
|
|
11
14
|
"@types/node": "^25.0.9",
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client Component Unit Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Simple mock function to track calls
|
|
6
|
+
function mockFn() {
|
|
7
|
+
const calls: any[] = [];
|
|
8
|
+
const fn = (...args: any[]) => {
|
|
9
|
+
calls.push(args);
|
|
10
|
+
return undefined;
|
|
11
|
+
};
|
|
12
|
+
fn.calls = calls;
|
|
13
|
+
fn.mockClear = () => {
|
|
14
|
+
calls.length = 0;
|
|
15
|
+
};
|
|
16
|
+
return fn;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Mock requestAnimationFrame
|
|
20
|
+
let rafId = 0;
|
|
21
|
+
const rafCallbacks: Map<number, FrameRequestCallback> = new Map();
|
|
22
|
+
const mockRequestAnimationFrame = (callback: FrameRequestCallback) => {
|
|
23
|
+
const id = ++rafId;
|
|
24
|
+
rafCallbacks.set(id, callback);
|
|
25
|
+
return id;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const mockCancelAnimationFrame = (id: number) => {
|
|
29
|
+
rafCallbacks.delete(id);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// SETUP GLOBALS BEFORE IMPORT
|
|
33
|
+
// Set up global mocks
|
|
34
|
+
(global as any).requestAnimationFrame = mockRequestAnimationFrame;
|
|
35
|
+
(global as any).cancelAnimationFrame = mockCancelAnimationFrame;
|
|
36
|
+
(globalThis as any).requestAnimationFrame = mockRequestAnimationFrame;
|
|
37
|
+
(globalThis as any).cancelAnimationFrame = mockCancelAnimationFrame;
|
|
38
|
+
|
|
39
|
+
// NOW import the component (after mocks are set up)
|
|
40
|
+
import { client } from './client';
|
|
41
|
+
import type { VNode } from 'elit/types';
|
|
42
|
+
|
|
43
|
+
// Helper function to render VNode to HTML string
|
|
44
|
+
function renderToString(vNode: VNode | string | number | undefined | null): string {
|
|
45
|
+
if (vNode == null || vNode === false) return '';
|
|
46
|
+
if (typeof vNode !== 'object') return String(vNode);
|
|
47
|
+
|
|
48
|
+
const { tagName, props, children } = vNode;
|
|
49
|
+
const attrs = props ? Object.entries(props)
|
|
50
|
+
.filter(([k, v]) => v != null && v !== false && k !== 'children' && k !== 'ref' && !k.startsWith('on'))
|
|
51
|
+
.map(([k, v]) => {
|
|
52
|
+
if (k === 'className' || k === 'class') return `class="${Array.isArray(v) ? v.join(' ') : v}"`;
|
|
53
|
+
if (k === 'style') return `style="${typeof v === 'string' ? v : Object.entries(v).map(([sk, sv]) => `${sk.replace(/([A-Z])/g, '-$1').toLowerCase()}:${sv}`).join(';')}"`;
|
|
54
|
+
if (v === true) return k;
|
|
55
|
+
return `${k}="${v}"`;
|
|
56
|
+
})
|
|
57
|
+
.join(' ') : '';
|
|
58
|
+
|
|
59
|
+
const childrenStr = children && children.length > 0
|
|
60
|
+
? children.map(c => renderToString(c as any)).join('')
|
|
61
|
+
: '';
|
|
62
|
+
|
|
63
|
+
if (tagName === '!doctype') {
|
|
64
|
+
return `<!DOCTYPE html>`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Self-closing tags
|
|
68
|
+
const selfClosing = ['meta', 'link', 'img', 'br', 'hr', 'input', 'script'];
|
|
69
|
+
if (selfClosing.includes(tagName.toLowerCase())) {
|
|
70
|
+
return `<${tagName}${attrs ? ' ' + attrs : ''}>`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return `<${tagName}${attrs ? ' ' + attrs : ''}>${childrenStr}</${tagName}>`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Helper function to find children by tag name
|
|
77
|
+
function findChildrenByTagName(vNode: VNode, tagName: string): VNode[] {
|
|
78
|
+
const results: VNode[] = [];
|
|
79
|
+
|
|
80
|
+
function search(node: VNode | string | number | null | undefined) {
|
|
81
|
+
if (node && typeof node === 'object' && 'tagName' in node) {
|
|
82
|
+
if (node.tagName === tagName) {
|
|
83
|
+
results.push(node);
|
|
84
|
+
}
|
|
85
|
+
if (node.children) {
|
|
86
|
+
for (const child of node.children) {
|
|
87
|
+
search(child as any);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
search(vNode);
|
|
94
|
+
return results;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Helper function to find child by tag name
|
|
98
|
+
function findChildByTagName(vNode: VNode, tagName: string): VNode | null {
|
|
99
|
+
if (vNode && typeof vNode === 'object' && 'tagName' in vNode) {
|
|
100
|
+
if (vNode.tagName === tagName) {
|
|
101
|
+
return vNode;
|
|
102
|
+
}
|
|
103
|
+
if (vNode.children) {
|
|
104
|
+
for (const child of vNode.children) {
|
|
105
|
+
if (typeof child === 'object' && child !== null) {
|
|
106
|
+
const found = findChildByTagName(child as VNode, tagName);
|
|
107
|
+
if (found) return found;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
describe('Client Component', () => {
|
|
116
|
+
beforeEach(() => {
|
|
117
|
+
// Clear RAF callbacks FIRST
|
|
118
|
+
rafCallbacks.clear();
|
|
119
|
+
rafId = 0;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
afterEach(() => {
|
|
123
|
+
// Clear RAF callbacks after each test
|
|
124
|
+
rafCallbacks.clear();
|
|
125
|
+
rafId = 0;
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('structure', () => {
|
|
129
|
+
it('should export client constant', () => {
|
|
130
|
+
expect(client).toBeDefined();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should have html as root element', () => {
|
|
134
|
+
expect(client.tagName).toBe('html');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should have children', () => {
|
|
138
|
+
expect(client.children).toBeDefined();
|
|
139
|
+
expect(client.children?.length).toBeGreaterThan(0);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('head section', () => {
|
|
144
|
+
it('should have head element', () => {
|
|
145
|
+
const headElement = findChildByTagName(client, 'head');
|
|
146
|
+
expect(headElement).toBeDefined();
|
|
147
|
+
expect(headElement).not.toBeNull();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should have title element', () => {
|
|
151
|
+
const titleElements = findChildrenByTagName(client, 'title');
|
|
152
|
+
expect(titleElements.length).toBeGreaterThan(0);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should have correct title text', () => {
|
|
156
|
+
const titleElements = findChildrenByTagName(client, 'title');
|
|
157
|
+
const titleElement = titleElements[0];
|
|
158
|
+
const titleText = titleElement.children?.[0];
|
|
159
|
+
expect(titleText).toContain('my-elit-app');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should have favicon link', () => {
|
|
163
|
+
const linkElements = findChildrenByTagName(client, 'link');
|
|
164
|
+
const faviconLink = linkElements.find(link =>
|
|
165
|
+
link.props?.rel === 'icon' && link.props?.type === 'image/svg+xml'
|
|
166
|
+
);
|
|
167
|
+
expect(faviconLink).toBeDefined();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should have favicon href', () => {
|
|
171
|
+
const linkElements = findChildrenByTagName(client, 'link');
|
|
172
|
+
const faviconLink = linkElements.find(link =>
|
|
173
|
+
link.props?.rel === 'icon'
|
|
174
|
+
);
|
|
175
|
+
expect(faviconLink?.props?.href).toBe('public/favicon.svg');
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('meta tags', () => {
|
|
180
|
+
it('should have meta elements', () => {
|
|
181
|
+
const metaElements = findChildrenByTagName(client, 'meta');
|
|
182
|
+
expect(metaElements.length).toBeGreaterThanOrEqual(2);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should have charset meta', () => {
|
|
186
|
+
const metaElements = findChildrenByTagName(client, 'meta');
|
|
187
|
+
const charsetMeta = metaElements.find(meta => meta.props?.charset === 'UTF-8');
|
|
188
|
+
expect(charsetMeta).toBeDefined();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should have viewport meta', () => {
|
|
192
|
+
const metaElements = findChildrenByTagName(client, 'meta');
|
|
193
|
+
const viewportMeta = metaElements.find(meta => meta.props?.name === 'viewport');
|
|
194
|
+
expect(viewportMeta).toBeDefined();
|
|
195
|
+
expect(viewportMeta?.props?.content).toContain('width=device-width');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should have description meta', () => {
|
|
199
|
+
const metaElements = findChildrenByTagName(client, 'meta');
|
|
200
|
+
const descMeta = metaElements.find(meta => meta.props?.name === 'description');
|
|
201
|
+
expect(descMeta).toBeDefined();
|
|
202
|
+
expect(descMeta?.props?.content).toContain('Elit');
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe('body section', () => {
|
|
207
|
+
it('should have body element', () => {
|
|
208
|
+
const bodyElement = findChildByTagName(client, 'body');
|
|
209
|
+
expect(bodyElement).toBeDefined();
|
|
210
|
+
expect(bodyElement).not.toBeNull();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should have app div', () => {
|
|
214
|
+
const bodyElement = findChildByTagName(client, 'body');
|
|
215
|
+
expect(bodyElement).toBeDefined();
|
|
216
|
+
const divs = findChildrenByTagName(bodyElement!, 'div');
|
|
217
|
+
const appDiv = divs.find(div => div.props?.id === 'app');
|
|
218
|
+
expect(appDiv).toBeDefined();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should have script element', () => {
|
|
222
|
+
const bodyElement = findChildByTagName(client, 'body');
|
|
223
|
+
expect(bodyElement).toBeDefined();
|
|
224
|
+
const scripts = findChildrenByTagName(bodyElement!, 'script');
|
|
225
|
+
expect(scripts.length).toBeGreaterThan(0);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should have script with module type', () => {
|
|
229
|
+
const bodyElement = findChildByTagName(client, 'body');
|
|
230
|
+
const scripts = findChildrenByTagName(bodyElement!, 'script');
|
|
231
|
+
const moduleScript = scripts.find(script => script.props?.type === 'module');
|
|
232
|
+
expect(moduleScript).toBeDefined();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('should have script with correct src', () => {
|
|
236
|
+
const bodyElement = findChildByTagName(client, 'body');
|
|
237
|
+
const scripts = findChildrenByTagName(bodyElement!, 'script');
|
|
238
|
+
const moduleScript = scripts.find(script => script.props?.type === 'module');
|
|
239
|
+
expect(moduleScript?.props?.src).toBe('/src/main.js');
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe('rendering', () => {
|
|
244
|
+
it('should render without errors', () => {
|
|
245
|
+
expect(() => {
|
|
246
|
+
renderToString(client);
|
|
247
|
+
}).not.toThrow();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should contain doctype', () => {
|
|
251
|
+
// The html wrapper typically includes doctype
|
|
252
|
+
expect(client).toBeDefined();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('should render to string containing basic HTML structure', () => {
|
|
256
|
+
const html = renderToString(client);
|
|
257
|
+
expect(html).toBeDefined();
|
|
258
|
+
expect(typeof html).toBe('string');
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('should render with title', () => {
|
|
262
|
+
const html = renderToString(client);
|
|
263
|
+
expect(html).toContain('my-elit-app');
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
describe('component consistency', () => {
|
|
268
|
+
it('should be immutable', () => {
|
|
269
|
+
const originalTagName = client.tagName;
|
|
270
|
+
// client is a constant export
|
|
271
|
+
expect(client.tagName).toBe(originalTagName);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe('HTML structure validation', () => {
|
|
276
|
+
it('should have proper HTML document structure', () => {
|
|
277
|
+
expect(client.tagName).toBe('html');
|
|
278
|
+
const head = findChildByTagName(client, 'head');
|
|
279
|
+
const body = findChildByTagName(client, 'body');
|
|
280
|
+
expect(head).toBeDefined();
|
|
281
|
+
expect(body).toBeDefined();
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('should have all essential elements', () => {
|
|
285
|
+
const html = renderToString(client);
|
|
286
|
+
expect(html).toContain('html');
|
|
287
|
+
expect(html).toContain('head');
|
|
288
|
+
expect(html).toContain('body');
|
|
289
|
+
expect(html).toContain('title');
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
});
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Footer Component Unit Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests the actual Footer component structure.
|
|
5
|
+
* Uses real ES module import to test the component.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Footer } from './Footer';
|
|
9
|
+
import type { VNode } from 'elit/types';
|
|
10
|
+
|
|
11
|
+
// Type guard to check if a child is a VNode
|
|
12
|
+
function isVNode(child: any): child is VNode {
|
|
13
|
+
return child && typeof child === 'object' && 'tagName' in child;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
describe('Footer Component', () => {
|
|
18
|
+
describe('component structure', () => {
|
|
19
|
+
it('should create a footer element with correct attributes', () => {
|
|
20
|
+
const footerElement = Footer();
|
|
21
|
+
|
|
22
|
+
expect(footerElement).toBeDefined();
|
|
23
|
+
expect(footerElement.tagName).toBe('footer');
|
|
24
|
+
expect(footerElement.props?.className).toBe('footer');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should have footer-content wrapper', () => {
|
|
28
|
+
const footerElement = Footer();
|
|
29
|
+
|
|
30
|
+
expect(footerElement.children).toBeDefined();
|
|
31
|
+
expect(footerElement.children?.length).toBeGreaterThan(0);
|
|
32
|
+
|
|
33
|
+
const content = footerElement.children?.[0] as VNode | undefined;
|
|
34
|
+
expect(content?.tagName).toBe('div');
|
|
35
|
+
expect(content?.props?.className).toBe('footer-content');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should have three footer sections', () => {
|
|
39
|
+
const footerElement = Footer();
|
|
40
|
+
const content = footerElement.children?.[0] as VNode | undefined;
|
|
41
|
+
|
|
42
|
+
expect(content?.children?.length).toBe(3);
|
|
43
|
+
|
|
44
|
+
content?.children?.forEach((child) => {
|
|
45
|
+
if (isVNode(child)) {
|
|
46
|
+
expect(child.tagName).toBe('div');
|
|
47
|
+
expect(child.props?.className).toBe('footer-section');
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('title section', () => {
|
|
54
|
+
it('should have app title', () => {
|
|
55
|
+
const footerElement = Footer();
|
|
56
|
+
const content = footerElement.children?.[0] as VNode | undefined;
|
|
57
|
+
const titleSection = content?.children?.[0] as VNode | undefined;
|
|
58
|
+
|
|
59
|
+
expect(titleSection?.children?.length).toBeGreaterThan(0);
|
|
60
|
+
|
|
61
|
+
const title = titleSection?.children?.[0] as VNode | undefined;
|
|
62
|
+
expect(title?.tagName).toBe('p');
|
|
63
|
+
expect(title?.props?.className).toBe('footer-title');
|
|
64
|
+
expect(title?.children?.[0]).toBe('My Elit App');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should have framework attribution', () => {
|
|
68
|
+
const footerElement = Footer();
|
|
69
|
+
|
|
70
|
+
const content = footerElement.children?.[0] as VNode | undefined;
|
|
71
|
+
const titleSection = content?.children?.[0] as VNode | undefined;
|
|
72
|
+
|
|
73
|
+
const text = titleSection?.children?.[1] as VNode | undefined;
|
|
74
|
+
expect(text?.tagName).toBe('p');
|
|
75
|
+
expect(text?.props?.className).toBe('footer-text');
|
|
76
|
+
expect(text?.children?.[0]).toBe('Built with Elit Framework');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('links section', () => {
|
|
81
|
+
it('should have GitHub link', () => {
|
|
82
|
+
const footerElement = Footer();
|
|
83
|
+
const content = footerElement.children?.[0] as VNode | undefined;
|
|
84
|
+
const linksSection = content?.children?.[1] as VNode | undefined;
|
|
85
|
+
|
|
86
|
+
const githubLink = linksSection?.children?.[0] as VNode | undefined;
|
|
87
|
+
expect(githubLink?.tagName).toBe('a');
|
|
88
|
+
expect(githubLink?.props?.href).toBe('https://github.com');
|
|
89
|
+
expect(githubLink?.props?.target).toBe('_blank');
|
|
90
|
+
expect(githubLink?.props?.className).toBe('footer-link');
|
|
91
|
+
expect(githubLink?.children?.[0]).toBe('GitHub');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should have Documentation link', () => {
|
|
95
|
+
const footerElement = Footer();
|
|
96
|
+
const content = footerElement.children?.[0] as VNode | undefined;
|
|
97
|
+
const linksSection = content?.children?.[1] as VNode | undefined;
|
|
98
|
+
|
|
99
|
+
const docLink = linksSection?.children?.[1] as VNode | undefined;
|
|
100
|
+
expect(docLink?.tagName).toBe('a');
|
|
101
|
+
expect(docLink?.props?.href).toBe('#');
|
|
102
|
+
expect(docLink?.props?.className).toBe('footer-link');
|
|
103
|
+
expect(docLink?.children?.[0]).toBe('Documentation');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should have Support link', () => {
|
|
107
|
+
const footerElement = Footer();
|
|
108
|
+
const content = footerElement.children?.[0] as VNode | undefined;
|
|
109
|
+
const linksSection = content?.children?.[1] as VNode | undefined;
|
|
110
|
+
|
|
111
|
+
const supportLink = linksSection?.children?.[2] as VNode | undefined;
|
|
112
|
+
expect(supportLink?.tagName).toBe('a');
|
|
113
|
+
expect(supportLink?.props?.href).toBe('#');
|
|
114
|
+
expect(supportLink?.props?.className).toBe('footer-link');
|
|
115
|
+
expect(supportLink?.children?.[0]).toBe('Support');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('copyright section', () => {
|
|
120
|
+
it('should have copyright element with correct text', () => {
|
|
121
|
+
const footerElement = Footer();
|
|
122
|
+
const content = footerElement.children?.[0] as VNode | undefined;
|
|
123
|
+
const copyrightSection = content?.children?.[2] as VNode | undefined;
|
|
124
|
+
|
|
125
|
+
const copyright = copyrightSection?.children?.[0] as VNode | undefined;
|
|
126
|
+
expect(copyright?.tagName).toBe('p');
|
|
127
|
+
expect(copyright?.props?.className).toBe('footer-copyright');
|
|
128
|
+
|
|
129
|
+
const copyrightText = copyright?.children?.[0] as string;
|
|
130
|
+
expect(copyrightText).toContain('©');
|
|
131
|
+
expect(copyrightText).toContain('2026');
|
|
132
|
+
expect(copyrightText).toContain('My Elit App');
|
|
133
|
+
expect(copyrightText).toContain('All rights reserved');
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('CSS classes', () => {
|
|
138
|
+
it('should use footer class for main element', () => {
|
|
139
|
+
const footerElement = Footer();
|
|
140
|
+
expect(footerElement.props?.className).toBe('footer');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should use footer-content class for wrapper', () => {
|
|
144
|
+
const footerElement = Footer();
|
|
145
|
+
const content = footerElement.children?.[0] as VNode | undefined;
|
|
146
|
+
expect(content?.props?.className).toBe('footer-content');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should use footer-section class for each section', () => {
|
|
150
|
+
const footerElement = Footer();
|
|
151
|
+
const content = footerElement.children?.[0] as VNode | undefined;
|
|
152
|
+
|
|
153
|
+
content?.children?.forEach((section) => {
|
|
154
|
+
if (isVNode(section)) {
|
|
155
|
+
expect(section.props?.className).toBe('footer-section');
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should use footer-link class for links', () => {
|
|
161
|
+
const footerElement = Footer();
|
|
162
|
+
const content = footerElement.children?.[0] as VNode | undefined;
|
|
163
|
+
const linksSection = content?.children?.[1] as VNode | undefined;
|
|
164
|
+
|
|
165
|
+
linksSection?.children?.forEach((link) => {
|
|
166
|
+
if (isVNode(link)) {
|
|
167
|
+
expect(link.props?.className).toBe('footer-link');
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should use footer-title class for title', () => {
|
|
173
|
+
const footerElement = Footer();
|
|
174
|
+
const content = footerElement.children?.[0] as VNode | undefined;
|
|
175
|
+
const titleSection = content?.children?.[0] as VNode | undefined;
|
|
176
|
+
const title = titleSection?.children?.[0] as VNode | undefined;
|
|
177
|
+
|
|
178
|
+
expect(title?.props?.className).toBe('footer-title');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should use footer-text class for description', () => {
|
|
182
|
+
const footerElement = Footer();
|
|
183
|
+
const content = footerElement.children?.[0] as VNode | undefined;
|
|
184
|
+
const titleSection = content?.children?.[0] as VNode | undefined;
|
|
185
|
+
const text = titleSection?.children?.[1] as VNode | undefined;
|
|
186
|
+
|
|
187
|
+
expect(text?.props?.className).toBe('footer-text');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should use footer-copyright class for copyright', () => {
|
|
191
|
+
const footerElement = Footer();
|
|
192
|
+
const content = footerElement.children?.[0] as VNode | undefined;
|
|
193
|
+
const copyrightSection = content?.children?.[2] as VNode | undefined;
|
|
194
|
+
const copyright = copyrightSection?.children?.[0] as VNode | undefined;
|
|
195
|
+
|
|
196
|
+
expect(copyright?.props?.className).toBe('footer-copyright');
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe('component consistency', () => {
|
|
201
|
+
it('should always return the same structure', () => {
|
|
202
|
+
const structure1 = Footer();
|
|
203
|
+
const structure2 = Footer();
|
|
204
|
+
|
|
205
|
+
expect(structure1.tagName).toBe(structure2.tagName);
|
|
206
|
+
expect(structure1.props).toEqual(structure2.props);
|
|
207
|
+
expect(structure1.children?.length).toBe(structure2.children?.length);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should have no undefined children', () => {
|
|
211
|
+
const footerElement = Footer();
|
|
212
|
+
const content = footerElement.children?.[0] as VNode | undefined;
|
|
213
|
+
|
|
214
|
+
expect(content?.children).toBeDefined();
|
|
215
|
+
expect(content?.children?.length).toBeGreaterThan(0);
|
|
216
|
+
|
|
217
|
+
content?.children?.forEach((section) => {
|
|
218
|
+
if (isVNode(section)) {
|
|
219
|
+
expect(section).toBeDefined();
|
|
220
|
+
expect(section).not.toBeNull();
|
|
221
|
+
expect(section.children).toBeDefined();
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
});
|