import { VueWrapper, mount, shallowMount } from '@vue/test-utils' import { beforeEach, describe, expect, it } from 'vitest' import AmeliproClickableTile from '../AmeliproClickableTile.vue' import type { ComponentProps } from 'vue-component-type-helpers' import type { ExpectedPropOptions } from '@tests/types' import type { PropType } from 'vue' import type { RouteLocationRaw } from 'vue-router' import TestHelper from '@tests/helpers/TestHelper' // Stub VBtn qui préserve les attributs comme aria-label const VBtnStub = { template: '', inheritAttrs: false, } const expectedPropOptions: ExpectedPropOptions = { borderedIcon: { type: Boolean, default: false, }, disabled: { type: Boolean, default: false, }, href: { type: String, default: undefined, }, icon: { type: String, default: undefined, }, imgMaxWidth: { type: String, default: undefined, }, imgMinWidth: { type: String, default: undefined, }, imgSrc: { type: String, default: undefined, }, imgWidth: { type: String, default: '100px', }, tileTitle: { type: String, default: undefined, }, tileWidth: { type: String, default: '100%', }, to: { type: Object as PropType, default: undefined, }, uniqueId: { type: String, default: undefined, }, onlyIconIsClickable: { type: Boolean, default: false, }, } // Values pour les props "required" const requiredPropValues = (): ComponentProps => ({ icon: 'utilisateur', tileTitle: 'titre de la tuile', uniqueId: 'required-unique-id', }) // Valeurs pour les props "modified" const modifiedPropValues = (): ComponentProps => ({ icon: 'document', tileTitle: 'titre modifié', uniqueId: 'modified-unique-id', }) const testHelper = new TestHelper(AmeliproClickableTile) testHelper.setExpectedPropOptions(expectedPropOptions) .setRequiredPropValues(requiredPropValues) .setModifiedPropValues(modifiedPropValues) describe('AmeliproClickableTile', () => { describe('Snapshots', () => { testHelper.snapshots() }) describe('Main container', () => { let vueWrapper: VueWrapper> beforeEach(() => { vueWrapper = mount(AmeliproClickableTile, { props: { icon: 'utilisateur', tileTitle: 'Test', uniqueId: 'test-tile', }, global: { stubs: { VBtn: VBtnStub } }, }) }) it('prop uniqueId sets attribute id on container', async () => { expect(vueWrapper.attributes('id')).toBe('test-tile') await vueWrapper.setProps({ uniqueId: 'new-id' }) expect(vueWrapper.attributes('id')).toBe('new-id') }) it('prop tileWidth sets width attribute', async () => { expect(vueWrapper.find('button').attributes('width')).toBe('100%') await vueWrapper.setProps({ tileWidth: '50%' }) expect(vueWrapper.find('button').attributes('width')).toBe('50%') }) it('prop disabled disables the button', async () => { expect(vueWrapper.find('button').attributes('disabled')).toBeUndefined() await vueWrapper.setProps({ disabled: true }) expect(vueWrapper.find('button').attributes('disabled')).toBeDefined() }) it('prop href sets href attribute', async () => { expect(vueWrapper.find('button').attributes('href')).toBeUndefined() await vueWrapper.setProps({ href: 'https://example.com' }) expect(vueWrapper.find('button').attributes('href')).toBe('https://example.com') }) it('prop to sets to attribute', async () => { expect(vueWrapper.find('button').attributes('to')).toBeUndefined() await vueWrapper.setProps({ to: '/dashboard' }) expect(vueWrapper.find('button').attributes('to')).toBe('/dashboard') }) }) describe('Icon or Image display', () => { let vueWrapper: VueWrapper> beforeEach(() => { vueWrapper = mount(AmeliproClickableTile, { props: { icon: 'utilisateur', tileTitle: 'Test', }, global: { stubs: { VBtn: VBtnStub } }, }) }) it('displays icon when icon prop is provided', async () => { expect(vueWrapper.findComponent({ name: 'AmeliproIcon' }).exists()).toBe(true) }) it('displays image when imgSrc prop is provided', async () => { await vueWrapper.setProps({ icon: undefined, imgSrc: 'https://example.com/image.png' }) expect(vueWrapper.find('img').exists()).toBe(true) expect(vueWrapper.find('img').attributes('src')).toBe('https://example.com/image.png') }) it('prop imgWidth sets image width style', async () => { await vueWrapper.setProps({ icon: undefined, imgSrc: 'https://example.com/image.png' }) expect(vueWrapper.find('img').attributes('style')).toContain('width: 100px') await vueWrapper.setProps({ imgWidth: '150px' }) expect(vueWrapper.find('img').attributes('style')).toContain('width: 150px') }) it('prop imgMaxWidth sets image maxWidth style', async () => { await vueWrapper.setProps({ icon: undefined, imgSrc: 'https://example.com/image.png', imgMaxWidth: '200px', }) expect(vueWrapper.find('img').attributes('style')).toContain('max-width: 200px') }) it('prop imgMinWidth sets image minWidth style', async () => { await vueWrapper.setProps({ icon: undefined, imgSrc: 'https://example.com/image.png', imgMinWidth: '50px', }) expect(vueWrapper.find('img').attributes('style')).toContain('min-width: 50px') }) it('does not display both icon and image', async () => { await vueWrapper.setProps({ icon: 'utilisateur', imgSrc: 'https://example.com/image.png', }) const images = vueWrapper.findAll('img') const icons = vueWrapper.findAllComponents({ name: 'AmeliproIcon' }) expect(images.length === 0 || icons.length === 0).toBe(true) }) }) describe('Title display', () => { let vueWrapper: VueWrapper> beforeEach(() => { vueWrapper = mount(AmeliproClickableTile, { props: { icon: 'utilisateur', tileTitle: 'titre de la tuile', }, global: { stubs: { VBtn: VBtnStub } }, }) }) it('prop tileTitle displays title text', async () => { expect(vueWrapper.text()).toContain('titre de la tuile') await vueWrapper.setProps({ tileTitle: 'nouveau titre' }) expect(vueWrapper.text()).toContain('nouveau titre') }) it('displays slot content when provided', async () => { const slotWrapper = mount(AmeliproClickableTile, { props: { icon: 'utilisateur' }, slots: { default: 'Slot content', }, global: { stubs: { VBtn: VBtnStub } }, }) expect(slotWrapper.text()).toContain('Slot content') }) }) describe('Icon styling', () => { let vueWrapper: VueWrapper> beforeEach(() => { vueWrapper = mount(AmeliproClickableTile, { props: { icon: 'utilisateur', tileTitle: 'Test', }, global: { stubs: { VBtn: VBtnStub } }, }) }) it('prop borderedIcon changes icon style', async () => { let icons = vueWrapper.findAllComponents({ name: 'AmeliproIcon' }) expect(icons.length).toBeGreaterThan(0) expect(icons[0]?.props('bordered')).toBe(false) await vueWrapper.setProps({ borderedIcon: true }) icons = vueWrapper.findAllComponents({ name: 'AmeliproIcon' }) expect(icons[0]?.props('bordered')).toBe(true) }) it('icon has correct size', async () => { const icons = vueWrapper.findAllComponents({ name: 'AmeliproIcon' }) expect(icons.length).toBeGreaterThan(0) expect(icons[0]?.props('size')).toBe('32px') }) it('arrow icon has correct size', async () => { const icons = vueWrapper.findAllComponents({ name: 'AmeliproIcon' }) expect(icons.length).toBeGreaterThan(0) const arrowIcon = icons[icons.length - 1] expect(arrowIcon?.props('size')).toBe('16px') expect(arrowIcon?.props('icon')).toBe('chevronRight') }) }) describe('onlyIconIsClickable mode', () => { let vueWrapper: VueWrapper> beforeEach(() => { vueWrapper = mount(AmeliproClickableTile, { props: { icon: 'utilisateur', tileTitle: 'Test', uniqueId: 'test-tile', onlyIconIsClickable: true, }, global: { stubs: { VBtn: VBtnStub } }, }) }) it('renders span wrapper instead of VBtn when onlyIconIsClickable is true', () => { expect(vueWrapper.find('span.amelipro-clickable-tile').exists()).toBe(true) }) it('prop uniqueId sets attribute id on span wrapper', async () => { expect(vueWrapper.find('span.amelipro-clickable-tile').attributes('id')).toBe('test-tile') await vueWrapper.setProps({ uniqueId: 'new-id' }) expect(vueWrapper.find('span.amelipro-clickable-tile').attributes('id')).toBe('new-id') }) it('contains a VBtn for icon button only', () => { const buttons = vueWrapper.findAll('button') expect(buttons.length > 0).toBe(true) }) it('icon button has uniqueId suffix', async () => { const buttons = vueWrapper.findAll('button') expect(buttons.length).toBeGreaterThan(0) expect(buttons[0]?.attributes('id')).toBe('test-tile-icon-button') }) it('content is not clickable, only icon button is', () => { const span = vueWrapper.find('span.amelipro-clickable-tile') // Span should not have onclick handler expect(span.attributes('onclick')).toBeUndefined() }) }) describe('Events', () => { let vueWrapper: VueWrapper> beforeEach(() => { vueWrapper = shallowMount(AmeliproClickableTile, { props: { icon: 'utilisateur', tileTitle: 'Test', uniqueId: 'test-id', }, }) }) it('emits click event when clicked in normal mode', async () => { expect(vueWrapper.emitted('click')).toBeUndefined() await vueWrapper.trigger('click') expect(vueWrapper.emitted('click')).toBeTruthy() expect(vueWrapper.emitted('click')?.[0]).toEqual(['test-id']) }) it('emits click event with uniqueId', async () => { await vueWrapper.setProps({ uniqueId: 'new-id' }) await vueWrapper.trigger('click') expect(vueWrapper.emitted('click')?.[0]).toEqual(['new-id']) }) it('does not emit click when disabled', async () => { await vueWrapper.setProps({ disabled: true }) // VBtn with disabled prop should not emit click }) }) describe('Hover and Focus states', () => { let vueWrapper: VueWrapper> beforeEach(() => { vueWrapper = mount(AmeliproClickableTile, { props: { icon: 'utilisateur', tileTitle: 'Test', }, global: { stubs: { VBtn: VBtnStub } }, }) }) it('applies hover class on hover', async () => { const btn = vueWrapper.find('button') await btn.trigger('mouseenter') // Check that the hover class is applied to the container expect(vueWrapper.classes()).toContain('amelipro-clickable-tile--hover') }) it('applies focus class on focus', async () => { const btn = vueWrapper.find('button') await btn.trigger('focus') // Check that the hover/focus class is applied to the container expect(vueWrapper.classes()).toContain('amelipro-clickable-tile--hover') }) it('removes hover state on mouseleave', async () => { const btn = vueWrapper.find('button') await btn.trigger('mouseenter') expect(vueWrapper.classes()).toContain('amelipro-clickable-tile--hover') await btn.trigger('mouseleave') // State should be reset expect(vueWrapper.classes()).not.toContain('amelipro-clickable-tile--hover') }) it('removes focus state on blur', async () => { const btn = vueWrapper.find('button') await btn.trigger('focus') expect(vueWrapper.classes()).toContain('amelipro-clickable-tile--hover') await btn.trigger('blur') // State should be reset expect(vueWrapper.classes()).not.toContain('amelipro-clickable-tile--hover') }) }) describe('Disabled state styling', () => { let vueWrapper: VueWrapper> beforeEach(() => { vueWrapper = mount(AmeliproClickableTile, { props: { icon: 'utilisateur', tileTitle: 'Test', disabled: true, }, global: { stubs: { VBtn: VBtnStub } }, }) }) it('applies disabled styling to button', () => { expect(vueWrapper.find('button').attributes('disabled')).toBeDefined() }) it('button has disabled attribute when disabled prop is true', async () => { expect(vueWrapper.find('button').attributes('disabled')).toBeDefined() await vueWrapper.setProps({ disabled: false }) expect(vueWrapper.find('button').attributes('disabled')).toBeUndefined() }) }) describe('ID generation', () => { it('generates proper IDs for all elements when uniqueId is provided', () => { const vueWrapper = mount(AmeliproClickableTile, { props: { icon: 'utilisateur', tileTitle: 'Test', uniqueId: 'test-tile', }, global: { stubs: { VBtn: VBtnStub } }, }) expect(vueWrapper.attributes('id')).toBe('test-tile') expect(vueWrapper.find('#test-tile-icon').exists()).toBe(true) expect(vueWrapper.find('#test-tile-icon-arrow').exists()).toBe(true) }) it('works without uniqueId', () => { const vueWrapper = mount(AmeliproClickableTile, { props: { icon: 'utilisateur', tileTitle: 'Test', }, global: { stubs: { VBtn: VBtnStub } }, }) expect(vueWrapper.attributes('id')).toBeUndefined() expect(vueWrapper.find('[id*="-icon"]').exists()).toBe(false) }) }) })