@nutin/cli 1.2.0 → 1.2.1
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/CHANGELOG.md +63 -30
- package/README.md +9 -8
- package/lib/index.mjs +6 -6
- package/lib/src/file-generator.mjs +18 -5
- package/lib/src/package-manager.mjs +33 -21
- package/lib/src/prompts.mjs +2 -2
- package/lib/src/utils.mjs +2 -0
- package/package.json +4 -3
- package/templates/base/.dockerignore.hbs +2 -1
- package/templates/base/builder.config.js.hbs +28 -0
- package/templates/base/src/app/components/common/anchor/anchor.component.ts.hbs +11 -12
- package/templates/base/src/app/dom.test.js.hbs +8 -1
- package/templates/base/src/core/base-classes/base-component/helpers/event.helper.ts.hbs +3 -0
- package/templates/base/src/core/base-classes/base-component/helpers/pipe.helper.ts.hbs +3 -0
- package/templates/base/src/core/base-classes/base-component/helpers/token.helper.ts.hbs +2 -2
- package/templates/base/src/core/services/router/helpers/navigation-manager.helper.ts.hbs +3 -3
- package/templates/base/src/core/services/router/helpers/route-guard-manager.helper.ts.hbs +2 -2
- package/templates/base/src/core/utils/anchor-manager/anchor-manager.utils.ts.hbs +4 -3
- package/templates/base/src/core/utils/button-manager/button-manager.utils.test.js.hbs +1 -1
- package/templates/base/src/index.html.hbs +1 -0
- package/templates/base/src/styles/main.scss.hbs +2 -0
- package/templates/base/tools/builder/builder.js.hbs +10 -7
- package/templates/base/tools/builder/core/copy-static.js.hbs +27 -2
- package/templates/base/tools/builder/core/esbuild.js.hbs +5 -15
- package/templates/base/tools/builder/core/finalize-build.js.hbs +2 -2
- package/templates/base/tools/builder/core/minify-html.js.hbs +2 -2
- package/templates/base/tools/builder/core/{sass.config.js.hbs → sass.js.hbs} +5 -7
- package/templates/base/tools/builder/core/tsc.js.hbs +8 -4
- package/templates/base/tools/builder/core/validate-html.js.hbs +19 -14
- package/templates/base/tools/deployment/Dockerfile.hbs +22 -0
- package/templates/base/tools/deployment/gzip.conf.hbs +19 -0
- package/templates/base/tools/deployment/nginx.conf.hbs +78 -0
- package/templates/base/tools/dev/dev-serve.js.hbs +32 -0
- package/templates/base/tools/{watcher.js.hbs → dev/watcher.js.hbs} +1 -1
- package/templates/base/tools/generator/generator.js.hbs +13 -5
- package/templates/base/tools/generator/handle-file.js.hbs +15 -8
- package/templates/base/tools/utils/export-flags.js.hbs +2 -1
- package/templates/base/tools/utils/index.js.hbs +4 -1
- package/templates/base/tools/utils/run-command.js.hbs +10 -3
- package/templates/base/tools/utils/run-script.js.hbs +1 -1
- package/templates/features/i18n/src/core/config.ts.hbs +3 -0
- package/templates/features/i18n/src/core/services/i18n/i18n.test.js.hbs +2 -0
- package/templates/features/i18n/src/core/services/i18n/i18n.ts.hbs +10 -1
- package/templates/features/i18n/src/core/services/i18n/languages.ts.hbs +7 -0
- package/templates/features/i18n/tools/builder/core/build-i18n.js.hbs +2 -2
- package/templates/features/i18n/tools/utils/languages.js.hbs +7 -0
- package/templates/features/stylinNutin/src/styles/components/_index.scss.hbs +0 -3
- package/templates/features/stylinNutin/src/styles/views/_index.scss.hbs +0 -0
- package/templates/features/template/tools/builder/core/merge-templates.js.hbs +1 -1
- package/templates/features/testinNutin/{test → testin-nutin}/core/index.js.hbs +1 -1
- package/templates/features/testinNutin/testin-nutin/core/src/globals/assertion-lib.js.hbs +136 -0
- package/templates/features/testinNutin/testin-nutin/core/src/globals/jsdom-setup.js.hbs +101 -0
- package/templates/features/testinNutin/{test → testin-nutin}/core/src/globals/register-test-globals.js.hbs +15 -1
- package/templates/features/testinNutin/testin-nutin/core/src/globals/spyon.js.hbs +59 -0
- package/templates/features/testinNutin/{test → testin-nutin}/core/src/utils/print.js.hbs +3 -1
- package/templates/features/testinNutin/{test → testin-nutin}/core/src/utils/test-discovery.js.hbs +5 -1
- package/templates/features/testinNutin/{test → testin-nutin}/core/src/utils/test-queue.js.hbs +3 -1
- package/templates/features/testinNutin/{test → testin-nutin}/core/tests/assertions.test.js.hbs +26 -2
- package/templates/features/testinNutin/testin-nutin/runner.js.hbs +35 -0
- package/templates/features/testinNutin/{test → testin-nutin}/watch-tests.js.hbs +1 -1
- package/templates/features/testinNutin/testin-nutin.config.js.hbs +14 -0
- package/templates/base/Dockerfile.hbs +0 -33
- package/templates/base/nginx.conf.hbs +0 -43
- package/templates/base/tools/builder/docs.md.hbs +0 -9
- package/templates/base/tools/dev.js.hbs +0 -43
- package/templates/features/testinNutin/test/core/src/globals/assertion-lib.js.hbs +0 -118
- package/templates/features/testinNutin/test/core/src/globals/spyon.js.hbs +0 -22
- package/templates/features/testinNutin/test/core/src/jsdom-setup.js.hbs +0 -12
- package/templates/features/testinNutin/test/docs.md.hbs +0 -39
- package/templates/features/testinNutin/test/runner.js.hbs +0 -36
- /package/templates/base/tools/{serve.js.hbs → dev/serve.js.hbs} +0 -0
- /package/templates/features/testinNutin/{test → testin-nutin}/core/src/utils/queue.js.hbs +0 -0
- /package/templates/features/testinNutin/{test → testin-nutin}/core/tests/globals-each-all.test.js.hbs +0 -0
- /package/templates/features/testinNutin/{test → testin-nutin}/mocks/create-mock-method.js.hbs +0 -0
- /package/templates/features/testinNutin/{test → testin-nutin}/mocks/mock-event-bus.js.hbs +0 -0
- /package/templates/features/testinNutin/{test → testin-nutin}/mocks/mock-http-client.js.hbs +0 -0
- /package/templates/features/testinNutin/{test → testin-nutin}/mocks/mock-i18n.js.hbs +0 -0
- /package/templates/features/testinNutin/{test → testin-nutin}/mocks/mock-router.js.hbs +0 -0
- /package/templates/features/testinNutin/{test → testin-nutin}/mocks/mock-store.js.hbs +0 -0
|
@@ -72,7 +72,7 @@ async function removeJsonFiles(directory) {
|
|
|
72
72
|
|
|
73
73
|
await removeEmptyDirectories(directory);
|
|
74
74
|
|
|
75
|
-
if (isVerbose) print.
|
|
75
|
+
if (isVerbose) print.boldInfo(`JSON files cleanup complete.`);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
async function removeEmptyDirectories(directory) {
|
|
@@ -121,7 +121,7 @@ async function mergeJson() {
|
|
|
121
121
|
`${destDir}/${locale}.json`,
|
|
122
122
|
JSON.stringify(combined, null, 2)
|
|
123
123
|
);
|
|
124
|
-
print.
|
|
124
|
+
if (isVerbose) print.boldInfo(`Json files for locale ${locale} merged successfully`);
|
|
125
125
|
}
|
|
126
126
|
removeJsonFiles(PATHS.tempApp);
|
|
127
127
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
const langsPath = path.join(process.cwd(), 'config', 'languages.json');
|
|
5
|
+
const langs = JSON.parse(fs.readFileSync(langsPath, 'utf-8'));
|
|
6
|
+
export const LANGUAGES = langs.languages;
|
|
7
|
+
export const DEFAULT_LANGUAGE = langs.defaultLanguage;
|
|
File without changes
|
|
@@ -40,7 +40,7 @@ async function mergeTemplates() {
|
|
|
40
40
|
print.boldError(`ERROR: Failed to read ${htmlFilename}. ${err.message}`);
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
-
print.info(`HTML templates minified and merged in scripts
|
|
43
|
+
if (isVerbose) print.info(`HTML templates minified and merged in scripts.\n`)
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
mergeTemplates().catch((err) => {
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
global.expect = (actual) => {
|
|
2
|
+
const expectObj = {
|
|
3
|
+
toBe(expected) {
|
|
4
|
+
if (actual !== expected) {
|
|
5
|
+
throw new Error(`Expected ${actual} to be strictly equal to ${expected}`);
|
|
6
|
+
}
|
|
7
|
+
},
|
|
8
|
+
toEqual(expected) {
|
|
9
|
+
const deepEqual = (a, b) => {
|
|
10
|
+
if (a === b) return true;
|
|
11
|
+
if (typeof a !== typeof b) return false;
|
|
12
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
13
|
+
return a.length === b.length && a.every((v, i) => deepEqual(v, b[i]));
|
|
14
|
+
}
|
|
15
|
+
if (a && b && typeof a === 'object') {
|
|
16
|
+
const keysA = Object.keys(a);
|
|
17
|
+
const keysB = Object.keys(b);
|
|
18
|
+
if (keysA.length !== keysB.length) return false;
|
|
19
|
+
return keysA.every(k => deepEqual(a[k], b[k]));
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
if (!deepEqual(actual, expected)) {
|
|
25
|
+
throw new Error(`Expected ${JSON.stringify(actual)} to deeply equal ${JSON.stringify(expected)}`);
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
toContain(substring) {
|
|
29
|
+
if (!actual.includes(substring)) {
|
|
30
|
+
throw new Error(`Expected '${actual}' to contain '${substring}'`);
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
toBeTruthy() {
|
|
34
|
+
if (!actual) {
|
|
35
|
+
throw new Error(`Expected ${actual} to be truthy`);
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
toBeFalsy() {
|
|
39
|
+
if (actual) {
|
|
40
|
+
throw new Error(`Expected ${actual} to be falsy`);
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
toBeDefined() {
|
|
44
|
+
if (actual === null || actual === undefined) {
|
|
45
|
+
throw new Error(`Expected value to be defined, but got ${actual}`);
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
toBeUndefined() {
|
|
49
|
+
if (actual !== null && actual !== undefined) {
|
|
50
|
+
throw new Error(`Expected value to be undefined, but got ${actual}`);
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
toBeInstanceOf(constructor) {
|
|
54
|
+
if (!(actual instanceof constructor)) {
|
|
55
|
+
throw new Error(`Expected ${actual} to be instance of ${constructor.name}`);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
toHaveBeenCalled() {
|
|
59
|
+
if (!actual?.calls || actual.calls.length === 0) {
|
|
60
|
+
throw new Error(`Expected function to have been called, but it was not`);
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
toHaveBeenCalledWith(...expectedArgs) {
|
|
64
|
+
const deepEqual = (a, b) => {
|
|
65
|
+
if (a === b) return true;
|
|
66
|
+
if (typeof a !== typeof b) return false;
|
|
67
|
+
|
|
68
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
69
|
+
return a.length === b.length && a.every((v, i) => deepEqual(v, b[i]));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (a && b && typeof a === 'object') {
|
|
73
|
+
const keysA = Object.keys(a);
|
|
74
|
+
const keysB = Object.keys(b);
|
|
75
|
+
if (keysA.length !== keysB.length) return false;
|
|
76
|
+
return keysA.every(k => deepEqual(a[k], b[k]));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return false;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const matched = actual?.calls?.some(args =>
|
|
83
|
+
args.length === expectedArgs.length &&
|
|
84
|
+
args.every((arg, i) => deepEqual(arg, expectedArgs[i]))
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
if (!matched) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`Expected function to have been called with ${JSON.stringify(expectedArgs)}, but it was not.\n` +
|
|
90
|
+
`Actual calls:\n${JSON.stringify(actual?.calls, null, 2)}`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
toBeLessThan(expected) {
|
|
95
|
+
if (actual > expected) {
|
|
96
|
+
throw new Error(`Expected ${actual} to be less than ${expected}`)
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
toBeGreaterThan(expected) {
|
|
100
|
+
if (actual < expected) {
|
|
101
|
+
throw new Error(`Expected ${actual} to be greater than ${expected}`)
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
toThrow(expectedMessage) {
|
|
105
|
+
let threw = false;
|
|
106
|
+
try {
|
|
107
|
+
actual();
|
|
108
|
+
} catch (err) {
|
|
109
|
+
threw = true;
|
|
110
|
+
if (expectedMessage && !err.message.includes(expectedMessage)) {
|
|
111
|
+
throw new Error(`Expected error message to include "${expectedMessage}", but got "${err.message}"`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (!threw) {
|
|
115
|
+
throw new Error(`Expected function to throw, but it did not`);
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// 'not' variants
|
|
121
|
+
expectObj.not = {};
|
|
122
|
+
for (const [name, fn] of Object.entries(expectObj)) {
|
|
123
|
+
if (name === 'not') continue;
|
|
124
|
+
if (typeof fn !== 'function') continue;
|
|
125
|
+
|
|
126
|
+
expectObj.not[name] = (...args) => {
|
|
127
|
+
try {
|
|
128
|
+
fn(...args);
|
|
129
|
+
} catch {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
throw new Error(`Expected NOT: ${name}(${JSON.stringify(args)})`);
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return expectObj;
|
|
136
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { JSDOM } from 'jsdom';
|
|
4
|
+
import config from '#root/testin-nutin.config.js';
|
|
5
|
+
let currentDom = null;
|
|
6
|
+
|
|
7
|
+
export function setupJsdom(htmlPath = 'dist/src/index.html') {
|
|
8
|
+
const html = fs.readFileSync(path.resolve(process.cwd(), htmlPath), 'utf8');
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
runScripts,
|
|
12
|
+
resources,
|
|
13
|
+
freezeGlobals,
|
|
14
|
+
pretendToBeVisual,
|
|
15
|
+
} = config.jsdomOptions;
|
|
16
|
+
|
|
17
|
+
const dom = new JSDOM(html, {
|
|
18
|
+
url: 'http://localhost',
|
|
19
|
+
runScripts: runScripts ? 'dangerously' : undefined,
|
|
20
|
+
resources: resources ? 'usable' : undefined,
|
|
21
|
+
pretendToBeVisual,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
currentDom = dom;
|
|
25
|
+
|
|
26
|
+
global.window = dom.window;
|
|
27
|
+
global.document = dom.window.document;
|
|
28
|
+
|
|
29
|
+
// DOM globals
|
|
30
|
+
Object.getOwnPropertyNames(dom.window)
|
|
31
|
+
.filter(prop => !(prop in global))
|
|
32
|
+
.forEach(prop => {
|
|
33
|
+
global[prop] = dom.window[prop];
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
global.localStorage = dom.window.localStorage;
|
|
37
|
+
|
|
38
|
+
window.matchMedia ??= () => ({
|
|
39
|
+
matches: false,
|
|
40
|
+
addEventListener: () => {},
|
|
41
|
+
removeEventListener: () => {},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
window.scrollTo ??= () => {};
|
|
45
|
+
|
|
46
|
+
window.requestAnimationFrame ??= (cb) => setTimeout(cb, 0);
|
|
47
|
+
window.cancelAnimationFrame ??= (id) => clearTimeout(id);
|
|
48
|
+
|
|
49
|
+
window.IntersectionObserver ??= class {
|
|
50
|
+
constructor() {}
|
|
51
|
+
observe() {}
|
|
52
|
+
unobserve() {}
|
|
53
|
+
disconnect() {}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
window.ResizeObserver ??= class {
|
|
57
|
+
constructor() {}
|
|
58
|
+
observe() {}
|
|
59
|
+
unobserve() {}
|
|
60
|
+
disconnect() {}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
if (freezeGlobals) {
|
|
64
|
+
Object.freeze(global.window);
|
|
65
|
+
Object.freeze(global.document);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return dom;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function teardownJsdom() {
|
|
72
|
+
if (currentDom) {
|
|
73
|
+
try {
|
|
74
|
+
currentDom.window.close();
|
|
75
|
+
} catch {}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
currentDom = null;
|
|
79
|
+
|
|
80
|
+
delete global.window;
|
|
81
|
+
delete global.document;
|
|
82
|
+
delete global.localStorage;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function resetDom() {
|
|
86
|
+
if (!global.document) return;
|
|
87
|
+
|
|
88
|
+
document.body.innerHTML = '';
|
|
89
|
+
document.head.innerHTML = '<meta charset="UTF-8">';
|
|
90
|
+
|
|
91
|
+
if (global.localStorage) {
|
|
92
|
+
localStorage.clear();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function flushPromises() {
|
|
97
|
+
return new Promise(setImmediate);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const $ = (selector) => document.querySelector(selector);
|
|
101
|
+
export const $$ = (selector) => [...document.querySelectorAll(selector)];
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { print, addTest } from '../../index.js';
|
|
2
|
+
import config from '#root/testin-nutin.config.js';
|
|
3
|
+
import { setupJsdom, teardownJsdom, resetDom, flushPromises } from './jsdom-setup.js';
|
|
2
4
|
import './assertion-lib.js';
|
|
3
5
|
import './spyon.js';
|
|
4
6
|
|
|
@@ -14,7 +16,7 @@ export function registerTestGlobals() {
|
|
|
14
16
|
afterEach: null,
|
|
15
17
|
afterAll: null
|
|
16
18
|
};
|
|
17
|
-
print.boldSuccess('📂 ' + name);
|
|
19
|
+
if (config.verbose) print.boldSuccess('📂 ' + name);
|
|
18
20
|
fn();
|
|
19
21
|
};
|
|
20
22
|
|
|
@@ -42,4 +44,16 @@ export function registerTestGlobals() {
|
|
|
42
44
|
global.afterEach = (fn) => {
|
|
43
45
|
currentSuiteHooks.afterEach = fn;
|
|
44
46
|
};
|
|
47
|
+
|
|
48
|
+
global.setupJsdom = setupJsdom;
|
|
49
|
+
global.teardownJsdom = teardownJsdom;
|
|
50
|
+
global.resetDom = resetDom;
|
|
51
|
+
global.flushPromises = flushPromises;
|
|
52
|
+
|
|
53
|
+
global.$ = (selector) => document.querySelector(selector);
|
|
54
|
+
global.$$ = (selector) => [...document.querySelectorAll(selector)];
|
|
55
|
+
|
|
56
|
+
global.click = (el) => el.dispatchEvent(new window.MouseEvent("click", { bubbles: true }));
|
|
57
|
+
global.type = (el, text) => { el.value = text; el.dispatchEvent(new window.InputEvent("input", { bubbles: true })); };
|
|
58
|
+
|
|
45
59
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
global.spyOn = (obj, methodName) => {
|
|
2
|
+
if (!obj || typeof methodName !== 'string') {
|
|
3
|
+
throw new Error('spyOn requires an object and a method name');
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const originalFn = obj[methodName];
|
|
7
|
+
|
|
8
|
+
if (typeof originalFn !== 'function') {
|
|
9
|
+
throw new Error(`Cannot spy on ${methodName} – not a function`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const calls = [];
|
|
13
|
+
|
|
14
|
+
function spyFunction(...args) {
|
|
15
|
+
calls.push(args.slice());
|
|
16
|
+
if (spyFunction._fake) {
|
|
17
|
+
return spyFunction._fake.apply(this, args);
|
|
18
|
+
}
|
|
19
|
+
if (spyFunction._hasReturn) {
|
|
20
|
+
return spyFunction._returnValue;
|
|
21
|
+
}
|
|
22
|
+
return originalFn.apply(this, args);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
spyFunction._isSpy = true;
|
|
26
|
+
spyFunction._fake = null;
|
|
27
|
+
spyFunction._hasReturn = false;
|
|
28
|
+
spyFunction._returnValue = undefined;
|
|
29
|
+
|
|
30
|
+
obj[methodName] = spyFunction;
|
|
31
|
+
|
|
32
|
+
const handle = {
|
|
33
|
+
restore() {
|
|
34
|
+
if (obj[methodName] === spyFunction) {
|
|
35
|
+
obj[methodName] = originalFn;
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
get calls() {
|
|
39
|
+
return calls;
|
|
40
|
+
},
|
|
41
|
+
get callCount() {
|
|
42
|
+
return calls.length;
|
|
43
|
+
},
|
|
44
|
+
get lastCall() {
|
|
45
|
+
return calls.length ? calls[calls.length - 1] : undefined;
|
|
46
|
+
},
|
|
47
|
+
andCallFake(fn) {
|
|
48
|
+
spyFunction._fake = fn;
|
|
49
|
+
spyFunction._hasReturn = false;
|
|
50
|
+
},
|
|
51
|
+
andReturn(value) {
|
|
52
|
+
spyFunction._returnValue = value;
|
|
53
|
+
spyFunction._hasReturn = true;
|
|
54
|
+
spyFunction._fake = null;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return handle;
|
|
59
|
+
};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import config from '#root/testin-nutin.config.js';
|
|
2
|
+
|
|
1
3
|
export function printSummary(passed, failed, time) {
|
|
2
4
|
const totalTime = time.toFixed(2);
|
|
3
5
|
const total = passed + failed;
|
|
@@ -17,7 +19,7 @@ export function printResults(results) {
|
|
|
17
19
|
|
|
18
20
|
sortedResults.forEach(result => {
|
|
19
21
|
if (result.status === 'passed') {
|
|
20
|
-
console.log(`${chalk.green('✓')} ${chalk.cyan(`${result.suiteName}`)} - ${chalk.green(`${result.name}`)}`);
|
|
22
|
+
if (config.verbose ) console.log(`${chalk.green('✓')} ${chalk.cyan(`${result.suiteName}`)} - ${chalk.green(`${result.name}`)}`);
|
|
21
23
|
} else {
|
|
22
24
|
print.error(`✗ ${result.suiteName} - ${result.name}`);
|
|
23
25
|
print.grayError(result.error);
|
package/templates/features/testinNutin/{test → testin-nutin}/core/src/utils/test-discovery.js.hbs
RENAMED
|
@@ -3,9 +3,13 @@ import path from 'path';
|
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
|
|
5
5
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const relativeToCwd = path.relative(process.cwd(), __dirname);
|
|
7
|
+
const levelsUp = relativeToCwd.split(path.sep).length-1;
|
|
8
|
+
const projectRoot = path.resolve(__dirname, ...Array(levelsUp).fill('..'));
|
|
6
9
|
|
|
7
10
|
export function getTestFiles(dir) {
|
|
8
|
-
const fullDir = path.
|
|
11
|
+
const fullDir = path.join(projectRoot, dir);
|
|
12
|
+
|
|
9
13
|
if (!fs.existsSync(fullDir)) return [];
|
|
10
14
|
|
|
11
15
|
function findTestFilesRecursively(currentDir) {
|
package/templates/features/testinNutin/{test → testin-nutin}/core/src/utils/test-queue.js.hbs
RENAMED
|
@@ -34,9 +34,11 @@ export async function runQueuedTests() {
|
|
|
34
34
|
previousSuiteAfterAll = test.afterAll;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
setupJsdom();
|
|
37
38
|
if (test.beforeEach) await test.beforeEach();
|
|
38
39
|
await test.testFn();
|
|
39
40
|
if (test.afterEach) await test.afterEach();
|
|
41
|
+
teardownJsdom();
|
|
40
42
|
|
|
41
43
|
passed++;
|
|
42
44
|
results.push({
|
|
@@ -50,7 +52,7 @@ export async function runQueuedTests() {
|
|
|
50
52
|
suiteName: test.suiteName,
|
|
51
53
|
status: 'failed',
|
|
52
54
|
name: test.testName,
|
|
53
|
-
error: err.stack
|
|
55
|
+
error: err.stack
|
|
54
56
|
});
|
|
55
57
|
}
|
|
56
58
|
}
|
package/templates/features/testinNutin/{test → testin-nutin}/core/tests/assertions.test.js.hbs
RENAMED
|
@@ -8,11 +8,17 @@ describe('assertions', () => {
|
|
|
8
8
|
it('should check if value matches expected', () => {
|
|
9
9
|
const five = obj.add(2,3);
|
|
10
10
|
const ten = obj.add(3,7);
|
|
11
|
+
const object = { five: five, ten: ten };
|
|
12
|
+
const arr = [ five, ten ];
|
|
11
13
|
|
|
12
14
|
expect(five).toEqual(5);
|
|
13
|
-
expect(five).toBe(
|
|
15
|
+
expect(five).toBe(5);
|
|
14
16
|
expect(ten).not.toEqual(11);
|
|
15
|
-
expect(ten).not.toBe(
|
|
17
|
+
expect(ten).not.toBe(11);
|
|
18
|
+
expect(object).toEqual({ five: five, ten: ten });
|
|
19
|
+
expect(object).not.toBe({ five: five, ten: ten });
|
|
20
|
+
expect(arr).toEqual([five, ten]);
|
|
21
|
+
expect(arr).not.toBe([five, ten]);
|
|
16
22
|
});
|
|
17
23
|
it('should check if function has been called', () => {
|
|
18
24
|
const spy = spyOn(obj, 'add');
|
|
@@ -29,6 +35,12 @@ describe('assertions', () => {
|
|
|
29
35
|
expect(null).toBeUndefined();
|
|
30
36
|
expect(null).not.toBeDefined();
|
|
31
37
|
});
|
|
38
|
+
it('should check for if an element is true / false', () => {
|
|
39
|
+
const isTrue = 1 == '1';
|
|
40
|
+
const isFalse = 1 === '1';
|
|
41
|
+
expect(isTrue).toBeTruthy();
|
|
42
|
+
expect(isFalse).toBeFalsy();
|
|
43
|
+
});
|
|
32
44
|
it('should check if substring is contained within string', () => {
|
|
33
45
|
const str1 = 'Creativity is allowing yourself to make mistakes.';
|
|
34
46
|
const str2 = 'Art is knowing which ones to keep.';
|
|
@@ -40,7 +52,19 @@ describe('assertions', () => {
|
|
|
40
52
|
});
|
|
41
53
|
it('should check for instance', () => {
|
|
42
54
|
const date = new Date();
|
|
55
|
+
const fn = () => {};
|
|
43
56
|
expect(date).toBeInstanceOf(Date);
|
|
57
|
+
expect(fn).toBeInstanceOf(Function);
|
|
44
58
|
expect(date).not.toBeInstanceOf(Function);
|
|
59
|
+
expect(fn).not.toBeInstanceOf(Date);
|
|
60
|
+
});
|
|
61
|
+
it('should check for error throw', () => {
|
|
62
|
+
const err = () => { throw new Error('message'); };
|
|
63
|
+
const notErr = () => {};
|
|
64
|
+
expect(err).toThrow();
|
|
65
|
+
expect(err).toThrow('message');
|
|
66
|
+
expect(err).not.toThrow('another message');
|
|
67
|
+
expect(notErr).not.toThrow();
|
|
68
|
+
expect(notErr).not.toThrow('messsage');
|
|
45
69
|
});
|
|
46
70
|
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { registerTestGlobals, getTestFiles, runQueuedTests, print } from './core/index.js';
|
|
2
|
+
import config from '#root/testin-nutin.config.js';
|
|
3
|
+
|
|
4
|
+
registerTestGlobals();
|
|
5
|
+
|
|
6
|
+
let testFiles = config.origins.flatMap(
|
|
7
|
+
origin => getTestFiles(`../${origin}`)
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
if (args.length) {
|
|
12
|
+
testFiles = testFiles.filter(file => args.some(f => file.includes(f)));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function loadTestFile(file) {
|
|
16
|
+
setupJsdom();
|
|
17
|
+
await import(file);
|
|
18
|
+
teardownJsdom();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function runTests() {
|
|
22
|
+
if (config.verbose) print.head('Test suites :');
|
|
23
|
+
for (const file of testFiles) {
|
|
24
|
+
try {
|
|
25
|
+
await loadTestFile(file);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
print.boldError(`✗ ${file} failed to load:`);
|
|
28
|
+
print.grayError(error.stack);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
await runQueuedTests();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
runTests();
|
|
@@ -14,7 +14,7 @@ watcher.on('change', (filePath) => {
|
|
|
14
14
|
console.clear();
|
|
15
15
|
print.boldInfo(`\n🔄 File changed: ${path.relative(process.cwd(), filePath)}\n`);
|
|
16
16
|
|
|
17
|
-
exec('node --experimental-modules
|
|
17
|
+
exec('node --experimental-modules testin-nutin/runner.js', (err, stdout, stderr) => {
|
|
18
18
|
if (stdout) process.stdout.write(stdout);
|
|
19
19
|
if (stderr) process.stderr.write(stderr);
|
|
20
20
|
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
origins: [
|
|
3
|
+
'src/app', // actual app code
|
|
4
|
+
// 'src/core', // toolkit core logic
|
|
5
|
+
// 'testin-nutin/core/tests', // test globals
|
|
6
|
+
],
|
|
7
|
+
verbose: false,
|
|
8
|
+
jsdomOptions: {
|
|
9
|
+
runScripts: false, // or true ("dangerously") if needed
|
|
10
|
+
resources: false, // or true ("usable") if needed
|
|
11
|
+
freezeGlobals: false,
|
|
12
|
+
pretendToBeVisual: true,
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
ARG NODE_VERSION=22.3.0
|
|
2
|
-
ARG NGINX_VERSION=1.27-alpine
|
|
3
|
-
|
|
4
|
-
FROM node:${NODE_VERSION} AS builder
|
|
5
|
-
WORKDIR /usr/src/app
|
|
6
|
-
COPY . .
|
|
7
|
-
RUN {{ciCommand}} && \
|
|
8
|
-
{{packageManager}} run build:prod
|
|
9
|
-
|
|
10
|
-
FROM nginx:${NGINX_VERSION}
|
|
11
|
-
|
|
12
|
-
RUN rm /etc/nginx/conf.d/default.conf
|
|
13
|
-
COPY --from=builder /usr/src/app/nginx.conf /etc/nginx/conf.d/default.conf
|
|
14
|
-
|
|
15
|
-
RUN sed -i '/http {/a \
|
|
16
|
-
# Gzip compression\n\
|
|
17
|
-
gzip on;\n\
|
|
18
|
-
gzip_vary on;\n\
|
|
19
|
-
gzip_proxied any;\n\
|
|
20
|
-
gzip_comp_level 6;\n\
|
|
21
|
-
gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss application/rss+xml font/truetype font/opentype application/vnd.ms-fontobject image/svg+xml;\n\
|
|
22
|
-
gzip_min_length 1000;\n\
|
|
23
|
-
' /etc/nginx/nginx.conf && \
|
|
24
|
-
touch /var/run/nginx.pid
|
|
25
|
-
|
|
26
|
-
COPY --from=builder /usr/src/app/dist/src /usr/share/nginx/html
|
|
27
|
-
|
|
28
|
-
EXPOSE 9090
|
|
29
|
-
|
|
30
|
-
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
|
31
|
-
CMD curl -f http://localhost:9090/health || exit 1
|
|
32
|
-
|
|
33
|
-
CMD ["nginx", "-g", "daemon off;"]
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
server {
|
|
2
|
-
listen 9090;
|
|
3
|
-
server_name _;
|
|
4
|
-
root /usr/share/nginx/html;
|
|
5
|
-
index index.html;
|
|
6
|
-
|
|
7
|
-
# Security headers
|
|
8
|
-
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
9
|
-
add_header X-Content-Type-Options "nosniff" always;
|
|
10
|
-
add_header X-XSS-Protection "1; mode=block" always;
|
|
11
|
-
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
12
|
-
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
|
|
13
|
-
|
|
14
|
-
# Content Security Policy - ADJUST BASED ON YOUR APP'S NEEDS
|
|
15
|
-
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self';" always;
|
|
16
|
-
|
|
17
|
-
location / {
|
|
18
|
-
try_files $uri $uri/ /index.html;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
# Static assets with long cache
|
|
22
|
-
location ~* \.(js|css|png|jpg|jpeg|gif|avif|ico|svg|woff|woff2|ttf|eot)$ {
|
|
23
|
-
expires 1y;
|
|
24
|
-
add_header Cache-Control "public, immutable";
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
# HTML files - no cache
|
|
28
|
-
location ~* \.html$ {
|
|
29
|
-
expires -1;
|
|
30
|
-
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
|
31
|
-
add_header Pragma "no-cache";
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
# Health check endpoint
|
|
35
|
-
location /health {
|
|
36
|
-
access_log off;
|
|
37
|
-
return 200 "healthy\n";
|
|
38
|
-
add_header Content-Type text/plain;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
# Hide nginx version
|
|
42
|
-
server_tokens off;
|
|
43
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
- Adding additnal file formats to the build
|
|
2
|
-
Default file formats are `"html" "json" "ttf" "avif" "png" "jpg"`.
|
|
3
|
-
Only the files in src/ that match one of the format are copied.
|
|
4
|
-
In `tools/builder/variables/file-types.js`, add desired format to existing list. Adding a file extension with no match in src/ will have no effect.
|
|
5
|
-
|
|
6
|
-
- Build Sequence
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
[**Back to index**](../../README.md)
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { spawn } from 'child_process';
|
|
4
|
-
import { print, runCommand } from './utils/index.js';
|
|
5
|
-
|
|
6
|
-
function run(command, args = [], label = '') {
|
|
7
|
-
return new Promise((resolve, reject) => {
|
|
8
|
-
print.info(`\n▶ ${label || command}...`);
|
|
9
|
-
|
|
10
|
-
const child = spawn(command, args, { stdio: 'inherit', shell: true });
|
|
11
|
-
|
|
12
|
-
child.on('close', (code) => {
|
|
13
|
-
if (code === 0) resolve();
|
|
14
|
-
else reject(new Error(`${command} exited with code ${code}`));
|
|
15
|
-
});
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
async function startDev() {
|
|
20
|
-
try {
|
|
21
|
-
console.clear();
|
|
22
|
-
print.boldHead('🚀 Starting Dev Environment...');
|
|
23
|
-
|
|
24
|
-
await run('{{packageManager}}', ['run', 'build'], 'Running initial build');
|
|
25
|
-
|
|
26
|
-
const serve = spawn('{{packageManager}}', ['run', 'serve:only'], { stdio: 'inherit', shell: true });
|
|
27
|
-
const watcher = spawn('node', ['tools/watcher.js'], { stdio: 'inherit', shell: true });
|
|
28
|
-
|
|
29
|
-
print.boldSuccess('\nDev server and watcher running!\n');
|
|
30
|
-
|
|
31
|
-
serve.on('close', (code) => {
|
|
32
|
-
print.error(`live-server exited with code ${code}`);
|
|
33
|
-
process.exit(code);
|
|
34
|
-
});
|
|
35
|
-
watcher.on('close', (code) => {
|
|
36
|
-
print.error(`watcher exited with code ${code}`);
|
|
37
|
-
});
|
|
38
|
-
} catch (err) {
|
|
39
|
-
print.boldError(`Dev startup failed: ${err.message}`);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
startDev();
|