From 8ae50d140d2d760f7e379f81c48c2f7196f32e4a Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Sat, 13 Sep 2025 14:51:39 -0700 Subject: [PATCH] [test] Add component test for Vue button widget (#5468) * add component test for button widget * [move] relocate WidgetButton test to proper directory Move test from src/ to tests-ui/ directory structure and update import path to work from new location - addresses review comment about test organization * [refactor] make widget name optional in mock factory Add optional name parameter to createMockWidget function for more flexible test setup - addresses @DrJKL's suggestion about optional parameters * [refactor] use it.for for parameterized button tests Replace forEach loops with it.for syntax for testing button severities and variants - addresses @DrJKL's suggestion for better test structure * [auto-fix] Apply ESLint and Prettier fixes * Revert "[move] relocate WidgetButton test to proper directory" This reverts commit e9f4d57334fd054053903acf6e627638ef8f0333. * [test] increase rapid clicks test from 5 to 16 Better test coverage for concurrent click handling * name click number a shared variable --------- Co-authored-by: GitHub Action --- .../widgets/components/WidgetButton.test.ts | 248 ++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 src/renderer/extensions/vueNodes/widgets/components/WidgetButton.test.ts diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetButton.test.ts b/src/renderer/extensions/vueNodes/widgets/components/WidgetButton.test.ts new file mode 100644 index 000000000..72d6726a5 --- /dev/null +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetButton.test.ts @@ -0,0 +1,248 @@ +import { mount } from '@vue/test-utils' +import Button from 'primevue/button' +import type { ButtonProps } from 'primevue/button' +import PrimeVue from 'primevue/config' +import { describe, expect, it, vi } from 'vitest' + +import WidgetButton from '@/renderer/extensions/vueNodes/widgets/components/WidgetButton.vue' +import type { SimplifiedWidget } from '@/types/simplifiedWidget' + +describe('WidgetButton Interactions', () => { + const createMockWidget = ( + options: Partial = {}, + callback?: () => void, + name: string = 'test_button' + ): SimplifiedWidget => ({ + name, + type: 'button', + value: undefined, + options, + callback + }) + + const mountComponent = (widget: SimplifiedWidget, readonly = false) => { + return mount(WidgetButton, { + global: { + plugins: [PrimeVue], + components: { Button } + }, + props: { + widget, + readonly + } + }) + } + + const clickButton = async (wrapper: ReturnType) => { + const button = wrapper.findComponent({ name: 'Button' }) + await button.trigger('click') + return button + } + + describe('Click Handling', () => { + it('calls callback when button is clicked', async () => { + const mockCallback = vi.fn() + const widget = createMockWidget({}, mockCallback) + const wrapper = mountComponent(widget) + + await clickButton(wrapper) + + expect(mockCallback).toHaveBeenCalledTimes(1) + }) + + it('does not call callback when button is readonly', async () => { + const mockCallback = vi.fn() + const widget = createMockWidget({}, mockCallback) + const wrapper = mountComponent(widget, true) + + await clickButton(wrapper) + + expect(mockCallback).not.toHaveBeenCalled() + }) + + it('handles missing callback gracefully', async () => { + const widget = createMockWidget({}, undefined) + const wrapper = mountComponent(widget) + + // Should not throw error when clicking without callback + await expect(clickButton(wrapper)).resolves.toBeDefined() + }) + + it('calls callback multiple times when clicked multiple times', async () => { + const mockCallback = vi.fn() + const widget = createMockWidget({}, mockCallback) + const wrapper = mountComponent(widget) + + const numClicks = 8 + + await clickButton(wrapper) + for (let i = 0; i < numClicks; i++) { + await clickButton(wrapper) + } + + expect(mockCallback).toHaveBeenCalledTimes(numClicks) + }) + }) + + describe('Component Rendering', () => { + it('renders button component', () => { + const widget = createMockWidget() + const wrapper = mountComponent(widget) + + const button = wrapper.findComponent({ name: 'Button' }) + expect(button.exists()).toBe(true) + }) + + it('renders widget label when name is provided', () => { + const widget = createMockWidget() + const wrapper = mountComponent(widget) + + const label = wrapper.find('label') + expect(label.exists()).toBe(true) + expect(label.text()).toBe('test_button') + }) + + it('does not render label when widget name is empty', () => { + const widget = createMockWidget({}, undefined, '') + const wrapper = mountComponent(widget) + + const label = wrapper.find('label') + expect(label.exists()).toBe(false) + }) + + it('sets button size to small', () => { + const widget = createMockWidget() + const wrapper = mountComponent(widget) + + const button = wrapper.findComponent({ name: 'Button' }) + expect(button.props('size')).toBe('small') + }) + + it('passes widget options to button component', () => { + const buttonOptions = { + label: 'Custom Label', + icon: 'pi pi-check', + severity: 'success' as const + } + const widget = createMockWidget(buttonOptions) + const wrapper = mountComponent(widget) + + const button = wrapper.findComponent({ name: 'Button' }) + expect(button.props('label')).toBe('Custom Label') + expect(button.props('icon')).toBe('pi pi-check') + expect(button.props('severity')).toBe('success') + }) + }) + + describe('Readonly Mode', () => { + it('disables button when readonly', () => { + const widget = createMockWidget() + const wrapper = mountComponent(widget, true) + + // Test the actual DOM button element instead of the Vue component props + const buttonElement = wrapper.find('button') + expect(buttonElement.element.disabled).toBe(true) + }) + + it('enables button when not readonly', () => { + const widget = createMockWidget() + const wrapper = mountComponent(widget, false) + + // Test the actual DOM button element instead of the Vue component props + const buttonElement = wrapper.find('button') + expect(buttonElement.element.disabled).toBe(false) + }) + }) + + describe('Widget Options', () => { + it('handles button with text only', () => { + const widget = createMockWidget({ label: 'Click Me' }) + const wrapper = mountComponent(widget) + + const button = wrapper.findComponent({ name: 'Button' }) + expect(button.props('label')).toBe('Click Me') + expect(button.props('icon')).toBeNull() + }) + + it('handles button with icon only', () => { + const widget = createMockWidget({ icon: 'pi pi-star' }) + const wrapper = mountComponent(widget) + + const button = wrapper.findComponent({ name: 'Button' }) + expect(button.props('icon')).toBe('pi pi-star') + }) + + it('handles button with both text and icon', () => { + const widget = createMockWidget({ + label: 'Save', + icon: 'pi pi-save' + }) + const wrapper = mountComponent(widget) + + const button = wrapper.findComponent({ name: 'Button' }) + expect(button.props('label')).toBe('Save') + expect(button.props('icon')).toBe('pi pi-save') + }) + + it.for([ + 'secondary', + 'success', + 'info', + 'warning', + 'danger', + 'help', + 'contrast' + ] as const)('handles button severity: %s', (severity) => { + const widget = createMockWidget({ severity }) + const wrapper = mountComponent(widget) + const button = wrapper.findComponent({ name: 'Button' }) + expect(button.props('severity')).toBe(severity) + }) + + it.for(['outlined', 'text'] as const)( + 'handles button variant: %s', + (variant) => { + const widget = createMockWidget({ variant }) + const wrapper = mountComponent(widget) + const button = wrapper.findComponent({ name: 'Button' }) + expect(button.props('variant')).toBe(variant) + } + ) + }) + + describe('Edge Cases', () => { + it('handles widget with no options', () => { + const widget = createMockWidget() + const wrapper = mountComponent(widget) + + const button = wrapper.findComponent({ name: 'Button' }) + expect(button.exists()).toBe(true) + }) + + it('handles callback that throws error', async () => { + const mockCallback = vi.fn(() => { + throw new Error('Callback error') + }) + const widget = createMockWidget({}, mockCallback) + const wrapper = mountComponent(widget) + + // Should not break the component when callback throws + await expect(clickButton(wrapper)).rejects.toThrow('Callback error') + expect(mockCallback).toHaveBeenCalledTimes(1) + }) + + it('handles rapid consecutive clicks', async () => { + const mockCallback = vi.fn() + const widget = createMockWidget({}, mockCallback) + const wrapper = mountComponent(widget) + + // Simulate rapid clicks + const clickPromises = Array.from({ length: 16 }, () => + clickButton(wrapper) + ) + await Promise.all(clickPromises) + + expect(mockCallback).toHaveBeenCalledTimes(16) + }) + }) +})