From 32aa41f59f2be122dfac9abfaa257e976d3a0ee3 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 31 May 2025 07:59:01 +0000 Subject: [PATCH 1/2] feat: Add default index view setting for modules This commit introduces a new customization option in the appearance settings, allowing you to choose your default index view (card or table) for each module (Empleados, Tareas, Planillas, Asistencias). Key changes: - **Store Updates (`useUi.js`):** - Added new state properties (e.g., `defaultViewEmpleados`) to store the preferred view for each module. - Added corresponding actions (e.g., `setDefaultViewEmpleados`) to update these settings. - Settings are persisted to local storage. - **Settings UI (`SettingsView.vue`):** - Added dropdown selectors in the module-specific sections of the appearance settings page for you to choose 'Table' or 'Card' as your default view. - These selectors are bound to the new store properties. - **Module Index Views (e.g., `EmpleadosIndex.vue`):** - Modified to read the default view setting from the `useUi` store. - Conditionally render either the table component or the card component based on this setting. - Removed manual view toggle buttons, as the default is now managed via settings. - **Testing:** - I added unit tests for the new store actions and state. - I added component tests for `SettingsView.vue` to verify the new UI elements and their interaction with the store. - I added component tests for each module's index view to ensure they render the correct view (table or card) based on the global setting and display data or "no data" messages appropriately. This feature provides you with more control over your preferred data display format within each module, enhancing the user experience. --- ui/src/stores/__tests__/useUi.spec.js | 175 ++++++- ui/src/stores/useUi.js | 34 ++ ui/src/views/SettingsView.vue | 50 +- ui/src/views/__tests__/SettingsView.spec.js | 468 +++++++++--------- ui/src/views/asistencias/AsistenciasIndex.vue | 23 +- .../__tests__/AsistenciasIndex.spec.js | 135 +++++ ui/src/views/empleados/EmpleadosIndex.vue | 30 +- .../__tests__/EmpleadosIndex.spec.js | 135 +++++ ui/src/views/planillas/PlanillasIndex.vue | 23 +- .../__tests__/PlanillasIndex.spec.js | 128 +++++ ui/src/views/tareas/TareasIndex.vue | 23 +- .../tareas/__tests__/TareasIndex.spec.js | 128 +++++ 12 files changed, 1018 insertions(+), 334 deletions(-) create mode 100644 ui/src/views/asistencias/__tests__/AsistenciasIndex.spec.js create mode 100644 ui/src/views/empleados/__tests__/EmpleadosIndex.spec.js create mode 100644 ui/src/views/planillas/__tests__/PlanillasIndex.spec.js create mode 100644 ui/src/views/tareas/__tests__/TareasIndex.spec.js 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..087fc42 100644 --- a/ui/src/views/asistencias/AsistenciasIndex.vue +++ b/ui/src/views/asistencias/AsistenciasIndex.vue @@ -7,15 +7,7 @@ -
- Cambiar Vista: - - -
+
Cargando asistencias... @@ -60,18 +52,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..6854eb9 --- /dev/null +++ b/ui/src/views/asistencias/__tests__/AsistenciasIndex.spec.js @@ -0,0 +1,135 @@ +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'); + }); + }) +}) diff --git a/ui/src/views/empleados/EmpleadosIndex.vue b/ui/src/views/empleados/EmpleadosIndex.vue index 72e29c0..a7240fe 100644 --- a/ui/src/views/empleados/EmpleadosIndex.vue +++ b/ui/src/views/empleados/EmpleadosIndex.vue @@ -19,21 +19,7 @@ -
- Cambiar Vista: - - -
+
@@ -80,6 +66,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 +74,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,15 +87,7 @@ 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`; - } - // Inactive button uses secondary/gray styling - return `${baseClasses} bg-gray-200 text-gray-700 hover:bg-gray-300 focus:ring-gray-400`; -}; +// Removed btnClass as manual toggle buttons are removed // --- fetch inicial --- const fetchEmployees = async () => { 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..0dc3202 --- /dev/null +++ b/ui/src/views/empleados/__tests__/EmpleadosIndex.spec.js @@ -0,0 +1,135 @@ +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.'); + }); + }) +}) diff --git a/ui/src/views/planillas/PlanillasIndex.vue b/ui/src/views/planillas/PlanillasIndex.vue index fb455fc..8240899 100644 --- a/ui/src/views/planillas/PlanillasIndex.vue +++ b/ui/src/views/planillas/PlanillasIndex.vue @@ -7,15 +7,7 @@ -
- Cambiar Vista: - - -
+
Cargando planillas... @@ -60,18 +52,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..eea69b5 --- /dev/null +++ b/ui/src/views/planillas/__tests__/PlanillasIndex.spec.js @@ -0,0 +1,128 @@ +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'); + }); + }) +}) diff --git a/ui/src/views/tareas/TareasIndex.vue b/ui/src/views/tareas/TareasIndex.vue index b28ce0c..c31127b 100644 --- a/ui/src/views/tareas/TareasIndex.vue +++ b/ui/src/views/tareas/TareasIndex.vue @@ -7,15 +7,7 @@ -
- Cambiar Vista: - - -
+
Cargando tareas... @@ -60,18 +52,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..27bd6e4 --- /dev/null +++ b/ui/src/views/tareas/__tests__/TareasIndex.spec.js @@ -0,0 +1,128 @@ +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'); + }); + }) +}) From 35a64ff7bf6c2cd18ca783b9e119aef2642f5813 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 31 May 2025 08:05:59 +0000 Subject: [PATCH 2/2] feat: Reinstate local view toggles in module indexes This commit updates the module index pages to re-introduce local view toggle buttons, allowing you to temporarily switch between table and card visualizations for the current session. This change is based on your feedback to retain this flexibility alongside the new global default view settings. Key changes: - **Module Index Views (e.g., `EmpleadosIndex.vue`):** - Re-added icon-based toggle buttons for 'Table' and 'Card' views. - Styled these buttons using Tailwind CSS for a subtle and modern appearance. The active view's button is highlighted using the module's accent color. - Clicking these buttons updates a local `currentView` ref, which determines the displayed component (table or card). - This local selection overrides the global default view for the current session only and does not modify the saved default setting in the `useUi` store. - **Testing:** - Updated component tests for each module's index view (`AsistenciasIndex.spec.js`, `EmpleadosIndex.spec.js`, etc.). - Tests now verify: - Correct rendering and initial styling of the new toggle buttons based on the global default. - Local view switching functionality upon button clicks. - Correct update of button styling to reflect the active local view. - Confirmation that local view changes do not affect the global default view settings in the `useUi` store. This enhancement ensures that you can set a global default view for each module via settings, while still having the option to quickly toggle the view for your immediate needs without changing your saved preferences. --- ui/src/views/asistencias/AsistenciasIndex.vue | 28 ++++++- .../__tests__/AsistenciasIndex.spec.js | 81 +++++++++++++++++++ ui/src/views/empleados/EmpleadosIndex.vue | 26 +++++- .../__tests__/EmpleadosIndex.spec.js | 73 +++++++++++++++++ ui/src/views/planillas/PlanillasIndex.vue | 28 ++++++- .../__tests__/PlanillasIndex.spec.js | 75 +++++++++++++++++ ui/src/views/tareas/TareasIndex.vue | 28 ++++++- .../tareas/__tests__/TareasIndex.spec.js | 75 +++++++++++++++++ 8 files changed, 407 insertions(+), 7 deletions(-) diff --git a/ui/src/views/asistencias/AsistenciasIndex.vue b/ui/src/views/asistencias/AsistenciasIndex.vue index 087fc42..7da4e5f 100644 --- a/ui/src/views/asistencias/AsistenciasIndex.vue +++ b/ui/src/views/asistencias/AsistenciasIndex.vue @@ -7,7 +7,25 @@ - + +
+ + +
Cargando asistencias... @@ -95,7 +113,13 @@ const handleEditAsistencia = (asistenciaId) => { router.push({ name: 'asistencias-edit', params: { id: asistenciaId } }); }; -// Removed btnClass as manual toggle buttons are removed +const btnViewClass = (viewType) => { + const base = 'p-2 rounded-md transition-colors duration-150 ease-in-out'; + if (currentView.value === viewType) { + return `${base} bg-[var(--accent-color-asistencias)] text-white shadow-lg`; + } + 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`; +}; diff --git a/ui/src/views/asistencias/__tests__/AsistenciasIndex.spec.js b/ui/src/views/asistencias/__tests__/AsistenciasIndex.spec.js index 6854eb9..b04b9fa 100644 --- a/ui/src/views/asistencias/__tests__/AsistenciasIndex.spec.js +++ b/ui/src/views/asistencias/__tests__/AsistenciasIndex.spec.js @@ -132,4 +132,85 @@ describe('AsistenciasIndex.vue', () => { 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 a7240fe..90ec10b 100644 --- a/ui/src/views/empleados/EmpleadosIndex.vue +++ b/ui/src/views/empleados/EmpleadosIndex.vue @@ -19,7 +19,24 @@ - +
+ + +
@@ -88,6 +105,13 @@ const employees = empleados; // --- helpers --- // 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`; + } + 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 --- const fetchEmployees = async () => { diff --git a/ui/src/views/empleados/__tests__/EmpleadosIndex.spec.js b/ui/src/views/empleados/__tests__/EmpleadosIndex.spec.js index 0dc3202..496134a 100644 --- a/ui/src/views/empleados/__tests__/EmpleadosIndex.spec.js +++ b/ui/src/views/empleados/__tests__/EmpleadosIndex.spec.js @@ -132,4 +132,77 @@ describe('EmpleadosIndex.vue', () => { 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 8240899..9e76a88 100644 --- a/ui/src/views/planillas/PlanillasIndex.vue +++ b/ui/src/views/planillas/PlanillasIndex.vue @@ -7,7 +7,25 @@ - + +
+ + +
Cargando planillas... @@ -101,7 +119,13 @@ const handleEditPlanilla = (planillaId) => { router.push({ name: 'planillas-edit', params: { id: planillaId } }); }; -// Removed btnClass as manual toggle buttons are removed +const btnViewClass = (viewType) => { + const base = 'p-2 rounded-md transition-colors duration-150 ease-in-out'; + if (currentView.value === viewType) { + return `${base} bg-[var(--accent-color-planillas)] text-white shadow-lg`; + } + 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`; +}; diff --git a/ui/src/views/planillas/__tests__/PlanillasIndex.spec.js b/ui/src/views/planillas/__tests__/PlanillasIndex.spec.js index eea69b5..b168fbb 100644 --- a/ui/src/views/planillas/__tests__/PlanillasIndex.spec.js +++ b/ui/src/views/planillas/__tests__/PlanillasIndex.spec.js @@ -125,4 +125,79 @@ describe('PlanillasIndex.vue', () => { 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 c31127b..4ec5d98 100644 --- a/ui/src/views/tareas/TareasIndex.vue +++ b/ui/src/views/tareas/TareasIndex.vue @@ -7,7 +7,25 @@ - + +
+ + +
Cargando tareas... @@ -95,7 +113,13 @@ const handleEditTarea = (tareaId) => { router.push({ name: 'tareas-edit', params: { id: tareaId } }); }; -// Removed btnClass as manual toggle buttons are removed +const btnViewClass = (viewType) => { + const base = 'p-2 rounded-md transition-colors duration-150 ease-in-out'; + if (currentView.value === viewType) { + return `${base} bg-[var(--accent-color-tareas)] text-white shadow-lg`; + } + 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`; +}; diff --git a/ui/src/views/tareas/__tests__/TareasIndex.spec.js b/ui/src/views/tareas/__tests__/TareasIndex.spec.js index 27bd6e4..2ec8820 100644 --- a/ui/src/views/tareas/__tests__/TareasIndex.spec.js +++ b/ui/src/views/tareas/__tests__/TareasIndex.spec.js @@ -125,4 +125,79 @@ describe('TareasIndex.vue', () => { 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(); + }); + }); })