diff --git a/ui/src/stores/__tests__/useUi.spec.js b/ui/src/stores/__tests__/useUi.spec.js index e869c82..59be8b1 100644 --- a/ui/src/stores/__tests__/useUi.spec.js +++ b/ui/src/stores/__tests__/useUi.spec.js @@ -48,10 +48,16 @@ describe('useUi Store', () => { expect(store.accentColorTareas).toBe('#4CAF50') expect(store.accentColorPlanillas).toBe('#FF9800') expect(store.accentColorAsistencias).toBe('#E91E63') - expect(localStorageMock.getItem).toHaveBeenCalledWith(APPEARANCE_STORAGE_KEY) - }) - - it('loads settings from localStorage including new accent colors if present', () => { + expect(store.accentColorConfiguracion).toBe('#607D8B') + // Check new default view defaults + expect(store.defaultViewEmpleados).toBe('table') + expect(store.defaultViewTareas).toBe('table') + expect(store.defaultViewPlanillas).toBe('table') + expect(store.defaultViewAsistencias).toBe('table') + expect(store.defaultViewConfiguracion).toBe('table') + // Check other new defaults + expect(store.tableBgColorEmpleados).toBe('#FFFFFF') + expect(store.desktopNavbarPersistent).toBe(false) expect(localStorageMock.getItem).toHaveBeenCalledWith(APPEARANCE_STORAGE_KEY) }) @@ -65,7 +71,18 @@ describe('useUi Store', () => { accentColorTareas: '#00FF00', accentColorPlanillas: '#FFFF00', accentColorAsistencias: '#FF00FF', - // other settings... + accentColorConfiguracion: '#112233', + tableBgColorEmpleados: '#EEEEEE', + tableBgColorTareas: '#DDDDDD', + tableBgColorPlanillas: '#CCCCCC', + tableBgColorAsistencias: '#BBBBBB', + tableBgColorConfiguracion: '#AAAAAA', + desktopNavbarPersistent: true, + defaultViewEmpleados: 'card', + defaultViewTareas: 'card', + defaultViewPlanillas: 'card', + defaultViewAsistencias: 'card', + defaultViewConfiguracion: 'card', } localStorageMock.getItem.mockReturnValueOnce(JSON.stringify(storedSettings)) @@ -80,9 +97,21 @@ describe('useUi Store', () => { expect(store.accentColorTareas).toBe('#00FF00') expect(store.accentColorPlanillas).toBe('#FFFF00') expect(store.accentColorAsistencias).toBe('#FF00FF') + expect(store.accentColorConfiguracion).toBe('#112233') + expect(store.tableBgColorEmpleados).toBe('#EEEEEE') + expect(store.tableBgColorTareas).toBe('#DDDDDD') + expect(store.tableBgColorPlanillas).toBe('#CCCCCC') + expect(store.tableBgColorAsistencias).toBe('#BBBBBB') + expect(store.tableBgColorConfiguracion).toBe('#AAAAAA') + expect(store.desktopNavbarPersistent).toBe(true) + expect(store.defaultViewEmpleados).toBe('card') + expect(store.defaultViewTareas).toBe('card') + expect(store.defaultViewPlanillas).toBe('card') + expect(store.defaultViewAsistencias).toBe('card') + expect(store.defaultViewConfiguracion).toBe('card') }) - it('loads older settings from localStorage and uses defaults for new accent colors if not present', () => { + it('loads older settings from localStorage and uses defaults for new settings if not present', () => { const olderStoredSettings = { primaryColor: '#ABCDEF', theme: 'dark', @@ -96,10 +125,20 @@ describe('useUi Store', () => { expect(store.theme).toBe('dark') expect(store.fontSize).toBe(18) // New accent colors should fall back to defaults - expect(store.accentColorEmpleados).toBe('#2196F3') - expect(store.accentColorTareas).toBe('#4CAF50') - expect(store.accentColorPlanillas).toBe('#FF9800') - expect(store.accentColorAsistencias).toBe('#E91E63') + expect(store.accentColorEmpleados).toBe('#2196F3') // Default + expect(store.accentColorTareas).toBe('#4CAF50') // Default + expect(store.accentColorPlanillas).toBe('#FF9800') // Default + expect(store.accentColorAsistencias).toBe('#E91E63') // Default + expect(store.accentColorConfiguracion).toBe('#607D8B') // Default + // New default view keys should fall back to 'table' + expect(store.defaultViewEmpleados).toBe('table') + expect(store.defaultViewTareas).toBe('table') + expect(store.defaultViewPlanillas).toBe('table') + expect(store.defaultViewAsistencias).toBe('table') + expect(store.defaultViewConfiguracion).toBe('table') + // Other new keys + expect(store.tableBgColorEmpleados).toBe('#FFFFFF') // Default + expect(store.desktopNavbarPersistent).toBe(false) // Default }) it('falls back to default settings if localStorage data is invalid JSON', () => { @@ -129,11 +168,14 @@ describe('useUi Store', () => { const appearanceSettingKeysInTest = [ 'primaryColor', 'secondaryColor', 'warningColor', 'fontFamily', 'fontSize', 'animationsEnabled', 'backgroundColor', 'theme', - 'accentColorEmpleados', 'accentColorTareas', 'accentColorPlanillas', 'accentColorAsistencias', - const appearanceSettingKeys = [ - 'primaryColor', 'secondaryColor', 'warningColor', 'fontFamily', - 'fontSize', 'animationsEnabled', 'backgroundColor', 'theme', - ] + 'accentColorEmpleados', 'accentColorTareas', 'accentColorPlanillas', + 'accentColorAsistencias', 'accentColorConfiguracion', + 'tableBgColorEmpleados', 'tableBgColorTareas', 'tableBgColorPlanillas', + 'tableBgColorAsistencias', 'tableBgColorConfiguracion', + 'desktopNavbarPersistent', + 'defaultViewEmpleados', 'defaultViewTareas', 'defaultViewPlanillas', + 'defaultViewAsistencias', 'defaultViewConfiguracion', + ]; it('setPrimaryColor updates state and saves to localStorage', () => { const store = useUi() @@ -215,7 +257,9 @@ describe('useUi Store', () => { expect(Object.keys(savedData).length).toBe(appearanceSettingKeysInTest.length); expect(savedData.sidebarOpen).toBeUndefined() // Ensure non-appearance data is not saved appearanceSettingKeysInTest.forEach(key => { - expect(savedData.hasOwnProperty(key)).toBe(true) + //This assertion needs to be robust if some keys are not initialized (e.g. undefined) + //However, our store initializes all appearance keys + expect(savedData.hasOwnProperty(key)).toBe(true); }) }) @@ -259,11 +303,100 @@ describe('useUi Store', () => { expect.stringContaining('"accentColorAsistencias":"#FF7788"') ) }) - expect(Object.keys(savedData).length).toBe(appearanceSettingKeys.length); - expect(savedData.sidebarOpen).toBeUndefined() // Ensure non-appearance data is not saved - appearanceSettingKeys.forEach(key => { - expect(savedData.hasOwnProperty(key)).toBe(true) - }) + + it('setAccentColorConfiguracion updates state and saves to localStorage', () => { + const store = useUi() + store.setAccentColorConfiguracion('#99AABB') + expect(store.accentColorConfiguracion).toBe('#99AABB') + expect(localStorageMock.setItem).toHaveBeenCalledWith( + APPEARANCE_STORAGE_KEY, + expect.stringContaining('"accentColorConfiguracion":"#99AABB"') + ) + }) + + // Tests for table background color actions + it('setTableBgColorEmpleados updates state and saves to localStorage', () => { + const store = useUi() + store.setTableBgColorEmpleados('#EEECCC') + expect(store.tableBgColorEmpleados).toBe('#EEECCC') + expect(localStorageMock.setItem).toHaveBeenCalledWith( + APPEARANCE_STORAGE_KEY, + expect.stringContaining('"tableBgColorEmpleados":"#EEECCC"') + ) + }) + // Similar tests for Tareas, Planillas, Asistencias, Configuracion table bg colors... + + it('setDesktopNavbarPersistent updates state and saves to localStorage', () => { + const store = useUi() + store.setDesktopNavbarPersistent(true) + expect(store.desktopNavbarPersistent).toBe(true) + expect(localStorageMock.setItem).toHaveBeenCalledWith( + APPEARANCE_STORAGE_KEY, + expect.stringContaining('"desktopNavbarPersistent":true') + ) + store.setDesktopNavbarPersistent(false) + expect(store.desktopNavbarPersistent).toBe(false) + expect(localStorageMock.setItem).toHaveBeenCalledWith( + APPEARANCE_STORAGE_KEY, + expect.stringContaining('"desktopNavbarPersistent":false') + ) + }) + + // Tests for new default view actions + it('setDefaultViewEmpleados updates state and saves to localStorage', () => { + const store = useUi() + store.setDefaultViewEmpleados('card') + expect(store.defaultViewEmpleados).toBe('card') + expect(localStorageMock.setItem).toHaveBeenCalledWith( + APPEARANCE_STORAGE_KEY, + expect.stringContaining('"defaultViewEmpleados":"card"') + ) + store.setDefaultViewEmpleados('table') + expect(store.defaultViewEmpleados).toBe('table') + expect(localStorageMock.setItem).toHaveBeenCalledWith( + APPEARANCE_STORAGE_KEY, + expect.stringContaining('"defaultViewEmpleados":"table"') + ) + }) + + it('setDefaultViewTareas updates state and saves to localStorage', () => { + const store = useUi() + store.setDefaultViewTareas('card') + expect(store.defaultViewTareas).toBe('card') + expect(localStorageMock.setItem).toHaveBeenCalledWith( + APPEARANCE_STORAGE_KEY, + expect.stringContaining('"defaultViewTareas":"card"') + ) + }) + + it('setDefaultViewPlanillas updates state and saves to localStorage', () => { + const store = useUi() + store.setDefaultViewPlanillas('card') + expect(store.defaultViewPlanillas).toBe('card') + expect(localStorageMock.setItem).toHaveBeenCalledWith( + APPEARANCE_STORAGE_KEY, + expect.stringContaining('"defaultViewPlanillas":"card"') + ) + }) + + it('setDefaultViewAsistencias updates state and saves to localStorage', () => { + const store = useUi() + store.setDefaultViewAsistencias('card') + expect(store.defaultViewAsistencias).toBe('card') + expect(localStorageMock.setItem).toHaveBeenCalledWith( + APPEARANCE_STORAGE_KEY, + expect.stringContaining('"defaultViewAsistencias":"card"') + ) + }) + + it('setDefaultViewConfiguracion updates state and saves to localStorage', () => { + const store = useUi() + store.setDefaultViewConfiguracion('card') + expect(store.defaultViewConfiguracion).toBe('card') + expect(localStorageMock.setItem).toHaveBeenCalledWith( + APPEARANCE_STORAGE_KEY, + expect.stringContaining('"defaultViewConfiguracion":"card"') + ) }) }) }) diff --git a/ui/src/stores/useUi.js b/ui/src/stores/useUi.js index 6aae6ae..45eb65f 100644 --- a/ui/src/stores/useUi.js +++ b/ui/src/stores/useUi.js @@ -24,6 +24,12 @@ const appearanceSettingKeys = [ 'tableBgColorAsistencias', 'tableBgColorConfiguracion', 'desktopNavbarPersistent', + // Default module views + 'defaultViewEmpleados', + 'defaultViewTareas', + 'defaultViewPlanillas', + 'defaultViewAsistencias', + 'defaultViewConfiguracion', ] const loadSettingsFromLocalStorage = () => { @@ -92,6 +98,12 @@ export const useUi = defineStore('ui', { tableBgColorAsistencias: '#FFFFFF', tableBgColorConfiguracion: '#FFFFFF', desktopNavbarPersistent: false, + // Default module views + 'defaultViewEmpleados': 'table', + 'defaultViewTareas': 'table', + 'defaultViewPlanillas': 'table', + 'defaultViewAsistencias': 'table', + 'defaultViewConfiguracion': 'table', } const loadedSettings = loadSettingsFromLocalStorage() @@ -203,6 +215,28 @@ export const useUi = defineStore('ui', { setDesktopNavbarPersistent(enabled) { this.desktopNavbarPersistent = !!enabled // Ensure boolean _saveAppearanceState(this) + }, + + // Actions for default module views + setDefaultViewEmpleados(view) { + this.defaultViewEmpleados = view + _saveAppearanceState(this) + }, + setDefaultViewTareas(view) { + this.defaultViewTareas = view + _saveAppearanceState(this) + }, + setDefaultViewPlanillas(view) { + this.defaultViewPlanillas = view + _saveAppearanceState(this) + }, + setDefaultViewAsistencias(view) { + this.defaultViewAsistencias = view + _saveAppearanceState(this) + }, + setDefaultViewConfiguracion(view) { + this.defaultViewConfiguracion = view + _saveAppearanceState(this) } }, }) diff --git a/ui/src/views/SettingsView.vue b/ui/src/views/SettingsView.vue index 35331e5..6553161 100644 --- a/ui/src/views/SettingsView.vue +++ b/ui/src/views/SettingsView.vue @@ -78,7 +78,7 @@

Empleados Module

-
+
+
+ + +

Tareas Module

-
+
+
+ + +

Planillas Module

-
+
+
+ + +

Asistencias Module

-
+
+
+ + +

Configuración Module

-
+
+
+ + +
diff --git a/ui/src/views/__tests__/SettingsView.spec.js b/ui/src/views/__tests__/SettingsView.spec.js index 0af6493..aff71cd 100644 --- a/ui/src/views/__tests__/SettingsView.spec.js +++ b/ui/src/views/__tests__/SettingsView.spec.js @@ -4,89 +4,68 @@ import { createPinia, setActivePinia } from 'pinia' import { useUi } from '../../stores/useUi' // Adjust path import SettingsView from '../SettingsView.vue' // Adjust path -// Helper to create a fresh store for each test or group -const getFreshStore = () => { - setActivePinia(createPinia()) - return useUi() -} +// No global 'store' variable here, it will be test-specific or wrapper-specific +// Helper to manage store and wrapper creation for tests +const setupTestEnvironment = (initialStoreState = {}) => { + setActivePinia(createPinia()); // Ensures a fresh Pinia instance + + // Mock localStorage for the store if not already globally mocked by Vitest setup + // Ensure this mock is consistent with how useUi.js uses localStorage + const localStorageMock = (() => { + let lsStore = { ...initialStoreState }; // Initialize with initialStoreState for loading + return { + getItem: vi.fn((key) => { + const value = lsStore[key]; + // Pinia/useUi expects JSON string or null + return typeof value === 'object' ? JSON.stringify(value) : value; + }), + setItem: vi.fn((key, value) => { + // Store as string, as localStorage does + lsStore[key] = value.toString(); + // Make actual saved string available for inspection if needed + // This helps verify what _saveAppearanceState would do + if (key === 'appearanceSettings') { + lsStore['_raw_appearanceSettings'] = value.toString(); + } + }), + clear: vi.fn(() => { lsStore = {}; }), + removeItem: vi.fn((key) => { delete lsStore[key]; }), + // Helper to get the raw string for assertion + getRawItem: (key) => lsStore[key] + }; + })(); + Object.defineProperty(window, 'localStorage', { value: localStorageMock, writable: true }); + + // If initialStoreState is provided, simulate it being in localStorage + // The useUi store reads from localStorage upon creation. + if (Object.keys(initialStoreState).length > 0) { + localStorage.setItem('appearanceSettings', JSON.stringify(initialStoreState)); + } else { + localStorage.removeItem('appearanceSettings'); // Ensure no settings if none provided + } + + const store = useUi(); // This will now load from the mocked localStorage + + const wrapper = mount(SettingsView, { + global: { + // Pinia store is automatically available due to setActivePinia + }, + }); + + return { wrapper, store, localStorageMock }; +}; + describe('SettingsView.vue', () => { - // No global 'store' variable here, it will be test-specific or wrapper-specific - beforeEach(() => { - // Ensure a fresh Pinia instance is active for each test. - setActivePinia(createPinia()) - - // Clear localStorage mock before each test - // Note: The mock itself is defined globally in this file or via setupFiles in Vitest config. - // Re-stubbing it or ensuring its state is clean. - const localStorageMock = window.localStorage; // Assuming it's already stubbed globally by Vitest setup or top of file - localStorageMock.clear(); - if (localStorageMock.setItem.mockClear) { // vi.fn() specific - localStorageMock.setItem.mockClear(); - localStorageMock.getItem.mockClear(); - } - }) - - // createWrapper now also handles store creation and returns the store for spying if needed - const createWrapperAndStore = (initialStoreState = {}) => { - if (Object.keys(initialStoreState).length > 0) { - localStorage.setItem('appearanceSettings', JSON.stringify(initialStoreState)); - } - - const currentStore = useUi(); // Store is created AFTER localStorage is set for this test - - const wrapper = mount(SettingsView, { - global: { - // Pinia is active via setActivePinia, component should pick it up - }, - }); - return { wrapper, store: currentStore }; // Return store for spying - } + // Vitest's vi.clearAllMocks() might be useful here if mocks persist unexpectedly + // This setupTestEnvironment function already handles fresh store and localStorage mock per call. + // If window.localStorage was stubbed globally (e.g. in vitest.setup.js), ensure it's reset. + // For this example, setupTestEnvironment manages its own localStorage mock. + }); it('renders all input elements with initial values from store', () => { - const { wrapper } = createWrapperAndStore({ // Store is created inside here after LS mock - let store - - beforeEach(() => { - // Create a new Pinia instance and activate it for each test - // This also resets the store state for each test - store = getFreshStore() - - // Mock localStorage for the store - const localStorageMock = (() => { - let lsStore = {} - return { - getItem: vi.fn((key) => lsStore[key] || null), - setItem: vi.fn((key, value) => { lsStore[key] = value.toString() }), - clear: vi.fn(() => { lsStore = {} }), - removeItem: vi.fn((key) => { delete lsStore[key] }), - } - })() - Object.defineProperty(window, 'localStorage', { value: localStorageMock, writable: true }) - localStorageMock.clear() - }) - - const createWrapper = (initialStoreState = {}) => { - // Apply initial state to the store if provided - // This is a bit of a workaround as direct state mutation isn't ideal, - // but for testing initial binding it can be simpler than calling actions. - // Alternatively, set up localStorage then init store. - if (Object.keys(initialStoreState).length > 0) { - localStorage.setItem('appearanceSettings', JSON.stringify(initialStoreState)) - } - // Re-initialize store to pick up mocked localStorage if initialStoreState was set - store = getFreshStore() - - return mount(SettingsView, { - global: { - // plugins: [store.$pinia], // Removed: setActivePinia should make it available - }, - }) - } - - it('renders all input elements with initial values from store', () => { - const wrapper = createWrapper({ + const { wrapper } = setupTestEnvironment({ primaryColor: '#111111', secondaryColor: '#222222', warningColor: '#333333', @@ -96,148 +75,181 @@ describe('SettingsView.vue', () => { animationsEnabled: false, theme: 'dark', // Add new accent colors for comprehensive initial state testing + primaryColor: '#111111', + secondaryColor: '#222222', + warningColor: '#333333', + backgroundColor: '#444444', + fontFamily: 'Arial', + fontSize: 18, + animationsEnabled: false, + theme: 'dark', accentColorEmpleados: '#123456', accentColorTareas: '#654321', accentColorPlanillas: '#abcdef', accentColorAsistencias: '#fedcba', - }) + accentColorConfiguracion: '#aabbcc', + defaultViewEmpleados: 'card', // New default view + defaultViewTareas: 'table', + defaultViewPlanillas: 'card', + defaultViewAsistencias: 'table', + defaultViewConfiguracion: 'card', + }); - // Check general appearance settings - }) - - expect(wrapper.find('input#primaryColor').element.value).toBe('#111111') - expect(wrapper.find('input#secondaryColor').element.value).toBe('#222222') + expect(wrapper.find('input#primaryColor').element.value).toBe('#111111'); + expect(wrapper.find('input#secondaryColor').element.value).toBe('#222222'); expect(wrapper.find('input#warningColor').element.value).toBe('#333333') expect(wrapper.find('input#backgroundColor').element.value).toBe('#444444') expect(wrapper.find('input#fontFamily').element.value).toBe('Arial') expect(wrapper.find('input#fontSize').element.value).toBe('18') - expect(wrapper.find('input#animationsEnabled').element.checked).toBe(false) - expect(wrapper.find('select#theme').element.value).toBe('dark') + expect(wrapper.find('input#animationsEnabled').element.checked).toBe(false); + expect(wrapper.find('select#theme').element.value).toBe('dark'); - // Check new module accent color pickers - const h2Elements = wrapper.findAll('h2') - const sectionTitles = h2Elements.map(h => h.text()) - expect(sectionTitles).toContain('Module Accent Colors') - expect(wrapper.find('input#accentColorEmpleados').element.value).toBe('#123456') - expect(wrapper.find('input#accentColorTareas').element.value).toBe('#654321') - expect(wrapper.find('input#accentColorPlanillas').element.value).toBe('#abcdef') - expect(wrapper.find('input#accentColorAsistencias').element.value).toBe('#fedcba') - }) + // Check module accent color pickers + expect(wrapper.find('input#accentColorEmpleados').element.value).toBe('#123456'); + expect(wrapper.find('input#accentColorTareas').element.value).toBe('#654321'); + expect(wrapper.find('input#accentColorPlanillas').element.value).toBe('#abcdef'); + expect(wrapper.find('input#accentColorAsistencias').element.value).toBe('#fedcba'); + expect(wrapper.find('input#accentColorConfiguracion').element.value).toBe('#aabbcc'); + + // Check new default view select elements + expect(wrapper.find('select#defaultViewEmpleados').element.value).toBe('card'); + expect(wrapper.find('select#defaultViewTareas').element.value).toBe('table'); + expect(wrapper.find('select#defaultViewPlanillas').element.value).toBe('card'); + expect(wrapper.find('select#defaultViewAsistencias').element.value).toBe('table'); + expect(wrapper.find('select#defaultViewConfiguracion').element.value).toBe('card'); + }); it('calls setPrimaryColor action when primary color input changes', async () => { - const { wrapper, store } = createWrapperAndStore() - const spy = vi.spyOn(store, 'setPrimaryColor') - const colorInput = wrapper.find('input#primaryColor') - - colorInput.element.value = '#FF00FF' - await colorInput.trigger('input') - - expect(spy).toHaveBeenCalledWith('#ff00ff') - }) + const { wrapper, store } = setupTestEnvironment(); + const spy = vi.spyOn(store, 'setPrimaryColor'); + const colorInput = wrapper.find('input#primaryColor'); + colorInput.element.value = '#FF00FF'; + await colorInput.trigger('input'); + expect(spy).toHaveBeenCalledWith('#ff00ff'); + }); it('calls setFontFamily action when font family input changes', async () => { - const { wrapper, store } = createWrapperAndStore() - const spy = vi.spyOn(store, 'setFontFamily') - const input = wrapper.find('input#fontFamily') - await input.setValue('Helvetica') - }) - - it('calls setPrimaryColor action when primary color input changes', async () => { - const wrapper = createWrapper() - const spy = vi.spyOn(store, 'setPrimaryColor') - const colorInput = wrapper.find('input#primaryColor') - - // Simulate color picker actually setting the value and then dispatching input - // For input type=color, setting .value and then .trigger('input') is typical - colorInput.element.value = '#FF00FF' - await colorInput.trigger('input') - - expect(spy).toHaveBeenCalledWith('#ff00ff') // Changed to lowercase - }) - - it('calls setFontFamily action when font family input changes', async () => { - const wrapper = createWrapper() - const spy = vi.spyOn(store, 'setFontFamily') - const input = wrapper.find('input#fontFamily') - await input.setValue('Helvetica') // .setValue also triggers 'input' - expect(spy).toHaveBeenCalledWith('Helvetica') - }) + const { wrapper, store } = setupTestEnvironment(); + const spy = vi.spyOn(store, 'setFontFamily'); + const input = wrapper.find('input#fontFamily'); + await input.setValue('Helvetica'); + expect(spy).toHaveBeenCalledWith('Helvetica'); + }); it('calls setFontSize action when font size input changes', async () => { - const { wrapper, store } = createWrapperAndStore() - const wrapper = createWrapper() - const spy = vi.spyOn(store, 'setFontSize') - const input = wrapper.find('input#fontSize') - await input.setValue('22') - expect(spy).toHaveBeenCalledWith(22) - }) + const { wrapper, store } = setupTestEnvironment(); + const spy = vi.spyOn(store, 'setFontSize'); + const input = wrapper.find('input#fontSize'); + await input.setValue('22'); + expect(spy).toHaveBeenCalledWith(22); + }); it('calls setAnimationsEnabled action when animations checkbox changes', async () => { - const { wrapper, store } = createWrapperAndStore({ animationsEnabled: true }) - const spy = vi.spyOn(store, 'setAnimationsEnabled') - const checkbox = wrapper.find('input#animationsEnabled') - - await checkbox.setChecked(false) - const wrapper = createWrapper({ animationsEnabled: true }) // Start with true - const spy = vi.spyOn(store, 'setAnimationsEnabled') - const checkbox = wrapper.find('input#animationsEnabled') - - // For checkboxes, .setValue(false) or .setChecked(false) and then trigger 'change' - await checkbox.setChecked(false) // This should trigger the change event for v-model - - expect(spy).toHaveBeenCalledWith(false) - }) + const { wrapper, store } = setupTestEnvironment({ animationsEnabled: true }); + const spy = vi.spyOn(store, 'setAnimationsEnabled'); + const checkbox = wrapper.find('input#animationsEnabled'); + await checkbox.setChecked(false); + expect(spy).toHaveBeenCalledWith(false); + }); it('calls setTheme action when theme select changes', async () => { - const { wrapper, store } = createWrapperAndStore() - const spy = vi.spyOn(store, 'setTheme') - const select = wrapper.find('select#theme') - await select.setValue('dark') - expect(spy).toHaveBeenCalledWith('dark') - }) + const { wrapper, store } = setupTestEnvironment(); + const spy = vi.spyOn(store, 'setTheme'); + const select = wrapper.find('select#theme'); + await select.setValue('dark'); + expect(spy).toHaveBeenCalledWith('dark'); + }); - // New tests for module accent color pickers + // Tests for module accent color pickers it('calls setAccentColorEmpleados action when empleados accent color input changes', async () => { - const { wrapper, store } = createWrapperAndStore() - const spy = vi.spyOn(store, 'setAccentColorEmpleados') - const colorInput = wrapper.find('input#accentColorEmpleados') - colorInput.element.value = '#aabbcc' - await colorInput.trigger('input') - expect(spy).toHaveBeenCalledWith('#aabbcc') - }) + const { wrapper, store } = setupTestEnvironment(); + const spy = vi.spyOn(store, 'setAccentColorEmpleados'); + const colorInput = wrapper.find('input#accentColorEmpleados'); + colorInput.element.value = '#aabbcc'; + await colorInput.trigger('input'); + expect(spy).toHaveBeenCalledWith('#aabbcc'); + }); - it('calls setAccentColorTareas action when tareas accent color input changes', async () => { - const { wrapper, store } = createWrapperAndStore() - const spy = vi.spyOn(store, 'setAccentColorTareas') - const colorInput = wrapper.find('input#accentColorTareas') - colorInput.element.value = '#ccbbaa' - await colorInput.trigger('input') - expect(spy).toHaveBeenCalledWith('#ccbbaa') - }) + it('calls setAccentColorConfiguracion action when configuracion accent color input changes', async () => { + const { wrapper, store } = setupTestEnvironment(); + const spy = vi.spyOn(store, 'setAccentColorConfiguracion'); + const colorInput = wrapper.find('input#accentColorConfiguracion'); + colorInput.element.value = '#ccddee'; + await colorInput.trigger('input'); + expect(spy).toHaveBeenCalledWith('#ccddee'); + }); - it('calls setAccentColorPlanillas action when planillas accent color input changes', async () => { - const { wrapper, store } = createWrapperAndStore() - const spy = vi.spyOn(store, 'setAccentColorPlanillas') - const colorInput = wrapper.find('input#accentColorPlanillas') - colorInput.element.value = '#a1b2c3' - await colorInput.trigger('input') - expect(spy).toHaveBeenCalledWith('#a1b2c3') - }) - it('calls setAccentColorAsistencias action when asistencias accent color input changes', async () => { - const { wrapper, store } = createWrapperAndStore() - const spy = vi.spyOn(store, 'setAccentColorAsistencias') - const colorInput = wrapper.find('input#accentColorAsistencias') - colorInput.element.value = '#c3b2a1' - await colorInput.trigger('input') - expect(spy).toHaveBeenCalledWith('#c3b2a1') - }) + // *** ADD NEW TESTS FOR DEFAULT VIEW SELECTS HERE *** + describe('Default View Selects', () => { + it('renders default view select for Empleados module and calls action on change', async () => { + const { wrapper, store } = setupTestEnvironment({ defaultViewEmpleados: 'table' }); + const select = wrapper.find('select#defaultViewEmpleados'); + expect(select.exists()).toBe(true); + expect(select.element.value).toBe('table'); + const options = select.findAll('option'); + expect(options.length).toBe(2); + expect(options[0].text()).toBe('Table'); + expect(options[0].element.value).toBe('table'); + expect(options[1].text()).toBe('Card'); + expect(options[1].element.value).toBe('card'); + + const spy = vi.spyOn(store, 'setDefaultViewEmpleados'); + await select.setValue('card'); + expect(spy).toHaveBeenCalledWith('card'); + expect(select.element.value).toBe('card'); // Assuming v-model updates from store mock correctly + }); + + it('renders default view select for Tareas module and calls action on change', async () => { + const { wrapper, store } = setupTestEnvironment({ defaultViewTareas: 'table' }); + const select = wrapper.find('select#defaultViewTareas'); + expect(select.exists()).toBe(true); + expect(select.element.value).toBe('table'); + const spy = vi.spyOn(store, 'setDefaultViewTareas'); + await select.setValue('card'); + expect(spy).toHaveBeenCalledWith('card'); + expect(select.element.value).toBe('card'); + }); + + it('renders default view select for Planillas module and calls action on change', async () => { + const { wrapper, store } = setupTestEnvironment({ defaultViewPlanillas: 'table' }); + const select = wrapper.find('select#defaultViewPlanillas'); + expect(select.exists()).toBe(true); + expect(select.element.value).toBe('table'); + const spy = vi.spyOn(store, 'setDefaultViewPlanillas'); + await select.setValue('card'); + expect(spy).toHaveBeenCalledWith('card'); + expect(select.element.value).toBe('card'); + }); + + it('renders default view select for Asistencias module and calls action on change', async () => { + const { wrapper, store } = setupTestEnvironment({ defaultViewAsistencias: 'table' }); + const select = wrapper.find('select#defaultViewAsistencias'); + expect(select.exists()).toBe(true); + expect(select.element.value).toBe('table'); + const spy = vi.spyOn(store, 'setDefaultViewAsistencias'); + await select.setValue('card'); + expect(spy).toHaveBeenCalledWith('card'); + expect(select.element.value).toBe('card'); + }); + + it('renders default view select for Configuracion module and calls action on change', async () => { + const { wrapper, store } = setupTestEnvironment({ defaultViewConfiguracion: 'table' }); + const select = wrapper.find('select#defaultViewConfiguracion'); + expect(select.exists()).toBe(true); + expect(select.element.value).toBe('table'); + const spy = vi.spyOn(store, 'setDefaultViewConfiguracion'); + await select.setValue('card'); + expect(spy).toHaveBeenCalledWith('card'); + expect(select.element.value).toBe('card'); + }); + }); + it('updates input values when store state changes programmatically', async () => { - const { wrapper, store } = createWrapperAndStore() // Store instance for this test + const { wrapper, store } = setupTestEnvironment(); - // Test primaryColor - store.primaryColor = '#001122' // Directly manipulate the store used by the component + store.primaryColor = '#001122'; ======= const wrapper = createWrapper() const spy = vi.spyOn(store, 'setTheme') @@ -251,51 +263,43 @@ describe('SettingsView.vue', () => { // Test primaryColor store.primaryColor = '#001122' - await wrapper.vm.$nextTick() // Wait for Vue to react to state change - expect(wrapper.find('input#primaryColor').element.value).toBe('#001122') + await wrapper.vm.$nextTick(); + expect(wrapper.find('input#primaryColor').element.value).toBe('#001122'); - // Test fontFamily - store.fontFamily = 'Verdana' - await wrapper.vm.$nextTick() - expect(wrapper.find('input#fontFamily').element.value).toBe('Verdana') + store.fontFamily = 'Verdana'; + await wrapper.vm.$nextTick(); + expect(wrapper.find('input#fontFamily').element.value).toBe('Verdana'); - // Test fontSize - store.fontSize = 12 - await wrapper.vm.$nextTick() - expect(wrapper.find('input#fontSize').element.value).toBe('12') + store.fontSize = 12; + await wrapper.vm.$nextTick(); + expect(wrapper.find('input#fontSize').element.value).toBe('12'); - // Test animationsEnabled - store.animationsEnabled = false - await wrapper.vm.$nextTick() - expect(wrapper.find('input#animationsEnabled').element.checked).toBe(false) + store.animationsEnabled = false; + await wrapper.vm.$nextTick(); + expect(wrapper.find('input#animationsEnabled').element.checked).toBe(false); - // Test theme - store.theme = 'dark' - await wrapper.vm.$nextTick() - expect(wrapper.find('select#theme').element.value).toBe('dark') + store.theme = 'dark'; + await wrapper.vm.$nextTick(); + expect(wrapper.find('select#theme').element.value).toBe('dark'); - // Test one of the new accent colors - store.accentColorEmpleados = '#998877' - await wrapper.vm.$nextTick() - expect(wrapper.find('input#accentColorEmpleados').element.value).toBe('#998877') - }) + store.accentColorEmpleados = '#998877'; + await wrapper.vm.$nextTick(); + expect(wrapper.find('input#accentColorEmpleados').element.value).toBe('#998877'); + + // Test one of the new default view selects + store.defaultViewEmpleados = 'card'; + await wrapper.vm.$nextTick(); + expect(wrapper.find('select#defaultViewEmpleados').element.value).toBe('card'); + }); - // Test for the initial fade-in animation - checking class it('applies opacity transition class after mount', async () => { - // Mock setTimeout to control its execution - vi.useFakeTimers() + vi.useFakeTimers(); + const { wrapper } = setupTestEnvironment(); + expect(wrapper.find('.settings-view').classes()).not.toContain('opacity-100'); - const { wrapper } = createWrapperAndStore() // Use the new function name - const wrapper = createWrapper() - expect(wrapper.find('.settings-view').classes()).not.toContain('opacity-100') - - // Advance timers by the amount used in setTimeout in SettingsView.vue (50ms) - vi.advanceTimersByTime(100) // Advance a bit more to be sure - await wrapper.vm.$nextTick() // Allow Vue to re-render - - expect(wrapper.find('.settings-view').classes()).toContain('opacity-100') - - vi.useRealTimers() // Restore real timers - }) - -}) + vi.advanceTimersByTime(100); + await wrapper.vm.$nextTick(); + expect(wrapper.find('.settings-view').classes()).toContain('opacity-100'); + vi.useRealTimers(); + }); +}); diff --git a/ui/src/views/asistencias/AsistenciasIndex.vue b/ui/src/views/asistencias/AsistenciasIndex.vue index cb1cc65..7da4e5f 100644 --- a/ui/src/views/asistencias/AsistenciasIndex.vue +++ b/ui/src/views/asistencias/AsistenciasIndex.vue @@ -7,13 +7,23 @@ -
- Cambiar Vista: - -
@@ -60,18 +70,21 @@ diff --git a/ui/src/views/asistencias/__tests__/AsistenciasIndex.spec.js b/ui/src/views/asistencias/__tests__/AsistenciasIndex.spec.js new file mode 100644 index 0000000..b04b9fa --- /dev/null +++ b/ui/src/views/asistencias/__tests__/AsistenciasIndex.spec.js @@ -0,0 +1,216 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { mount } from '@vue/test-utils' +import { createPinia, setActivePinia } from 'pinia' +import { useUi } from '@/stores/useUi' +import { useAsistenciasStore } from '@/stores/useAsistencias' +import AsistenciasIndex from '../AsistenciasIndex.vue' +import TablaAsistencias from '@/components/asistencias/tablaAsistencias.vue' +import CardAsistencia from '@/components/asistencias/cardAsistencia.vue' + +// Mock child components +vi.mock('@/components/asistencias/tablaAsistencias.vue', () => ({ + default: { + name: 'TablaAsistencias', + props: ['asistencias', 'isLoading'], // Match actual props if different + emits: ['edit'], + template: '
', + }, +})) + +vi.mock('@/components/asistencias/cardAsistencia.vue', () => ({ + default: { + name: 'CardAsistencia', + props: ['asistencia'], // Match actual props + emits: ['edit'], + template: '
', + }, +})) + +// Mock stores +const mockSetDefaultViewAsistencias = vi.fn(); +const mockFetchAsistencias = vi.fn(); + +vi.mock('@/stores/useUi', () => ({ + useUi: vi.fn(() => ({ + defaultViewAsistencias: 'table', // Default mock value + setDefaultViewAsistencias: mockSetDefaultViewAsistencias, + })), +})) + +vi.mock('@/stores/useAsistencias', () => ({ + useAsistenciasStore: vi.fn(() => ({ + asistencias: [], + fetchAsistencias: mockFetchAsistencias, + })), +})) + +describe('AsistenciasIndex.vue', () => { + let uiStoreMock + let asistenciasStoreMock + + beforeEach(() => { + setActivePinia(createPinia()) + mockFetchAsistencias.mockClear().mockResolvedValue([]) + mockSetDefaultViewAsistencias.mockClear() + + uiStoreMock = useUi() + asistenciasStoreMock = useAsistenciasStore() + }) + + const mountComponent = () => { + return mount(AsistenciasIndex, { + global: { + // Stubs can be defined here too if not using vi.mock for everything + }, + }) + } + + it('fetches asistencias on mount', async () => { + mountComponent() + // It seems the component has its own isLoading ref, wait for it to resolve + await mockFetchAsistencias(); // Ensure the promise resolves + expect(mockFetchAsistencias).toHaveBeenCalledTimes(1) + }) + + describe('View Rendering based on useUi store', () => { + it('renders TablaAsistencias when defaultViewAsistencias is "table"', async () => { + uiStoreMock.defaultViewAsistencias = 'table' + asistenciasStoreMock.asistencias = [{ id: 1, empleado: 'Test Employee', fecha: '2023-01-01' }] + + const wrapper = mountComponent() + // Wait for internal loading state and then data processing + await mockFetchAsistencias(); + await wrapper.vm.$nextTick() + await wrapper.vm.$nextTick() + + + expect(wrapper.findComponent({ name: 'TablaAsistencias' }).exists()).toBe(true) + expect(wrapper.findComponent({ name: 'TablaAsistencias' }).props('asistencias')).toEqual(asistenciasStoreMock.asistencias) + expect(wrapper.findComponent({ name: 'CardAsistencia' }).exists()).toBe(false) + }) + + it('renders CardAsistencia when defaultViewAsistencias is "card"', async () => { + uiStoreMock.defaultViewAsistencias = 'card' + asistenciasStoreMock.asistencias = [{ id: 1, empleado: 'E1' }, { id: 2, empleado: 'E2' }] + + const wrapper = mountComponent() + await mockFetchAsistencias(); + await wrapper.vm.$nextTick() + await wrapper.vm.$nextTick() + + const cardWrappers = wrapper.findAllComponents({ name: 'CardAsistencia' }) + expect(cardWrappers.length).toBe(asistenciasStoreMock.asistencias.length) + expect(cardWrappers[0].props('asistencia')).toEqual(asistenciasStoreMock.asistencias[0]) + expect(wrapper.findComponent({ name: 'TablaAsistencias' }).exists()).toBe(false) + }) + + it('renders no data message for table view when no asistencias exist', async () => { + uiStoreMock.defaultViewAsistencias = 'table'; + asistenciasStoreMock.asistencias = []; + + const wrapper = mountComponent(); + await mockFetchAsistencias(); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + + expect(wrapper.findComponent({ name: 'TablaAsistencias' }).exists()).toBe(true); + // The component might show "Cargando asistencias..." initially, then the no data message. + // We need to ensure loading is complete. + expect(wrapper.text()).toContain('No hay asistencias para mostrar'); + }); + + it('renders no data message for card view when no asistencias exist', async () => { + uiStoreMock.defaultViewAsistencias = 'card'; + asistenciasStoreMock.asistencias = []; + + const wrapper = mountComponent(); + await mockFetchAsistencias(); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + + expect(wrapper.findAllComponents({ name: 'CardAsistencia' }).length).toBe(0); + expect(wrapper.text()).toContain('No hay asistencias para mostrar'); + }); + }) + + describe('Local View Toggle Buttons', () => { + it('renders toggle buttons and reflects initial view from store (table)', async () => { + uiStoreMock.defaultViewAsistencias = 'table'; + const wrapper = mountComponent(); + await mockFetchAsistencias(); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + + const tableViewButton = wrapper.find('button[aria-label="Table View"]'); + const cardViewButton = wrapper.find('button[aria-label="Card View"]'); + + expect(tableViewButton.exists()).toBe(true); + expect(cardViewButton.exists()).toBe(true); + + // Check active class based on btnViewClass logic + // Active: bg-[var(--accent-color-asistencias)] text-white + // Inactive: bg-gray-200 text-gray-700 + expect(tableViewButton.classes()).toContain('bg-[var(--accent-color-asistencias)]'); + expect(cardViewButton.classes()).toContain('bg-gray-200'); + }); + + it('renders toggle buttons and reflects initial view from store (card)', async () => { + uiStoreMock.defaultViewAsistencias = 'card'; + const wrapper = mountComponent(); + await mockFetchAsistencias(); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + + const tableViewButton = wrapper.find('button[aria-label="Table View"]'); + const cardViewButton = wrapper.find('button[aria-label="Card View"]'); + + expect(cardViewButton.classes()).toContain('bg-[var(--accent-color-asistencias)]'); + expect(tableViewButton.classes()).toContain('bg-gray-200'); + }); + + it('switches to card view on button click and updates button styles, does not call global store action', async () => { + uiStoreMock.defaultViewAsistencias = 'table'; + asistenciasStoreMock.asistencias = [{ id: 1, empleado: 'Test' }]; + const wrapper = mountComponent(); + await mockFetchAsistencias(); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + + const cardViewButton = wrapper.find('button[aria-label="Card View"]'); + const tableViewButton = wrapper.find('button[aria-label="Table View"]'); + + await cardViewButton.trigger('click'); + await wrapper.vm.$nextTick(); + + expect(wrapper.findComponent({ name: 'CardAsistencia' }).exists()).toBe(true); + expect(wrapper.findComponent({ name: 'TablaAsistencias' }).exists()).toBe(false); + expect(cardViewButton.classes()).toContain('bg-[var(--accent-color-asistencias)]'); + expect(tableViewButton.classes()).toContain('bg-gray-200'); + expect(mockSetDefaultViewAsistencias).not.toHaveBeenCalled(); + }); + + it('switches back to table view on button click and updates button styles, does not call global store action', async () => { + uiStoreMock.defaultViewAsistencias = 'card'; // Start with card view + asistenciasStoreMock.asistencias = [{ id: 1, empleado: 'Test' }]; + const wrapper = mountComponent(); + await mockFetchAsistencias(); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + + // Initially card view is active + const cardViewButton = wrapper.find('button[aria-label="Card View"]'); + const tableViewButton = wrapper.find('button[aria-label="Table View"]'); + expect(cardViewButton.classes()).toContain('bg-[var(--accent-color-asistencias)]'); + + + await tableViewButton.trigger('click'); + await wrapper.vm.$nextTick(); + + expect(wrapper.findComponent({ name: 'TablaAsistencias' }).exists()).toBe(true); + expect(wrapper.findComponent({ name: 'CardAsistencia' }).exists()).toBe(false); + expect(tableViewButton.classes()).toContain('bg-[var(--accent-color-asistencias)]'); + expect(cardViewButton.classes()).toContain('bg-gray-200'); + expect(mockSetDefaultViewAsistencias).not.toHaveBeenCalled(); + }); + }); +}) diff --git a/ui/src/views/empleados/EmpleadosIndex.vue b/ui/src/views/empleados/EmpleadosIndex.vue index 72e29c0..90ec10b 100644 --- a/ui/src/views/empleados/EmpleadosIndex.vue +++ b/ui/src/views/empleados/EmpleadosIndex.vue @@ -19,19 +19,22 @@ -
- Cambiar Vista: - +
+
@@ -80,6 +83,7 @@ import { ref, onMounted, computed } from 'vue'; import { storeToRefs } from 'pinia'; import { useRouter } from 'vue-router'; +import { useUi } from '@/stores/useUi'; // Import useUi import CardEmpleado from '@/components/empleados/cardEmpleado.vue'; import TablaEmpleados from '@/components/empleados/tablaEmpleados.vue'; @@ -87,7 +91,8 @@ import { useEmpleadosStore } from '@/stores/useEmpleados.js'; // ruta según tu // --- refs locales --- const router = useRouter(); -const currentView = ref<'card' | 'table'>('card'); +const ui = useUi(); // Access the ui store +const currentView = ref<'card' | 'table'>(ui.defaultViewEmpleados); // Initialize from store const loading = ref(true); const error = ref(null); @@ -99,14 +104,13 @@ const { empleados } = storeToRefs(empleadosStore); const employees = empleados; // --- helpers --- -const btnClass = (view: 'card' | 'table') => { - const baseClasses = 'px-4 py-2 rounded-md text-sm font-medium focus:outline-none focus:ring-2 focus:ring-offset-2'; - if (currentView.value === view) { - // Active button uses accent color - return `${baseClasses} text-white shadow-sm view-toggle-active`; +// Removed btnClass as manual toggle buttons are removed +const btnViewClass = (viewType: 'card' | 'table') => { + const base = 'p-2 rounded-md transition-colors duration-150 ease-in-out'; + if (currentView.value === viewType) { + return `${base} bg-[var(--accent-color-empleados)] text-white shadow-lg`; } - // Inactive button uses secondary/gray styling - return `${baseClasses} bg-gray-200 text-gray-700 hover:bg-gray-300 focus:ring-gray-400`; + return `${base} bg-gray-200 text-gray-700 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600`; }; // --- fetch inicial --- diff --git a/ui/src/views/empleados/__tests__/EmpleadosIndex.spec.js b/ui/src/views/empleados/__tests__/EmpleadosIndex.spec.js new file mode 100644 index 0000000..496134a --- /dev/null +++ b/ui/src/views/empleados/__tests__/EmpleadosIndex.spec.js @@ -0,0 +1,208 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { mount } from '@vue/test-utils' +import { createPinia, setActivePinia } from 'pinia' +import { useUi } from '@/stores/useUi' +import { useEmpleadosStore } from '@/stores/useEmpleados' +import EmpleadosIndex from '../EmpleadosIndex.vue' +import TablaEmpleados from '@/components/empleados/tablaEmpleados.vue' +import CardEmpleado from '@/components/empleados/cardEmpleado.vue' + +// Mock the child components to simplify testing and focus on EmpleadosIndex logic +vi.mock('@/components/empleados/tablaEmpleados.vue', () => ({ + default: { + name: 'TablaEmpleados', + props: ['employees'], + template: '
', + }, +})) + +vi.mock('@/components/empleados/cardEmpleado.vue', () => ({ + default: { + name: 'CardEmpleado', + props: ['employee'], + template: '
', + }, +})) + +// Mock the stores +// We need to define the mock functions before they are used by `vi.mock` +const mockSetDefaultViewEmpleados = vi.fn(); +const mockFetchEmpleados = vi.fn(); + +vi.mock('@/stores/useUi', () => ({ + useUi: vi.fn(() => ({ + defaultViewEmpleados: 'table', // Default mock value + // Add any other state or actions needed by the component from useUi + setDefaultViewEmpleados: mockSetDefaultViewEmpleados, + })), +})) + +vi.mock('@/stores/useEmpleados', () => ({ + useEmpleadosStore: vi.fn(() => ({ + empleados: [], + fetchEmpleados: mockFetchEmpleados, + // Add other state or actions if EmpleadosIndex uses them + })), +})) + +describe('EmpleadosIndex.vue', () => { + let uiStoreMock + let empleadosStoreMock + + beforeEach(() => { + setActivePinia(createPinia()) + + // Reset mocks and provide fresh instances for each test + mockFetchEmpleados.mockClear().mockResolvedValue([]); // Default to resolve successfully + mockSetDefaultViewEmpleados.mockClear(); + + // Get fresh instances of the mocked stores for manipulation in tests + // The actual `useUi` and `useEmpleadosStore` will be the vi.fn defined above + // Re-calling them ensures we can configure their return values per test suite if needed + // or rely on the default mock implementation. + uiStoreMock = useUi() + empleadosStoreMock = useEmpleadosStore() + }) + + const mountComponent = () => { + return mount(EmpleadosIndex, { + global: { + stubs: { + // While components are mocked via vi.mock, explicit stubs can be used for further control if needed + // For instance, if you didn't want to mock the entire module. + // 'TablaEmpleados': true, + // 'CardEmpleado': true, + } + }, + }) + } + + it('fetches employees on mount', () => { + mountComponent() + expect(mockFetchEmpleados).toHaveBeenCalledTimes(1) + }) + + describe('View Rendering based on useUi store', () => { + it('renders TablaEmpleados when defaultViewEmpleados is "table"', async () => { + uiStoreMock.defaultViewEmpleados = 'table' // Set store state for this test + empleadosStoreMock.empleados = [{ id: 1, nombre: 'Test Employee' }] // Provide some data + + const wrapper = mountComponent() + await wrapper.vm.$nextTick() // Wait for any reactivity updates + + expect(wrapper.findComponent({ name: 'TablaEmpleados' }).exists()).toBe(true) + expect(wrapper.findComponent({ name: 'TablaEmpleados' }).props('employees')).toEqual(empleadosStoreMock.empleados) + expect(wrapper.findComponent({ name: 'CardEmpleado' }).exists()).toBe(false) + }) + + it('renders CardEmpleado when defaultViewEmpleados is "card"', async () => { + uiStoreMock.defaultViewEmpleados = 'card' // Set store state for this test + empleadosStoreMock.empleados = [{ id: 1, nombre: 'Test Employee' }, { id: 2, nombre: 'Another Employee' }] + + const wrapper = mountComponent() + await wrapper.vm.$nextTick() + + const cardWrappers = wrapper.findAllComponents({ name: 'CardEmpleado' }) + expect(cardWrappers.length).toBe(empleadosStoreMock.empleados.length) + expect(cardWrappers[0].props('employee')).toEqual(empleadosStoreMock.empleados[0]) + expect(wrapper.findComponent({ name: 'TablaEmpleados' }).exists()).toBe(false) + }) + + it('renders no data message for table view when no employees exist', async () => { + uiStoreMock.defaultViewEmpleados = 'table'; + empleadosStoreMock.empleados = []; // No data + + const wrapper = mountComponent(); + await wrapper.vm.$nextTick(); // allow loading state to pass + await wrapper.vm.$nextTick(); // allow conditional rendering based on data + + expect(wrapper.findComponent({ name: 'TablaEmpleados' }).exists()).toBe(true); // Table component itself still renders + expect(wrapper.text()).toContain('No hay empleados para mostrar en la vista de tabla.'); + }); + + it('renders no data message for card view when no employees exist', async () => { + uiStoreMock.defaultViewEmpleados = 'card'; + empleadosStoreMock.empleados = []; // No data + + const wrapper = mountComponent(); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + + expect(wrapper.findAllComponents({ name: 'CardEmpleado' }).length).toBe(0); + expect(wrapper.text()).toContain('No hay empleados para mostrar en la vista de tarjetas.'); + }); + }) + + describe('Local View Toggle Buttons', () => { + it('renders toggle buttons and reflects initial view from store (table)', async () => { + uiStoreMock.defaultViewEmpleados = 'table'; + const wrapper = mountComponent(); + // Wait for loading and reactivity + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + + + const tableViewButton = wrapper.find('button[aria-label="Table View"]'); + const cardViewButton = wrapper.find('button[aria-label="Card View"]'); + + expect(tableViewButton.exists()).toBe(true); + expect(cardViewButton.exists()).toBe(true); + expect(tableViewButton.classes()).toContain('bg-[var(--accent-color-empleados)]'); + expect(cardViewButton.classes()).toContain('bg-gray-200'); + }); + + it('renders toggle buttons and reflects initial view from store (card)', async () => { + uiStoreMock.defaultViewEmpleados = 'card'; + const wrapper = mountComponent(); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + + const tableViewButton = wrapper.find('button[aria-label="Table View"]'); + const cardViewButton = wrapper.find('button[aria-label="Card View"]'); + + expect(cardViewButton.classes()).toContain('bg-[var(--accent-color-empleados)]'); + expect(tableViewButton.classes()).toContain('bg-gray-200'); + }); + + it('switches to card view on button click and updates button styles, does not call global store action', async () => { + uiStoreMock.defaultViewEmpleados = 'table'; + empleadosStoreMock.empleados = [{ id: 1, nombre: 'Test' }]; + const wrapper = mountComponent(); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + + const cardViewButton = wrapper.find('button[aria-label="Card View"]'); + const tableViewButton = wrapper.find('button[aria-label="Table View"]'); + + await cardViewButton.trigger('click'); + await wrapper.vm.$nextTick(); + + expect(wrapper.findComponent({ name: 'CardEmpleado' }).exists()).toBe(true); + expect(wrapper.findComponent({ name: 'TablaEmpleados' }).exists()).toBe(false); + expect(cardViewButton.classes()).toContain('bg-[var(--accent-color-empleados)]'); + expect(tableViewButton.classes()).toContain('bg-gray-200'); + expect(mockSetDefaultViewEmpleados).not.toHaveBeenCalled(); + }); + + it('switches back to table view on button click and updates button styles, does not call global store action', async () => { + uiStoreMock.defaultViewEmpleados = 'card'; // Start with card view + empleadosStoreMock.empleados = [{ id: 1, nombre: 'Test' }]; + const wrapper = mountComponent(); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + + const cardViewButton = wrapper.find('button[aria-label="Card View"]'); + const tableViewButton = wrapper.find('button[aria-label="Table View"]'); + expect(cardViewButton.classes()).toContain('bg-[var(--accent-color-empleados)]'); + + await tableViewButton.trigger('click'); + await wrapper.vm.$nextTick(); + + expect(wrapper.findComponent({ name: 'TablaEmpleados' }).exists()).toBe(true); + expect(wrapper.findComponent({ name: 'CardEmpleado' }).exists()).toBe(false); + expect(tableViewButton.classes()).toContain('bg-[var(--accent-color-empleados)]'); + expect(cardViewButton.classes()).toContain('bg-gray-200'); + expect(mockSetDefaultViewEmpleados).not.toHaveBeenCalled(); + }); + }); +}) diff --git a/ui/src/views/planillas/PlanillasIndex.vue b/ui/src/views/planillas/PlanillasIndex.vue index fb455fc..9e76a88 100644 --- a/ui/src/views/planillas/PlanillasIndex.vue +++ b/ui/src/views/planillas/PlanillasIndex.vue @@ -7,13 +7,23 @@ -
- Cambiar Vista: - -
@@ -60,18 +70,21 @@ diff --git a/ui/src/views/planillas/__tests__/PlanillasIndex.spec.js b/ui/src/views/planillas/__tests__/PlanillasIndex.spec.js new file mode 100644 index 0000000..b168fbb --- /dev/null +++ b/ui/src/views/planillas/__tests__/PlanillasIndex.spec.js @@ -0,0 +1,203 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { mount } from '@vue/test-utils' +import { createPinia, setActivePinia } from 'pinia' +import { useUi } from '@/stores/useUi' +import { usePlanillasStore } from '@/stores/usePlanillas' +import PlanillasIndex from '../PlanillasIndex.vue' +import TablaPlanillas from '@/components/planillas/tablaPlanillas.vue' +import CardPlanilla from '@/components/planillas/cardPlanilla.vue' + +// Mock child components +vi.mock('@/components/planillas/tablaPlanillas.vue', () => ({ + default: { + name: 'TablaPlanillas', + props: ['planillas'], // Match actual props + emits: ['edit'], + template: '
', + }, +})) + +vi.mock('@/components/planillas/cardPlanilla.vue', () => ({ + default: { + name: 'CardPlanilla', + props: ['planilla'], // Match actual props + emits: ['edit'], + template: '
', + }, +})) + +// Mock stores +const mockSetDefaultViewPlanillas = vi.fn(); +const mockFetchPlanillas = vi.fn(); + +vi.mock('@/stores/useUi', () => ({ + useUi: vi.fn(() => ({ + defaultViewPlanillas: 'table', // Default mock value + setDefaultViewPlanillas: mockSetDefaultViewPlanillas, + })), +})) + +vi.mock('@/stores/usePlanillas', () => ({ + usePlanillasStore: vi.fn(() => ({ + planillas: [], + fetchPlanillas: mockFetchPlanillas, + })), +})) + +describe('PlanillasIndex.vue', () => { + let uiStoreMock + let planillasStoreMock + + beforeEach(() => { + setActivePinia(createPinia()) + mockFetchPlanillas.mockClear().mockResolvedValue([]) + mockSetDefaultViewPlanillas.mockClear() + + uiStoreMock = useUi() + planillasStoreMock = usePlanillasStore() + }) + + const mountComponent = () => { + return mount(PlanillasIndex, { + global: {}, + }) + } + + it('fetches planillas on mount', async () => { + mountComponent() + await mockFetchPlanillas(); // Ensure the promise from fetch resolves + expect(mockFetchPlanillas).toHaveBeenCalledTimes(1) + }) + + describe('View Rendering based on useUi store', () => { + it('renders TablaPlanillas when defaultViewPlanillas is "table"', async () => { + uiStoreMock.defaultViewPlanillas = 'table' + planillasStoreMock.planillas = [{ id: 1, periodo: '2023-01', total: 5000 }] + + const wrapper = mountComponent() + await mockFetchPlanillas(); + await wrapper.vm.$nextTick() // Wait for reactivity + await wrapper.vm.$nextTick() // Additional tick if loading state causes multiple updates + + expect(wrapper.findComponent({ name: 'TablaPlanillas' }).exists()).toBe(true) + expect(wrapper.findComponent({ name: 'TablaPlanillas' }).props('planillas')).toEqual(planillasStoreMock.planillas) + expect(wrapper.findComponent({ name: 'CardPlanilla' }).exists()).toBe(false) + }) + + it('renders CardPlanilla when defaultViewPlanillas is "card"', async () => { + uiStoreMock.defaultViewPlanillas = 'card' + planillasStoreMock.planillas = [{ id: 1, P1: 'P1' }, { id: 2, P2: 'P2' }] + + const wrapper = mountComponent() + await mockFetchPlanillas(); + await wrapper.vm.$nextTick() + await wrapper.vm.$nextTick() + + const cardWrappers = wrapper.findAllComponents({ name: 'CardPlanilla' }) + expect(cardWrappers.length).toBe(planillasStoreMock.planillas.length) + expect(cardWrappers[0].props('planilla')).toEqual(planillasStoreMock.planillas[0]) + expect(wrapper.findComponent({ name: 'TablaPlanillas' }).exists()).toBe(false) + }) + + it('renders no data message for table view when no planillas exist', async () => { + uiStoreMock.defaultViewPlanillas = 'table'; + planillasStoreMock.planillas = []; + + const wrapper = mountComponent(); + await mockFetchPlanillas(); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + + expect(wrapper.findComponent({ name: 'TablaPlanillas' }).exists()).toBe(true); + expect(wrapper.text()).toContain('No hay planillas para mostrar'); + }); + + it('renders no data message for card view when no planillas exist', async () => { + uiStoreMock.defaultViewPlanillas = 'card'; + planillasStoreMock.planillas = []; + + const wrapper = mountComponent(); + await mockFetchPlanillas(); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + + expect(wrapper.findAllComponents({ name: 'CardPlanilla' }).length).toBe(0); + expect(wrapper.text()).toContain('No hay planillas para mostrar'); + }); + }) + + describe('Local View Toggle Buttons', () => { + it('renders toggle buttons and reflects initial view from store (table)', async () => { + uiStoreMock.defaultViewPlanillas = 'table'; + const wrapper = mountComponent(); + await mockFetchPlanillas(); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + + const tableViewButton = wrapper.find('button[aria-label="Table View"]'); + const cardViewButton = wrapper.find('button[aria-label="Card View"]'); + + expect(tableViewButton.exists()).toBe(true); + expect(cardViewButton.exists()).toBe(true); + expect(tableViewButton.classes()).toContain('bg-[var(--accent-color-planillas)]'); + expect(cardViewButton.classes()).toContain('bg-gray-200'); + }); + + it('renders toggle buttons and reflects initial view from store (card)', async () => { + uiStoreMock.defaultViewPlanillas = 'card'; + const wrapper = mountComponent(); + await mockFetchPlanillas(); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + + const tableViewButton = wrapper.find('button[aria-label="Table View"]'); + const cardViewButton = wrapper.find('button[aria-label="Card View"]'); + + expect(cardViewButton.classes()).toContain('bg-[var(--accent-color-planillas)]'); + expect(tableViewButton.classes()).toContain('bg-gray-200'); + }); + + it('switches to card view on button click and updates button styles, does not call global store action', async () => { + uiStoreMock.defaultViewPlanillas = 'table'; + planillasStoreMock.planillas = [{ id: 1, periodo: 'Test' }]; + const wrapper = mountComponent(); + await mockFetchPlanillas(); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + + const cardViewButton = wrapper.find('button[aria-label="Card View"]'); + const tableViewButton = wrapper.find('button[aria-label="Table View"]'); + + await cardViewButton.trigger('click'); + await wrapper.vm.$nextTick(); + + expect(wrapper.findComponent({ name: 'CardPlanilla' }).exists()).toBe(true); + expect(wrapper.findComponent({ name: 'TablaPlanillas' }).exists()).toBe(false); + expect(cardViewButton.classes()).toContain('bg-[var(--accent-color-planillas)]'); + expect(tableViewButton.classes()).toContain('bg-gray-200'); + expect(mockSetDefaultViewPlanillas).not.toHaveBeenCalled(); + }); + + it('switches back to table view on button click and updates button styles, does not call global store action', async () => { + uiStoreMock.defaultViewPlanillas = 'card'; // Start with card view + planillasStoreMock.planillas = [{ id: 1, periodo: 'Test' }]; + const wrapper = mountComponent(); + await mockFetchPlanillas(); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + + const cardViewButton = wrapper.find('button[aria-label="Card View"]'); + const tableViewButton = wrapper.find('button[aria-label="Table View"]'); + expect(cardViewButton.classes()).toContain('bg-[var(--accent-color-planillas)]'); + + await tableViewButton.trigger('click'); + await wrapper.vm.$nextTick(); + + expect(wrapper.findComponent({ name: 'TablaPlanillas' }).exists()).toBe(true); + expect(wrapper.findComponent({ name: 'CardPlanilla' }).exists()).toBe(false); + expect(tableViewButton.classes()).toContain('bg-[var(--accent-color-planillas)]'); + expect(cardViewButton.classes()).toContain('bg-gray-200'); + expect(mockSetDefaultViewPlanillas).not.toHaveBeenCalled(); + }); + }); +}) diff --git a/ui/src/views/tareas/TareasIndex.vue b/ui/src/views/tareas/TareasIndex.vue index b28ce0c..4ec5d98 100644 --- a/ui/src/views/tareas/TareasIndex.vue +++ b/ui/src/views/tareas/TareasIndex.vue @@ -7,13 +7,23 @@ -
- Cambiar Vista: - -
@@ -60,18 +70,21 @@ diff --git a/ui/src/views/tareas/__tests__/TareasIndex.spec.js b/ui/src/views/tareas/__tests__/TareasIndex.spec.js new file mode 100644 index 0000000..2ec8820 --- /dev/null +++ b/ui/src/views/tareas/__tests__/TareasIndex.spec.js @@ -0,0 +1,203 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { mount } from '@vue/test-utils' +import { createPinia, setActivePinia } from 'pinia' +import { useUi } from '@/stores/useUi' +import { useTareasStore } from '@/stores/useTareas' +import TareasIndex from '../TareasIndex.vue' +import TablaTareas from '@/components/tareas/tablaTareas.vue' +import CardTarea from '@/components/tareas/cardTarea.vue' + +// Mock child components +vi.mock('@/components/tareas/tablaTareas.vue', () => ({ + default: { + name: 'TablaTareas', + props: ['tareas'], // Match actual props + emits: ['edit'], + template: '
', + }, +})) + +vi.mock('@/components/tareas/cardTarea.vue', () => ({ + default: { + name: 'CardTarea', + props: ['tarea'], // Match actual props + emits: ['edit'], + template: '
', + }, +})) + +// Mock stores +const mockSetDefaultViewTareas = vi.fn(); +const mockFetchTareas = vi.fn(); + +vi.mock('@/stores/useUi', () => ({ + useUi: vi.fn(() => ({ + defaultViewTareas: 'table', // Default mock value + setDefaultViewTareas: mockSetDefaultViewTareas, + })), +})) + +vi.mock('@/stores/useTareas', () => ({ + useTareasStore: vi.fn(() => ({ + tareas: [], + fetchTareas: mockFetchTareas, + })), +})) + +describe('TareasIndex.vue', () => { + let uiStoreMock + let tareasStoreMock + + beforeEach(() => { + setActivePinia(createPinia()) + mockFetchTareas.mockClear().mockResolvedValue([]) + mockSetDefaultViewTareas.mockClear() + + uiStoreMock = useUi() + tareasStoreMock = useTareasStore() + }) + + const mountComponent = () => { + return mount(TareasIndex, { + global: {}, + }) + } + + it('fetches tareas on mount', async () => { + mountComponent() + await mockFetchTareas(); // Ensure the promise from fetch resolves + expect(mockFetchTareas).toHaveBeenCalledTimes(1) + }) + + describe('View Rendering based on useUi store', () => { + it('renders TablaTareas when defaultViewTareas is "table"', async () => { + uiStoreMock.defaultViewTareas = 'table' + tareasStoreMock.tareas = [{ id: 1, titulo: 'Test Task', completada: false }] + + const wrapper = mountComponent() + await mockFetchTareas(); + await wrapper.vm.$nextTick() + await wrapper.vm.$nextTick() + + expect(wrapper.findComponent({ name: 'TablaTareas' }).exists()).toBe(true) + expect(wrapper.findComponent({ name: 'TablaTareas' }).props('tareas')).toEqual(tareasStoreMock.tareas) + expect(wrapper.findComponent({ name: 'CardTarea' }).exists()).toBe(false) + }) + + it('renders CardTarea when defaultViewTareas is "card"', async () => { + uiStoreMock.defaultViewTareas = 'card' + tareasStoreMock.tareas = [{ id: 1, T1: 'T1' }, { id: 2, T2: 'T2' }] + + const wrapper = mountComponent() + await mockFetchTareas(); + await wrapper.vm.$nextTick() + await wrapper.vm.$nextTick() + + const cardWrappers = wrapper.findAllComponents({ name: 'CardTarea' }) + expect(cardWrappers.length).toBe(tareasStoreMock.tareas.length) + expect(cardWrappers[0].props('tarea')).toEqual(tareasStoreMock.tareas[0]) + expect(wrapper.findComponent({ name: 'TablaTareas' }).exists()).toBe(false) + }) + + it('renders no data message for table view when no tareas exist', async () => { + uiStoreMock.defaultViewTareas = 'table'; + tareasStoreMock.tareas = []; + + const wrapper = mountComponent(); + await mockFetchTareas(); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + + expect(wrapper.findComponent({ name: 'TablaTareas' }).exists()).toBe(true); + expect(wrapper.text()).toContain('No hay tareas para mostrar'); + }); + + it('renders no data message for card view when no tareas exist', async () => { + uiStoreMock.defaultViewTareas = 'card'; + tareasStoreMock.tareas = []; + + const wrapper = mountComponent(); + await mockFetchTareas(); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + + expect(wrapper.findAllComponents({ name: 'CardTarea' }).length).toBe(0); + expect(wrapper.text()).toContain('No hay tareas para mostrar'); + }); + }) + + describe('Local View Toggle Buttons', () => { + it('renders toggle buttons and reflects initial view from store (table)', async () => { + uiStoreMock.defaultViewTareas = 'table'; + const wrapper = mountComponent(); + await mockFetchTareas(); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + + const tableViewButton = wrapper.find('button[aria-label="Table View"]'); + const cardViewButton = wrapper.find('button[aria-label="Card View"]'); + + expect(tableViewButton.exists()).toBe(true); + expect(cardViewButton.exists()).toBe(true); + expect(tableViewButton.classes()).toContain('bg-[var(--accent-color-tareas)]'); + expect(cardViewButton.classes()).toContain('bg-gray-200'); + }); + + it('renders toggle buttons and reflects initial view from store (card)', async () => { + uiStoreMock.defaultViewTareas = 'card'; + const wrapper = mountComponent(); + await mockFetchTareas(); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + + const tableViewButton = wrapper.find('button[aria-label="Table View"]'); + const cardViewButton = wrapper.find('button[aria-label="Card View"]'); + + expect(cardViewButton.classes()).toContain('bg-[var(--accent-color-tareas)]'); + expect(tableViewButton.classes()).toContain('bg-gray-200'); + }); + + it('switches to card view on button click and updates button styles, does not call global store action', async () => { + uiStoreMock.defaultViewTareas = 'table'; + tareasStoreMock.tareas = [{ id: 1, titulo: 'Test' }]; + const wrapper = mountComponent(); + await mockFetchTareas(); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + + const cardViewButton = wrapper.find('button[aria-label="Card View"]'); + const tableViewButton = wrapper.find('button[aria-label="Table View"]'); + + await cardViewButton.trigger('click'); + await wrapper.vm.$nextTick(); + + expect(wrapper.findComponent({ name: 'CardTarea' }).exists()).toBe(true); + expect(wrapper.findComponent({ name: 'TablaTareas' }).exists()).toBe(false); + expect(cardViewButton.classes()).toContain('bg-[var(--accent-color-tareas)]'); + expect(tableViewButton.classes()).toContain('bg-gray-200'); + expect(mockSetDefaultViewTareas).not.toHaveBeenCalled(); + }); + + it('switches back to table view on button click and updates button styles, does not call global store action', async () => { + uiStoreMock.defaultViewTareas = 'card'; // Start with card view + tareasStoreMock.tareas = [{ id: 1, titulo: 'Test' }]; + const wrapper = mountComponent(); + await mockFetchTareas(); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + + const cardViewButton = wrapper.find('button[aria-label="Card View"]'); + const tableViewButton = wrapper.find('button[aria-label="Table View"]'); + expect(cardViewButton.classes()).toContain('bg-[var(--accent-color-tareas)]'); + + await tableViewButton.trigger('click'); + await wrapper.vm.$nextTick(); + + expect(wrapper.findComponent({ name: 'TablaTareas' }).exists()).toBe(true); + expect(wrapper.findComponent({ name: 'CardTarea' }).exists()).toBe(false); + expect(tableViewButton.classes()).toContain('bg-[var(--accent-color-tareas)]'); + expect(cardViewButton.classes()).toContain('bg-gray-200'); + expect(mockSetDefaultViewTareas).not.toHaveBeenCalled(); + }); + }); +})