create-nativecore 0.1.1 → 0.2.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 +6 -14
- package/bin/index.mjs +402 -431
- package/package.json +3 -2
- package/template/.env.example +28 -0
- package/template/.htmlhintrc +14 -0
- package/template/api/data/dashboard.json +11 -0
- package/template/api/data/users.json +18 -0
- package/template/api/mockApi.js +161 -0
- package/template/assets/icon.svg +13 -0
- package/template/assets/logo.svg +25 -0
- package/template/eslint.config.js +94 -0
- package/template/index.html +137 -0
- package/template/manifest.json +19 -0
- package/template/public/.well-known/security.txt +9 -0
- package/template/public/_headers +24 -0
- package/template/public/_redirects +14 -0
- package/template/public/assets/icon.svg +13 -0
- package/template/public/assets/logo.svg +25 -0
- package/template/public/manifest.json +19 -0
- package/template/public/robots.txt +13 -0
- package/template/public/sitemap.xml +27 -0
- package/template/scripts/build-for-bots.mjs +121 -0
- package/template/scripts/convert-to-ts.mjs +106 -0
- package/template/scripts/fix-encoding.mjs +38 -0
- package/template/scripts/fix-svg-paths.mjs +32 -0
- package/template/scripts/generate-cf-router.mjs +52 -0
- package/template/scripts/inject-dev-tools.mjs +41 -0
- package/template/scripts/inject-version.mjs +65 -0
- package/template/scripts/make-component.mjs +445 -0
- package/template/scripts/make-component.mjs.backup +432 -0
- package/template/scripts/make-controller.mjs +119 -0
- package/template/scripts/make-core-component.mjs +303 -0
- package/template/scripts/make-view.mjs +346 -0
- package/template/scripts/minify.mjs +71 -0
- package/template/scripts/prepare-static-assets.mjs +141 -0
- package/template/scripts/prompt-bot-build.mjs +223 -0
- package/template/scripts/remove-component.mjs +170 -0
- package/template/scripts/remove-core-component.mjs +156 -0
- package/template/scripts/remove-dev.mjs +13 -0
- package/template/scripts/remove-view.mjs +200 -0
- package/template/scripts/strip-dev-blocks.mjs +30 -0
- package/template/scripts/watch-compile.mjs +69 -0
- package/template/server.js +1066 -0
- package/template/src/app.ts +115 -0
- package/template/src/components/appRegistry.ts +8 -0
- package/template/src/components/core/app-footer.ts +27 -0
- package/template/src/components/core/app-header.ts +175 -0
- package/template/src/components/core/app-sidebar.ts +238 -0
- package/template/src/components/core/loading-spinner.ts +25 -0
- package/template/src/components/core/nc-a.ts +313 -0
- package/template/src/components/core/nc-accordion.ts +186 -0
- package/template/src/components/core/nc-alert.ts +153 -0
- package/template/src/components/core/nc-animation.ts +1150 -0
- package/template/src/components/core/nc-autocomplete.ts +271 -0
- package/template/src/components/core/nc-avatar-group.ts +113 -0
- package/template/src/components/core/nc-avatar.ts +148 -0
- package/template/src/components/core/nc-badge.ts +86 -0
- package/template/src/components/core/nc-bottom-nav.ts +214 -0
- package/template/src/components/core/nc-breadcrumb.ts +96 -0
- package/template/src/components/core/nc-button.ts +307 -0
- package/template/src/components/core/nc-card.ts +160 -0
- package/template/src/components/core/nc-checkbox.ts +282 -0
- package/template/src/components/core/nc-chip.ts +115 -0
- package/template/src/components/core/nc-code.ts +314 -0
- package/template/src/components/core/nc-collapsible.ts +154 -0
- package/template/src/components/core/nc-color-picker.ts +268 -0
- package/template/src/components/core/nc-copy-button.ts +119 -0
- package/template/src/components/core/nc-date-picker.ts +443 -0
- package/template/src/components/core/nc-div.ts +280 -0
- package/template/src/components/core/nc-divider.ts +81 -0
- package/template/src/components/core/nc-drawer.ts +230 -0
- package/template/src/components/core/nc-dropdown.ts +178 -0
- package/template/src/components/core/nc-empty-state.ts +134 -0
- package/template/src/components/core/nc-file-upload.ts +354 -0
- package/template/src/components/core/nc-form.ts +312 -0
- package/template/src/components/core/nc-image.ts +184 -0
- package/template/src/components/core/nc-input.ts +383 -0
- package/template/src/components/core/nc-kbd.ts +48 -0
- package/template/src/components/core/nc-menu-item.ts +193 -0
- package/template/src/components/core/nc-menu.ts +376 -0
- package/template/src/components/core/nc-modal.ts +238 -0
- package/template/src/components/core/nc-nav-item.ts +151 -0
- package/template/src/components/core/nc-number-input.ts +350 -0
- package/template/src/components/core/nc-otp-input.ts +235 -0
- package/template/src/components/core/nc-pagination.ts +178 -0
- package/template/src/components/core/nc-popover.ts +260 -0
- package/template/src/components/core/nc-progress-circular.ts +119 -0
- package/template/src/components/core/nc-progress.ts +134 -0
- package/template/src/components/core/nc-radio.ts +235 -0
- package/template/src/components/core/nc-rating.ts +266 -0
- package/template/src/components/core/nc-rich-text.ts +283 -0
- package/template/src/components/core/nc-scroll-top.ts +116 -0
- package/template/src/components/core/nc-select.ts +452 -0
- package/template/src/components/core/nc-skeleton.ts +107 -0
- package/template/src/components/core/nc-slider.ts +285 -0
- package/template/src/components/core/nc-snackbar.ts +230 -0
- package/template/src/components/core/nc-splash.ts +343 -0
- package/template/src/components/core/nc-stepper.ts +247 -0
- package/template/src/components/core/nc-switch.ts +281 -0
- package/template/src/components/core/nc-tab-item.ts +138 -0
- package/template/src/components/core/nc-table.ts +279 -0
- package/template/src/components/core/nc-tabs.ts +554 -0
- package/template/src/components/core/nc-tag-input.ts +279 -0
- package/template/src/components/core/nc-textarea.ts +216 -0
- package/template/src/components/core/nc-time-picker.ts +438 -0
- package/template/src/components/core/nc-timeline.ts +186 -0
- package/template/src/components/core/nc-tooltip.ts +143 -0
- package/template/src/components/frameworkRegistry.ts +68 -0
- package/template/src/components/preloadRegistry.ts +28 -0
- package/template/src/components/registry.ts +8 -0
- package/template/src/components/ui/dashboard-signal-lab.ts +284 -0
- package/template/src/constants/apiEndpoints.ts +27 -0
- package/template/src/constants/errorMessages.ts +23 -0
- package/template/src/constants/index.ts +8 -0
- package/template/src/constants/routePaths.ts +15 -0
- package/template/src/constants/storageKeys.ts +18 -0
- package/template/src/controllers/dashboard.controller.ts +200 -0
- package/template/src/controllers/home.controller.ts +21 -0
- package/template/src/controllers/index.ts +11 -0
- package/template/src/controllers/login.controller.ts +131 -0
- package/template/src/core/component.ts +354 -0
- package/template/src/core/errorHandler.ts +85 -0
- package/template/src/core/gpu-animation.ts +604 -0
- package/template/src/core/http.ts +173 -0
- package/template/src/core/lazyComponents.ts +90 -0
- package/template/src/core/router.ts +642 -0
- package/template/src/core/signals.ts +146 -0
- package/template/src/core/state.ts +248 -0
- package/template/src/dev/component-editor.ts +1363 -0
- package/template/src/dev/component-overlay.ts +278 -0
- package/template/src/dev/context-menu.ts +223 -0
- package/template/src/dev/denc-tools.ts +250 -0
- package/template/src/dev/hmr.ts +189 -0
- package/template/src/dev/nfbs.code-workspace +27 -0
- package/template/src/dev/outline-panel.ts +1247 -0
- package/template/src/middleware/auth.middleware.ts +23 -0
- package/template/src/routes/routes.ts +38 -0
- package/template/src/services/api.service.ts +394 -0
- package/template/src/services/auth.service.ts +176 -0
- package/template/src/services/index.ts +8 -0
- package/template/src/services/logger.service.ts +74 -0
- package/template/src/services/storage.service.ts +88 -0
- package/template/src/stores/appStore.ts +57 -0
- package/template/src/stores/uiStore.ts +36 -0
- package/template/src/styles/core-variables.css +219 -0
- package/template/src/styles/core.css +710 -0
- package/template/src/styles/main.css +3164 -0
- package/template/src/styles/variables.css +152 -0
- package/template/src/types/global.d.ts +47 -0
- package/template/src/utils/cacheBuster.ts +20 -0
- package/template/src/utils/dom.ts +149 -0
- package/template/src/utils/events.ts +203 -0
- package/template/src/utils/form.ts +176 -0
- package/template/src/utils/formatters.ts +169 -0
- package/template/src/utils/helpers.ts +195 -0
- package/template/src/utils/markdown.ts +307 -0
- package/template/src/utils/sidebar.ts +96 -0
- package/template/src/utils/smoothScroll.ts +85 -0
- package/template/src/utils/templates.ts +23 -0
- package/template/src/utils/validation.ts +73 -0
- package/template/src/views/protected/dashboard.html +293 -0
- package/template/src/views/public/home.html +150 -0
- package/template/src/views/public/login.html +102 -0
- package/template/tests/unit/component.test.ts +87 -0
- package/template/tests/unit/computed.test.ts +79 -0
- package/template/tests/unit/form.test.ts +68 -0
- package/template/tests/unit/formatters.test.ts +49 -0
- package/template/tests/unit/lazy-components.test.ts +59 -0
- package/template/tests/unit/markdown.test.ts +62 -0
- package/template/tests/unit/router.test.ts +112 -0
- package/template/tests/unit/signals.test.ts +54 -0
- package/template/tests/unit/validation.test.ts +50 -0
- package/template/tsconfig.build.json +21 -0
- package/template/tsconfig.json +51 -0
- package/template/vitest.config.ts +36 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { computed, effect, useState } from '../../src/core/state.js';
|
|
3
|
+
|
|
4
|
+
describe('core/state', () => {
|
|
5
|
+
it('computes an initial value and recomputes when dependencies change', () => {
|
|
6
|
+
const firstName = useState('John');
|
|
7
|
+
const lastName = useState('Doe');
|
|
8
|
+
const fullName = computed(() => `${firstName.value} ${lastName.value}`);
|
|
9
|
+
|
|
10
|
+
expect(fullName.value).toBe('John Doe');
|
|
11
|
+
|
|
12
|
+
firstName.value = 'Jane';
|
|
13
|
+
expect(fullName.value).toBe('Jane Doe');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('releases stale dependencies when the computed branch changes', () => {
|
|
17
|
+
const toggle = useState(true);
|
|
18
|
+
const left = useState('left');
|
|
19
|
+
const right = useState('right');
|
|
20
|
+
const branch = computed(() => (toggle.value ? left.value : right.value));
|
|
21
|
+
|
|
22
|
+
expect(branch.value).toBe('left');
|
|
23
|
+
|
|
24
|
+
toggle.value = false;
|
|
25
|
+
expect(branch.value).toBe('right');
|
|
26
|
+
|
|
27
|
+
left.value = 'updated-left';
|
|
28
|
+
expect(branch.value).toBe('right');
|
|
29
|
+
|
|
30
|
+
right.value = 'updated-right';
|
|
31
|
+
expect(branch.value).toBe('updated-right');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('chains computed values', () => {
|
|
35
|
+
const base = useState(2);
|
|
36
|
+
const doubled = computed(() => base.value * 2);
|
|
37
|
+
const quadrupled = computed(() => doubled.value * 2);
|
|
38
|
+
|
|
39
|
+
expect(quadrupled.value).toBe(8);
|
|
40
|
+
|
|
41
|
+
base.value = 5;
|
|
42
|
+
expect(quadrupled.value).toBe(20);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('keeps computed values read-only', () => {
|
|
46
|
+
const base = useState(5);
|
|
47
|
+
const doubled = computed(() => base.value * 2);
|
|
48
|
+
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
49
|
+
|
|
50
|
+
doubled.value = 100;
|
|
51
|
+
|
|
52
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Computed values are read-only'));
|
|
53
|
+
expect(doubled.value).toBe(10);
|
|
54
|
+
|
|
55
|
+
consoleSpy.mockRestore();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('tracks reactive side effects and runs cleanup before re-running', () => {
|
|
59
|
+
const count = useState(1);
|
|
60
|
+
const calls: number[] = [];
|
|
61
|
+
const cleanup = vi.fn();
|
|
62
|
+
const stop = effect(() => {
|
|
63
|
+
calls.push(count.value);
|
|
64
|
+
return cleanup;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(calls).toEqual([1]);
|
|
68
|
+
|
|
69
|
+
count.value = 2;
|
|
70
|
+
expect(calls).toEqual([1, 2]);
|
|
71
|
+
expect(cleanup).toHaveBeenCalledTimes(1);
|
|
72
|
+
|
|
73
|
+
stop();
|
|
74
|
+
expect(cleanup).toHaveBeenCalledTimes(2);
|
|
75
|
+
|
|
76
|
+
count.value = 3;
|
|
77
|
+
expect(calls).toEqual([1, 2]);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { useForm } from '../../src/utils/form.js';
|
|
3
|
+
import { isRequired, isValidEmail, minLength } from '../../src/utils/validation.js';
|
|
4
|
+
|
|
5
|
+
describe('utils/form', () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
document.body.innerHTML = '';
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('tracks errors, touched flags, and dirtiness reactively', async () => {
|
|
11
|
+
const form = useForm({
|
|
12
|
+
initialValues: {
|
|
13
|
+
email: '',
|
|
14
|
+
name: ''
|
|
15
|
+
},
|
|
16
|
+
rules: {
|
|
17
|
+
email: [isRequired, isValidEmail],
|
|
18
|
+
name: [minLength(2)]
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
expect(form.isValid.value).toBe(false);
|
|
23
|
+
expect(form.errors.value.email).toBe('Validation failed for email');
|
|
24
|
+
|
|
25
|
+
form.fields.email.value = 'dev@example.com';
|
|
26
|
+
form.fields.name.value = 'Dev';
|
|
27
|
+
form.dirty.email.value = true;
|
|
28
|
+
form.dirty.name.value = true;
|
|
29
|
+
|
|
30
|
+
expect(form.isValid.value).toBe(true);
|
|
31
|
+
expect(form.isDirty.value).toBe(true);
|
|
32
|
+
|
|
33
|
+
const submitHandler = vi.fn(async () => {});
|
|
34
|
+
const submitted = await form.submit(submitHandler)();
|
|
35
|
+
|
|
36
|
+
expect(submitted).toBe(true);
|
|
37
|
+
expect(submitHandler).toHaveBeenCalledWith({
|
|
38
|
+
email: 'dev@example.com',
|
|
39
|
+
name: 'Dev'
|
|
40
|
+
});
|
|
41
|
+
expect(form.isDirty.value).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('binds input elements to field state', () => {
|
|
45
|
+
const input = document.createElement('input');
|
|
46
|
+
document.body.appendChild(input);
|
|
47
|
+
|
|
48
|
+
const form = useForm({
|
|
49
|
+
initialValues: {
|
|
50
|
+
email: ''
|
|
51
|
+
},
|
|
52
|
+
rules: {
|
|
53
|
+
email: [isRequired, isValidEmail]
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const cleanup = form.bindField('email', input);
|
|
58
|
+
input.value = 'user@example.com';
|
|
59
|
+
input.dispatchEvent(new Event('input'));
|
|
60
|
+
input.dispatchEvent(new Event('blur'));
|
|
61
|
+
|
|
62
|
+
expect(form.fields.email.value).toBe('user@example.com');
|
|
63
|
+
expect(form.touched.email.value).toBe(true);
|
|
64
|
+
expect(form.isValid.value).toBe(true);
|
|
65
|
+
|
|
66
|
+
cleanup();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example Formatter Test
|
|
3
|
+
* Tests for formatting utilities
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
import {
|
|
7
|
+
formatCurrency,
|
|
8
|
+
formatNumber,
|
|
9
|
+
formatPercentage,
|
|
10
|
+
truncate,
|
|
11
|
+
capitalize,
|
|
12
|
+
} from '../../src/utils/formatters.js';
|
|
13
|
+
|
|
14
|
+
describe('Formatter Utilities', () => {
|
|
15
|
+
describe('formatCurrency', () => {
|
|
16
|
+
it('should format currency correctly', () => {
|
|
17
|
+
expect(formatCurrency(99.99)).toBe('$99.99');
|
|
18
|
+
expect(formatCurrency(1000)).toBe('$1,000.00');
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('formatNumber', () => {
|
|
23
|
+
it('should format numbers with commas', () => {
|
|
24
|
+
expect(formatNumber(1000)).toBe('1,000');
|
|
25
|
+
expect(formatNumber(1000000)).toBe('1,000,000');
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('formatPercentage', () => {
|
|
30
|
+
it('should format percentages', () => {
|
|
31
|
+
expect(formatPercentage(0.5)).toBe('50%');
|
|
32
|
+
expect(formatPercentage(0.123, 2)).toBe('12.30%');
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('truncate', () => {
|
|
37
|
+
it('should truncate long text', () => {
|
|
38
|
+
expect(truncate('This is a long text', 10)).toBe('This is...');
|
|
39
|
+
expect(truncate('Short', 10)).toBe('Short');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('capitalize', () => {
|
|
44
|
+
it('should capitalize first letter', () => {
|
|
45
|
+
expect(capitalize('hello')).toBe('Hello');
|
|
46
|
+
expect(capitalize('HELLO')).toBe('HELLO');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { componentRegistry } from '../../src/core/lazyComponents.js';
|
|
3
|
+
|
|
4
|
+
function resetRegistry(): void {
|
|
5
|
+
const registry = componentRegistry as unknown as {
|
|
6
|
+
components: Map<string, string>;
|
|
7
|
+
loaded: Set<string>;
|
|
8
|
+
observer: MutationObserver | null;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
registry.components.clear();
|
|
12
|
+
registry.loaded.clear();
|
|
13
|
+
registry.observer?.disconnect();
|
|
14
|
+
registry.observer = null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('Lazy component registry', () => {
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
resetRegistry();
|
|
20
|
+
document.body.innerHTML = '';
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
componentRegistry.stopObserving();
|
|
25
|
+
vi.restoreAllMocks();
|
|
26
|
+
document.body.innerHTML = '';
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('loads registered components when matching elements are found', async () => {
|
|
30
|
+
componentRegistry.register('lazy-sample', './ui/lazy-sample.js');
|
|
31
|
+
document.body.innerHTML = '<lazy-sample></lazy-sample>';
|
|
32
|
+
|
|
33
|
+
const loadSpy = vi.spyOn(componentRegistry, 'loadComponent').mockResolvedValue();
|
|
34
|
+
|
|
35
|
+
await componentRegistry.scanAndLoad();
|
|
36
|
+
|
|
37
|
+
expect(loadSpy).toHaveBeenCalledWith('lazy-sample');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('warns when attempting to load an unregistered component', async () => {
|
|
41
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
42
|
+
|
|
43
|
+
await componentRegistry.loadComponent('missing-widget');
|
|
44
|
+
|
|
45
|
+
expect(warnSpy).toHaveBeenCalledWith('Component missing-widget not registered');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('observes DOM additions and scans new nodes', async () => {
|
|
49
|
+
const scanSpy = vi.spyOn(componentRegistry, 'scanAndLoad').mockResolvedValue();
|
|
50
|
+
componentRegistry.startObserving();
|
|
51
|
+
|
|
52
|
+
const wrapper = document.createElement('div');
|
|
53
|
+
wrapper.innerHTML = '<lazy-observed></lazy-observed>';
|
|
54
|
+
document.body.appendChild(wrapper);
|
|
55
|
+
await Promise.resolve();
|
|
56
|
+
|
|
57
|
+
expect(scanSpy).toHaveBeenCalled();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { renderMarkdown, renderMarkdownToc, splitMarkdownIntoSections } from '../../src/utils/markdown.js';
|
|
3
|
+
|
|
4
|
+
describe('utils/markdown', () => {
|
|
5
|
+
it('creates stable unique ids for duplicate headings', () => {
|
|
6
|
+
const result = renderMarkdown(`
|
|
7
|
+
## Chapter 1
|
|
8
|
+
### Routing
|
|
9
|
+
### Routing
|
|
10
|
+
`);
|
|
11
|
+
|
|
12
|
+
expect(result.headings.map(heading => heading.id)).toEqual([
|
|
13
|
+
'chapter-1',
|
|
14
|
+
'routing',
|
|
15
|
+
'routing-2',
|
|
16
|
+
]);
|
|
17
|
+
expect(result.html).toContain('<h3 id="routing">Routing</h3>');
|
|
18
|
+
expect(result.html).toContain('<h3 id="routing-2">Routing</h3>');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('includes level 4 headings in the table of contents', () => {
|
|
22
|
+
const toc = renderMarkdownToc([
|
|
23
|
+
{ level: 2, text: 'Chapter 6: Client-Side Routing', id: 'chapter-6-client-side-routing' },
|
|
24
|
+
{ level: 4, text: 'Query Strings and Search Parameters', id: 'query-strings-and-search-parameters' },
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
expect(toc).toContain('docs-toc__link--h2');
|
|
28
|
+
expect(toc).toContain('docs-toc__link--h4');
|
|
29
|
+
expect(toc).toContain('#query-strings-and-search-parameters');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('splits handbook markdown into overview and chapter sections', () => {
|
|
33
|
+
const sections = splitMarkdownIntoSections(`
|
|
34
|
+
# NativeCore Handbook
|
|
35
|
+
|
|
36
|
+
## Preface
|
|
37
|
+
|
|
38
|
+
[Getting Started](#chapter-1-start)
|
|
39
|
+
|
|
40
|
+
Welcome.
|
|
41
|
+
|
|
42
|
+
## Chapter 1: Getting Started with NativeCore
|
|
43
|
+
|
|
44
|
+
### Setup
|
|
45
|
+
|
|
46
|
+
## Chapter 2: Routing
|
|
47
|
+
|
|
48
|
+
### Setup
|
|
49
|
+
`);
|
|
50
|
+
|
|
51
|
+
expect(sections.map(section => section.title)).toEqual([
|
|
52
|
+
'Overview',
|
|
53
|
+
'Chapter 1: Getting Started with NativeCore',
|
|
54
|
+
'Chapter 2: Routing',
|
|
55
|
+
]);
|
|
56
|
+
expect(sections[0].isChapter).toBe(false);
|
|
57
|
+
expect(sections[1].id).toBe('chapter-1-start');
|
|
58
|
+
expect(sections[1].aliases).toEqual(['chapter-1-getting-started-with-nativecore']);
|
|
59
|
+
expect(sections[1].headings[1].id).toBe('setup');
|
|
60
|
+
expect(sections[2].headings[1].id).toBe('setup-2');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { Router } from '../../src/core/router.js';
|
|
3
|
+
|
|
4
|
+
describe('core/router', () => {
|
|
5
|
+
let router: Router;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
document.body.innerHTML = `
|
|
9
|
+
<main class="main-content">
|
|
10
|
+
<div id="main-content"></div>
|
|
11
|
+
</main>
|
|
12
|
+
<div id="page-progress"></div>
|
|
13
|
+
`;
|
|
14
|
+
router = new Router();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
vi.restoreAllMocks();
|
|
19
|
+
document.body.innerHTML = '';
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('supports optional params and wildcard params', () => {
|
|
23
|
+
router.register('/user/:id?', '/user.html');
|
|
24
|
+
router.register('/docs/*', '/docs.html');
|
|
25
|
+
|
|
26
|
+
const optionalRoute = (router as any).matchRoute('/user');
|
|
27
|
+
const dynamicRoute = (router as any).matchRoute('/user/42');
|
|
28
|
+
const wildcardRoute = (router as any).matchRoute('/docs/guides/intro');
|
|
29
|
+
|
|
30
|
+
expect(optionalRoute?.params).toEqual({});
|
|
31
|
+
expect(dynamicRoute?.params).toEqual({ id: '42' });
|
|
32
|
+
expect(wildcardRoute?.params).toEqual({ wildcard: 'guides/intro' });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('renders child routes into a shared layout outlet without refetching the layout', async () => {
|
|
36
|
+
const fetchMock = vi.fn((input: RequestInfo | URL) => {
|
|
37
|
+
const url = String(input).split('?')[0];
|
|
38
|
+
const htmlByFile: Record<string, string> = {
|
|
39
|
+
'/layout.html': '<section><header>Layout</header><div id="route-outlet"></div></section>',
|
|
40
|
+
'/settings.html': '<div class="settings-page">Settings</div>',
|
|
41
|
+
'/profile.html': '<div class="profile-page">Profile</div>'
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return Promise.resolve({
|
|
45
|
+
ok: true,
|
|
46
|
+
text: () => Promise.resolve(htmlByFile[url])
|
|
47
|
+
} as Response);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
51
|
+
|
|
52
|
+
router.register('/dashboard', '/layout.html');
|
|
53
|
+
router.register('/dashboard/settings', '/settings.html', null, { layout: '/dashboard' });
|
|
54
|
+
router.register('/dashboard/profile', '/profile.html', null, { layout: '/dashboard' });
|
|
55
|
+
|
|
56
|
+
await (router as any).handleRoute('/dashboard/settings', {});
|
|
57
|
+
await (router as any).handleRoute('/dashboard/profile', {});
|
|
58
|
+
|
|
59
|
+
const mainContent = document.getElementById('main-content');
|
|
60
|
+
expect(mainContent?.querySelector('header')?.textContent).toBe('Layout');
|
|
61
|
+
expect(mainContent?.querySelector('#route-outlet')?.innerHTML).toContain('Profile');
|
|
62
|
+
expect(fetchMock.mock.calls.filter(([request]) => String(request).includes('/layout.html'))).toHaveLength(1);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('prefetches route HTML and uses the prefetched response on navigation', async () => {
|
|
66
|
+
const fetchMock = vi.fn((input: RequestInfo | URL) => {
|
|
67
|
+
const url = String(input).split('?')[0];
|
|
68
|
+
|
|
69
|
+
return Promise.resolve({
|
|
70
|
+
ok: true,
|
|
71
|
+
text: () => Promise.resolve(url === '/prefetch.html' ? '<div>Prefetched</div>' : '')
|
|
72
|
+
} as Response);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
76
|
+
|
|
77
|
+
router.register('/prefetch', '/prefetch.html');
|
|
78
|
+
|
|
79
|
+
await router.prefetch('/prefetch');
|
|
80
|
+
await (router as any).handleRoute('/prefetch', {});
|
|
81
|
+
|
|
82
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
83
|
+
expect(document.getElementById('main-content')?.innerHTML).toContain('Prefetched');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('resets the page scroll position on navigation', async () => {
|
|
87
|
+
const fetchMock = vi.fn(() => Promise.resolve({
|
|
88
|
+
ok: true,
|
|
89
|
+
text: () => Promise.resolve('<div style="height: 2000px;">Scrolled page</div>')
|
|
90
|
+
} as Response));
|
|
91
|
+
|
|
92
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
93
|
+
const windowScrollSpy = vi.spyOn(window, 'scrollTo').mockImplementation(() => {});
|
|
94
|
+
|
|
95
|
+
router.register('/scrolled', '/scrolled.html');
|
|
96
|
+
|
|
97
|
+
const mainContent = document.getElementById('main-content') as HTMLElement;
|
|
98
|
+
const scrollContainer = document.querySelector('.main-content') as HTMLElement;
|
|
99
|
+
scrollContainer.scrollTop = 240;
|
|
100
|
+
scrollContainer.scrollLeft = 32;
|
|
101
|
+
mainContent.scrollTop = 240;
|
|
102
|
+
mainContent.scrollLeft = 32;
|
|
103
|
+
|
|
104
|
+
await (router as any).handleRoute('/scrolled', {});
|
|
105
|
+
|
|
106
|
+
expect(scrollContainer.scrollTop).toBe(0);
|
|
107
|
+
expect(scrollContainer.scrollLeft).toBe(0);
|
|
108
|
+
expect(mainContent.scrollTop).toBe(0);
|
|
109
|
+
expect(mainContent.scrollLeft).toBe(0);
|
|
110
|
+
expect(windowScrollSpy).toHaveBeenCalledWith(0, 0);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { createComputed, createEffect, createSignal } from '../../src/core/signals.js';
|
|
3
|
+
|
|
4
|
+
describe('core/signals', () => {
|
|
5
|
+
it('keeps computed values live when source signals change', () => {
|
|
6
|
+
const count = createSignal(2);
|
|
7
|
+
const doubled = createComputed(() => count.get() * 2);
|
|
8
|
+
|
|
9
|
+
expect(doubled()).toBe(4);
|
|
10
|
+
|
|
11
|
+
count.set(5);
|
|
12
|
+
expect(doubled()).toBe(10);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('releases stale dependencies when a computed branch changes', () => {
|
|
16
|
+
const toggle = createSignal(true);
|
|
17
|
+
const left = createSignal('left');
|
|
18
|
+
const right = createSignal('right');
|
|
19
|
+
const branch = createComputed(() => (toggle.get() ? left.get() : right.get()));
|
|
20
|
+
|
|
21
|
+
expect(branch()).toBe('left');
|
|
22
|
+
|
|
23
|
+
toggle.set(false);
|
|
24
|
+
expect(branch()).toBe('right');
|
|
25
|
+
|
|
26
|
+
left.set('updated-left');
|
|
27
|
+
expect(branch()).toBe('right');
|
|
28
|
+
|
|
29
|
+
right.set('updated-right');
|
|
30
|
+
expect(branch()).toBe('updated-right');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('tracks effects and runs cleanup before rerunning', () => {
|
|
34
|
+
const count = createSignal(1);
|
|
35
|
+
const seen: number[] = [];
|
|
36
|
+
const cleanup = vi.fn();
|
|
37
|
+
const stop = createEffect(() => {
|
|
38
|
+
seen.push(count.get());
|
|
39
|
+
return cleanup;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(seen).toEqual([1]);
|
|
43
|
+
|
|
44
|
+
count.set(2);
|
|
45
|
+
expect(seen).toEqual([1, 2]);
|
|
46
|
+
expect(cleanup).toHaveBeenCalledTimes(1);
|
|
47
|
+
|
|
48
|
+
stop();
|
|
49
|
+
expect(cleanup).toHaveBeenCalledTimes(2);
|
|
50
|
+
|
|
51
|
+
count.set(3);
|
|
52
|
+
expect(seen).toEqual([1, 2]);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
isNumber,
|
|
4
|
+
isRequired,
|
|
5
|
+
isValidEmail,
|
|
6
|
+
matchesPattern,
|
|
7
|
+
maxLength,
|
|
8
|
+
minLength,
|
|
9
|
+
validateForm
|
|
10
|
+
} from '../../src/utils/validation.js';
|
|
11
|
+
|
|
12
|
+
describe('utils/validation', () => {
|
|
13
|
+
it('validates email addresses', () => {
|
|
14
|
+
expect(isValidEmail('test@example.com')).toBe(true);
|
|
15
|
+
expect(isValidEmail('invalid')).toBe(false);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('validates required values', () => {
|
|
19
|
+
expect(isRequired('value')).toBe(true);
|
|
20
|
+
expect(isRequired(' ')).toBe(false);
|
|
21
|
+
expect(isRequired(null)).toBe(false);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('validates length rules and patterns', () => {
|
|
25
|
+
expect(minLength(3)('abcd')).toBe(true);
|
|
26
|
+
expect(maxLength(3)('abcd')).toBe(false);
|
|
27
|
+
expect(matchesPattern(/^\d+$/)('1234')).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('validates numbers', () => {
|
|
31
|
+
expect(isNumber(123)).toBe(true);
|
|
32
|
+
expect(isNumber('123.45')).toBe(true);
|
|
33
|
+
expect(isNumber('abc')).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('returns field-level form errors', () => {
|
|
37
|
+
const errors = validateForm(
|
|
38
|
+
{ email: 'bad-email', name: 'A' },
|
|
39
|
+
{
|
|
40
|
+
email: [isRequired, isValidEmail],
|
|
41
|
+
name: [minLength(2)]
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
expect(errors).toEqual({
|
|
46
|
+
email: 'Validation failed for email',
|
|
47
|
+
name: 'Validation failed for name'
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"sourceMap": false,
|
|
5
|
+
"declaration": false,
|
|
6
|
+
"declarationMap": false,
|
|
7
|
+
"removeComments": true
|
|
8
|
+
},
|
|
9
|
+
"include": [
|
|
10
|
+
"src/**/*.ts"
|
|
11
|
+
],
|
|
12
|
+
"exclude": [
|
|
13
|
+
"node_modules",
|
|
14
|
+
"dist",
|
|
15
|
+
"tests",
|
|
16
|
+
"**/*.test.ts",
|
|
17
|
+
"**/*.spec.ts",
|
|
18
|
+
"src/dev/**/*.ts",
|
|
19
|
+
"src/dev"
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ES2020",
|
|
5
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
6
|
+
"moduleResolution": "node",
|
|
7
|
+
"incremental": true,
|
|
8
|
+
"tsBuildInfoFile": "./dist/.tsbuildinfo",
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
"checkJs": false,
|
|
11
|
+
"declaration": true,
|
|
12
|
+
"declarationMap": true,
|
|
13
|
+
"sourceMap": true,
|
|
14
|
+
"outDir": "./dist",
|
|
15
|
+
"rootDir": "./src",
|
|
16
|
+
"baseUrl": "./src",
|
|
17
|
+
"paths": {
|
|
18
|
+
"@core/*": ["core/*"],
|
|
19
|
+
"@components/*": ["components/*"],
|
|
20
|
+
"@config/*": ["config/*"],
|
|
21
|
+
"@routes/*": ["routes/*"],
|
|
22
|
+
"@services/*": ["services/*"],
|
|
23
|
+
"@utils/*": ["utils/*"],
|
|
24
|
+
"@stores/*": ["stores/*"],
|
|
25
|
+
"@middleware/*": ["middleware/*"],
|
|
26
|
+
"@types/*": ["types/*"],
|
|
27
|
+
"@constants/*": ["constants/*"]
|
|
28
|
+
},
|
|
29
|
+
"strict": true,
|
|
30
|
+
"ignoreDeprecations": "5.0",
|
|
31
|
+
"noImplicitAny": false,
|
|
32
|
+
"strictNullChecks": false,
|
|
33
|
+
"noUnusedLocals": false,
|
|
34
|
+
"noUnusedParameters": false,
|
|
35
|
+
"noImplicitReturns": true,
|
|
36
|
+
"noFallthroughCasesInSwitch": true,
|
|
37
|
+
"esModuleInterop": true,
|
|
38
|
+
"skipLibCheck": true,
|
|
39
|
+
"forceConsistentCasingInFileNames": true,
|
|
40
|
+
"resolveJsonModule": true,
|
|
41
|
+
"jsx": "preserve"
|
|
42
|
+
},
|
|
43
|
+
"include": [
|
|
44
|
+
"src/**/*"
|
|
45
|
+
],
|
|
46
|
+
"exclude": [
|
|
47
|
+
"node_modules",
|
|
48
|
+
"dist",
|
|
49
|
+
"tests"
|
|
50
|
+
]
|
|
51
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
import { defineConfig } from 'vitest/config';
|
|
3
|
+
|
|
4
|
+
const srcDir = fileURLToPath(new URL('./src', import.meta.url));
|
|
5
|
+
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
resolve: {
|
|
8
|
+
alias: {
|
|
9
|
+
'@core': fileURLToPath(new URL('./src/core', import.meta.url)),
|
|
10
|
+
'@components': fileURLToPath(new URL('./src/components', import.meta.url)),
|
|
11
|
+
'@config': fileURLToPath(new URL('./src/config', import.meta.url)),
|
|
12
|
+
'@routes': fileURLToPath(new URL('./src/routes', import.meta.url)),
|
|
13
|
+
'@services': fileURLToPath(new URL('./src/services', import.meta.url)),
|
|
14
|
+
'@utils': fileURLToPath(new URL('./src/utils', import.meta.url)),
|
|
15
|
+
'@stores': fileURLToPath(new URL('./src/stores', import.meta.url)),
|
|
16
|
+
'@middleware': fileURLToPath(new URL('./src/middleware', import.meta.url)),
|
|
17
|
+
'@types': fileURLToPath(new URL('./src/types', import.meta.url)),
|
|
18
|
+
'@constants': fileURLToPath(new URL('./src/constants', import.meta.url)),
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
test: {
|
|
22
|
+
environment: 'happy-dom',
|
|
23
|
+
globals: true,
|
|
24
|
+
coverage: {
|
|
25
|
+
provider: 'v8',
|
|
26
|
+
reporter: ['text', 'json', 'html'],
|
|
27
|
+
exclude: [
|
|
28
|
+
'node_modules/',
|
|
29
|
+
'tests/',
|
|
30
|
+
'**/*.config.js',
|
|
31
|
+
'server.js',
|
|
32
|
+
'api/',
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
});
|