From 56436608067d56acf5a421e2301dc70d0483fc6f Mon Sep 17 00:00:00 2001 From: David Joseph Guzsik Date: Wed, 1 May 2024 00:20:14 +0200 Subject: [PATCH 01/10] Eliminate chai-dom and restore @testing-library/jest-dom (#244) --- assets/js/utils/__tests__/dom.spec.ts | 84 ++-- assets/js/utils/__tests__/draggable.spec.ts | 24 +- assets/js/utils/__tests__/image.spec.ts | 68 +-- assets/package-lock.json | 507 +++++++++++--------- assets/package.json | 3 +- assets/test/vitest-setup.ts | 5 +- 6 files changed, 368 insertions(+), 323 deletions(-) diff --git a/assets/js/utils/__tests__/dom.spec.ts b/assets/js/utils/__tests__/dom.spec.ts index 51207776..a1bb03eb 100644 --- a/assets/js/utils/__tests__/dom.spec.ts +++ b/assets/js/utils/__tests__/dom.spec.ts @@ -83,7 +83,7 @@ describe('DOM Utilities', () => { it(`should remove the ${hiddenClass} class from the provided element`, () => { const mockElement = createHiddenElement('div'); showEl(mockElement); - expect(mockElement).not.to.have.class(hiddenClass); + expect(mockElement).not.toHaveClass(hiddenClass); }); it(`should remove the ${hiddenClass} class from all provided elements`, () => { @@ -93,9 +93,9 @@ describe('DOM Utilities', () => { createHiddenElement('strong'), ]; showEl(mockElements); - expect(mockElements[0]).not.to.have.class(hiddenClass); - expect(mockElements[1]).not.to.have.class(hiddenClass); - expect(mockElements[2]).not.to.have.class(hiddenClass); + expect(mockElements[0]).not.toHaveClass(hiddenClass); + expect(mockElements[1]).not.toHaveClass(hiddenClass); + expect(mockElements[2]).not.toHaveClass(hiddenClass); }); it(`should remove the ${hiddenClass} class from elements provided in multiple arrays`, () => { @@ -108,10 +108,10 @@ describe('DOM Utilities', () => { createHiddenElement('em'), ]; showEl(mockElements1, mockElements2); - expect(mockElements1[0]).not.to.have.class(hiddenClass); - expect(mockElements1[1]).not.to.have.class(hiddenClass); - expect(mockElements2[0]).not.to.have.class(hiddenClass); - expect(mockElements2[1]).not.to.have.class(hiddenClass); + expect(mockElements1[0]).not.toHaveClass(hiddenClass); + expect(mockElements1[1]).not.toHaveClass(hiddenClass); + expect(mockElements2[0]).not.toHaveClass(hiddenClass); + expect(mockElements2[1]).not.toHaveClass(hiddenClass); }); }); @@ -119,7 +119,7 @@ describe('DOM Utilities', () => { it(`should add the ${hiddenClass} class to the provided element`, () => { const mockElement = document.createElement('div'); hideEl(mockElement); - expect(mockElement).to.have.class(hiddenClass); + expect(mockElement).toHaveClass(hiddenClass); }); it(`should add the ${hiddenClass} class to all provided elements`, () => { @@ -129,9 +129,9 @@ describe('DOM Utilities', () => { document.createElement('strong'), ]; hideEl(mockElements); - expect(mockElements[0]).to.have.class(hiddenClass); - expect(mockElements[1]).to.have.class(hiddenClass); - expect(mockElements[2]).to.have.class(hiddenClass); + expect(mockElements[0]).toHaveClass(hiddenClass); + expect(mockElements[1]).toHaveClass(hiddenClass); + expect(mockElements[2]).toHaveClass(hiddenClass); }); it(`should add the ${hiddenClass} class to elements provided in multiple arrays`, () => { @@ -144,10 +144,10 @@ describe('DOM Utilities', () => { document.createElement('em'), ]; hideEl(mockElements1, mockElements2); - expect(mockElements1[0]).to.have.class(hiddenClass); - expect(mockElements1[1]).to.have.class(hiddenClass); - expect(mockElements2[0]).to.have.class(hiddenClass); - expect(mockElements2[1]).to.have.class(hiddenClass); + expect(mockElements1[0]).toHaveClass(hiddenClass); + expect(mockElements1[1]).toHaveClass(hiddenClass); + expect(mockElements2[0]).toHaveClass(hiddenClass); + expect(mockElements2[1]).toHaveClass(hiddenClass); }); }); @@ -155,7 +155,7 @@ describe('DOM Utilities', () => { it('should set the disabled attribute to true', () => { const mockElement = document.createElement('button'); disableEl(mockElement); - expect(mockElement).to.have.property('disabled', true); + expect(mockElement).toBeDisabled(); }); it('should set the disabled attribute to true on all provided elements', () => { @@ -164,8 +164,8 @@ describe('DOM Utilities', () => { document.createElement('button'), ]; disableEl(mockElements); - expect(mockElements[0]).to.have.property('disabled', true); - expect(mockElements[1]).to.have.property('disabled', true); + expect(mockElements[0]).toBeDisabled(); + expect(mockElements[1]).toBeDisabled(); }); it('should set the disabled attribute to true on elements provided in multiple arrays', () => { @@ -178,10 +178,10 @@ describe('DOM Utilities', () => { document.createElement('button'), ]; disableEl(mockElements1, mockElements2); - expect(mockElements1[0]).to.have.property('disabled', true); - expect(mockElements1[1]).to.have.property('disabled', true); - expect(mockElements2[0]).to.have.property('disabled', true); - expect(mockElements2[1]).to.have.property('disabled', true); + expect(mockElements1[0]).toBeDisabled(); + expect(mockElements1[1]).toBeDisabled(); + expect(mockElements2[0]).toBeDisabled(); + expect(mockElements2[1]).toBeDisabled(); }); }); @@ -189,7 +189,7 @@ describe('DOM Utilities', () => { it('should set the disabled attribute to false', () => { const mockElement = document.createElement('button'); enableEl(mockElement); - expect(mockElement).to.have.property('disabled', false); + expect(mockElement).toBeEnabled(); }); it('should set the disabled attribute to false on all provided elements', () => { @@ -198,8 +198,8 @@ describe('DOM Utilities', () => { document.createElement('button'), ]; enableEl(mockElements); - expect(mockElements[0]).to.have.property('disabled', false); - expect(mockElements[1]).to.have.property('disabled', false); + expect(mockElements[0]).toBeEnabled(); + expect(mockElements[1]).toBeEnabled(); }); it('should set the disabled attribute to false on elements provided in multiple arrays', () => { @@ -212,10 +212,10 @@ describe('DOM Utilities', () => { document.createElement('button'), ]; enableEl(mockElements1, mockElements2); - expect(mockElements1[0]).to.have.property('disabled', false); - expect(mockElements1[1]).to.have.property('disabled', false); - expect(mockElements2[0]).to.have.property('disabled', false); - expect(mockElements2[1]).to.have.property('disabled', false); + expect(mockElements1[0]).toBeEnabled(); + expect(mockElements1[1]).toBeEnabled(); + expect(mockElements2[0]).toBeEnabled(); + expect(mockElements2[1]).toBeEnabled(); }); }); @@ -223,11 +223,11 @@ describe('DOM Utilities', () => { it(`should toggle the ${hiddenClass} class on the provided element`, () => { const mockVisibleElement = document.createElement('div'); toggleEl(mockVisibleElement); - expect(mockVisibleElement).to.have.class(hiddenClass); + expect(mockVisibleElement).toHaveClass(hiddenClass); const mockHiddenElement = createHiddenElement('div'); toggleEl(mockHiddenElement); - expect(mockHiddenElement).not.to.have.class(hiddenClass); + expect(mockHiddenElement).not.toHaveClass(hiddenClass); }); it(`should toggle the ${hiddenClass} class on all provided elements`, () => { @@ -238,10 +238,10 @@ describe('DOM Utilities', () => { createHiddenElement('em'), ]; toggleEl(mockElements); - expect(mockElements[0]).to.have.class(hiddenClass); - expect(mockElements[1]).not.to.have.class(hiddenClass); - expect(mockElements[2]).to.have.class(hiddenClass); - expect(mockElements[3]).not.to.have.class(hiddenClass); + expect(mockElements[0]).toHaveClass(hiddenClass); + expect(mockElements[1]).not.toHaveClass(hiddenClass); + expect(mockElements[2]).toHaveClass(hiddenClass); + expect(mockElements[3]).not.toHaveClass(hiddenClass); }); it(`should toggle the ${hiddenClass} class on elements provided in multiple arrays`, () => { @@ -254,10 +254,10 @@ describe('DOM Utilities', () => { document.createElement('em'), ]; toggleEl(mockElements1, mockElements2); - expect(mockElements1[0]).not.to.have.class(hiddenClass); - expect(mockElements1[1]).to.have.class(hiddenClass); - expect(mockElements2[0]).not.to.have.class(hiddenClass); - expect(mockElements2[1]).to.have.class(hiddenClass); + expect(mockElements1[0]).not.toHaveClass(hiddenClass); + expect(mockElements1[1]).toHaveClass(hiddenClass); + expect(mockElements2[0]).not.toHaveClass(hiddenClass); + expect(mockElements2[1]).toHaveClass(hiddenClass); }); }); @@ -361,8 +361,8 @@ describe('DOM Utilities', () => { const mockClassTwo = 'class-two'; const el = makeEl('p', { className: `${mockClassOne} ${mockClassTwo}` }); expect(el.nodeName).toEqual('P'); - expect(el).to.have.class(mockClassOne); - expect(el).to.have.class(mockClassTwo); + expect(el).toHaveClass(mockClassOne); + expect(el).toHaveClass(mockClassTwo); }); }); diff --git a/assets/js/utils/__tests__/draggable.spec.ts b/assets/js/utils/__tests__/draggable.spec.ts index 835a4d24..e9e3daad 100644 --- a/assets/js/utils/__tests__/draggable.spec.ts +++ b/assets/js/utils/__tests__/draggable.spec.ts @@ -63,7 +63,7 @@ describe('Draggable Utilities', () => { fireEvent(mockDraggable, mockEvent); - expect(mockDraggable).to.have.class(draggingClass); + expect(mockDraggable).toHaveClass(draggingClass); }); it('should add dummy data to the dragstart event if it\'s empty', () => { @@ -146,7 +146,7 @@ describe('Draggable Utilities', () => { fireEvent(mockDraggable, mockEvent); - expect(mockDraggable).to.have.class(dragOverClass); + expect(mockDraggable).toHaveClass(dragOverClass); }); }); @@ -159,7 +159,7 @@ describe('Draggable Utilities', () => { fireEvent(mockDraggable, mockEvent); - expect(mockDraggable).not.to.have.class(dragOverClass); + expect(mockDraggable).not.toHaveClass(dragOverClass); }); }); @@ -170,13 +170,13 @@ describe('Draggable Utilities', () => { const mockStartEvent = createDragEvent('dragstart'); fireEvent(mockDraggable, mockStartEvent); - expect(mockDraggable).to.have.class(draggingClass); + expect(mockDraggable).toHaveClass(draggingClass); const mockDropEvent = createDragEvent('drop'); fireEvent(mockDraggable, mockDropEvent); expect(mockDropEvent.defaultPrevented).toBe(true); - expect(mockDraggable).not.to.have.class(draggingClass); + expect(mockDraggable).not.toHaveClass(draggingClass); }); it('should cancel the event and insert source before target if dropped on left side', () => { @@ -188,7 +188,7 @@ describe('Draggable Utilities', () => { const mockStartEvent = createDragEvent('dragstart'); fireEvent(mockSecondDraggable, mockStartEvent); - expect(mockSecondDraggable).to.have.class(draggingClass); + expect(mockSecondDraggable).toHaveClass(draggingClass); const mockDropEvent = createDragEvent('drop'); Object.assign(mockDropEvent, { clientX: 124 }); @@ -200,7 +200,7 @@ describe('Draggable Utilities', () => { try { expect(mockDropEvent.defaultPrevented).toBe(true); - expect(mockSecondDraggable).not.to.have.class(draggingClass); + expect(mockSecondDraggable).not.toHaveClass(draggingClass); expect(mockSecondDraggable.nextElementSibling).toBe(mockDraggable); } finally { @@ -217,7 +217,7 @@ describe('Draggable Utilities', () => { const mockStartEvent = createDragEvent('dragstart'); fireEvent(mockSecondDraggable, mockStartEvent); - expect(mockSecondDraggable).to.have.class(draggingClass); + expect(mockSecondDraggable).toHaveClass(draggingClass); const mockDropEvent = createDragEvent('drop'); Object.assign(mockDropEvent, { clientX: 125 }); @@ -229,7 +229,7 @@ describe('Draggable Utilities', () => { try { expect(mockDropEvent.defaultPrevented).toBe(true); - expect(mockSecondDraggable).not.to.have.class(draggingClass); + expect(mockSecondDraggable).not.toHaveClass(draggingClass); expect(mockDraggable.nextElementSibling).toBe(mockSecondDraggable); } finally { @@ -259,7 +259,7 @@ describe('Draggable Utilities', () => { const mockStartEvent = createDragEvent('dragstart'); fireEvent(mockDraggable, mockStartEvent); - expect(mockDraggable).to.have.class(draggingClass); + expect(mockDraggable).toHaveClass(draggingClass); const mockOverElement = createDraggableElement(); mockOverElement.classList.add(dragOverClass); @@ -270,8 +270,8 @@ describe('Draggable Utilities', () => { const mockDropEvent = createDragEvent('dragend'); fireEvent(mockDraggable, mockDropEvent); - expect(mockDraggable).not.to.have.class(draggingClass); - expect(mockOverElement).not.to.have.class(dragOverClass); + expect(mockDraggable).not.toHaveClass(draggingClass); + expect(mockOverElement).not.toHaveClass(dragOverClass); }); }); diff --git a/assets/js/utils/__tests__/image.spec.ts b/assets/js/utils/__tests__/image.spec.ts index fb43b52f..b091380d 100644 --- a/assets/js/utils/__tests__/image.spec.ts +++ b/assets/js/utils/__tests__/image.spec.ts @@ -146,7 +146,7 @@ describe('Image utils', () => { const result = showThumb(mockElement); - expect(mockImage).to.have.class(hiddenClass); + expect(mockImage).toHaveClass(hiddenClass); expect(mockVideo.children).toHaveLength(2); const webmSourceElement = mockVideo.children[0]; @@ -160,10 +160,10 @@ describe('Image utils', () => { expect(mp4SourceElement.getAttribute('type')).toEqual('video/mp4'); expect(mp4SourceElement.getAttribute('src')).toEqual(webmSource.replace('webm', 'mp4')); - expect(mockVideo).not.to.have.class(hiddenClass); + expect(mockVideo).not.toHaveClass(hiddenClass); expect(playSpy).toHaveBeenCalledTimes(1); - expect(mockSpoilerOverlay).to.have.class(hiddenClass); + expect(mockSpoilerOverlay).toHaveClass(hiddenClass); expect(result).toBe(true); }); @@ -238,7 +238,7 @@ describe('Image utils', () => { expect(mockSizeImage.src).toBe(mockSizeUrls[mockSize]); expect(mockSizeImage.srcset).toBe(''); - expect(mockSpoilerOverlay).to.have.class(hiddenClass); + expect(mockSpoilerOverlay).toHaveClass(hiddenClass); expect(result).toBe(true); }); @@ -255,7 +255,7 @@ describe('Image utils', () => { expect(mockSizeImage.src).toBe(mockSizeUrls[mockSize]); expect(mockSizeImage.srcset).toBe(''); - expect(mockSpoilerOverlay).to.have.class(hiddenClass); + expect(mockSpoilerOverlay).toHaveClass(hiddenClass); expect(result).toBe(true); }); @@ -272,8 +272,8 @@ describe('Image utils', () => { expect(mockSizeImage.src).toBe(mockSizeUrls[mockSize].replace('webm', 'gif')); expect(mockSizeImage.srcset).toBe(''); - expect(mockSpoilerOverlay).not.to.have.class(hiddenClass); - expect(mockSpoilerOverlay).to.have.text('WebM'); + expect(mockSpoilerOverlay).not.toHaveClass(hiddenClass); + expect(mockSpoilerOverlay).toHaveTextContent('WebM'); expect(result).toBe(true); }); @@ -296,7 +296,7 @@ describe('Image utils', () => { expect(mockSizeImage.srcset).toContain(`${mockSizeUrls[size]} 1x`); expect(mockSizeImage.srcset).toContain(`${mockSizeUrls[x2size]} 2x`); - expect(mockSpoilerOverlay).to.have.class(hiddenClass); + expect(mockSpoilerOverlay).toHaveClass(hiddenClass); return result; }; @@ -323,7 +323,7 @@ describe('Image utils', () => { expect(mockSizeImage.src).toBe(mockSizeUrls[mockSize]); expect(mockSizeImage.srcset).toBe(''); - expect(mockSpoilerOverlay).to.have.class(hiddenClass); + expect(mockSpoilerOverlay).toHaveClass(hiddenClass); expect(result).toBe(true); }); }); @@ -364,9 +364,9 @@ describe('Image utils', () => { showBlock(mockElement); - expect(mockFilteredImageElement).to.have.class(hiddenClass); - expect(mockShowElement).not.to.have.class(hiddenClass); - expect(mockShowElement).to.have.class(spoilerPendingClass); + expect(mockFilteredImageElement).toHaveClass(hiddenClass); + expect(mockShowElement).not.toHaveClass(hiddenClass); + expect(mockShowElement).toHaveClass(spoilerPendingClass); }); it('should not throw if image-filtered element is missing', () => { @@ -416,7 +416,7 @@ describe('Image utils', () => { expect(querySelectorSpy).toHaveBeenNthCalledWith(2, 'video'); expect(querySelectorSpy).toHaveBeenNthCalledWith(3, 'img'); expect(querySelectorSpy).toHaveBeenNthCalledWith(4, `.${spoilerOverlayClass}`); - expect(mockVideo).not.to.have.class(hiddenClass); + expect(mockVideo).not.toHaveClass(hiddenClass); } finally { querySelectorSpy.mockRestore(); @@ -439,11 +439,11 @@ describe('Image utils', () => { hideThumb(mockElement, mockSpoilerUri, mockSpoilerReason); try { - expect(mockImage).not.to.have.class(hiddenClass); - expect(mockImage).to.have.attribute('src', mockSpoilerUri); - expect(mockOverlay).to.have.text(mockSpoilerReason); - expect(mockVideo).not.to.have.descendants('*'); - expect(mockVideo).to.have.class(hiddenClass); + expect(mockImage).not.toHaveClass(hiddenClass); + expect(mockImage).toHaveAttribute('src', mockSpoilerUri); + expect(mockOverlay).toHaveTextContent(mockSpoilerReason); + expect(mockVideo).toBeEmptyDOMElement(); + expect(mockVideo).toHaveClass(hiddenClass); expect(pauseSpy).toHaveBeenCalled(); } finally { @@ -488,10 +488,10 @@ describe('Image utils', () => { hideThumb(mockElement, mockSpoilerUri, mockSpoilerReason); - expect(mockImage).to.have.attribute('srcset', ''); - expect(mockImage).to.have.attribute('src', mockSpoilerUri); - expect(mockOverlay).to.contain.html(mockSpoilerReason); - expect(mockOverlay).not.to.have.class(hiddenClass); + expect(mockImage).toHaveAttribute('srcset', ''); + expect(mockImage).toHaveAttribute('src', mockSpoilerUri); + expect(mockOverlay).toContainHTML(mockSpoilerReason); + expect(mockOverlay).not.toHaveClass(hiddenClass); }); }); @@ -503,9 +503,9 @@ describe('Image utils', () => { spoilerThumb(mockElement, mockSpoilerUri, mockSpoilerReason); // Element should be hidden by the call - expect(mockSizeImage).to.have.attribute('src', mockSpoilerUri); - expect(mockSpoilerOverlay).not.to.have.class(hiddenClass); - expect(mockSpoilerOverlay).to.contain.html(mockSpoilerReason); + expect(mockSizeImage).toHaveAttribute('src', mockSpoilerUri); + expect(mockSpoilerOverlay).not.toHaveClass(hiddenClass); + expect(mockSpoilerOverlay).toContainHTML(mockSpoilerReason); // If addEventListener calls are not expected, bail if (!handlers) { @@ -526,8 +526,8 @@ describe('Image utils', () => { if (firstHandler === 'click') { expect(clickEvent.defaultPrevented).toBe(true); } - expect(mockSizeImage).not.to.have.attribute('src', mockSpoilerUri); - expect(mockSpoilerOverlay).to.have.class(hiddenClass); + expect(mockSizeImage).not.toHaveAttribute('src', mockSpoilerUri); + expect(mockSpoilerOverlay).toHaveClass(hiddenClass); if (firstHandler === 'click') { // Second attempt to click a shown spoiler should not cause default prevention @@ -539,9 +539,9 @@ describe('Image utils', () => { // Moving the mouse away should hide the image and show the overlay again const mouseLeaveEvent = createEvent.mouseLeave(mockElement); fireEvent(mockElement, mouseLeaveEvent); - expect(mockSizeImage).to.have.attribute('src', mockSpoilerUri); - expect(mockSpoilerOverlay).not.to.have.class(hiddenClass); - expect(mockSpoilerOverlay).to.contain.html(mockSpoilerReason); + expect(mockSizeImage).toHaveAttribute('src', mockSpoilerUri); + expect(mockSpoilerOverlay).not.toHaveClass(hiddenClass); + expect(mockSpoilerOverlay).toContainHTML(mockSpoilerReason); }; let lastSpoilerType: SpoilerType; @@ -618,10 +618,10 @@ describe('Image utils', () => { spoilerBlock(mockElement, mockSpoilerUri, mockSpoilerReason); - expect(mockImage).to.have.attribute('src', mockSpoilerUri); - expect(mockExplanation).to.contain.html(mockSpoilerReason); - expect(mockImageShow).to.have.class(hiddenClass); - expect(mockImageFiltered).not.to.have.class(hiddenClass); + expect(mockImage).toHaveAttribute('src', mockSpoilerUri); + expect(mockExplanation).toContainHTML(mockSpoilerReason); + expect(mockImageShow).toHaveClass(hiddenClass); + expect(mockImageFiltered).not.toHaveClass(hiddenClass); }); it('should not throw if image-filtered element is missing', () => { diff --git a/assets/package-lock.json b/assets/package-lock.json index 2dae097a..bdd7c28a 100644 --- a/assets/package-lock.json +++ b/assets/package-lock.json @@ -20,16 +20,22 @@ }, "devDependencies": { "@testing-library/dom": "^10.1.0", + "@testing-library/jest-dom": "^6.4.2", "@types/chai-dom": "^1.11.3", "@vitest/coverage-v8": "^1.5.3", - "c8": "^9.1.0", - "chai-dom": "^1.12.0", + "chai": "^5", "eslint-plugin-vitest": "^0.5.4", "jsdom": "^24.0.0", "vitest": "^1.5.3", "vitest-fetch-mock": "^0.2.2" } }, + "node_modules/@adobe/css-tools": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.3.tgz", + "integrity": "sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==", + "dev": true + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -1048,6 +1054,70 @@ "node": ">=18" } }, + "node_modules/@testing-library/jest-dom": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.4.2.tgz", + "integrity": "sha512-CzqH0AFymEMG48CpzXFriYYkOjk6ZGPCLMhW9e9jg3KMCn5OfJecF8GtGW7yGfR/IgCe3SX8BSwjdzI6BBbZLw==", + "dev": true, + "dependencies": { + "@adobe/css-tools": "^4.3.2", + "@babel/runtime": "^7.9.2", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "lodash": "^4.17.15", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + }, + "peerDependencies": { + "@jest/globals": ">= 28", + "@types/bun": "latest", + "@types/jest": ">= 28", + "jest": ">= 28", + "vitest": ">= 0.32" + }, + "peerDependenciesMeta": { + "@jest/globals": { + "optional": true + }, + "@types/bun": { + "optional": true + }, + "@types/jest": { + "optional": true + }, + "jest": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, + "node_modules/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -1387,6 +1457,66 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@vitest/expect/node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/@vitest/expect/node_modules/chai": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", + "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.0.8" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@vitest/expect/node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@vitest/expect/node_modules/deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@vitest/expect/node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/@vitest/runner": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.5.3.tgz", @@ -1659,12 +1789,12 @@ } }, "node_modules/assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, "engines": { - "node": "*" + "node": ">=12" } }, "node_modules/asynckit": { @@ -1774,31 +1904,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/c8": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/c8/-/c8-9.1.0.tgz", - "integrity": "sha512-mBWcT5iqNir1zIkzSPyI3NCR9EZCVI3WUD+AVO17MVWTSFNyUueXE82qTeampNtTr+ilN/5Ua3j24LgbCKjDVg==", - "dev": true, - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@istanbuljs/schema": "^0.1.3", - "find-up": "^5.0.0", - "foreground-child": "^3.1.1", - "istanbul-lib-coverage": "^3.2.0", - "istanbul-lib-report": "^3.0.1", - "istanbul-reports": "^3.1.6", - "test-exclude": "^6.0.0", - "v8-to-istanbul": "^9.0.0", - "yargs": "^17.7.2", - "yargs-parser": "^21.1.1" - }, - "bin": { - "c8": "bin/c8.js" - }, - "engines": { - "node": ">=14.14.0" - } - }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -1836,33 +1941,28 @@ ] }, "node_modules/chai": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", - "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.0.tgz", + "integrity": "sha512-kDZ7MZyM6Q1DhR9jy7dalKohXQ2yrlXkk59CR52aRKxJrobmlBNqnFQxX9xOX8w+4mz8SYlKJa/7D7ddltFXCw==", "dev": true, "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.3", - "deep-eql": "^4.1.3", - "get-func-name": "^2.0.2", - "loupe": "^2.3.6", - "pathval": "^1.1.1", - "type-detect": "^4.0.8" + "assertion-error": "^2.0.1", + "check-error": "^2.0.0", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" }, "engines": { - "node": ">=4" + "node": ">=12" } }, - "node_modules/chai-dom": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/chai-dom/-/chai-dom-1.12.0.tgz", - "integrity": "sha512-pLP8h6IBR8z1AdeQ+EMcJ7dXPdsax/1Q7gdGZjsnAmSBl3/gItQUYSCo32br1qOy4SlcBjvqId7ilAf3uJ2K1w==", + "node_modules/chai/node_modules/loupe": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.0.tgz", + "integrity": "sha512-qKl+FrLXUhFuHUoDJG7f8P8gEMHq9NFS0c6ghXG1J0rldmZFQZoNVv/vyirE9qwCIhWZDsvEFd1sbFu3GvRQFg==", "dev": true, - "engines": { - "node": ">= 0.12.0" - }, - "peerDependencies": { - "chai": ">= 3" + "dependencies": { + "get-func-name": "^2.0.1" } }, "node_modules/chalk": { @@ -1881,15 +1981,12 @@ } }, "node_modules/check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.0.0.tgz", + "integrity": "sha512-tjLAOBHKVxtPoHe/SA7kNOMvhCRdCJ3vETdeY0RuAc9popf+hyaSV6ZEg9hr4cpWF7jmo/JSWEnLDrnijS9Tog==", "dev": true, - "dependencies": { - "get-func-name": "^2.0.2" - }, "engines": { - "node": "*" + "node": ">= 16" } }, "node_modules/chokidar": { @@ -1940,20 +2037,6 @@ "node": ">=8" } }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1992,12 +2075,6 @@ "integrity": "sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==", "dev": true }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true - }, "node_modules/cross-env": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", @@ -2037,6 +2114,12 @@ "node": ">= 8" } }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true + }, "node_modules/cssom": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", @@ -2089,13 +2172,10 @@ "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" }, "node_modules/deep-eql": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", - "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.1.tgz", + "integrity": "sha512-nwQCf6ne2gez3o1MxWifqkciwt0zhl0LO1/UwVu4uMBuPmflWM4oQ70XMqHqnBJA+nhzncaqL9HVL6KkHJ28lw==", "dev": true, - "dependencies": { - "type-detect": "^4.0.0" - }, "engines": { "node": ">=6" } @@ -2176,12 +2256,6 @@ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.751.tgz", "integrity": "sha512-2DEPi++qa89SMGRhufWTiLmzqyuGmNF3SK4+PQetW1JKiZdEpF4XQonJXJCzyuYSA6mauiMhbyVhqYAP45Hvfw==" }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -2597,34 +2671,6 @@ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==" }, - "node_modules/foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -2668,15 +2714,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, "node_modules/get-func-name": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", @@ -2878,6 +2915,15 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -2911,15 +2957,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -3412,6 +3449,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -3515,6 +3558,15 @@ "node": ">= 0.6" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "9.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", @@ -3754,12 +3806,12 @@ "dev": true }, "node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", "dev": true, "engines": { - "node": "*" + "node": ">= 14.16" } }, "node_modules/picocolors": { @@ -3909,21 +3961,25 @@ "node": ">=8.10.0" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", "dev": true }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -4165,20 +4221,6 @@ "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", "dev": true }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -4190,6 +4232,18 @@ "node": ">=8" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -4464,20 +4518,6 @@ "requires-port": "^1.0.0" } }, - "node_modules/v8-to-istanbul": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", - "integrity": "sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==", - "dev": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^2.0.0" - }, - "engines": { - "node": ">=10.12.0" - } - }, "node_modules/vite": { "version": "5.2.10", "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.10.tgz", @@ -4634,6 +4674,57 @@ "vitest": ">=0.16.0" } }, + "node_modules/vitest/node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/vitest/node_modules/chai": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", + "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.0.8" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/vitest/node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/vitest/node_modules/deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/vitest/node_modules/execa": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", @@ -4744,6 +4835,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/vitest/node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/vitest/node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -4860,23 +4960,6 @@ "node": ">=0.10.0" } }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -4916,42 +4999,6 @@ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "engines": { - "node": ">=12" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/assets/package.json b/assets/package.json index a3aa0df9..eba4cbe7 100644 --- a/assets/package.json +++ b/assets/package.json @@ -25,9 +25,10 @@ }, "devDependencies": { "@testing-library/dom": "^10.1.0", + "@testing-library/jest-dom": "^6.4.2", "@types/chai-dom": "^1.11.3", "@vitest/coverage-v8": "^1.5.3", - "chai-dom": "^1.12.0", + "chai": "^5", "eslint-plugin-vitest": "^0.5.4", "jsdom": "^24.0.0", "vitest": "^1.5.3", diff --git a/assets/test/vitest-setup.ts b/assets/test/vitest-setup.ts index 03c0d647..d889b27f 100644 --- a/assets/test/vitest-setup.ts +++ b/assets/test/vitest-setup.ts @@ -1,12 +1,9 @@ import { matchNone } from '../js/query/boolean'; -import chai from 'chai'; -import chaiDom from 'chai-dom'; +import '@testing-library/jest-dom/vitest'; import { URL } from 'node:url'; import { Blob } from 'node:buffer'; import { fireEvent } from '@testing-library/dom'; -chai.use(chaiDom); - window.booru = { // eslint-disable-next-line @typescript-eslint/no-empty-function timeAgo: () => {}, From 71196604514df7f3256ba0e164b34548525956d7 Mon Sep 17 00:00:00 2001 From: Liam Date: Tue, 30 Apr 2024 21:27:02 -0400 Subject: [PATCH 02/10] wrap --- assets/css/common/_blocks.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/css/common/_blocks.scss b/assets/css/common/_blocks.scss index fbf2c7f7..44538285 100644 --- a/assets/css/common/_blocks.scss +++ b/assets/css/common/_blocks.scss @@ -125,6 +125,7 @@ a.block__header--single-item, .block__header a { @extend .block__header--light; background: transparent; display: flex; + flex-wrap: wrap; border-bottom: $border; a { From 70cde5d4b2e1a4617d99d288c3a13e3a4709ba42 Mon Sep 17 00:00:00 2001 From: "Luna D." Date: Wed, 1 May 2024 00:15:06 +0200 Subject: [PATCH 03/10] import mockinstance --- assets/js/utils/__tests__/draggable.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/js/utils/__tests__/draggable.spec.ts b/assets/js/utils/__tests__/draggable.spec.ts index e9e3daad..cda0598a 100644 --- a/assets/js/utils/__tests__/draggable.spec.ts +++ b/assets/js/utils/__tests__/draggable.spec.ts @@ -1,6 +1,7 @@ import { clearDragSource, initDraggables } from '../draggable'; import { fireEvent } from '@testing-library/dom'; import { getRandomArrayItem } from '../../../test/randomness'; +import { MockInstance } from 'vitest'; describe('Draggable Utilities', () => { // jsdom lacks proper support for window.DragEvent so this is an attempt at a minimal recreation From 852f870ccf13b0b0a66ade7ef3616e824fea864e Mon Sep 17 00:00:00 2001 From: Liam Date: Fri, 3 May 2024 21:06:15 -0400 Subject: [PATCH 04/10] USe compile-time environment checks --- config/dev.exs | 6 +++++ config/runtime.exs | 12 ---------- lib/philomena_web/config.ex | 12 ++++++++++ .../plugs/content_security_policy_plug.ex | 22 ++++++++++--------- .../templates/layout/app.html.slime | 2 +- lib/philomena_web/views/layout_view.ex | 1 + 6 files changed, 32 insertions(+), 23 deletions(-) create mode 100644 lib/philomena_web/config.ex diff --git a/config/dev.exs b/config/dev.exs index 781d0ee2..674709ff 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -72,6 +72,12 @@ config :philomena, PhilomenaWeb.Endpoint, ] ] +# Relax CSP rules in development +config :philomena, csp_relaxed: true + +# Enable Vite HMR +config :philomena, vite_reload: true + # Do not include metadata nor timestamps in development logs config :logger, :console, format: "[$level] $message\n" diff --git a/config/runtime.exs b/config/runtime.exs index b7a71c3e..9cd91ed5 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -134,22 +134,10 @@ if config_env() == :prod do url: [host: System.fetch_env!("APP_HOSTNAME"), scheme: "https", port: 443], secret_key_base: System.fetch_env!("SECRET_KEY_BASE"), server: not is_nil(System.get_env("START_ENDPOINT")) - - # Do not relax CSP in production - config :philomena, csp_relaxed: false - - # Disable Vite HMR in prod - config :philomena, vite_reload: false else # Don't send email in development config :philomena, Philomena.Mailer, adapter: Bamboo.LocalAdapter # Use this to debug slime templates # config :slime, :keep_lines, true - - # Relax CSP rules in development and test servers - config :philomena, csp_relaxed: true - - # Enable Vite HMR - config :philomena, vite_reload: true end diff --git a/lib/philomena_web/config.ex b/lib/philomena_web/config.ex new file mode 100644 index 00000000..c661550e --- /dev/null +++ b/lib/philomena_web/config.ex @@ -0,0 +1,12 @@ +defmodule PhilomenaWeb.Config do + @reload_enabled Application.compile_env(:philomena, :vite_reload, false) + @csp_relaxed Application.compile_env(:philomena, :csp_relaxed, false) + + defmacro vite_hmr?(do: do_clause, else: else_clause) do + if(@reload_enabled, do: do_clause, else: else_clause) + end + + defmacro csp_relaxed?(do: do_clause, else: else_clause) do + if(@csp_relaxed, do: do_clause, else: else_clause) + end +end diff --git a/lib/philomena_web/plugs/content_security_policy_plug.ex b/lib/philomena_web/plugs/content_security_policy_plug.ex index d958541e..210ff645 100644 --- a/lib/philomena_web/plugs/content_security_policy_plug.ex +++ b/lib/philomena_web/plugs/content_security_policy_plug.ex @@ -1,4 +1,5 @@ defmodule PhilomenaWeb.ContentSecurityPolicyPlug do + import PhilomenaWeb.Config import Plug.Conn @allowed_sources [ @@ -42,11 +43,15 @@ defmodule PhilomenaWeb.ContentSecurityPolicyPlug do |> Enum.map(&cspify_element/1) |> Enum.join("; ") - if conn.status == 500 and allow_relaxed_csp() do - # Allow Plug.Debugger to function in this case - delete_resp_header(conn, "content-security-policy") + csp_relaxed? do + if conn.status == 500 do + # Allow Plug.Debugger to function in this case + delete_resp_header(conn, "content-security-policy") + else + # Enforce CSP otherwise + put_resp_header(conn, "content-security-policy", csp_value) + end else - # Enforce CSP otherwise put_resp_header(conn, "content-security-policy", csp_value) end end) @@ -64,14 +69,13 @@ defmodule PhilomenaWeb.ContentSecurityPolicyPlug do defp cdn_uri, do: Application.get_env(:philomena, :cdn_host) |> to_uri() defp camo_uri, do: Application.get_env(:philomena, :camo_host) |> to_uri() - defp vite_reload?, do: Application.get_env(:philomena, :vite_reload) - defp default_script_src, do: if(vite_reload?(), do: "'self' localhost:5173", else: "'self'") + defp default_script_src, do: vite_hmr?(do: "'self' localhost:5173", else: "'self'") defp default_connect_src, - do: if(vite_reload?(), do: "'self' localhost:5173 ws://localhost:5173", else: "'self'") + do: vite_hmr?(do: "'self' localhost:5173 ws://localhost:5173", else: "'self'") - defp default_style_src, do: if(vite_reload?(), do: "'self' 'unsafe-inline'", else: "'self'") + defp default_style_src, do: vite_hmr?(do: "'self' 'unsafe-inline'", else: "'self'") defp to_uri(host) when host in [nil, ""], do: "" defp to_uri(host), do: URI.to_string(%URI{scheme: "https", host: host}) @@ -84,6 +88,4 @@ defmodule PhilomenaWeb.ContentSecurityPolicyPlug do Enum.join([key | value], " ") end - - defp allow_relaxed_csp, do: Application.get_env(:philomena, :csp_relaxed, false) end diff --git a/lib/philomena_web/templates/layout/app.html.slime b/lib/philomena_web/templates/layout/app.html.slime index 16a43d7a..e86528eb 100644 --- a/lib/philomena_web/templates/layout/app.html.slime +++ b/lib/philomena_web/templates/layout/app.html.slime @@ -20,7 +20,7 @@ html lang="en" meta name="format-detection" content="telephone=no" = csrf_meta_tag() - = if vite_reload?() do + = vite_hmr? do script type="module" src="http://localhost:5173/@vite/client" script type="module" src="http://localhost:5173/js/app.js" - else diff --git a/lib/philomena_web/views/layout_view.ex b/lib/philomena_web/views/layout_view.ex index 3ea772a7..fe338f09 100644 --- a/lib/philomena_web/views/layout_view.ex +++ b/lib/philomena_web/views/layout_view.ex @@ -1,6 +1,7 @@ defmodule PhilomenaWeb.LayoutView do use PhilomenaWeb, :view + import PhilomenaWeb.Config alias PhilomenaWeb.ImageView alias Philomena.Config alias Plug.Conn From 32619be58b3299361045f9725e54087f570a8738 Mon Sep 17 00:00:00 2001 From: liamwhite Date: Fri, 3 May 2024 23:15:14 -0400 Subject: [PATCH 05/10] Ensure HTML raw insertion is not used in template (#247) --- lib/philomena_web/markdown_renderer.ex | 12 ++++++++++-- .../templates/admin/dnp_entry/index.html.slime | 2 +- .../templates/admin/mod_note/_table.html.slime | 2 +- .../templates/admin/report/show.html.slime | 2 +- .../templates/comment/_comment.html.slime | 4 ++-- .../templates/comment/_comment_with_image.html.slime | 4 ++-- .../templates/dnp_entry/index.html.slime | 2 +- .../templates/dnp_entry/show.html.slime | 6 +++--- .../templates/image/_description.html.slime | 4 ++-- .../templates/message/_message.html.slime | 2 +- lib/philomena_web/templates/page/show.html.slime | 2 +- lib/philomena_web/templates/post/_post.html.slime | 4 ++-- .../templates/post/preview/create.html.slime | 2 +- .../templates/profile/_about_me.html.slime | 2 +- .../templates/profile/_commission.html.slime | 2 +- .../profile/commission/_listing_items.html.slime | 4 ++-- .../profile/commission/_listing_sidebar.html.slime | 8 ++++---- lib/philomena_web/templates/profile/show.html.slime | 4 ++-- .../templates/tag/_tag_info_row.html.slime | 4 ++-- 19 files changed, 40 insertions(+), 32 deletions(-) diff --git a/lib/philomena_web/markdown_renderer.ex b/lib/philomena_web/markdown_renderer.ex index 508a960f..7caff5c9 100644 --- a/lib/philomena_web/markdown_renderer.ex +++ b/lib/philomena_web/markdown_renderer.ex @@ -10,6 +10,8 @@ defmodule PhilomenaWeb.MarkdownRenderer do hd(render_collection([item], conn)) end + # This is rendered Markdown + # sobelow_skip ["XSS.Raw"] def render_collection(collection, conn) do representations = collection @@ -19,15 +21,21 @@ defmodule PhilomenaWeb.MarkdownRenderer do |> render_representations(conn) Enum.map(collection, fn %{body: text} -> - Markdown.to_html(text || "", representations) + (text || "") + |> Markdown.to_html(representations) + |> Phoenix.HTML.raw() end) end + # This is rendered Markdown for use on static pages + # sobelow_skip ["XSS.Raw"] def render_unsafe(text, conn) do images = find_images(text) representations = render_representations(images, conn) - Markdown.to_html_unsafe(text, representations) + text + |> Markdown.to_html_unsafe(representations) + |> Phoenix.HTML.raw() end defp find_images(text) do diff --git a/lib/philomena_web/templates/admin/dnp_entry/index.html.slime b/lib/philomena_web/templates/admin/dnp_entry/index.html.slime index 50f351dd..bc646d59 100644 --- a/lib/philomena_web/templates/admin/dnp_entry/index.html.slime +++ b/lib/philomena_web/templates/admin/dnp_entry/index.html.slime @@ -44,7 +44,7 @@ h2 Do-Not-Post Requests = request.dnp_type td - == body + = body td class=dnp_entry_row_class(request) => pretty_state(request) diff --git a/lib/philomena_web/templates/admin/mod_note/_table.html.slime b/lib/philomena_web/templates/admin/mod_note/_table.html.slime index fa457243..a47663a7 100644 --- a/lib/philomena_web/templates/admin/mod_note/_table.html.slime +++ b/lib/philomena_web/templates/admin/mod_note/_table.html.slime @@ -13,7 +13,7 @@ table.table = link_to_noted_thing(@conn, note.notable) td - == body + = body td = pretty_time note.created_at diff --git a/lib/philomena_web/templates/admin/report/show.html.slime b/lib/philomena_web/templates/admin/report/show.html.slime index deec703b..f046aae3 100644 --- a/lib/philomena_web/templates/admin/report/show.html.slime +++ b/lib/philomena_web/templates/admin/report/show.html.slime @@ -11,7 +11,7 @@ article.block.communication br = render PhilomenaWeb.UserAttributionView, "_anon_user_title.html", object: @report, conn: @conn .communication__body__text - ==<> @body + =<> @body .block__content.communication__options .flex.flex--wrap.flex--spaced-out diff --git a/lib/philomena_web/templates/comment/_comment.html.slime b/lib/philomena_web/templates/comment/_comment.html.slime index e79f3852..508202ef 100644 --- a/lib/philomena_web/templates/comment/_comment.html.slime +++ b/lib/philomena_web/templates/comment/_comment.html.slime @@ -45,10 +45,10 @@ article.block.communication id="comment_#{@comment.id}" | This comment's contents have been destroyed. - else br - ==<> @body + =<> @body - else - ==<> @body + =<> @body .block__content.communication__options .flex.flex--wrap.flex--spaced-out diff --git a/lib/philomena_web/templates/comment/_comment_with_image.html.slime b/lib/philomena_web/templates/comment/_comment_with_image.html.slime index ed7da1fe..8faaca31 100644 --- a/lib/philomena_web/templates/comment/_comment_with_image.html.slime +++ b/lib/philomena_web/templates/comment/_comment_with_image.html.slime @@ -28,10 +28,10 @@ article.block.communication id="comment_#{@comment.id}" | This comment's contents have been destroyed. - else br - ==<> @body + =<> @body - else - ==<> @body + =<> @body .block__content.communication__options .flex.flex--wrap.flex--spaced-out diff --git a/lib/philomena_web/templates/dnp_entry/index.html.slime b/lib/philomena_web/templates/dnp_entry/index.html.slime index b76ba967..01952ede 100644 --- a/lib/philomena_web/templates/dnp_entry/index.html.slime +++ b/lib/philomena_web/templates/dnp_entry/index.html.slime @@ -59,7 +59,7 @@ h3 The List = entry.dnp_type td - == body + = body = if @status_column do td diff --git a/lib/philomena_web/templates/dnp_entry/show.html.slime b/lib/philomena_web/templates/dnp_entry/show.html.slime index 572fa09e..a672573a 100644 --- a/lib/philomena_web/templates/dnp_entry/show.html.slime +++ b/lib/philomena_web/templates/dnp_entry/show.html.slime @@ -28,19 +28,19 @@ h2 tr td Conditions: td - == @conditions + = @conditions = if can?(@conn, :show_reason, @dnp_entry) do tr td Reason: td - == @reason + = @reason = if can?(@conn, :show_feedback, @dnp_entry) do tr td Instructions: td - == @instructions + = @instructions tr td Feedback: td diff --git a/lib/philomena_web/templates/image/_description.html.slime b/lib/philomena_web/templates/image/_description.html.slime index 7f5b2533..5f1fa052 100644 --- a/lib/philomena_web/templates/image/_description.html.slime +++ b/lib/philomena_web/templates/image/_description.html.slime @@ -10,7 +10,7 @@ ' Edit .block__content p - = if String.length(@body) > 0 do - == @body + = if String.length(@image.description) > 0 do + = @body - else em No description provided. diff --git a/lib/philomena_web/templates/message/_message.html.slime b/lib/philomena_web/templates/message/_message.html.slime index bf24ea6c..27f4bd96 100644 --- a/lib/philomena_web/templates/message/_message.html.slime +++ b/lib/philomena_web/templates/message/_message.html.slime @@ -25,7 +25,7 @@ article.block.communication = render PhilomenaWeb.UserAttributionView, "_user_title.html", object: %{user: @message.from}, conn: @conn .communication__body__text - == @body + = @body .block__content.communication__options .flex.flex--wrap.flex--spaced-out diff --git a/lib/philomena_web/templates/page/show.html.slime b/lib/philomena_web/templates/page/show.html.slime index fa4bc580..9eadee5f 100644 --- a/lib/philomena_web/templates/page/show.html.slime +++ b/lib/philomena_web/templates/page/show.html.slime @@ -12,4 +12,4 @@ p i.fa.fa-edit> ' Edit -== @rendered += @rendered diff --git a/lib/philomena_web/templates/post/_post.html.slime b/lib/philomena_web/templates/post/_post.html.slime index 64712c84..d6f5c6a8 100644 --- a/lib/philomena_web/templates/post/_post.html.slime +++ b/lib/philomena_web/templates/post/_post.html.slime @@ -45,10 +45,10 @@ article.block.communication id="post_#{@post.id}" | This post's contents have been destroyed. - else br - ==<> @body + =<> @body - else - ==<> @body + =<> @body .block__content.communication__options .flex.flex--wrap.flex--spaced-out diff --git a/lib/philomena_web/templates/post/preview/create.html.slime b/lib/philomena_web/templates/post/preview/create.html.slime index 8b5febb8..469efc80 100644 --- a/lib/philomena_web/templates/post/preview/create.html.slime +++ b/lib/philomena_web/templates/post/preview/create.html.slime @@ -7,4 +7,4 @@ = render PhilomenaWeb.UserAttributionView, "_anon_user.html", object: @post, conn: @conn, awards: true .communication__body__text - == @body + = @body diff --git a/lib/philomena_web/templates/profile/_about_me.html.slime b/lib/philomena_web/templates/profile/_about_me.html.slime index 31aba220..e4c4a782 100644 --- a/lib/philomena_web/templates/profile/_about_me.html.slime +++ b/lib/philomena_web/templates/profile/_about_me.html.slime @@ -1,7 +1,7 @@ .block__content.profile-about = cond do - @user.description not in [nil, ""] -> - == @about_me + = @about_me - current?(@user, @conn.assigns.current_user) -> em diff --git a/lib/philomena_web/templates/profile/_commission.html.slime b/lib/philomena_web/templates/profile/_commission.html.slime index b6a47d74..db8e8d46 100644 --- a/lib/philomena_web/templates/profile/_commission.html.slime +++ b/lib/philomena_web/templates/profile/_commission.html.slime @@ -17,7 +17,7 @@ / Lotta space here br - == @commission_information + = @commission_information br br diff --git a/lib/philomena_web/templates/profile/commission/_listing_items.html.slime b/lib/philomena_web/templates/profile/commission/_listing_items.html.slime index b57a80b1..778daa01 100644 --- a/lib/philomena_web/templates/profile/commission/_listing_items.html.slime +++ b/lib/philomena_web/templates/profile/commission/_listing_items.html.slime @@ -42,13 +42,13 @@ br br - == description + = description td | $ = Decimal.round(item.base_price, 2) td - == add_ons + = add_ons = if can?(@conn, :edit, @commission) do td diff --git a/lib/philomena_web/templates/profile/commission/_listing_sidebar.html.slime b/lib/philomena_web/templates/profile/commission/_listing_sidebar.html.slime index ec68f27e..544ed939 100644 --- a/lib/philomena_web/templates/profile/commission/_listing_sidebar.html.slime +++ b/lib/philomena_web/templates/profile/commission/_listing_sidebar.html.slime @@ -24,14 +24,14 @@ br br - == @rendered.information + = @rendered.information / Contact information block .block .block__header span.block__header__title Contact information .block__content.commission__block_body - == @rendered.contact + = @rendered.contact / Categories block .block @@ -48,7 +48,7 @@ .block__header span.block__header__title Will draw/create .block__content.commission__block_body - == @rendered.will_create + = @rendered.will_create / Will not create block = if @commission.will_not_create not in [nil, ""] do @@ -56,7 +56,7 @@ .block__header span.block__header__title Will not draw/create .block__content.commission__block_body - == @rendered.will_not_create + = @rendered.will_not_create / Artist link block /.block diff --git a/lib/philomena_web/templates/profile/show.html.slime b/lib/philomena_web/templates/profile/show.html.slime index ef368950..21eacf74 100644 --- a/lib/philomena_web/templates/profile/show.html.slime +++ b/lib/philomena_web/templates/profile/show.html.slime @@ -146,13 +146,13 @@ tbody = for {body, mod_note} <- @mod_notes do tr - td == body + td = body td = pretty_time(mod_note.created_at) = if can_index_user?(@conn) do .block a.block__header--single-item href=Routes.profile_scratchpad_path(@conn, :edit, @user) Moderation Scratchpad .block__content.profile-about - == @scratchpad + = @scratchpad .column-layout__main = render PhilomenaWeb.ProfileView, "_statistics.html", user: @user, statistics: @statistics, conn: @conn diff --git a/lib/philomena_web/templates/tag/_tag_info_row.html.slime b/lib/philomena_web/templates/tag/_tag_info_row.html.slime index de013d35..5e38db97 100644 --- a/lib/philomena_web/templates/tag/_tag_info_row.html.slime +++ b/lib/philomena_web/templates/tag/_tag_info_row.html.slime @@ -101,7 +101,7 @@ = if @tag.description not in [nil, ""] do strong> Detailed description: br - == @body + = @body = if Enum.any?(@dnp_entries) do hr @@ -114,7 +114,7 @@ strong => entry.dnp_type - ==> body + => body | ( = link "more info", to: Routes.dnp_entry_path(@conn, :show, entry) From eba094fe47ae9944a3e0047325c9921b58070e5d Mon Sep 17 00:00:00 2001 From: mdashlw Date: Sat, 4 May 2024 19:58:51 +0300 Subject: [PATCH 06/10] feat: add "oc only" to quick tag table Often Forgotten (#249) --- config/quick_tag_table.json | 1 + 1 file changed, 1 insertion(+) diff --git a/config/quick_tag_table.json b/config/quick_tag_table.json index 396fedb4..6810305e 100644 --- a/config/quick_tag_table.json +++ b/config/quick_tag_table.json @@ -76,6 +76,7 @@ "image macro", "monochrome", "oc", + "oc only", "photo", "Tag original characters oc:name" ] From ca9cb3a50edac2d4d725e85b4e4361d33c8f3358 Mon Sep 17 00:00:00 2001 From: mdashlw Date: Sat, 4 May 2024 20:00:55 +0300 Subject: [PATCH 07/10] feat: add "retained" column to tag changes table (#246) * feat: add "retained" column to tag changes table * Update lib/philomena_web/views/tag_change_view.ex Co-authored-by: liamwhite --------- Co-authored-by: liamwhite --- .../templates/tag_change/index.html.slime | 5 +++++ lib/philomena_web/views/tag_change_view.ex | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/lib/philomena_web/templates/tag_change/index.html.slime b/lib/philomena_web/templates/tag_change/index.html.slime index 36f0630e..a5f6b64b 100644 --- a/lib/philomena_web/templates/tag_change/index.html.slime +++ b/lib/philomena_web/templates/tag_change/index.html.slime @@ -19,6 +19,7 @@ th Action th Timestamp th User + th Retained? = if reverts_tag_changes?(@conn) do th Moderation @@ -62,6 +63,10 @@ ' This user is a staff member. br ' Ask them before reverting their changes. + = if tag_change_retained(tag_change) do + td.success Yes + - else + td.danger No = if reverts_tag_changes?(@conn) do td a href=Routes.image_tag_change_path(@conn, :delete, tag_change.image, tag_change) data-method="delete" data-confirm="Are you really, really sure?" diff --git a/lib/philomena_web/views/tag_change_view.ex b/lib/philomena_web/views/tag_change_view.ex index e821ca93..6db71454 100644 --- a/lib/philomena_web/views/tag_change_view.ex +++ b/lib/philomena_web/views/tag_change_view.ex @@ -15,4 +15,14 @@ defmodule PhilomenaWeb.TagChangeView do def reverts_tag_changes?(conn), do: can?(conn, :revert, Philomena.TagChanges.TagChange) + + def tag_change_retained(%{image: image, added: added, tag: %{id: tag_id}}) do + added == Enum.any?(image.tags, &(&1.id == tag_id)) + end + + def tag_change_retained(%{image: image, added: added, tag_name_cache: tag_name}) do + added == Enum.any?(image.tags, &(&1.name == tag_name)) + end + + def tag_change_retained(_), do: false end From 5b836580e1244ec3cadc77e5d046d78a584fefc5 Mon Sep 17 00:00:00 2001 From: liamwhite Date: Sun, 5 May 2024 10:12:17 -0400 Subject: [PATCH 08/10] profile/tag_change: show total affected image count (#248) * profile/tag_change: show total affected image count * Incorporate count into block header --------- Co-authored-by: prg --- .../controllers/profile/tag_change_controller.ex | 13 +++++++++++-- .../templates/profile/tag_change/index.html.slime | 6 ++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/philomena_web/controllers/profile/tag_change_controller.ex b/lib/philomena_web/controllers/profile/tag_change_controller.ex index 6aaa4c3a..3fb67e28 100644 --- a/lib/philomena_web/controllers/profile/tag_change_controller.ex +++ b/lib/philomena_web/controllers/profile/tag_change_controller.ex @@ -14,7 +14,7 @@ defmodule PhilomenaWeb.Profile.TagChangeController do def index(conn, params) do user = conn.assigns.user - tag_changes = + common_query = TagChange |> join(:inner, [tc], i in Image, on: tc.image_id == i.id) |> only_tag_join(params) @@ -24,10 +24,18 @@ defmodule PhilomenaWeb.Profile.TagChangeController do ) |> added_filter(params) |> only_tag_filter(params) + + tag_changes = + common_query |> preload([:tag, :user, image: [:user, :sources, tags: :aliases]]) |> order_by(desc: :id) |> Repo.paginate(conn.assigns.scrivener) + image_count = + common_query + |> select([_, i], count(i.id, :distinct)) + |> Repo.one() + # params.permit(:added, :only_tag) ... pagination_params = [added: conn.params["added"], only_tag: conn.params["only_tag"]] @@ -37,7 +45,8 @@ defmodule PhilomenaWeb.Profile.TagChangeController do title: "Tag Changes for User `#{user.name}'", user: user, tag_changes: tag_changes, - pagination_params: pagination_params + pagination_params: pagination_params, + image_count: image_count ) end diff --git a/lib/philomena_web/templates/profile/tag_change/index.html.slime b/lib/philomena_web/templates/profile/tag_change/index.html.slime index 563a7fc3..806d843a 100644 --- a/lib/philomena_web/templates/profile/tag_change/index.html.slime +++ b/lib/philomena_web/templates/profile/tag_change/index.html.slime @@ -16,4 +16,10 @@ h1 = link "Added", to: Routes.profile_tag_change_path(@conn, :index, @user, Keyword.merge(@pagination_params, added: 1)) = link "All", to: Routes.profile_tag_change_path(@conn, :index, @user, Keyword.delete(@pagination_params, :added)) + .block__header.block__header--light + span.block__header__title.page__info + ' Listing changes for + => @image_count + = pluralize("image", "images", @image_count) + = render PhilomenaWeb.TagChangeView, "index.html", conn: @conn, tag_changes: @tag_changes, pagination: pagination From 377317a26b386da3d898efd67650c17f637859fd Mon Sep 17 00:00:00 2001 From: Eliot Partridge Date: Sun, 5 May 2024 17:38:22 -0400 Subject: [PATCH 09/10] Add local setting to enable audio on videos by default (#252) * Add local setting to enable audio on videos by default * Update copy * Use ternary over `let`+`if` --- assets/js/image_expansion.js | 6 ++++-- lib/philomena_web/controllers/setting_controller.ex | 1 + lib/philomena_web/templates/setting/edit.html.slime | 4 ++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/assets/js/image_expansion.js b/assets/js/image_expansion.js index d7bab189..66cc379e 100644 --- a/assets/js/image_expansion.js +++ b/assets/js/image_expansion.js @@ -86,10 +86,12 @@ function pickAndResize(elem) { clearEl(elem); } + const muted = store.get('unmute_videos') ? '' : 'muted'; + if (imageFormat === 'mp4') { elem.classList.add('full-height'); elem.insertAdjacentHTML('afterbegin', - `