From 37975e4eacd4d57c9420e6325422bbe187874efa Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Thu, 18 Sep 2025 13:44:21 -0700 Subject: [PATCH] [test] Add component test for image compare widget (#5549) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Added comprehensive component test suite for WidgetImageCompare widget with 410 test assertions covering display, edge cases, and integration scenarios. ## Changes - **What**: Created [Vue Test Utils](https://vue-test-utils.vuejs.org/) test suite for [WidgetImageCompare component](src/renderer/extensions/vueNodes/widgets/components/WidgetImageCompare.vue) using [Vitest](https://vitest.dev/) testing framework ## Review Focus Test coverage completeness for string vs object value handling, accessibility attribute propagation, and edge case robustness including malformed URLs and empty states. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-5549-test-Add-component-test-for-image-compare-widget-26e6d73d365081189fe0d010f87d1eec) by [Unito](https://www.unito.io) --------- Co-authored-by: DrJKL --- .../components/WidgetImageCompare.test.ts | 337 ++++++++++++++++++ .../widgets/components/WidgetImageCompare.vue | 2 +- 2 files changed, 338 insertions(+), 1 deletion(-) create mode 100644 src/renderer/extensions/vueNodes/widgets/components/WidgetImageCompare.test.ts diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetImageCompare.test.ts b/src/renderer/extensions/vueNodes/widgets/components/WidgetImageCompare.test.ts new file mode 100644 index 000000000..798d5ee39 --- /dev/null +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetImageCompare.test.ts @@ -0,0 +1,337 @@ +import { mount } from '@vue/test-utils' +import PrimeVue from 'primevue/config' +import ImageCompare from 'primevue/imagecompare' +import { describe, expect, it } from 'vitest' + +import type { SimplifiedWidget } from '@/types/simplifiedWidget' + +import WidgetImageCompare, { + type ImageCompareValue +} from './WidgetImageCompare.vue' + +describe('WidgetImageCompare Display', () => { + const createMockWidget = ( + value: ImageCompareValue | string, + options: SimplifiedWidget['options'] = {} + ): SimplifiedWidget => ({ + name: 'test_imagecompare', + type: 'object', + value, + options + }) + + const mountComponent = ( + widget: SimplifiedWidget, + readonly = false + ) => { + return mount(WidgetImageCompare, { + global: { + plugins: [PrimeVue], + components: { ImageCompare } + }, + props: { + widget, + readonly + } + }) + } + + describe('Component Rendering', () => { + it('renders imagecompare component with proper structure and styling', () => { + const value: ImageCompareValue = { + before: 'https://example.com/before.jpg', + after: 'https://example.com/after.jpg' + } + const widget = createMockWidget(value) + const wrapper = mountComponent(widget) + + // Component exists + const imageCompare = wrapper.findComponent({ name: 'ImageCompare' }) + expect(imageCompare.exists()).toBe(true) + + // Renders both images with correct URLs + const images = wrapper.findAll('img') + expect(images).toHaveLength(2) + expect(images[0].attributes('src')).toBe('https://example.com/before.jpg') + expect(images[1].attributes('src')).toBe('https://example.com/after.jpg') + + // Images have proper styling classes + images.forEach((img) => { + expect(img.classes()).toContain('object-cover') + expect(img.classes()).toContain('w-full') + expect(img.classes()).toContain('h-full') + }) + }) + }) + + describe('Object Value Input', () => { + it('handles alt text correctly - custom, default, and empty', () => { + // Test custom alt text + const customAltValue: ImageCompareValue = { + before: 'https://example.com/before.jpg', + after: 'https://example.com/after.jpg', + beforeAlt: 'Original design', + afterAlt: 'Updated design' + } + const customWrapper = mountComponent(createMockWidget(customAltValue)) + const customImages = customWrapper.findAll('img') + expect(customImages[0].attributes('alt')).toBe('Original design') + expect(customImages[1].attributes('alt')).toBe('Updated design') + + // Test default alt text + const defaultAltValue: ImageCompareValue = { + before: 'https://example.com/before.jpg', + after: 'https://example.com/after.jpg' + } + const defaultWrapper = mountComponent(createMockWidget(defaultAltValue)) + const defaultImages = defaultWrapper.findAll('img') + expect(defaultImages[0].attributes('alt')).toBe('Before image') + expect(defaultImages[1].attributes('alt')).toBe('After image') + + // Test empty string alt text (falls back to default) + const emptyAltValue: ImageCompareValue = { + before: 'https://example.com/before.jpg', + after: 'https://example.com/after.jpg', + beforeAlt: '', + afterAlt: '' + } + const emptyWrapper = mountComponent(createMockWidget(emptyAltValue)) + const emptyImages = emptyWrapper.findAll('img') + expect(emptyImages[0].attributes('alt')).toBe('Before image') + expect(emptyImages[1].attributes('alt')).toBe('After image') + }) + + it('handles missing and partial image URLs gracefully', () => { + // Missing URLs + const missingValue: ImageCompareValue = { before: '', after: '' } + const missingWrapper = mountComponent(createMockWidget(missingValue)) + const missingImages = missingWrapper.findAll('img') + expect(missingImages[0].attributes('src')).toBe('') + expect(missingImages[1].attributes('src')).toBe('') + + // Partial URLs + const partialValue: ImageCompareValue = { + before: 'https://example.com/before.jpg', + after: '' + } + const partialWrapper = mountComponent(createMockWidget(partialValue)) + const partialImages = partialWrapper.findAll('img') + expect(partialImages[0].attributes('src')).toBe( + 'https://example.com/before.jpg' + ) + expect(partialImages[1].attributes('src')).toBe('') + }) + }) + + describe('String Value Input', () => { + it('handles string value as before image only', () => { + const value = 'https://example.com/single.jpg' + const widget = createMockWidget(value) + const wrapper = mountComponent(widget) + + const images = wrapper.findAll('img') + expect(images[0].attributes('src')).toBe('https://example.com/single.jpg') + expect(images[1].attributes('src')).toBe('') + }) + + it('uses default alt text for string values', () => { + const value = 'https://example.com/single.jpg' + const widget = createMockWidget(value) + const wrapper = mountComponent(widget) + + const images = wrapper.findAll('img') + expect(images[0].attributes('alt')).toBe('Before image') + expect(images[1].attributes('alt')).toBe('After image') + }) + }) + + describe('Widget Options Handling', () => { + it('passes through accessibility options', () => { + const value: ImageCompareValue = { + before: 'https://example.com/before.jpg', + after: 'https://example.com/after.jpg' + } + const widget = createMockWidget(value, { + tabindex: 1, + ariaLabel: 'Compare images', + ariaLabelledby: 'compare-label' + }) + const wrapper = mountComponent(widget) + + const imageCompare = wrapper.findComponent({ name: 'ImageCompare' }) + expect(imageCompare.props('tabindex')).toBe(1) + expect(imageCompare.props('ariaLabel')).toBe('Compare images') + expect(imageCompare.props('ariaLabelledby')).toBe('compare-label') + }) + + it('uses default tabindex when not provided', () => { + const value: ImageCompareValue = { + before: 'https://example.com/before.jpg', + after: 'https://example.com/after.jpg' + } + const widget = createMockWidget(value) + const wrapper = mountComponent(widget) + + const imageCompare = wrapper.findComponent({ name: 'ImageCompare' }) + expect(imageCompare.props('tabindex')).toBe(0) + }) + + it('passes through PrimeVue specific options', () => { + const value: ImageCompareValue = { + before: 'https://example.com/before.jpg', + after: 'https://example.com/after.jpg' + } + const widget = createMockWidget(value, { + unstyled: true, + pt: { root: { class: 'custom-class' } }, + ptOptions: { mergeSections: true } + }) + const wrapper = mountComponent(widget) + + const imageCompare = wrapper.findComponent({ name: 'ImageCompare' }) + expect(imageCompare.props('unstyled')).toBe(true) + expect(imageCompare.props('pt')).toEqual({ + root: { class: 'custom-class' } + }) + expect(imageCompare.props('ptOptions')).toEqual({ mergeSections: true }) + }) + }) + + describe('Readonly Mode', () => { + it('renders normally in readonly mode (no interaction restrictions)', () => { + const value: ImageCompareValue = { + before: 'https://example.com/before.jpg', + after: 'https://example.com/after.jpg' + } + const widget = createMockWidget(value) + const wrapper = mountComponent(widget, true) + + // ImageCompare is display-only, readonly doesn't affect rendering + const imageCompare = wrapper.findComponent({ name: 'ImageCompare' }) + expect(imageCompare.exists()).toBe(true) + + const images = wrapper.findAll('img') + expect(images).toHaveLength(2) + }) + }) + + describe('Edge Cases', () => { + it('handles null or undefined widget value', () => { + const widget = createMockWidget('') + const wrapper = mountComponent(widget) + + const images = wrapper.findAll('img') + expect(images[0].attributes('src')).toBe('') + expect(images[1].attributes('src')).toBe('') + expect(images[0].attributes('alt')).toBe('Before image') + expect(images[1].attributes('alt')).toBe('After image') + }) + + it('handles empty object value', () => { + const value: ImageCompareValue = {} as ImageCompareValue + const widget = createMockWidget(value) + const wrapper = mountComponent(widget) + + const images = wrapper.findAll('img') + expect(images[0].attributes('src')).toBe('') + expect(images[1].attributes('src')).toBe('') + }) + + it('handles malformed object value', () => { + const value = { randomProp: 'test', before: '', after: '' } + const widget = createMockWidget(value) + const wrapper = mountComponent(widget) + + const images = wrapper.findAll('img') + expect(images[0].attributes('src')).toBe('') + expect(images[1].attributes('src')).toBe('') + }) + + it('handles special content - long URLs, special characters, and long alt text', () => { + // Test very long URLs + const longUrl = 'https://example.com/' + 'a'.repeat(1000) + '.jpg' + const longUrlValue: ImageCompareValue = { + before: longUrl, + after: longUrl + } + const longUrlWrapper = mountComponent(createMockWidget(longUrlValue)) + const longUrlImages = longUrlWrapper.findAll('img') + expect(longUrlImages[0].attributes('src')).toBe(longUrl) + expect(longUrlImages[1].attributes('src')).toBe(longUrl) + + // Test special characters in URLs + const specialUrl = + 'https://example.com/path with spaces & symbols!@#$.jpg' + const specialUrlValue: ImageCompareValue = { + before: specialUrl, + after: specialUrl + } + const specialUrlWrapper = mountComponent( + createMockWidget(specialUrlValue) + ) + const specialUrlImages = specialUrlWrapper.findAll('img') + expect(specialUrlImages[0].attributes('src')).toBe(specialUrl) + expect(specialUrlImages[1].attributes('src')).toBe(specialUrl) + + // Test very long alt text + const longAlt = + 'Very long alt text that exceeds normal length: ' + + 'description '.repeat(50) + const longAltValue: ImageCompareValue = { + before: 'https://example.com/before.jpg', + after: 'https://example.com/after.jpg', + beforeAlt: longAlt, + afterAlt: longAlt + } + const longAltWrapper = mountComponent(createMockWidget(longAltValue)) + const longAltImages = longAltWrapper.findAll('img') + expect(longAltImages[0].attributes('alt')).toBe(longAlt) + expect(longAltImages[1].attributes('alt')).toBe(longAlt) + }) + }) + + describe('Template Structure', () => { + it('correctly assigns images to left and right template slots', () => { + const value: ImageCompareValue = { + before: 'https://example.com/before.jpg', + after: 'https://example.com/after.jpg' + } + const widget = createMockWidget(value) + const wrapper = mountComponent(widget) + + const images = wrapper.findAll('img') + // First image (before) should be in left template slot + expect(images[0].attributes('src')).toBe('https://example.com/before.jpg') + // Second image (after) should be in right template slot + expect(images[1].attributes('src')).toBe('https://example.com/after.jpg') + }) + }) + + describe('Integration', () => { + it('works with various URL types - data URLs and blob URLs', () => { + // Test data URLs + const dataUrl = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==' + const dataUrlValue: ImageCompareValue = { + before: dataUrl, + after: dataUrl + } + const dataUrlWrapper = mountComponent(createMockWidget(dataUrlValue)) + const dataUrlImages = dataUrlWrapper.findAll('img') + expect(dataUrlImages[0].attributes('src')).toBe(dataUrl) + expect(dataUrlImages[1].attributes('src')).toBe(dataUrl) + + // Test blob URLs + const blobUrl = + 'blob:http://example.com/12345678-1234-1234-1234-123456789012' + const blobUrlValue: ImageCompareValue = { + before: blobUrl, + after: blobUrl + } + const blobUrlWrapper = mountComponent(createMockWidget(blobUrlValue)) + const blobUrlImages = blobUrlWrapper.findAll('img') + expect(blobUrlImages[0].attributes('src')).toBe(blobUrl) + expect(blobUrlImages[1].attributes('src')).toBe(blobUrl) + }) + }) +}) diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetImageCompare.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetImageCompare.vue index e51413a30..1cf455c78 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetImageCompare.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetImageCompare.vue @@ -30,7 +30,7 @@ import { computed } from 'vue' import type { SimplifiedWidget } from '@/types/simplifiedWidget' -interface ImageCompareValue { +export interface ImageCompareValue { before: string after: string beforeAlt?: string