@nutin/cli 2.1.1 → 3.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/CHANGELOG.md +268 -0
- package/README.md +13 -5
- package/bin/add-feature.mjs +1 -1
- package/bin/cli.mjs +1 -1
- package/bin/create-app.mjs +1 -1
- package/bin/update-app.mjs +1 -1
- package/lib/index.mjs +6 -2
- package/lib/src/add-feature/base-template-patcher.mjs +139 -0
- package/lib/src/add-feature/feature-adder.mjs +20 -9
- package/lib/src/add-feature/feature-context-builder.mjs +1 -0
- package/lib/src/add-feature/nutin-config-updater.mjs +34 -8
- package/lib/src/add-feature/package-json-updater.mjs +7 -9
- package/lib/src/common/feature-registry.mjs +4 -0
- package/lib/src/common/file-generator.mjs +25 -3
- package/lib/src/common/git.mjs +31 -0
- package/lib/src/common/meta-bootstrap-prompt.mjs +28 -2
- package/lib/src/common/package-json-helper.mjs +14 -4
- package/lib/src/common/project-meta.mjs +2 -2
- package/lib/src/common/template-compiler.mjs +2 -0
- package/lib/src/common/transpile-ts.mjs +94 -0
- package/lib/src/create-app/context-builder.mjs +1 -0
- package/lib/src/create-app/git-manager.mjs +12 -2
- package/lib/src/create-app/json-manager.mjs +21 -18
- package/lib/src/create-app/new-app-prompt.mjs +4 -2
- package/lib/src/create-app/success-message.mjs +1 -1
- package/lib/src/update-app/conflict-report-writer.mjs +33 -21
- package/lib/src/update-app/json-drift.mjs +54 -0
- package/lib/src/update-app/project-updater.mjs +123 -14
- package/lib/src/update-app/template-differ.mjs +5 -5
- package/lib/src/update-app/three-way-merge.mjs +42 -0
- package/lib/src/update-app/update-context-builder.mjs +1 -0
- package/lib/src/update-app/update-summary-printer.mjs +10 -2
- package/package.json +9 -8
- package/templates/base/AGENTS.md.hbs +42 -41
- package/templates/base/GETTING_STARTED.md.hbs +26 -20
- package/templates/base/README.md.hbs +7 -4
- package/templates/base/src/app/globals.d.ts.hbs +6 -3
- package/templates/base/src/app/guards.ts.hbs +1 -3
- package/templates/base/src/app/main.ts.hbs +2 -9
- package/templates/base/src/app/routes.ts.hbs +7 -2
- package/templates/base/src/app/views/home/home.view.html.hbs +1 -1
- package/templates/base/src/app/views/home/home.view.ts.hbs +2 -2
- package/templates/base/src/app/views/not-found/not-found.view.ts.hbs +2 -2
- package/templates/base/src/core/base-classes/base-component/base-component.test.js.hbs +461 -1
- package/templates/base/src/core/base-classes/base-component/base-component.ts.hbs +39 -48
- package/templates/base/src/core/base-classes/base-component/helpers/catalog.helper.test.js.hbs +36 -1
- package/templates/base/src/core/base-classes/base-component/helpers/catalog.helper.ts.hbs +47 -77
- package/templates/base/src/core/base-classes/base-component/helpers/children.helper.test.js.hbs +286 -24
- package/templates/base/src/core/base-classes/base-component/helpers/children.helper.ts.hbs +144 -26
- package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.test.js.hbs +66 -4
- package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.ts.hbs +135 -75
- package/templates/base/src/core/base-classes/base-component/helpers/event.helper.test.js.hbs +85 -4
- package/templates/base/src/core/base-classes/base-component/helpers/event.helper.ts.hbs +93 -54
- package/templates/base/src/core/base-classes/base-component/helpers/html.helper.test.js.hbs +404 -0
- package/templates/base/src/core/base-classes/base-component/helpers/html.helper.ts.hbs +242 -0
- package/templates/base/src/core/base-classes/base-component/helpers/i18n.helper.test.js.hbs +70 -1
- package/templates/base/src/core/base-classes/base-component/helpers/i18n.helper.ts.hbs +54 -15
- package/templates/base/src/core/base-classes/base-component/helpers/pipe.helper.test.js.hbs +12 -1
- package/templates/base/src/core/base-classes/base-component/helpers/pipe.helper.ts.hbs +39 -36
- package/templates/base/src/core/base-classes/base-component/helpers/preservation.helper.test.js.hbs +179 -0
- package/templates/base/src/core/base-classes/base-component/helpers/preservation.helper.ts.hbs +93 -0
- package/templates/base/src/core/base-classes/base-component/helpers/security.helper.test.js.hbs +135 -17
- package/templates/base/src/core/base-classes/base-component/helpers/security.helper.ts.hbs +199 -81
- package/templates/base/src/core/base-classes/base-component/helpers/token.helper.test.js.hbs +40 -23
- package/templates/base/src/core/base-classes/base-component/helpers/token.helper.ts.hbs +67 -65
- package/templates/base/src/core/base-classes/component/component.test.js.hbs +17 -17
- package/templates/base/src/core/base-classes/component/component.ts.hbs +8 -41
- package/templates/base/src/core/base-classes/component/helpers/config.helper.test.js.hbs +1 -1
- package/templates/base/src/core/base-classes/component/helpers/config.helper.ts.hbs +37 -39
- package/templates/base/src/core/base-classes/component/helpers/data-binding.helper.test.js.hbs +14 -1
- package/templates/base/src/core/base-classes/component/helpers/data-binding.helper.ts.hbs +33 -31
- package/templates/base/src/core/base-classes/service/service.test.js.hbs +32 -0
- package/templates/base/src/core/base-classes/service/service.ts.hbs +21 -13
- package/templates/base/src/core/base-classes/view/view.test.js.hbs +4 -0
- package/templates/base/src/core/base-classes/view/view.ts.hbs +11 -8
- package/templates/base/src/core/index.ts.hbs +4 -2
- package/templates/base/src/core/internals.d.ts.hbs +199 -20
- package/templates/base/src/core/services/event-bus/event-bus.test.js.hbs +18 -0
- package/templates/base/src/core/services/event-bus/event-bus.ts.hbs +26 -25
- package/templates/base/src/core/services/event-bus/lifecycle.facade.ts.hbs +3 -3
- package/templates/base/src/core/services/event-bus/navigation.facade.ts.hbs +3 -3
- package/templates/base/src/core/services/globals/globals.ts.hbs +7 -32
- package/templates/base/src/core/services/http-client/helpers/http-builder.helper.test.js.hbs +133 -50
- package/templates/base/src/core/services/http-client/helpers/http-builder.helper.ts.hbs +147 -39
- package/templates/base/src/core/services/http-client/helpers/http-manager.helper.test.js.hbs +75 -19
- package/templates/base/src/core/services/http-client/helpers/http-manager.helper.ts.hbs +73 -47
- package/templates/base/src/core/services/http-client/http-client.test.js.hbs +339 -232
- package/templates/base/src/core/services/http-client/http-client.ts.hbs +37 -25
- package/templates/base/src/core/services/i18n/i18n.test.js.hbs +57 -16
- package/templates/base/src/core/services/i18n/i18n.ts.hbs +42 -42
- package/templates/base/src/core/services/i18n/languages.ts.hbs +0 -2
- package/templates/base/src/core/services/pipe-registry/pipe-registry.ts.hbs +4 -6
- package/templates/base/src/core/services/pipe-registry/pipes.test.js.hbs +10 -0
- package/templates/base/src/core/services/pipe-registry/pipes.ts.hbs +4 -2
- package/templates/base/src/core/services/router/helpers/navigation-manager.helper.test.js.hbs +183 -4
- package/templates/base/src/core/services/router/helpers/navigation-manager.helper.ts.hbs +207 -84
- package/templates/base/src/core/services/router/helpers/route-guard-manager.helper.test.js.hbs +26 -27
- package/templates/base/src/core/services/router/helpers/route-guard-manager.helper.ts.hbs +38 -60
- package/templates/base/src/core/services/router/helpers/view-render-manager.helper.test.js.hbs +4 -4
- package/templates/base/src/core/services/router/helpers/view-render-manager.helper.ts.hbs +30 -30
- package/templates/base/src/core/services/router/router.test.js.hbs +306 -0
- package/templates/base/src/core/services/router/router.ts.hbs +113 -53
- package/templates/base/tools/builder/builder.config.js.hbs +6 -0
- package/templates/base/tools/builder/builder.js.hbs +18 -6
- package/templates/base/tools/builder/core/app/compile-ts.js.hbs +81 -6
- package/templates/base/tools/builder/core/app/copy-static.js.hbs +12 -1
- package/templates/base/tools/builder/core/app/handler-refs.js.hbs +114 -0
- package/templates/base/tools/builder/core/app/handler-refs.test.js.hbs +70 -0
- package/templates/base/tools/builder/core/app/paths.js.hbs +17 -0
- package/templates/base/tools/builder/core/app/route-keys.js.hbs +47 -0
- package/templates/base/tools/builder/core/app/route-keys.test.js.hbs +97 -0
- package/templates/base/tools/builder/core/app/ts-errors.js.hbs +58 -0
- package/templates/base/tools/builder/core/app/ts-errors.test.js.hbs +84 -0
- package/templates/base/tools/builder/core/app/validate-routes.js.hbs +21 -55
- package/templates/base/tools/builder/core/finalize-build.js.hbs +6 -0
- package/templates/base/tools/builder/core/html-index/validate-html.js.hbs +10 -2
- package/templates/base/tools/builder/core/html-templates/merge-templates.js.hbs +111 -23
- package/templates/base/tools/builder/core/html-templates/minify-templates.js.hbs +8 -0
- package/templates/base/tools/builder/core/html-templates/process-html-templates.js.hbs +5 -3
- package/templates/base/tools/builder/core/html-templates/template-literal.js.hbs +193 -0
- package/templates/base/tools/builder/core/html-templates/template-literal.test.js.hbs +69 -0
- package/templates/base/tools/builder/core/i18n/build-i18n.js.hbs +14 -6
- package/templates/base/tools/builder/core/prod-bundle/bundle-options.js.hbs +82 -0
- package/templates/base/tools/builder/core/prod-bundle/bundle-options.test.js.hbs +65 -0
- package/templates/base/tools/builder/core/prod-bundle/compress-files.js.hbs +6 -6
- package/templates/base/tools/builder/core/prod-bundle/esbuild.js.hbs +15 -7
- package/templates/base/tools/builder/core/prod-bundle/hash-files.js.hbs +4 -4
- package/templates/base/tools/builder/core/seo/generate-robots-txt.js.hbs +31 -13
- package/templates/base/tools/builder/core/seo/generate-robots-txt.test.js.hbs +48 -0
- package/templates/base/tools/builder/core/seo/generate-seo-files.js.hbs +3 -3
- package/templates/base/tools/builder/core/seo/generate-seo-html.js.hbs +43 -13
- package/templates/base/tools/builder/core/seo/generate-sitemap-xml.js.hbs +22 -7
- package/templates/base/tools/builder/core/seo/generate-sitemap-xml.test.js.hbs +30 -0
- package/templates/base/tools/builder/core/seo/html-substitutions.js.hbs +28 -21
- package/templates/base/tools/builder/core/seo/html-substitutions.test.js.hbs +45 -0
- package/templates/base/tools/builder/core/seo/route-output.js.hbs +24 -6
- package/templates/base/tools/builder/core/seo/route-output.test.js.hbs +31 -0
- package/templates/base/tools/builder/core/seo/seo-routes.js.hbs +29 -0
- package/templates/base/tools/builder/core/seo/seo-routes.test.js.hbs +21 -0
- package/templates/base/tools/builder/core/seo/ssr/ssr-bundle.js.hbs +6 -7
- package/templates/base/tools/builder/core/seo/ssr/ssr-polyfills.js.hbs +34 -5
- package/templates/base/tools/builder/core/seo/ssr/ssr-polyfills.test.js.hbs +48 -0
- package/templates/base/tools/builder/core/seo/ssr/ssr-render.js.hbs +24 -11
- package/templates/base/tools/builder/core/styles/sass.js.hbs +3 -2
- package/templates/base/tools/builder/core/styles/tailwind.js.hbs +19 -59
- package/templates/base/tools/dev/dev-serve.js.hbs +30 -5
- package/templates/base/tools/dev/serve.js.hbs +152 -22
- package/templates/base/tools/dev/watcher.js.hbs +34 -3
- package/templates/base/tools/generator/generator.js.hbs +1 -1
- package/templates/base/tools/generator/handle-file.js.hbs +4 -2
- package/templates/base/tools/generator/handle-file.test.js.hbs +3 -3
- package/templates/base/tools/generator/templates/component/component.template.js.hbs +3 -3
- package/templates/base/tools/generator/templates/component/component.template.test.js.hbs +3 -3
- package/templates/base/tools/generator/templates/service/service.template.js.hbs +1 -1
- package/templates/base/tools/generator/templates/service/service.template.test.js.hbs +1 -1
- package/templates/base/tools/generator/templates/view/view.template.js.hbs +2 -2
- package/templates/base/tools/generator/templates/view/view.template.test.js.hbs +2 -2
- package/templates/base/tools/testin-nutin/core/coverage/write-uncovered-report.js.hbs +1 -1
- package/templates/base/tools/testin-nutin/core/globals/assertion-lib.js.hbs +2 -2
- package/templates/base/tools/testin-nutin/core/{tests/assertions.test.js.hbs → globals/assertion-lib.test.js.hbs} +6 -0
- package/templates/base/tools/testin-nutin/core/globals/register-test-globals.js.hbs +6 -7
- package/templates/base/tools/testin-nutin/core/{tests/globals-each-all.test.js.hbs → globals/register-test-globals.test.js.hbs} +12 -0
- package/templates/base/tools/testin-nutin/core/printer.js.hbs +10 -4
- package/templates/base/tools/testin-nutin/core/queue/test-queue.js.hbs +8 -8
- package/templates/base/tools/testin-nutin/mocks/mock-event-bus.js.hbs +27 -25
- package/templates/base/tools/testin-nutin/mocks/mock-event-bus.test.js.hbs +30 -22
- package/templates/base/tools/testin-nutin/mocks/mock-http-client.js.hbs +7 -10
- package/templates/base/tools/testin-nutin/mocks/mock-http-client.test.js.hbs +12 -0
- package/templates/base/tools/testin-nutin/mocks/mock-i18n.js.hbs +47 -22
- package/templates/base/tools/testin-nutin/mocks/mock-i18n.test.js.hbs +47 -26
- package/templates/base/tools/testin-nutin/mocks/mock-router.js.hbs +19 -11
- package/templates/base/tools/testin-nutin/mocks/mock-router.test.js.hbs +24 -7
- package/templates/base/tools/testin-nutin/runner.js.hbs +12 -3
- package/templates/base/tools/testin-nutin/watch-tests.js.hbs +45 -20
- package/templates/base/tools/utils/build-lock.js.hbs +30 -2
- package/templates/base/tools/utils/ensure-deps.js.hbs +72 -0
- package/templates/base/tools/utils/ensure-deps.test.js.hbs +17 -0
- package/templates/base/tools/utils/format.js.hbs +1 -1
- package/templates/base/tools/utils/index.js.hbs +2 -1
- package/templates/base/tools/utils/print.js.hbs +2 -2
- package/templates/base/tools/utils/resolve-port.js.hbs +30 -0
- package/templates/base/tools/utils/resolve-port.test.js.hbs +47 -0
- package/templates/base/tools/utils/run-command.js.hbs +30 -1
- package/templates/base/tools/utils/run-command.test.js.hbs +12 -1
- package/templates/base/tools/utils/run-script.js.hbs +2 -2
- package/templates/features/docker/tools/docker/Dockerfile.template.hbs +3 -0
- package/templates/features/docker/tools/docker/nginx.conf.template +6 -4
- package/templates/features/docker/tools/docker/scripts/docker-ports.js.hbs +1 -1
- package/templates/features/docker/tools/docker/scripts/docker-ports.test.js.hbs +2 -2
- package/templates/features/docker/tools/docker/scripts/locale-redirects.js.hbs +28 -0
- package/templates/features/docker/tools/docker/scripts/locale-redirects.test.js.hbs +34 -0
- package/templates/features/docker/tools/docker/scripts/markdown-sources-copy.js.hbs +25 -0
- package/templates/features/docker/tools/docker/scripts/markdown-sources-copy.test.js.hbs +34 -0
- package/templates/features/docker/tools/docker/scripts/render-docker-template.js.hbs +3 -1
- package/templates/features/docker/tools/docker/scripts/render-docker-template.test.js.hbs +12 -0
- package/templates/features/docker/tools/docker/scripts/validate-docker.js.hbs +21 -4
- package/templates/features/markdown/markdown-content/getting-started.md.hbs +28 -0
- package/templates/features/markdown/markdown-content/writing-pages.md.hbs +19 -0
- package/templates/features/markdown/src/app/markdown/components/markdown-content/markdown-content.component.scss.hbs +75 -0
- package/templates/features/markdown/src/app/markdown/components/markdown-content/markdown-content.component.test.js.hbs +40 -0
- package/templates/features/markdown/src/app/markdown/components/markdown-content/markdown-content.component.ts.hbs +65 -0
- package/templates/features/markdown/src/app/markdown/components/markdown-landing/markdown-landing.component.test.js.hbs +67 -0
- package/templates/features/markdown/src/app/markdown/components/markdown-landing/markdown-landing.component.ts.hbs +87 -0
- package/templates/features/markdown/src/app/markdown/components/markdown-nav/markdown-nav.component.scss.hbs +42 -0
- package/templates/features/markdown/src/app/markdown/components/markdown-nav/markdown-nav.component.test.js.hbs +59 -0
- package/templates/features/markdown/src/app/markdown/components/markdown-nav/markdown-nav.component.ts.hbs +72 -0
- package/templates/features/markdown/src/app/markdown/locales/en.json.hbs +6 -0
- package/templates/features/markdown/src/app/markdown/markdown-guards.test.js.hbs +63 -0
- package/templates/features/markdown/src/app/markdown/markdown-guards.ts.hbs +31 -0
- package/templates/features/markdown/src/app/markdown/markdown-i18n.test.js.hbs +78 -0
- package/templates/features/markdown/src/app/markdown/markdown-i18n.ts.hbs +31 -0
- package/templates/features/markdown/src/app/markdown/markdown-manifest.test.js.hbs +98 -0
- package/templates/features/markdown/src/app/markdown/markdown-manifest.ts.hbs +157 -0
- package/templates/features/markdown/src/app/markdown/markdown-manifests.service.test.js.hbs +87 -0
- package/templates/features/markdown/src/app/markdown/markdown-manifests.service.ts.hbs +50 -0
- package/templates/features/markdown/src/app/markdown/markdown-routes.test.js.hbs +74 -0
- package/templates/features/markdown/src/app/markdown/markdown-routes.ts.hbs +26 -0
- package/templates/features/markdown/src/app/markdown/markdown-sources.test.js.hbs +22 -0
- package/templates/features/markdown/src/app/markdown/markdown-sources.ts.hbs +42 -0
- package/templates/features/markdown/src/app/markdown/view/markdown.view.html.hbs +4 -0
- package/templates/features/markdown/src/app/markdown/view/markdown.view.scss.hbs +19 -0
- package/templates/features/markdown/src/app/markdown/view/markdown.view.test.js.hbs +125 -0
- package/templates/features/markdown/src/app/markdown/view/markdown.view.ts.hbs +146 -0
- package/templates/features/markdown/tools/builder/core/markdown/generate-markdown-pages.js.hbs +71 -0
- package/templates/features/markdown/tools/builder/core/markdown/markdown-compiler.js.hbs +582 -0
- package/templates/features/markdown/tools/builder/core/markdown/markdown-compiler.test.js.hbs +390 -0
- package/templates/features/markdown/tools/builder/core/markdown/markdown-seo-routes.js.hbs +155 -0
- package/templates/features/markdown/tools/builder/core/markdown/markdown-seo-routes.test.js.hbs +112 -0
- /package/templates/base/tools/testin-nutin/core/{tests → globals}/clock.test.js.hbs +0 -0
package/templates/base/src/core/base-classes/base-component/helpers/security.helper.test.js.hbs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as SecurityHelper from '#root/dist/src/core/base-classes/base-component/helpers/security.helper.js';
|
|
2
2
|
|
|
3
3
|
describe('SecurityHelper', () => {
|
|
4
4
|
it('sanitizeTemplate returns an empty string for null and undefined', () => {
|
|
@@ -102,28 +102,146 @@ describe('SecurityHelper', () => {
|
|
|
102
102
|
expect(result.includes('javascript:')).toBe(false);
|
|
103
103
|
});
|
|
104
104
|
|
|
105
|
-
it('
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
it('sanitizeTemplate "normal" level strips javascript: hrefs, including whitespace-obfuscated ones', () => {
|
|
106
|
+
const result = SecurityHelper.sanitizeTemplate(
|
|
107
|
+
'<a href="javascript:alert(1)">a</a><a href=" java\tscript:alert(1)">b</a>',
|
|
108
|
+
'normal'
|
|
109
|
+
);
|
|
110
|
+
expect(result).toBe('<a>a</a><a>b</a>');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('sanitizeTemplate "normal" level strips javascript: from form action and formaction', () => {
|
|
114
|
+
const result = SecurityHelper.sanitizeTemplate(
|
|
115
|
+
'<form action="javascript:alert(1)"><button formaction="javascript:alert(1)">x</button></form>',
|
|
116
|
+
'normal'
|
|
117
|
+
);
|
|
118
|
+
expect(result.includes('javascript:')).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('sanitizeTemplate "normal" level strips javascript: from svg xlink:href', () => {
|
|
122
|
+
const result = SecurityHelper.sanitizeTemplate(
|
|
123
|
+
'<svg><a xlink:href="javascript:alert(1)"><text>x</text></a></svg>',
|
|
124
|
+
'normal'
|
|
125
|
+
);
|
|
126
|
+
expect(result.includes('javascript:')).toBe(false);
|
|
109
127
|
});
|
|
110
128
|
|
|
111
|
-
it('
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
129
|
+
it('sanitizeTemplate "normal" level keeps safe and data: URLs', () => {
|
|
130
|
+
const result = SecurityHelper.sanitizeTemplate(
|
|
131
|
+
'<a href="https://example.com">a</a><img src="data:image/png;base64,AAAA">',
|
|
132
|
+
'normal'
|
|
133
|
+
);
|
|
134
|
+
expect(result).toContain('href="https://example.com"');
|
|
135
|
+
expect(result).toContain('src="data:image/png;base64,AAAA"');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('sanitizeTemplate "normal" and "strict" levels strip srcdoc, even when its markup is escaped', () => {
|
|
139
|
+
const template = '<iframe srcdoc="<script>alert(1)</script>"></iframe>';
|
|
140
|
+
expect(SecurityHelper.sanitizeTemplate(template, 'normal')).toBe('<iframe></iframe>');
|
|
141
|
+
expect(SecurityHelper.sanitizeTemplate(template, 'strict')).toBe('');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('sanitizeTemplate "trusted" level keeps javascript: URLs and srcdoc', () => {
|
|
145
|
+
const template = '<a href="javascript:void(0)">a</a><iframe srcdoc="x"></iframe>';
|
|
146
|
+
expect(SecurityHelper.sanitizeTemplate(template, 'trusted')).toBe(template);
|
|
115
147
|
});
|
|
116
148
|
|
|
117
|
-
it('
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
149
|
+
it('sanitizeTemplate "normal" level strips svg <set>/<animate> that rewrite href', () => {
|
|
150
|
+
const result = SecurityHelper.sanitizeTemplate(
|
|
151
|
+
'<svg><a>' +
|
|
152
|
+
'<set attributeName="href" to="javascript:alert(1)"/>' +
|
|
153
|
+
'<animate attributeName=" HREF " values="javascript:alert(1)"/>' +
|
|
154
|
+
'<animate attributeName="xlink:href" from="javascript:alert(1)" to="x"/>' +
|
|
155
|
+
'<text>x</text></a></svg>',
|
|
156
|
+
'normal'
|
|
157
|
+
);
|
|
158
|
+
expect(result.includes('javascript:')).toBe(false);
|
|
159
|
+
expect(result).toContain('<text>x</text>');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('sanitizeTemplate "normal" level keeps svg animations of non-URL attributes', () => {
|
|
163
|
+
const result = SecurityHelper.sanitizeTemplate(
|
|
164
|
+
'<svg><circle r="5"><animate attributeName="opacity" values="0;1" dur="1s"></animate></circle></svg>',
|
|
165
|
+
'normal'
|
|
166
|
+
);
|
|
167
|
+
expect(result).toContain('attributeName="opacity"');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('sanitizeTemplate "normal" level keeps an svg <set> without attributeName', () => {
|
|
171
|
+
const result = SecurityHelper.sanitizeTemplate('<svg><set to="1"></set></svg>', 'normal');
|
|
172
|
+
expect(result).toContain('<set to="1">');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('stripBindings removes Nutin binding attributes and keeps the rest', () => {
|
|
176
|
+
const result = SecurityHelper.stripBindings(
|
|
177
|
+
'<button class="b" data-event="click:_remove" data-component="x" data-catalog="y" data-bind="z" ' +
|
|
178
|
+
'data-i18n="k" data-pipe="upper" data-pipe-source="s" data-id="7">x</button>'
|
|
179
|
+
);
|
|
180
|
+
expect(result).toBe('<button class="b" data-id="7">x</button>');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('stripBindingsFrom strips an element in place, keeping class, style, other data-* and text', () => {
|
|
184
|
+
const root = document.createElement('section');
|
|
185
|
+
root.innerHTML = '<a href="/x" class="link" style="color: red" data-event="click:_go" data-i18n="nav.x" data-card-description>X</a>'
|
|
186
|
+
+ '<div data-component="child" data-catalog="items" data-bind="name" data-pipe="upper" data-pipe-source="v"></div>'
|
|
187
|
+
+ '<pre><code><div data-component="hello"></div></code></pre>'
|
|
188
|
+
+ '<template><i data-event="click:_x">i</i></template>';
|
|
189
|
+
|
|
190
|
+
SecurityHelper.stripBindingsFrom(root);
|
|
191
|
+
|
|
192
|
+
const link = root.querySelector('a');
|
|
193
|
+
expect(link.getAttribute('class')).toBe('link');
|
|
194
|
+
expect(link.getAttribute('style')).toBe('color: red');
|
|
195
|
+
expect(link.hasAttribute('data-card-description')).toBe(true);
|
|
196
|
+
expect(link.hasAttribute('data-event') || link.hasAttribute('data-i18n')).toBe(false);
|
|
197
|
+
expect(root.querySelector('div').attributes.length).toBe(0);
|
|
198
|
+
expect(root.querySelector('code').textContent).toBe('<div data-component="hello"></div>');
|
|
199
|
+
expect(root.querySelector('template').innerHTML).toBe('<i>i</i>');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('stripBindings also strips nested <template> content', () => {
|
|
203
|
+
const result = SecurityHelper.stripBindings('<template><i data-event="click:_x">i</i></template>');
|
|
204
|
+
expect(result).toBe('<template><i>i</i></template>');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('sanitizeToFragment returns sanitized nodes without re-serializing them', () => {
|
|
208
|
+
const fragment = SecurityHelper.sanitizeToFragment('<p onclick="x()">a</p><script>x()</script>', 'normal');
|
|
209
|
+
expect(fragment.childNodes.length).toBe(1);
|
|
210
|
+
expect(fragment.firstChild.outerHTML).toBe('<p>a</p>');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('sanitizeToFragment keeps everything at "trusted" and handles null', () => {
|
|
214
|
+
const fragment = SecurityHelper.sanitizeToFragment('<p onclick="x()">a</p>', 'trusted');
|
|
215
|
+
expect(fragment.firstChild.getAttribute('onclick')).toBe('x()');
|
|
216
|
+
expect(SecurityHelper.sanitizeToFragment(null).childNodes.length).toBe(0);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('sanitizeTemplate "normal" level strips style, link, base and meta elements', () => {
|
|
220
|
+
const result = SecurityHelper.sanitizeTemplate(
|
|
221
|
+
'<style>p{}</style><link rel="stylesheet" href="/x.css"><base href="/x/"><meta http-equiv="refresh" content="0"><p>ok</p>',
|
|
222
|
+
'normal'
|
|
223
|
+
);
|
|
224
|
+
expect(result).toBe('<p>ok</p>');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('sanitizeTemplate "normal" level strips script and style inside svg', () => {
|
|
228
|
+
const result = SecurityHelper.sanitizeTemplate('<svg><script>x()</script><style>a{}</style><circle r="1"></circle></svg>', 'normal');
|
|
229
|
+
expect(result).toBe('<svg><circle r="1"></circle></svg>');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('sanitizeTemplate "normal" level strips data: and javascript: documents from frames but keeps https ones', () => {
|
|
233
|
+
const result = SecurityHelper.sanitizeTemplate(
|
|
234
|
+
'<iframe src="data:text/html,x"></iframe><object data="data:text/html,x"></object>' +
|
|
235
|
+
'<embed src="javascript:x()"><iframe src="https://example.com"></iframe>',
|
|
236
|
+
'normal'
|
|
237
|
+
);
|
|
238
|
+
expect(result).toBe('<iframe></iframe><object></object><embed><iframe src="https://example.com"></iframe>');
|
|
122
239
|
});
|
|
123
240
|
|
|
124
|
-
it('
|
|
125
|
-
const
|
|
126
|
-
expect(SecurityHelper.
|
|
241
|
+
it('sanitizeTemplate and stripBindings do not throw on a <template> inside svg', () => {
|
|
242
|
+
const markup = '<svg><template><text data-event="click:_x" onclick="x()">t</text></template></svg>';
|
|
243
|
+
expect(SecurityHelper.sanitizeTemplate(markup, 'normal')).toBe('<svg><template><text data-event="click:_x">t</text></template></svg>');
|
|
244
|
+
expect(SecurityHelper.stripBindings(markup)).toBe('<svg><template><text onclick="x()">t</text></template></svg>');
|
|
127
245
|
});
|
|
128
246
|
|
|
129
247
|
it('escapeHtml escapes all special characters', () => {
|
|
@@ -1,101 +1,219 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
private static readonly URL_ATTRS = new Set([
|
|
6
|
-
'href',
|
|
7
|
-
'src',
|
|
8
|
-
'action',
|
|
9
|
-
'formaction',
|
|
10
|
-
'poster',
|
|
11
|
-
'background',
|
|
12
|
-
]);
|
|
13
|
-
private static readonly DANGEROUS_URL_SCHEMES = /^(javascript|data):/i;
|
|
14
|
-
|
|
15
|
-
public static sanitizeTemplate(value: unknown, trustLevel: TrustLevel = 'normal'): string {
|
|
16
|
-
if (value === null || value === undefined) return '';
|
|
17
|
-
let template = String(value);
|
|
18
|
-
|
|
19
|
-
return this.sanitizeInputTemplate(template, trustLevel);
|
|
20
|
-
}
|
|
1
|
+
// Compared against localName, so SVG/MathML elements of the same name are caught too.
|
|
2
|
+
// style/link restyle the whole page, base moves every relative URL, meta can refresh/navigate.
|
|
3
|
+
const STRIPPED_TAGS = new Set(['script', 'style', 'link', 'base', 'meta']);
|
|
4
|
+
const STRICT_STRIPPED_TAGS = new Set(['iframe', 'object', 'embed', 'frame']);
|
|
21
5
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
6
|
+
// A data: document in a frame runs its own scripts (opaque origin): only allowed at 'trusted'.
|
|
7
|
+
const FRAME_TAGS = new Set(['iframe', 'object', 'embed', 'frame']);
|
|
8
|
+
const FRAME_URL_ATTRS = new Set(['src', 'data']);
|
|
9
|
+
const URL_ATTRS = new Set([
|
|
10
|
+
'href',
|
|
11
|
+
'src',
|
|
12
|
+
'action',
|
|
13
|
+
'formaction',
|
|
14
|
+
'poster',
|
|
15
|
+
'background',
|
|
16
|
+
'xlink:href',
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
// Attributes Nutin acts on after render: in injected markup they bind to the component's methods/children.
|
|
20
|
+
const BINDING_ATTRS = [
|
|
21
|
+
'data-event',
|
|
22
|
+
'data-component',
|
|
23
|
+
'data-catalog',
|
|
24
|
+
'data-bind',
|
|
25
|
+
'data-i18n',
|
|
26
|
+
'data-pipe',
|
|
27
|
+
'data-pipe-source',
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
// SVG <animate>/<set> can rewrite an attribute after sanitization: drop those targeting a URL attribute.
|
|
31
|
+
const ATTR_ANIMATION_TAGS = new Set(['animate', 'set']);
|
|
32
|
+
|
|
33
|
+
// javascript: is stripped from 'normal' up; data: only under 'strict' (data: images are legitimate).
|
|
34
|
+
const SCRIPT_URL_SCHEME = /^javascript:/i;
|
|
35
|
+
const STRICT_URL_SCHEMES = /^(javascript|data):/i;
|
|
36
|
+
|
|
37
|
+
// String form of sanitizeToFragment(). Rendering inserts the fragment instead: assigning
|
|
38
|
+
// this string to innerHTML parses it a second time.
|
|
39
|
+
export function sanitizeTemplate(value: unknown, trustLevel: Nutin.TrustLevel = 'normal'): string {
|
|
40
|
+
if (value === null || value === undefined) return '';
|
|
41
|
+
if (trustLevel === 'trusted' && !rawsOf(value)) return String(value);
|
|
42
|
+
|
|
43
|
+
const holder = document.createElement('template');
|
|
44
|
+
holder.content.append(sanitizeToFragment(value, trustLevel));
|
|
45
|
+
return holder.innerHTML;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function escapeHtml(value: unknown): string {
|
|
49
|
+
if (value === null || value === undefined) return '';
|
|
50
|
+
|
|
51
|
+
return String(value)
|
|
52
|
+
.replace(/&/g, '&')
|
|
53
|
+
.replace(/</g, '<')
|
|
54
|
+
.replace(/>/g, '>')
|
|
55
|
+
.replace(/"/g, '"')
|
|
56
|
+
.replace(/'/g, ''');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Parses once and returns the sanitized nodes: inserting them directly means the browser never re-parses the
|
|
60
|
+
// cleaned markup, which is where mutation XSS turns inert text back into elements.
|
|
61
|
+
// A SafeHtml from html`` may carry raw() sources behind placeholders: each is parsed once,
|
|
62
|
+
// in its real parent context, stripped of bindings and sanitized as nodes, then put in place.
|
|
63
|
+
export function sanitizeToFragment(value: unknown, trustLevel: Nutin.TrustLevel = 'normal'): DocumentFragment {
|
|
64
|
+
const holder = document.createElement('template');
|
|
65
|
+
const raws = rawsOf(value);
|
|
66
|
+
holder.innerHTML = value === null || value === undefined ? '' : raws ? (value as { value: string }).value : String(value);
|
|
67
|
+
if (trustLevel !== 'trusted') sanitizeNode(holder.content, trustLevel);
|
|
68
|
+
if (raws) resolveRaws(holder.content, raws, trustLevel);
|
|
69
|
+
return holder.content;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// raw() in attribute position (`<a ${raw('aria-current="page"')}>`): rebuilt from the parsed
|
|
73
|
+
// attributes, minus event handlers, srcdoc, bindings and script URLs.
|
|
74
|
+
export function sanitizeAttributes(source: string): string {
|
|
75
|
+
const holder = document.createElement('template');
|
|
76
|
+
holder.innerHTML = `<span ${source}></span>`;
|
|
77
|
+
// An unterminated quote makes the parser drop the whole tag: nothing to keep.
|
|
78
|
+
const el = holder.content.firstElementChild;
|
|
79
|
+
if (!el) return '';
|
|
80
|
+
return Array.from(el.attributes)
|
|
81
|
+
.filter((attr) => isAllowedRawAttribute(attr.name.toLowerCase(), attr.value))
|
|
82
|
+
.map((attr) => `${attr.name}="${escapeHtml(attr.value)}"`)
|
|
83
|
+
.join(' ');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// A javascript: URL in a URL attribute (name lowercased). html`` checks values from data with it at
|
|
87
|
+
// every trust level, since the render sanitizer doesn't run at 'trusted'.
|
|
88
|
+
export function isScriptUrl(name: string, value: string): boolean {
|
|
89
|
+
return URL_ATTRS.has(name) && isDangerousUrl(value, 'normal');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function stripBindings(markup: string): string {
|
|
93
|
+
const holder = document.createElement('template');
|
|
94
|
+
holder.innerHTML = markup;
|
|
95
|
+
stripBindingsFrom(holder.content);
|
|
96
|
+
return holder.innerHTML;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// In place, inside <template> content too. Also used by SEO prerendering: nothing binds a
|
|
100
|
+
// prerendered page (the app re-renders it), so its binding attributes are dead weight.
|
|
101
|
+
export function stripBindingsFrom(root: ParentNode): void {
|
|
102
|
+
const selector = BINDING_ATTRS.map((attr) => `[${attr}]`).join(',');
|
|
103
|
+
root.querySelectorAll(selector).forEach((el) => {
|
|
104
|
+
BINDING_ATTRS.forEach((attr) => el.removeAttribute(attr));
|
|
105
|
+
});
|
|
106
|
+
root.querySelectorAll('template').forEach((el) => {
|
|
107
|
+
const content = templateContent(el);
|
|
108
|
+
if (content) stripBindingsFrom(content);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function rawsOf(value: unknown): ReadonlyMap<string, string> | null {
|
|
113
|
+
const raws = (value as { raws?: unknown } | null)?.raws;
|
|
114
|
+
return raws instanceof Map ? raws : null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function resolveRaws(root: ParentNode, raws: ReadonlyMap<string, string>, trustLevel: Nutin.TrustLevel): void {
|
|
118
|
+
// Snapshot first: content put in place is never searched for placeholders again.
|
|
119
|
+
for (const placeholder of collectPlaceholders(root)) {
|
|
120
|
+
const source = raws.get(placeholder.getAttribute('data-nutin-raw') ?? '');
|
|
121
|
+
const parsed = parseInContext(source ?? '', placeholder.parentNode);
|
|
122
|
+
if (trustLevel !== 'trusted') sanitizeNode(parsed, trustLevel);
|
|
123
|
+
stripBindingsFrom(parsed);
|
|
124
|
+
placeholder.replaceWith(...Array.from(parsed.childNodes));
|
|
32
125
|
}
|
|
126
|
+
}
|
|
33
127
|
|
|
34
|
-
|
|
35
|
-
|
|
128
|
+
function collectPlaceholders(root: ParentNode): Element[] {
|
|
129
|
+
const found: Element[] = [];
|
|
130
|
+
root.querySelectorAll('template').forEach((el) => {
|
|
131
|
+
if (el.hasAttribute('data-nutin-raw')) found.push(el);
|
|
132
|
+
const content = templateContent(el);
|
|
133
|
+
if (content) found.push(...collectPlaceholders(content));
|
|
134
|
+
});
|
|
135
|
+
return found;
|
|
136
|
+
}
|
|
36
137
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
138
|
+
// Parses inside the inert document behind <template> (nothing loads or runs there), using an
|
|
139
|
+
// element like the placeholder's parent as context so <svg>/<math>/<table> content parses as
|
|
140
|
+
// it will once inserted.
|
|
141
|
+
function parseInContext(source: string, parent: Node | null): ParentNode {
|
|
142
|
+
const holder = document.createElement('template');
|
|
143
|
+
const parentEl = parent && parent.nodeType === 1 ? (parent as Element) : null;
|
|
144
|
+
if (parentEl && !templateContent(parentEl)) {
|
|
145
|
+
const context = holder.content.ownerDocument.createElementNS(parentEl.namespaceURI, parentEl.localName);
|
|
146
|
+
context.innerHTML = source;
|
|
147
|
+
return context;
|
|
43
148
|
}
|
|
149
|
+
holder.innerHTML = source;
|
|
150
|
+
return holder.content;
|
|
151
|
+
}
|
|
44
152
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
153
|
+
function isAllowedRawAttribute(name: string, value: string): boolean {
|
|
154
|
+
if (!/^[a-z_:][-a-z0-9_:.]*$/.test(name)) return false;
|
|
155
|
+
if (name.startsWith('on') || name === 'srcdoc' || BINDING_ATTRS.includes(name)) return false;
|
|
156
|
+
return !isScriptUrl(name, value);
|
|
157
|
+
}
|
|
50
158
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
159
|
+
// Only an HTML <template> has a content fragment; <svg><template> is a plain element.
|
|
160
|
+
function templateContent(el: Element): DocumentFragment | null {
|
|
161
|
+
const content = (el as HTMLTemplateElement).content;
|
|
162
|
+
return content && typeof content.querySelectorAll === 'function' ? content : null;
|
|
163
|
+
}
|
|
54
164
|
|
|
55
|
-
|
|
56
|
-
|
|
165
|
+
function sanitizeNode(root: ParentNode, trustLevel: Nutin.TrustLevel): void {
|
|
166
|
+
// Snapshot children before mutating — removing an element from a live
|
|
167
|
+
// collection while iterating it would skip its next sibling.
|
|
168
|
+
const children = Array.from(root.children);
|
|
57
169
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
// collection while iterating it would skip its next sibling.
|
|
61
|
-
const children = Array.from(root.children);
|
|
170
|
+
for (const el of children) {
|
|
171
|
+
const tag = el.localName.toLowerCase();
|
|
62
172
|
|
|
63
|
-
|
|
64
|
-
|
|
173
|
+
if (STRIPPED_TAGS.has(tag)) {
|
|
174
|
+
el.remove();
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (trustLevel === 'strict' && STRICT_STRIPPED_TAGS.has(tag)) {
|
|
178
|
+
el.remove();
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (animatesUrlAttribute(el)) {
|
|
182
|
+
el.remove();
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
65
185
|
|
|
66
|
-
|
|
67
|
-
|
|
186
|
+
for (const attr of Array.from(el.attributes)) {
|
|
187
|
+
const name = attr.name.toLowerCase();
|
|
188
|
+
// srcdoc is parsed as a same-origin document: escaped markup in it is decoded and runs.
|
|
189
|
+
if (name.startsWith('on') || name === 'srcdoc') {
|
|
190
|
+
el.removeAttribute(attr.name);
|
|
68
191
|
continue;
|
|
69
192
|
}
|
|
70
|
-
if (
|
|
71
|
-
el.
|
|
193
|
+
if (URL_ATTRS.has(name) && isDangerousUrl(attr.value, trustLevel)) {
|
|
194
|
+
el.removeAttribute(attr.name);
|
|
72
195
|
continue;
|
|
73
196
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const name = attr.name.toLowerCase();
|
|
77
|
-
if (name.startsWith('on')) {
|
|
78
|
-
el.removeAttribute(attr.name);
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
if (trustLevel === 'strict' && this.URL_ATTRS.has(name) && this.isDangerousUrl(attr.value)) {
|
|
82
|
-
el.removeAttribute(attr.name);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (tag === 'TEMPLATE') {
|
|
87
|
-
this.sanitizeNode((el as HTMLTemplateElement).content, trustLevel);
|
|
88
|
-
} else {
|
|
89
|
-
this.sanitizeNode(el, trustLevel);
|
|
197
|
+
if (FRAME_TAGS.has(tag) && FRAME_URL_ATTRS.has(name) && isDangerousUrl(attr.value, 'strict')) {
|
|
198
|
+
el.removeAttribute(attr.name);
|
|
90
199
|
}
|
|
91
200
|
}
|
|
92
|
-
}
|
|
93
201
|
|
|
94
|
-
|
|
95
|
-
// Browsers strip ASCII tab/newline/CR from a URL (and trim leading C0
|
|
96
|
-
// control/space) before scheme-sniffing, so "java\tscript:" still runs
|
|
97
|
-
// as javascript: — normalize the same way before checking the scheme.
|
|
98
|
-
const normalized = value.replace(/[\t\n\r]/g, '').replace(/^[\x00-\x20]+/, '');
|
|
99
|
-
return this.DANGEROUS_URL_SCHEMES.test(normalized);
|
|
202
|
+
sanitizeNode(templateContent(el) ?? el, trustLevel);
|
|
100
203
|
}
|
|
101
204
|
}
|
|
205
|
+
|
|
206
|
+
function animatesUrlAttribute(el: Element): boolean {
|
|
207
|
+
if (!ATTR_ANIMATION_TAGS.has(el.localName.toLowerCase())) return false;
|
|
208
|
+
const target = (el.getAttribute('attributeName') ?? '').trim().toLowerCase();
|
|
209
|
+
return URL_ATTRS.has(target) || URL_ATTRS.has(target.replace(/^xlink:/, ''));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function isDangerousUrl(value: string, trustLevel: Nutin.TrustLevel): boolean {
|
|
213
|
+
// Browsers strip ASCII tab/newline/CR from a URL (and trim leading C0
|
|
214
|
+
// control/space) before scheme-sniffing, so "java\tscript:" still runs
|
|
215
|
+
// as javascript: — normalize the same way before checking the scheme.
|
|
216
|
+
const normalized = value.replace(/[\t\n\r]/g, '').replace(/^[\x00-\x20]+/, '');
|
|
217
|
+
const schemes = trustLevel === 'strict' ? STRICT_URL_SCHEMES : SCRIPT_URL_SCHEME;
|
|
218
|
+
return schemes.test(normalized);
|
|
219
|
+
}
|
package/templates/base/src/core/base-classes/base-component/helpers/token.helper.test.js.hbs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as TokenHelper from '#root/dist/src/core/base-classes/base-component/helpers/token.helper.js';
|
|
2
2
|
|
|
3
3
|
describe('TokenHelper', () => {
|
|
4
4
|
const makeEvent = (overrides = {}) => ({
|
|
@@ -11,26 +11,26 @@ describe('TokenHelper', () => {
|
|
|
11
11
|
...overrides,
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
-
it('resolve
|
|
14
|
+
it('resolve leaves the event default alone', () => {
|
|
15
15
|
let called = false;
|
|
16
16
|
const ev = makeEvent({ preventDefault: () => { called = true; } });
|
|
17
17
|
TokenHelper.resolve('@id', document.createElement('div'), ev);
|
|
18
|
-
expect(called).toBe(
|
|
18
|
+
expect(called).toBe(false);
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
-
it('@id resolves
|
|
21
|
+
it('@id resolves the raw element id', () => {
|
|
22
22
|
const el = document.createElement('div');
|
|
23
23
|
el.id = '<x>';
|
|
24
|
-
expect(TokenHelper.resolve('@id', el, makeEvent())).toBe('
|
|
24
|
+
expect(TokenHelper.resolve('@id', el, makeEvent())).toBe('<x>');
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
-
it('@class resolves the
|
|
27
|
+
it('@class resolves the className', () => {
|
|
28
28
|
const el = document.createElement('div');
|
|
29
29
|
el.className = 'a b';
|
|
30
30
|
expect(TokenHelper.resolve('@class', el, makeEvent())).toBe('a b');
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
-
it('@name resolves the
|
|
33
|
+
it('@name resolves the name attribute', () => {
|
|
34
34
|
const el = document.createElement('input');
|
|
35
35
|
el.name = 'field';
|
|
36
36
|
expect(TokenHelper.resolve('@name', el, makeEvent())).toBe('field');
|
|
@@ -41,15 +41,32 @@ describe('TokenHelper', () => {
|
|
|
41
41
|
expect(TokenHelper.resolve('@name', el, makeEvent())).toBe('');
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
-
it('@tag resolves the
|
|
44
|
+
it('@tag resolves the tag name', () => {
|
|
45
45
|
const el = document.createElement('div');
|
|
46
46
|
expect(TokenHelper.resolve('@tag', el, makeEvent())).toBe('DIV');
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
-
it('@value resolves the
|
|
49
|
+
it('@value resolves the raw input value', () => {
|
|
50
50
|
const el = document.createElement('input');
|
|
51
|
-
el.value = '<b>';
|
|
52
|
-
expect(TokenHelper.resolve('@value', el, makeEvent())).toBe('
|
|
51
|
+
el.value = '<b> & "c"';
|
|
52
|
+
expect(TokenHelper.resolve('@value', el, makeEvent())).toBe('<b> & "c"');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('@value resolves the raw value of a textarea', () => {
|
|
56
|
+
const el = document.createElement('textarea');
|
|
57
|
+
el.value = '<b>bold</b>';
|
|
58
|
+
expect(TokenHelper.resolve('@value', el, makeEvent())).toBe('<b>bold</b>');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('@value resolves innerText for contenteditable elements', () => {
|
|
62
|
+
const el = document.createElement('div');
|
|
63
|
+
el.setAttribute('contenteditable', 'true');
|
|
64
|
+
el.innerText = '<i>hi</i>';
|
|
65
|
+
expect(TokenHelper.resolve('@value', el, makeEvent())).toBe('<i>hi</i>');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('@value resolves an empty string for a plain, non-editable element', () => {
|
|
69
|
+
expect(TokenHelper.resolve('@value', document.createElement('div'), makeEvent())).toBe('');
|
|
53
70
|
});
|
|
54
71
|
|
|
55
72
|
it('@checked resolves the checked state', () => {
|
|
@@ -67,30 +84,30 @@ describe('TokenHelper', () => {
|
|
|
67
84
|
expect(TokenHelper.resolve('@selected', option, makeEvent())).toBe(true);
|
|
68
85
|
});
|
|
69
86
|
|
|
70
|
-
it('@textContent resolves
|
|
87
|
+
it('@textContent resolves raw textContent', () => {
|
|
71
88
|
const el = document.createElement('div');
|
|
72
89
|
el.textContent = '<i>';
|
|
73
|
-
expect(TokenHelper.resolve('@textContent', el, makeEvent())).toBe('
|
|
90
|
+
expect(TokenHelper.resolve('@textContent', el, makeEvent())).toBe('<i>');
|
|
74
91
|
});
|
|
75
92
|
|
|
76
93
|
it('@textContent falls back to an empty string when el has no textContent', () => {
|
|
77
94
|
expect(TokenHelper.resolve('@textContent', {}, makeEvent())).toBe('');
|
|
78
95
|
});
|
|
79
96
|
|
|
80
|
-
it('@innerText resolves
|
|
97
|
+
it('@innerText resolves raw innerText', () => {
|
|
81
98
|
const el = document.createElement('div');
|
|
82
99
|
el.innerText = '<i>';
|
|
83
|
-
expect(TokenHelper.resolve('@innerText', el, makeEvent())).toBe('
|
|
100
|
+
expect(TokenHelper.resolve('@innerText', el, makeEvent())).toBe('<i>');
|
|
84
101
|
});
|
|
85
102
|
|
|
86
103
|
it('@innerText falls back to an empty string when el has no innerText', () => {
|
|
87
104
|
expect(TokenHelper.resolve('@innerText', {}, makeEvent())).toBe('');
|
|
88
105
|
});
|
|
89
106
|
|
|
90
|
-
it('@html resolves
|
|
107
|
+
it('@html resolves raw innerHTML', () => {
|
|
91
108
|
const el = document.createElement('div');
|
|
92
109
|
el.innerHTML = '<i>x</i>';
|
|
93
|
-
expect(TokenHelper.resolve('@html', el, makeEvent())).toBe('
|
|
110
|
+
expect(TokenHelper.resolve('@html', el, makeEvent())).toBe('<i>x</i>');
|
|
94
111
|
});
|
|
95
112
|
|
|
96
113
|
it('@html falls back to an empty string when el has no innerHTML', () => {
|
|
@@ -120,9 +137,9 @@ describe('TokenHelper', () => {
|
|
|
120
137
|
expect(TokenHelper.resolve('@y', document.createElement('div'), ev)).toBe(0);
|
|
121
138
|
});
|
|
122
139
|
|
|
123
|
-
it('@key and @code resolve to
|
|
140
|
+
it('@key and @code resolve to raw keyboard event properties', () => {
|
|
124
141
|
const ev = makeEvent({ key: '<', code: 'KeyA' });
|
|
125
|
-
expect(TokenHelper.resolve('@key', document.createElement('div'), ev)).toBe('
|
|
142
|
+
expect(TokenHelper.resolve('@key', document.createElement('div'), ev)).toBe('<');
|
|
126
143
|
expect(TokenHelper.resolve('@code', document.createElement('div'), ev)).toBe('KeyA');
|
|
127
144
|
});
|
|
128
145
|
|
|
@@ -132,10 +149,10 @@ describe('TokenHelper', () => {
|
|
|
132
149
|
expect(TokenHelper.resolve('@code', document.createElement('div'), ev)).toBe('');
|
|
133
150
|
});
|
|
134
151
|
|
|
135
|
-
it('@attr: resolves
|
|
152
|
+
it('@attr: resolves the raw attribute value', () => {
|
|
136
153
|
const el = document.createElement('div');
|
|
137
154
|
el.setAttribute('title', '<t>');
|
|
138
|
-
expect(TokenHelper.resolve('@attr:title', el, makeEvent())).toBe('
|
|
155
|
+
expect(TokenHelper.resolve('@attr:title', el, makeEvent())).toBe('<t>');
|
|
139
156
|
});
|
|
140
157
|
|
|
141
158
|
it('@attr: falls back to an empty string when the attribute is missing', () => {
|
|
@@ -143,10 +160,10 @@ describe('TokenHelper', () => {
|
|
|
143
160
|
expect(TokenHelper.resolve('@attr:title', el, makeEvent())).toBe('');
|
|
144
161
|
});
|
|
145
162
|
|
|
146
|
-
it('@dataset: resolves
|
|
163
|
+
it('@dataset: resolves the raw dataset value', () => {
|
|
147
164
|
const el = document.createElement('div');
|
|
148
165
|
el.dataset.foo = '<f>';
|
|
149
|
-
expect(TokenHelper.resolve('@dataset:foo', el, makeEvent())).toBe('
|
|
166
|
+
expect(TokenHelper.resolve('@dataset:foo', el, makeEvent())).toBe('<f>');
|
|
150
167
|
});
|
|
151
168
|
|
|
152
169
|
it('@dataset: falls back to an empty string when the dataset key is missing', () => {
|