From e550065f832867eea068d0674b76bdbdb368ecfd Mon Sep 17 00:00:00 2001 From: Jeremy Mees Date: Fri, 22 May 2026 14:38:12 +0200 Subject: [PATCH 01/13] feat: better empty state for pinned content widget --- .../initiative/Widgets/PinnedContent.vue | 22 +++++++++++++++---- i18n/locales/en.json | 5 ++++- i18n/locales/nl.json | 5 ++++- .../__snapshots__/pinnedContent.spec.ts.snap | 8 +++++++ .../initiative/Widgets/pinnedContent.spec.ts | 4 +++- 5 files changed, 37 insertions(+), 7 deletions(-) diff --git a/app/components/initiative/Widgets/PinnedContent.vue b/app/components/initiative/Widgets/PinnedContent.vue index 739835d4..56a4a35c 100644 --- a/app/components/initiative/Widgets/PinnedContent.vue +++ b/app/components/initiative/Widgets/PinnedContent.vue @@ -60,11 +60,25 @@ defineProps<{ value: DndItem[] }>() -

- {{ $t("pages.encounter.pinnedContent.empty") }} -

+ + {{ $t("pages.encounter.pinnedContent.empty.title") }} + +

+ {{ $t("pages.encounter.pinnedContent.empty.text") }} + + + + {{ $t('components.navbar.dnd-content') }} + + +

+ diff --git a/i18n/locales/en.json b/i18n/locales/en.json index 97a1de2e..7fa412e2 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -387,7 +387,10 @@ "options": "Encounter options", "maxCharacters": "Max characters amount", "pinnedContent": { - "empty": "You currently have no pinned items" + "empty": { + "title": "No pinned content yet", + "text": "You currently have no pinned items. Start by adding some pinned content from" + } }, "override": { "ac": "Override AC", diff --git a/i18n/locales/nl.json b/i18n/locales/nl.json index 3f968852..270fd30b 100644 --- a/i18n/locales/nl.json +++ b/i18n/locales/nl.json @@ -387,7 +387,10 @@ "options": "Encounter opties", "maxCharacters": "Maximum aantal personages", "pinnedContent": { - "empty": "Je hebt momenteel geen items gepinned" + "empty": { + "title": "Nog geen vastgepinde inhoud", + "text": "Je hebt momenteel geen items gepinned. Begin met het toevoegen van wat gepinde inhoud via" + } }, "override": { "ac": "AC overschrijven", diff --git a/test/nuxt/components/initiative/Widgets/__snapshots__/pinnedContent.spec.ts.snap b/test/nuxt/components/initiative/Widgets/__snapshots__/pinnedContent.spec.ts.snap index f6b0939f..8f245386 100644 --- a/test/nuxt/components/initiative/Widgets/__snapshots__/pinnedContent.spec.ts.snap +++ b/test/nuxt/components/initiative/Widgets/__snapshots__/pinnedContent.spec.ts.snap @@ -18,3 +18,11 @@ exports[`Initiative pinned content widget > Should match snapshot 1`] = ` " `; + +exports[`Initiative pinned content widget > Should show empty state when no items 1`] = ` +"
+
pages.encounter.pinnedContent.empty.title +

pages.encounter.pinnedContent.empty.text components.navbar.dnd-content

+
+
" +`; diff --git a/test/nuxt/components/initiative/Widgets/pinnedContent.spec.ts b/test/nuxt/components/initiative/Widgets/pinnedContent.spec.ts index 92dbba64..8709f5f2 100644 --- a/test/nuxt/components/initiative/Widgets/pinnedContent.spec.ts +++ b/test/nuxt/components/initiative/Widgets/pinnedContent.spec.ts @@ -22,7 +22,9 @@ describe('Initiative pinned content widget', async () => { it('Should show empty state when no items', async () => { const component = await mountSuspended(PinnedContent, { props: { value: [] } }) - expect(component.text()).toContain('pages.encounter.pinnedContent.empty') + expect(component.html()).toMatchSnapshot() + expect(component.text()).toContain('pages.encounter.pinnedContent.empty.title') + expect(component.text()).toContain('pages.encounter.pinnedContent.empty.text') }) it('Should show accordion when items are present', async () => { From c3f46e23b16e6ae344d45309420be6caa49cfe68 Mon Sep 17 00:00:00 2001 From: Jeremy Mees Date: Fri, 22 May 2026 14:41:16 +0200 Subject: [PATCH 02/13] feat: created drag and drop header component --- app/components/atoms/DragAndDropHeader.vue | 26 +++++++++++ .../atoms/DragAndDropHeader.spec.ts | 45 +++++++++++++++++++ .../DragAndDropHeader.spec.ts.snap | 8 ++++ 3 files changed, 79 insertions(+) create mode 100644 app/components/atoms/DragAndDropHeader.vue create mode 100644 test/nuxt/components/atoms/DragAndDropHeader.spec.ts create mode 100644 test/nuxt/components/atoms/__snapshots__/DragAndDropHeader.spec.ts.snap diff --git a/app/components/atoms/DragAndDropHeader.vue b/app/components/atoms/DragAndDropHeader.vue new file mode 100644 index 00000000..15c17eba --- /dev/null +++ b/app/components/atoms/DragAndDropHeader.vue @@ -0,0 +1,26 @@ + + + diff --git a/test/nuxt/components/atoms/DragAndDropHeader.spec.ts b/test/nuxt/components/atoms/DragAndDropHeader.spec.ts new file mode 100644 index 00000000..1abe9407 --- /dev/null +++ b/test/nuxt/components/atoms/DragAndDropHeader.spec.ts @@ -0,0 +1,45 @@ +import { mountSuspended } from '@nuxt/test-utils/runtime' +import { describe, expect, it } from 'vitest' +import DragAndDropHeader from '~/components/atoms/DragAndDropHeader.vue' + +describe('DragAndDropHeader', () => { + const props = { + title: 'My Widget', + } + + it('Should match snapshot', async () => { + const component = await mountSuspended(DragAndDropHeader, { props }) + + expect(component.html()).toMatchSnapshot() + }) + + it('Should render the drag handle icon', async () => { + const component = await mountSuspended(DragAndDropHeader, { props }) + + expect(component.find('.drag-handle').exists()).toBeTruthy() + }) + + it('Should render the title text', async () => { + const component = await mountSuspended(DragAndDropHeader, { props }) + + expect(component.find('[data-test-title]').text()).toBe('My Widget') + }) + + it('Should not render the slot container when no slot is provided', async () => { + const component = await mountSuspended(DragAndDropHeader, { props }) + + expect(component.find('[data-test-actions]').exists()).toBeFalsy() + }) + + it('Should render slot content when provided', async () => { + const component = await mountSuspended(DragAndDropHeader, { + props, + slots: { + default: () => '', + }, + }) + + expect(component.find('[data-test-actions]').exists()).toBeTruthy() + expect(component.find('[data-test-actions]').text()).toContain('Action') + }) +}) diff --git a/test/nuxt/components/atoms/__snapshots__/DragAndDropHeader.spec.ts.snap b/test/nuxt/components/atoms/__snapshots__/DragAndDropHeader.spec.ts.snap new file mode 100644 index 00000000..7aafa81a --- /dev/null +++ b/test/nuxt/components/atoms/__snapshots__/DragAndDropHeader.spec.ts.snap @@ -0,0 +1,8 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`DragAndDropHeader > Should match snapshot 1`] = ` +"
+
My Widget
+ +
" +`; From a49733a730167d96fefdf708ac0e0d62f3022c6f Mon Sep 17 00:00:00 2001 From: Jeremy Mees Date: Fri, 22 May 2026 14:41:38 +0200 Subject: [PATCH 03/13] feat: drag and drop widgets --- app/components/initiative/Widgets/index.vue | 122 ++++++++++++------ nuxt.config.ts | 35 ++--- .../Widgets/__snapshots__/index.spec.ts.snap | 54 +++++--- .../__snapshots__/table.spec.ts.snap | 54 +++++--- 4 files changed, 172 insertions(+), 93 deletions(-) diff --git a/app/components/initiative/Widgets/index.vue b/app/components/initiative/Widgets/index.vue index f5fb43d6..7e51f757 100644 --- a/app/components/initiative/Widgets/index.vue +++ b/app/components/initiative/Widgets/index.vue @@ -1,4 +1,5 @@ - - - - - - - - - - - - Should match snapshot 1`] = ` -
  • - - -
  • From 3ae56bc20867bf86d1334970184d7c8ac074fc3b Mon Sep 17 00:00:00 2001 From: Jeremy Mees Date: Tue, 16 Jun 2026 09:21:31 +0200 Subject: [PATCH 09/13] feat: fantasy name generator encounter widget --- app/components/form/InitiativeSettings.vue | 6 +-- .../Widgets/FantasyNameGenerator.vue | 8 ++++ app/components/initiative/Widgets/index.vue | 11 ++--- constants/validation.ts | 7 +++ shared/types/supabase.ts | 2 +- .../fantasyNameGenerator.spec.ts.snap | 41 ++++++++++++++++ .../Widgets/__snapshots__/index.spec.ts.snap | 47 +++++++++++++++++++ .../Widgets/fantasyNameGenerator.spec.ts | 31 ++++++++++++ .../__snapshots__/table.spec.ts.snap | 47 +++++++++++++++++++ test/nuxt/unit.setup.ts | 10 +++- test/nuxt/utils/dnd/names.spec.ts | 4 +- 11 files changed, 200 insertions(+), 14 deletions(-) create mode 100644 app/components/initiative/Widgets/FantasyNameGenerator.vue create mode 100644 test/nuxt/components/initiative/Widgets/__snapshots__/fantasyNameGenerator.spec.ts.snap create mode 100644 test/nuxt/components/initiative/Widgets/fantasyNameGenerator.spec.ts diff --git a/app/components/form/InitiativeSettings.vue b/app/components/form/InitiativeSettings.vue index c8a9b916..83170a89 100644 --- a/app/components/form/InitiativeSettings.vue +++ b/app/components/form/InitiativeSettings.vue @@ -5,6 +5,7 @@ import { initiativeDefaultRows, initiativePets, initiativeWidgets, + widgetLabels, } from '~~/constants/validation' import { toTypedSchema } from '@vee-validate/zod' import { useForm } from 'vee-validate' @@ -115,10 +116,7 @@ const onSubmit = form.handleSubmit(async (values) => { + + + + diff --git a/app/components/initiative/Widgets/index.vue b/app/components/initiative/Widgets/index.vue index 7e51f757..e37e2273 100644 --- a/app/components/initiative/Widgets/index.vue +++ b/app/components/initiative/Widgets/index.vue @@ -1,7 +1,7 @@ @@ -25,42 +21,6 @@ const maxCharacters = computed(() => hasMaxCharacters(sheet.value)) {{ $t('pages.encounter.options') }} - - - - - - - - - - - - Date: Tue, 16 Jun 2026 10:05:23 +0200 Subject: [PATCH 13/13] fix: happy-dom and dompurify not working anymore in not browser context --- package-lock.json | 542 +++++++++++++++++++++++++- package.json | 1 + test/nuxt/utils/sanitize-html.spec.ts | 68 ++++ test/nuxt/utils/ui-helpers.spec.ts | 62 --- 4 files changed, 610 insertions(+), 63 deletions(-) create mode 100644 test/nuxt/utils/sanitize-html.spec.ts diff --git a/package-lock.json b/package-lock.json index 50245d32..11151d49 100644 --- a/package-lock.json +++ b/package-lock.json @@ -80,6 +80,7 @@ "@vue/test-utils": "^2.4.10", "happy-dom": "^20.9.0", "husky": "^9.1.7", + "jsdom": "^29.1.1", "lint-staged": "^16.4.0", "typescript": "^5.9.3", "vitest": "^4.1.6", @@ -135,6 +136,57 @@ "@types/json-schema": "^7.0.15" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -724,6 +776,19 @@ "node": ">=6.9.0" } }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, "node_modules/@c15t/schema": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@c15t/schema/-/schema-2.1.0.tgz", @@ -800,6 +865,146 @@ "integrity": "sha512-kIxYSfA5T8HXjav55UaaH/o/cKivF6jCCGIb8eqtcsfI46wsvlSiT8jMDyrl779qLec3c2c2oHBZo4oAhvbjrQ==", "license": "MIT" }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.7.tgz", + "integrity": "sha512-CmjJFQTFQx/U/xNJhSjCQ0ilpesPmNQ8+eOUeM/+kDOVW33qsIjeOXc27vrQDdWVkf83ZSWwtg7kXSUvKDJ8cQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.2.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.5.tgz", + "integrity": "sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/@dicebear/adventurer": { "version": "9.4.2", "resolved": "https://registry.npmjs.org/@dicebear/adventurer/-/adventurer-9.4.2.tgz", @@ -1885,6 +2090,24 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, "node_modules/@fastify/accept-negotiator": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-2.0.1.tgz", @@ -13357,6 +13580,16 @@ "node": ">=6.0.0" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/bindings": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", @@ -14337,6 +14570,68 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/data-urls/node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/db0": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/db0/-/db0-0.3.4.tgz", @@ -14388,6 +14683,13 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/dedupe": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/dedupe/-/dedupe-4.0.3.tgz", @@ -17100,6 +17402,19 @@ "integrity": "sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==", "license": "MIT" }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/html-entities": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", @@ -17614,6 +17929,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-reference": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", @@ -17873,6 +18195,115 @@ "node": ">=20.0.0" } }, + "node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/jsdom/node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/jsdom/node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/jsdom/node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/jsdom/node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -22549,6 +22980,13 @@ "integrity": "sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==", "license": "MIT" }, + "node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true, + "license": "MIT" + }, "node_modules/parseley": { "version": "0.12.1", "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz", @@ -23467,7 +23905,6 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "license": "MIT", - "peer": true, "engines": { "node": ">=6" } @@ -23751,6 +24188,16 @@ "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==", "license": "MIT" }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-in-the-middle": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", @@ -24074,6 +24521,19 @@ "node": ">=11.0.0" } }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scslre": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/scslre/-/scslre-0.3.0.tgz", @@ -25322,6 +25782,13 @@ "node": ">=16" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/tagged-tag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", @@ -25596,6 +26063,26 @@ "@popperjs/core": "^2.9.0" } }, + "node_modules/tldts": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.3.tgz", + "integrity": "sha512-A3BDQBeeukYPzB4QdQ1DtdlUmp4x2OCH8n5UVhEWbyANxNep8GavottKzd1xYKFJKjUgMyPT7EzOfnBO55s8Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.3" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.3.tgz", + "integrity": "sha512-27ep5H9PzdBrNd5OFM/j3WCU8F3kPwM9D0BOaOf7uYfxMJfyr0K5Tjj69Gri+sZlh2WXd5buIm47NuPF29CDiw==", + "dev": true, + "license": "MIT" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -25656,6 +26143,19 @@ "node": ">=6" } }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -25867,6 +26367,16 @@ "node": ">=18.12.0" } }, + "node_modules/undici": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "7.24.6", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", @@ -27923,6 +28433,29 @@ "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", "license": "MIT" }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/w3c-xmlserializer/node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -28226,6 +28759,13 @@ "node": ">=16.0.0" } }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/xss": { "version": "1.0.15", "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.15.tgz", diff --git a/package.json b/package.json index af0defec..f0dc1b11 100644 --- a/package.json +++ b/package.json @@ -96,6 +96,7 @@ "@vue/test-utils": "^2.4.10", "happy-dom": "^20.9.0", "husky": "^9.1.7", + "jsdom": "^29.1.1", "lint-staged": "^16.4.0", "typescript": "^5.9.3", "vitest": "^4.1.6", diff --git a/test/nuxt/utils/sanitize-html.spec.ts b/test/nuxt/utils/sanitize-html.spec.ts new file mode 100644 index 00000000..9be9f918 --- /dev/null +++ b/test/nuxt/utils/sanitize-html.spec.ts @@ -0,0 +1,68 @@ +// @vitest-environment jsdom +// NOTE: This file uses the jsdom environment instead of the default nuxt/happy-dom +// one. DOMPurify produces incorrect output under happy-dom (it leaves some +// disallowed tags/siblings in place) — see happy-dom#1810 and DOMPurify#876. +// jsdom is spec-compliant and matches real browser behavior. +import { describe, expect, it } from 'vitest' +import { sanitizeHTML } from '~/utils/ui-helpers' + +describe('sanitizeHTML', () => { + it('should sanitize HTML by removing disallowed tags', () => { + const dirtyHtml = ` + + Image + + + + + ` + const result = sanitizeHTML(dirtyHtml) + + expect(result).not.toContain(' - Image - - - - - ` - const result = sanitizeHTML(dirtyHtml) - - expect(result).not.toContain('