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.
This commit is contained in:
google-labs-jules[bot]
2025-05-31 07:59:01 +00:00
parent ef28cc6c71
commit 32aa41f59f
12 changed files with 1018 additions and 334 deletions

View File

@@ -48,10 +48,16 @@ describe('useUi Store', () => {
expect(store.accentColorTareas).toBe('#4CAF50') expect(store.accentColorTareas).toBe('#4CAF50')
expect(store.accentColorPlanillas).toBe('#FF9800') expect(store.accentColorPlanillas).toBe('#FF9800')
expect(store.accentColorAsistencias).toBe('#E91E63') expect(store.accentColorAsistencias).toBe('#E91E63')
expect(localStorageMock.getItem).toHaveBeenCalledWith(APPEARANCE_STORAGE_KEY) expect(store.accentColorConfiguracion).toBe('#607D8B')
}) // Check new default view defaults
expect(store.defaultViewEmpleados).toBe('table')
it('loads settings from localStorage including new accent colors if present', () => { 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) expect(localStorageMock.getItem).toHaveBeenCalledWith(APPEARANCE_STORAGE_KEY)
}) })
@@ -65,7 +71,18 @@ describe('useUi Store', () => {
accentColorTareas: '#00FF00', accentColorTareas: '#00FF00',
accentColorPlanillas: '#FFFF00', accentColorPlanillas: '#FFFF00',
accentColorAsistencias: '#FF00FF', 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)) localStorageMock.getItem.mockReturnValueOnce(JSON.stringify(storedSettings))
@@ -80,9 +97,21 @@ describe('useUi Store', () => {
expect(store.accentColorTareas).toBe('#00FF00') expect(store.accentColorTareas).toBe('#00FF00')
expect(store.accentColorPlanillas).toBe('#FFFF00') expect(store.accentColorPlanillas).toBe('#FFFF00')
expect(store.accentColorAsistencias).toBe('#FF00FF') 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 = { const olderStoredSettings = {
primaryColor: '#ABCDEF', primaryColor: '#ABCDEF',
theme: 'dark', theme: 'dark',
@@ -96,10 +125,20 @@ describe('useUi Store', () => {
expect(store.theme).toBe('dark') expect(store.theme).toBe('dark')
expect(store.fontSize).toBe(18) expect(store.fontSize).toBe(18)
// New accent colors should fall back to defaults // New accent colors should fall back to defaults
expect(store.accentColorEmpleados).toBe('#2196F3') expect(store.accentColorEmpleados).toBe('#2196F3') // Default
expect(store.accentColorTareas).toBe('#4CAF50') expect(store.accentColorTareas).toBe('#4CAF50') // Default
expect(store.accentColorPlanillas).toBe('#FF9800') expect(store.accentColorPlanillas).toBe('#FF9800') // Default
expect(store.accentColorAsistencias).toBe('#E91E63') 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', () => { it('falls back to default settings if localStorage data is invalid JSON', () => {
@@ -129,11 +168,14 @@ describe('useUi Store', () => {
const appearanceSettingKeysInTest = [ const appearanceSettingKeysInTest = [
'primaryColor', 'secondaryColor', 'warningColor', 'fontFamily', 'primaryColor', 'secondaryColor', 'warningColor', 'fontFamily',
'fontSize', 'animationsEnabled', 'backgroundColor', 'theme', 'fontSize', 'animationsEnabled', 'backgroundColor', 'theme',
'accentColorEmpleados', 'accentColorTareas', 'accentColorPlanillas', 'accentColorAsistencias', 'accentColorEmpleados', 'accentColorTareas', 'accentColorPlanillas',
const appearanceSettingKeys = [ 'accentColorAsistencias', 'accentColorConfiguracion',
'primaryColor', 'secondaryColor', 'warningColor', 'fontFamily', 'tableBgColorEmpleados', 'tableBgColorTareas', 'tableBgColorPlanillas',
'fontSize', 'animationsEnabled', 'backgroundColor', 'theme', 'tableBgColorAsistencias', 'tableBgColorConfiguracion',
] 'desktopNavbarPersistent',
'defaultViewEmpleados', 'defaultViewTareas', 'defaultViewPlanillas',
'defaultViewAsistencias', 'defaultViewConfiguracion',
];
it('setPrimaryColor updates state and saves to localStorage', () => { it('setPrimaryColor updates state and saves to localStorage', () => {
const store = useUi() const store = useUi()
@@ -215,7 +257,9 @@ describe('useUi Store', () => {
expect(Object.keys(savedData).length).toBe(appearanceSettingKeysInTest.length); expect(Object.keys(savedData).length).toBe(appearanceSettingKeysInTest.length);
expect(savedData.sidebarOpen).toBeUndefined() // Ensure non-appearance data is not saved expect(savedData.sidebarOpen).toBeUndefined() // Ensure non-appearance data is not saved
appearanceSettingKeysInTest.forEach(key => { 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.stringContaining('"accentColorAsistencias":"#FF7788"')
) )
}) })
expect(Object.keys(savedData).length).toBe(appearanceSettingKeys.length);
expect(savedData.sidebarOpen).toBeUndefined() // Ensure non-appearance data is not saved it('setAccentColorConfiguracion updates state and saves to localStorage', () => {
appearanceSettingKeys.forEach(key => { const store = useUi()
expect(savedData.hasOwnProperty(key)).toBe(true) 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"')
)
}) })
}) })
}) })

View File

@@ -24,6 +24,12 @@ const appearanceSettingKeys = [
'tableBgColorAsistencias', 'tableBgColorAsistencias',
'tableBgColorConfiguracion', 'tableBgColorConfiguracion',
'desktopNavbarPersistent', 'desktopNavbarPersistent',
// Default module views
'defaultViewEmpleados',
'defaultViewTareas',
'defaultViewPlanillas',
'defaultViewAsistencias',
'defaultViewConfiguracion',
] ]
const loadSettingsFromLocalStorage = () => { const loadSettingsFromLocalStorage = () => {
@@ -92,6 +98,12 @@ export const useUi = defineStore('ui', {
tableBgColorAsistencias: '#FFFFFF', tableBgColorAsistencias: '#FFFFFF',
tableBgColorConfiguracion: '#FFFFFF', tableBgColorConfiguracion: '#FFFFFF',
desktopNavbarPersistent: false, desktopNavbarPersistent: false,
// Default module views
'defaultViewEmpleados': 'table',
'defaultViewTareas': 'table',
'defaultViewPlanillas': 'table',
'defaultViewAsistencias': 'table',
'defaultViewConfiguracion': 'table',
} }
const loadedSettings = loadSettingsFromLocalStorage() const loadedSettings = loadSettingsFromLocalStorage()
@@ -203,6 +215,28 @@ export const useUi = defineStore('ui', {
setDesktopNavbarPersistent(enabled) { setDesktopNavbarPersistent(enabled) {
this.desktopNavbarPersistent = !!enabled // Ensure boolean this.desktopNavbarPersistent = !!enabled // Ensure boolean
_saveAppearanceState(this) _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)
} }
}, },
}) })

View File

@@ -78,7 +78,7 @@
<!-- Per-Module Color Settings --> <!-- Per-Module Color Settings -->
<section class="mb-10 module-settings-group"> <section class="mb-10 module-settings-group">
<h3 class="text-xl font-semibold mb-4 text-[var(--primary-color)] border-b border-[var(--secondary-color)] pb-2">Empleados Module</h3> <h3 class="text-xl font-semibold mb-4 text-[var(--primary-color)] border-b border-[var(--secondary-color)] pb-2">Empleados Module</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6"> <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
<div class="setting-item"> <div class="setting-item">
<label for="accentColorEmpleados" class="block text-sm font-medium mb-1">Accent Color</label> <label for="accentColorEmpleados" class="block text-sm font-medium mb-1">Accent Color</label>
<input type="color" id="accentColorEmpleados" v-model="ui.accentColorEmpleados" @input="ui.setAccentColorEmpleados($event.target.value)" <input type="color" id="accentColorEmpleados" v-model="ui.accentColorEmpleados" @input="ui.setAccentColorEmpleados($event.target.value)"
@@ -89,12 +89,20 @@
<input type="color" id="tableBgColorEmpleados" v-model="ui.tableBgColorEmpleados" @input="ui.setTableBgColorEmpleados($event.target.value)" <input type="color" id="tableBgColorEmpleados" v-model="ui.tableBgColorEmpleados" @input="ui.setTableBgColorEmpleados($event.target.value)"
class="w-full h-12 p-1 border rounded-lg cursor-pointer shadow-sm hover:opacity-80 transition-opacity border-[var(--secondary-color)] focus:border-[var(--primary-color)] focus:ring-1 focus:ring-[var(--primary-color)]"> class="w-full h-12 p-1 border rounded-lg cursor-pointer shadow-sm hover:opacity-80 transition-opacity border-[var(--secondary-color)] focus:border-[var(--primary-color)] focus:ring-1 focus:ring-[var(--primary-color)]">
</div> </div>
<div class="setting-item">
<label for="defaultViewEmpleados" class="block text-sm font-medium mb-1">Default View</label>
<select id="defaultViewEmpleados" v-model="ui.defaultViewEmpleados" @change="ui.setDefaultViewEmpleados($event.target.value)"
class="w-full p-3 border rounded-lg shadow-sm focus:ring-[var(--primary-color)] focus:border-[var(--primary-color)] transition-all duration-150 ease-in-out bg-white/10 dark:bg-black/10 border-[var(--secondary-color)] hover:border-[var(--primary-color)]">
<option value="table">Table</option>
<option value="card">Card</option>
</select>
</div>
</div> </div>
</section> </section>
<section class="mb-10 module-settings-group"> <section class="mb-10 module-settings-group">
<h3 class="text-xl font-semibold mb-4 text-[var(--primary-color)] border-b border-[var(--secondary-color)] pb-2">Tareas Module</h3> <h3 class="text-xl font-semibold mb-4 text-[var(--primary-color)] border-b border-[var(--secondary-color)] pb-2">Tareas Module</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6"> <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
<div class="setting-item"> <div class="setting-item">
<label for="accentColorTareas" class="block text-sm font-medium mb-1">Accent Color</label> <label for="accentColorTareas" class="block text-sm font-medium mb-1">Accent Color</label>
<input type="color" id="accentColorTareas" v-model="ui.accentColorTareas" @input="ui.setAccentColorTareas($event.target.value)" <input type="color" id="accentColorTareas" v-model="ui.accentColorTareas" @input="ui.setAccentColorTareas($event.target.value)"
@@ -105,12 +113,20 @@
<input type="color" id="tableBgColorTareas" v-model="ui.tableBgColorTareas" @input="ui.setTableBgColorTareas($event.target.value)" <input type="color" id="tableBgColorTareas" v-model="ui.tableBgColorTareas" @input="ui.setTableBgColorTareas($event.target.value)"
class="w-full h-12 p-1 border rounded-lg cursor-pointer shadow-sm hover:opacity-80 transition-opacity border-[var(--secondary-color)] focus:border-[var(--primary-color)] focus:ring-1 focus:ring-[var(--primary-color)]"> class="w-full h-12 p-1 border rounded-lg cursor-pointer shadow-sm hover:opacity-80 transition-opacity border-[var(--secondary-color)] focus:border-[var(--primary-color)] focus:ring-1 focus:ring-[var(--primary-color)]">
</div> </div>
<div class="setting-item">
<label for="defaultViewTareas" class="block text-sm font-medium mb-1">Default View</label>
<select id="defaultViewTareas" v-model="ui.defaultViewTareas" @change="ui.setDefaultViewTareas($event.target.value)"
class="w-full p-3 border rounded-lg shadow-sm focus:ring-[var(--primary-color)] focus:border-[var(--primary-color)] transition-all duration-150 ease-in-out bg-white/10 dark:bg-black/10 border-[var(--secondary-color)] hover:border-[var(--primary-color)]">
<option value="table">Table</option>
<option value="card">Card</option>
</select>
</div>
</div> </div>
</section> </section>
<section class="mb-10 module-settings-group"> <section class="mb-10 module-settings-group">
<h3 class="text-xl font-semibold mb-4 text-[var(--primary-color)] border-b border-[var(--secondary-color)] pb-2">Planillas Module</h3> <h3 class="text-xl font-semibold mb-4 text-[var(--primary-color)] border-b border-[var(--secondary-color)] pb-2">Planillas Module</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6"> <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
<div class="setting-item"> <div class="setting-item">
<label for="accentColorPlanillas" class="block text-sm font-medium mb-1">Accent Color</label> <label for="accentColorPlanillas" class="block text-sm font-medium mb-1">Accent Color</label>
<input type="color" id="accentColorPlanillas" v-model="ui.accentColorPlanillas" @input="ui.setAccentColorPlanillas($event.target.value)" <input type="color" id="accentColorPlanillas" v-model="ui.accentColorPlanillas" @input="ui.setAccentColorPlanillas($event.target.value)"
@@ -121,12 +137,20 @@
<input type="color" id="tableBgColorPlanillas" v-model="ui.tableBgColorPlanillas" @input="ui.setTableBgColorPlanillas($event.target.value)" <input type="color" id="tableBgColorPlanillas" v-model="ui.tableBgColorPlanillas" @input="ui.setTableBgColorPlanillas($event.target.value)"
class="w-full h-12 p-1 border rounded-lg cursor-pointer shadow-sm hover:opacity-80 transition-opacity border-[var(--secondary-color)] focus:border-[var(--primary-color)] focus:ring-1 focus:ring-[var(--primary-color)]"> class="w-full h-12 p-1 border rounded-lg cursor-pointer shadow-sm hover:opacity-80 transition-opacity border-[var(--secondary-color)] focus:border-[var(--primary-color)] focus:ring-1 focus:ring-[var(--primary-color)]">
</div> </div>
<div class="setting-item">
<label for="defaultViewPlanillas" class="block text-sm font-medium mb-1">Default View</label>
<select id="defaultViewPlanillas" v-model="ui.defaultViewPlanillas" @change="ui.setDefaultViewPlanillas($event.target.value)"
class="w-full p-3 border rounded-lg shadow-sm focus:ring-[var(--primary-color)] focus:border-[var(--primary-color)] transition-all duration-150 ease-in-out bg-white/10 dark:bg-black/10 border-[var(--secondary-color)] hover:border-[var(--primary-color)]">
<option value="table">Table</option>
<option value="card">Card</option>
</select>
</div>
</div> </div>
</section> </section>
<section class="mb-10 module-settings-group"> <section class="mb-10 module-settings-group">
<h3 class="text-xl font-semibold mb-4 text-[var(--primary-color)] border-b border-[var(--secondary-color)] pb-2">Asistencias Module</h3> <h3 class="text-xl font-semibold mb-4 text-[var(--primary-color)] border-b border-[var(--secondary-color)] pb-2">Asistencias Module</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6"> <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
<div class="setting-item"> <div class="setting-item">
<label for="accentColorAsistencias" class="block text-sm font-medium mb-1">Accent Color</label> <label for="accentColorAsistencias" class="block text-sm font-medium mb-1">Accent Color</label>
<input type="color" id="accentColorAsistencias" v-model="ui.accentColorAsistencias" @input="ui.setAccentColorAsistencias($event.target.value)" <input type="color" id="accentColorAsistencias" v-model="ui.accentColorAsistencias" @input="ui.setAccentColorAsistencias($event.target.value)"
@@ -137,12 +161,20 @@
<input type="color" id="tableBgColorAsistencias" v-model="ui.tableBgColorAsistencias" @input="ui.setTableBgColorAsistencias($event.target.value)" <input type="color" id="tableBgColorAsistencias" v-model="ui.tableBgColorAsistencias" @input="ui.setTableBgColorAsistencias($event.target.value)"
class="w-full h-12 p-1 border rounded-lg cursor-pointer shadow-sm hover:opacity-80 transition-opacity border-[var(--secondary-color)] focus:border-[var(--primary-color)] focus:ring-1 focus:ring-[var(--primary-color)]"> class="w-full h-12 p-1 border rounded-lg cursor-pointer shadow-sm hover:opacity-80 transition-opacity border-[var(--secondary-color)] focus:border-[var(--primary-color)] focus:ring-1 focus:ring-[var(--primary-color)]">
</div> </div>
<div class="setting-item">
<label for="defaultViewAsistencias" class="block text-sm font-medium mb-1">Default View</label>
<select id="defaultViewAsistencias" v-model="ui.defaultViewAsistencias" @change="ui.setDefaultViewAsistencias($event.target.value)"
class="w-full p-3 border rounded-lg shadow-sm focus:ring-[var(--primary-color)] focus:border-[var(--primary-color)] transition-all duration-150 ease-in-out bg-white/10 dark:bg-black/10 border-[var(--secondary-color)] hover:border-[var(--primary-color)]">
<option value="table">Table</option>
<option value="card">Card</option>
</select>
</div>
</div> </div>
</section> </section>
<section class="mb-10 module-settings-group"> <section class="mb-10 module-settings-group">
<h3 class="text-xl font-semibold mb-4 text-[var(--primary-color)] border-b border-[var(--secondary-color)] pb-2">Configuración Module</h3> <h3 class="text-xl font-semibold mb-4 text-[var(--primary-color)] border-b border-[var(--secondary-color)] pb-2">Configuración Module</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6"> <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
<div class="setting-item"> <div class="setting-item">
<label for="accentColorConfiguracion" class="block text-sm font-medium mb-1">Accent Color</label> <label for="accentColorConfiguracion" class="block text-sm font-medium mb-1">Accent Color</label>
<input type="color" id="accentColorConfiguracion" v-model="ui.accentColorConfiguracion" @input="ui.setAccentColorConfiguracion($event.target.value)" <input type="color" id="accentColorConfiguracion" v-model="ui.accentColorConfiguracion" @input="ui.setAccentColorConfiguracion($event.target.value)"
@@ -153,6 +185,14 @@
<input type="color" id="tableBgColorConfiguracion" v-model="ui.tableBgColorConfiguracion" @input="ui.setTableBgColorConfiguracion($event.target.value)" <input type="color" id="tableBgColorConfiguracion" v-model="ui.tableBgColorConfiguracion" @input="ui.setTableBgColorConfiguracion($event.target.value)"
class="w-full h-12 p-1 border rounded-lg cursor-pointer shadow-sm hover:opacity-80 transition-opacity border-[var(--secondary-color)] focus:border-[var(--primary-color)] focus:ring-1 focus:ring-[var(--primary-color)]"> class="w-full h-12 p-1 border rounded-lg cursor-pointer shadow-sm hover:opacity-80 transition-opacity border-[var(--secondary-color)] focus:border-[var(--primary-color)] focus:ring-1 focus:ring-[var(--primary-color)]">
</div> </div>
<div class="setting-item">
<label for="defaultViewConfiguracion" class="block text-sm font-medium mb-1">Default View</label>
<select id="defaultViewConfiguracion" v-model="ui.defaultViewConfiguracion" @change="ui.setDefaultViewConfiguracion($event.target.value)"
class="w-full p-3 border rounded-lg shadow-sm focus:ring-[var(--primary-color)] focus:border-[var(--primary-color)] transition-all duration-150 ease-in-out bg-white/10 dark:bg-black/10 border-[var(--secondary-color)] hover:border-[var(--primary-color)]">
<option value="table">Table</option>
<option value="card">Card</option>
</select>
</div>
</div> </div>
</section> </section>
</div> </div>

View File

@@ -4,89 +4,68 @@ import { createPinia, setActivePinia } from 'pinia'
import { useUi } from '../../stores/useUi' // Adjust path import { useUi } from '../../stores/useUi' // Adjust path
import SettingsView from '../SettingsView.vue' // Adjust path import SettingsView from '../SettingsView.vue' // Adjust path
// Helper to create a fresh store for each test or group // No global 'store' variable here, it will be test-specific or wrapper-specific
const getFreshStore = () => { // Helper to manage store and wrapper creation for tests
setActivePinia(createPinia()) const setupTestEnvironment = (initialStoreState = {}) => {
return useUi() 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', () => { describe('SettingsView.vue', () => {
// No global 'store' variable here, it will be test-specific or wrapper-specific
beforeEach(() => { beforeEach(() => {
// Ensure a fresh Pinia instance is active for each test. // Vitest's vi.clearAllMocks() might be useful here if mocks persist unexpectedly
setActivePinia(createPinia()) // 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.
// Clear localStorage mock before each test // For this example, setupTestEnvironment manages its own localStorage mock.
// 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
}
it('renders all input elements with initial values from store', () => { it('renders all input elements with initial values from store', () => {
const { wrapper } = createWrapperAndStore({ // Store is created inside here after LS mock const { wrapper } = setupTestEnvironment({
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({
primaryColor: '#111111', primaryColor: '#111111',
secondaryColor: '#222222', secondaryColor: '#222222',
warningColor: '#333333', warningColor: '#333333',
@@ -96,148 +75,181 @@ describe('SettingsView.vue', () => {
animationsEnabled: false, animationsEnabled: false,
theme: 'dark', theme: 'dark',
// Add new accent colors for comprehensive initial state testing // 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', accentColorEmpleados: '#123456',
accentColorTareas: '#654321', accentColorTareas: '#654321',
accentColorPlanillas: '#abcdef', accentColorPlanillas: '#abcdef',
accentColorAsistencias: '#fedcba', 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#warningColor').element.value).toBe('#333333')
expect(wrapper.find('input#backgroundColor').element.value).toBe('#444444') expect(wrapper.find('input#backgroundColor').element.value).toBe('#444444')
expect(wrapper.find('input#fontFamily').element.value).toBe('Arial') expect(wrapper.find('input#fontFamily').element.value).toBe('Arial')
expect(wrapper.find('input#fontSize').element.value).toBe('18') expect(wrapper.find('input#fontSize').element.value).toBe('18')
expect(wrapper.find('input#animationsEnabled').element.checked).toBe(false) expect(wrapper.find('input#animationsEnabled').element.checked).toBe(false);
expect(wrapper.find('select#theme').element.value).toBe('dark') expect(wrapper.find('select#theme').element.value).toBe('dark');
// Check new module accent color pickers // Check module accent color pickers
const h2Elements = wrapper.findAll('h2') expect(wrapper.find('input#accentColorEmpleados').element.value).toBe('#123456');
const sectionTitles = h2Elements.map(h => h.text()) expect(wrapper.find('input#accentColorTareas').element.value).toBe('#654321');
expect(sectionTitles).toContain('Module Accent Colors') expect(wrapper.find('input#accentColorPlanillas').element.value).toBe('#abcdef');
expect(wrapper.find('input#accentColorEmpleados').element.value).toBe('#123456') expect(wrapper.find('input#accentColorAsistencias').element.value).toBe('#fedcba');
expect(wrapper.find('input#accentColorTareas').element.value).toBe('#654321') expect(wrapper.find('input#accentColorConfiguracion').element.value).toBe('#aabbcc');
expect(wrapper.find('input#accentColorPlanillas').element.value).toBe('#abcdef')
expect(wrapper.find('input#accentColorAsistencias').element.value).toBe('#fedcba') // 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 () => { it('calls setPrimaryColor action when primary color input changes', async () => {
const { wrapper, store } = createWrapperAndStore() const { wrapper, store } = setupTestEnvironment();
const spy = vi.spyOn(store, 'setPrimaryColor') const spy = vi.spyOn(store, 'setPrimaryColor');
const colorInput = wrapper.find('input#primaryColor') const colorInput = wrapper.find('input#primaryColor');
colorInput.element.value = '#FF00FF';
colorInput.element.value = '#FF00FF' await colorInput.trigger('input');
await colorInput.trigger('input') expect(spy).toHaveBeenCalledWith('#ff00ff');
});
expect(spy).toHaveBeenCalledWith('#ff00ff')
})
it('calls setFontFamily action when font family input changes', async () => { it('calls setFontFamily action when font family input changes', async () => {
const { wrapper, store } = createWrapperAndStore() const { wrapper, store } = setupTestEnvironment();
const spy = vi.spyOn(store, 'setFontFamily') const spy = vi.spyOn(store, 'setFontFamily');
const input = wrapper.find('input#fontFamily') const input = wrapper.find('input#fontFamily');
await input.setValue('Helvetica') await input.setValue('Helvetica');
}) expect(spy).toHaveBeenCalledWith('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')
})
it('calls setFontSize action when font size input changes', async () => { it('calls setFontSize action when font size input changes', async () => {
const { wrapper, store } = createWrapperAndStore() const { wrapper, store } = setupTestEnvironment();
const wrapper = createWrapper() const spy = vi.spyOn(store, 'setFontSize');
const spy = vi.spyOn(store, 'setFontSize') const input = wrapper.find('input#fontSize');
const input = wrapper.find('input#fontSize') await input.setValue('22');
await input.setValue('22') expect(spy).toHaveBeenCalledWith(22);
expect(spy).toHaveBeenCalledWith(22) });
})
it('calls setAnimationsEnabled action when animations checkbox changes', async () => { it('calls setAnimationsEnabled action when animations checkbox changes', async () => {
const { wrapper, store } = createWrapperAndStore({ animationsEnabled: true }) const { wrapper, store } = setupTestEnvironment({ animationsEnabled: true });
const spy = vi.spyOn(store, 'setAnimationsEnabled') const spy = vi.spyOn(store, 'setAnimationsEnabled');
const checkbox = wrapper.find('input#animationsEnabled') const checkbox = wrapper.find('input#animationsEnabled');
await checkbox.setChecked(false);
await checkbox.setChecked(false) expect(spy).toHaveBeenCalledWith(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)
})
it('calls setTheme action when theme select changes', async () => { it('calls setTheme action when theme select changes', async () => {
const { wrapper, store } = createWrapperAndStore() const { wrapper, store } = setupTestEnvironment();
const spy = vi.spyOn(store, 'setTheme') const spy = vi.spyOn(store, 'setTheme');
const select = wrapper.find('select#theme') const select = wrapper.find('select#theme');
await select.setValue('dark') await select.setValue('dark');
expect(spy).toHaveBeenCalledWith('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 () => { it('calls setAccentColorEmpleados action when empleados accent color input changes', async () => {
const { wrapper, store } = createWrapperAndStore() const { wrapper, store } = setupTestEnvironment();
const spy = vi.spyOn(store, 'setAccentColorEmpleados') const spy = vi.spyOn(store, 'setAccentColorEmpleados');
const colorInput = wrapper.find('input#accentColorEmpleados') const colorInput = wrapper.find('input#accentColorEmpleados');
colorInput.element.value = '#aabbcc' colorInput.element.value = '#aabbcc';
await colorInput.trigger('input') await colorInput.trigger('input');
expect(spy).toHaveBeenCalledWith('#aabbcc') expect(spy).toHaveBeenCalledWith('#aabbcc');
}) });
it('calls setAccentColorTareas action when tareas accent color input changes', async () => { it('calls setAccentColorConfiguracion action when configuracion accent color input changes', async () => {
const { wrapper, store } = createWrapperAndStore() const { wrapper, store } = setupTestEnvironment();
const spy = vi.spyOn(store, 'setAccentColorTareas') const spy = vi.spyOn(store, 'setAccentColorConfiguracion');
const colorInput = wrapper.find('input#accentColorTareas') const colorInput = wrapper.find('input#accentColorConfiguracion');
colorInput.element.value = '#ccbbaa' colorInput.element.value = '#ccddee';
await colorInput.trigger('input') await colorInput.trigger('input');
expect(spy).toHaveBeenCalledWith('#ccbbaa') 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 () => { // *** ADD NEW TESTS FOR DEFAULT VIEW SELECTS HERE ***
const { wrapper, store } = createWrapperAndStore() describe('Default View Selects', () => {
const spy = vi.spyOn(store, 'setAccentColorAsistencias') it('renders default view select for Empleados module and calls action on change', async () => {
const colorInput = wrapper.find('input#accentColorAsistencias') const { wrapper, store } = setupTestEnvironment({ defaultViewEmpleados: 'table' });
colorInput.element.value = '#c3b2a1' const select = wrapper.find('select#defaultViewEmpleados');
await colorInput.trigger('input') expect(select.exists()).toBe(true);
expect(spy).toHaveBeenCalledWith('#c3b2a1') 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 () => { 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';
store.primaryColor = '#001122' // Directly manipulate the store used by the component
======= =======
const wrapper = createWrapper() const wrapper = createWrapper()
const spy = vi.spyOn(store, 'setTheme') const spy = vi.spyOn(store, 'setTheme')
@@ -251,51 +263,43 @@ describe('SettingsView.vue', () => {
// Test primaryColor // Test primaryColor
store.primaryColor = '#001122' store.primaryColor = '#001122'
await wrapper.vm.$nextTick() // Wait for Vue to react to state change await wrapper.vm.$nextTick();
expect(wrapper.find('input#primaryColor').element.value).toBe('#001122') expect(wrapper.find('input#primaryColor').element.value).toBe('#001122');
// Test fontFamily store.fontFamily = 'Verdana';
store.fontFamily = 'Verdana' await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick() expect(wrapper.find('input#fontFamily').element.value).toBe('Verdana');
expect(wrapper.find('input#fontFamily').element.value).toBe('Verdana')
// Test fontSize store.fontSize = 12;
store.fontSize = 12 await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick() expect(wrapper.find('input#fontSize').element.value).toBe('12');
expect(wrapper.find('input#fontSize').element.value).toBe('12')
// Test animationsEnabled store.animationsEnabled = false;
store.animationsEnabled = false await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick() expect(wrapper.find('input#animationsEnabled').element.checked).toBe(false);
expect(wrapper.find('input#animationsEnabled').element.checked).toBe(false)
// Test theme store.theme = 'dark';
store.theme = 'dark' await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick() expect(wrapper.find('select#theme').element.value).toBe('dark');
expect(wrapper.find('select#theme').element.value).toBe('dark')
// Test one of the new accent colors store.accentColorEmpleados = '#998877';
store.accentColorEmpleados = '#998877' await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick() expect(wrapper.find('input#accentColorEmpleados').element.value).toBe('#998877');
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 () => { 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 vi.advanceTimersByTime(100);
const wrapper = createWrapper() await wrapper.vm.$nextTick();
expect(wrapper.find('.settings-view').classes()).not.toContain('opacity-100') expect(wrapper.find('.settings-view').classes()).toContain('opacity-100');
vi.useRealTimers();
// 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
})
})

View File

@@ -7,15 +7,7 @@
</button> </button>
</header> </header>
<div class="mb-6 flex justify-end items-center space-x-3"> <!-- Removed manual view toggle buttons -->
<span class="text-sm font-medium text-gray-700">Cambiar Vista:</span>
<button @click="currentView = 'card'" :class="btnClass('card')">
Tarjetas
</button>
<button @click="currentView = 'table'" :class="btnClass('table')">
Tabla
</button>
</div>
<div v-if="isLoading" class="loading-message"> <div v-if="isLoading" class="loading-message">
Cargando asistencias... Cargando asistencias...
@@ -60,18 +52,21 @@
<script setup> <script setup>
import { ref, computed, onMounted } from 'vue'; import { ref, computed, onMounted } from 'vue';
import { useAsistenciasStore } from '../../stores/useAsistencias'; import { useAsistenciasStore } from '../../stores/useAsistencias';
import { useUi } from '../../stores/useUi'; // Import useUi
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import TablaAsistencias from '../../components/asistencias/tablaAsistencias.vue'; import TablaAsistencias from '../../components/asistencias/tablaAsistencias.vue';
import CardAsistencia from '../../components/asistencias/cardAsistencia.vue'; import CardAsistencia from '../../components/asistencias/cardAsistencia.vue';
const asistenciasStore = useAsistenciasStore(); const asistenciasStore = useAsistenciasStore();
const ui = useUi(); // Access the ui store
const router = useRouter(); const router = useRouter();
const isLoading = ref(true); const isLoading = ref(true);
const errorLoading = ref(false); const errorLoading = ref(false);
const errorMessage = ref(''); const errorMessage = ref('');
const currentView = ref('table'); // Default to table view // Initialize currentView from the store's default setting for asistencias
const currentView = ref(ui.defaultViewAsistencias);
const asistenciasList = computed(() => asistenciasStore.asistencias); const asistenciasList = computed(() => asistenciasStore.asistencias);
@@ -100,13 +95,7 @@ const handleEditAsistencia = (asistenciaId) => {
router.push({ name: 'asistencias-edit', params: { id: asistenciaId } }); router.push({ name: 'asistencias-edit', params: { id: asistenciaId } });
}; };
const btnClass = (view) => { // Removed btnClass as manual toggle buttons are removed
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) {
return `${baseClasses} text-white shadow-sm view-toggle-active-asistencias`;
}
return `${baseClasses} bg-gray-200 text-gray-700 hover:bg-gray-300 focus:ring-gray-400`;
};
</script> </script>

View File

@@ -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: '<div data-testid="tabla-asistencias"></div>',
},
}))
vi.mock('@/components/asistencias/cardAsistencia.vue', () => ({
default: {
name: 'CardAsistencia',
props: ['asistencia'], // Match actual props
emits: ['edit'],
template: '<div data-testid="card-asistencia"></div>',
},
}))
// 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');
});
})
})

View File

@@ -19,21 +19,7 @@
</header> </header>
<!-- selector de vista --> <!-- selector de vista -->
<div class="mb-6 flex justify-end items-center space-x-3"> <!-- Removed manual view toggle buttons -->
<span class="text-sm font-medium text-gray-700">Cambiar Vista:</span>
<button
@click="currentView = 'card'"
:class="btnClass('card')"
>
Tarjetas
</button>
<button
@click="currentView = 'table'"
:class="btnClass('table')"
>
Tabla
</button>
</div>
<!-- contenido --> <!-- contenido -->
<div> <div>
@@ -80,6 +66,7 @@
import { ref, onMounted, computed } from 'vue'; import { ref, onMounted, computed } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useUi } from '@/stores/useUi'; // Import useUi
import CardEmpleado from '@/components/empleados/cardEmpleado.vue'; import CardEmpleado from '@/components/empleados/cardEmpleado.vue';
import TablaEmpleados from '@/components/empleados/tablaEmpleados.vue'; import TablaEmpleados from '@/components/empleados/tablaEmpleados.vue';
@@ -87,7 +74,8 @@ import { useEmpleadosStore } from '@/stores/useEmpleados.js'; // ruta según tu
// --- refs locales --- // --- refs locales ---
const router = useRouter(); 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 loading = ref(true);
const error = ref<string | null>(null); const error = ref<string | null>(null);
@@ -99,15 +87,7 @@ const { empleados } = storeToRefs(empleadosStore);
const employees = empleados; const employees = empleados;
// --- helpers --- // --- helpers ---
const btnClass = (view: 'card' | 'table') => { // Removed btnClass as manual toggle buttons are removed
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`;
};
// --- fetch inicial --- // --- fetch inicial ---
const fetchEmployees = async () => { const fetchEmployees = async () => {

View File

@@ -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: '<div data-testid="tabla-empleados"></div>',
},
}))
vi.mock('@/components/empleados/cardEmpleado.vue', () => ({
default: {
name: 'CardEmpleado',
props: ['employee'],
template: '<div data-testid="card-empleado"></div>',
},
}))
// 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.');
});
})
})

View File

@@ -7,15 +7,7 @@
</button> </button>
</header> </header>
<div class="mb-6 flex justify-end items-center space-x-3"> <!-- Removed manual view toggle buttons -->
<span class="text-sm font-medium text-gray-700">Cambiar Vista:</span>
<button @click="currentView = 'card'" :class="btnClass('card')">
Tarjetas
</button>
<button @click="currentView = 'table'" :class="btnClass('table')">
Tabla
</button>
</div>
<div v-if="isLoading" class="loading-message"> <div v-if="isLoading" class="loading-message">
Cargando planillas... Cargando planillas...
@@ -60,18 +52,21 @@
<script setup> <script setup>
import { ref, computed, onMounted } from 'vue'; import { ref, computed, onMounted } from 'vue';
import { usePlanillasStore } from '../../stores/usePlanillas'; import { usePlanillasStore } from '../../stores/usePlanillas';
import { useUi } from '../../stores/useUi'; // Import useUi
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import TablaPlanillas from '../../components/planillas/tablaPlanillas.vue'; // Corrected path import TablaPlanillas from '../../components/planillas/tablaPlanillas.vue'; // Corrected path
import CardPlanilla from '../../components/planillas/cardPlanilla.vue'; import CardPlanilla from '../../components/planillas/cardPlanilla.vue';
const planillasStore = usePlanillasStore(); const planillasStore = usePlanillasStore();
const ui = useUi(); // Access the ui store
const router = useRouter(); const router = useRouter();
const isLoading = ref(true); // Set to true initially const isLoading = ref(true); // Set to true initially
const errorLoading = ref(false); const errorLoading = ref(false);
const errorMessage = ref(''); const errorMessage = ref('');
const currentView = ref('table'); // Default to table view // Initialize currentView from the store's default setting for planillas
const currentView = ref(ui.defaultViewPlanillas);
// Computed property to get planillas from the store // Computed property to get planillas from the store
const planillasList = computed(() => planillasStore.planillas); const planillasList = computed(() => planillasStore.planillas);
@@ -106,13 +101,7 @@ const handleEditPlanilla = (planillaId) => {
router.push({ name: 'planillas-edit', params: { id: planillaId } }); router.push({ name: 'planillas-edit', params: { id: planillaId } });
}; };
const btnClass = (view) => { // Removed btnClass as manual toggle buttons are removed
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) {
return `${baseClasses} text-white shadow-sm view-toggle-active-planillas`;
}
return `${baseClasses} bg-gray-200 text-gray-700 hover:bg-gray-300 focus:ring-gray-400`;
};
</script> </script>

View File

@@ -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: '<div data-testid="tabla-planillas"></div>',
},
}))
vi.mock('@/components/planillas/cardPlanilla.vue', () => ({
default: {
name: 'CardPlanilla',
props: ['planilla'], // Match actual props
emits: ['edit'],
template: '<div data-testid="card-planilla"></div>',
},
}))
// 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');
});
})
})

View File

@@ -7,15 +7,7 @@
</button> </button>
</header> </header>
<div class="mb-6 flex justify-end items-center space-x-3"> <!-- Removed manual view toggle buttons -->
<span class="text-sm font-medium text-gray-700">Cambiar Vista:</span>
<button @click="currentView = 'card'" :class="btnClass('card')">
Tarjetas
</button>
<button @click="currentView = 'table'" :class="btnClass('table')">
Tabla
</button>
</div>
<div v-if="isLoading" class="loading-message"> <div v-if="isLoading" class="loading-message">
Cargando tareas... Cargando tareas...
@@ -60,18 +52,21 @@
<script setup> <script setup>
import { ref, computed, onMounted } from 'vue'; import { ref, computed, onMounted } from 'vue';
import { useTareasStore } from '../../stores/useTareas'; import { useTareasStore } from '../../stores/useTareas';
import { useUi } from '../../stores/useUi'; // Import useUi
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import TablaTareas from '../../components/tareas/tablaTareas.vue'; import TablaTareas from '../../components/tareas/tablaTareas.vue';
import CardTarea from '../../components/tareas/cardTarea.vue'; import CardTarea from '../../components/tareas/cardTarea.vue';
const tareasStore = useTareasStore(); const tareasStore = useTareasStore();
const ui = useUi(); // Access the ui store
const router = useRouter(); const router = useRouter();
const isLoading = ref(true); const isLoading = ref(true);
const errorLoading = ref(false); const errorLoading = ref(false);
const errorMessage = ref(''); const errorMessage = ref('');
const currentView = ref('table'); // Default to table view // Initialize currentView from the store's default setting for tareas
const currentView = ref(ui.defaultViewTareas);
const tareasList = computed(() => tareasStore.tareas); const tareasList = computed(() => tareasStore.tareas);
@@ -100,13 +95,7 @@ const handleEditTarea = (tareaId) => {
router.push({ name: 'tareas-edit', params: { id: tareaId } }); router.push({ name: 'tareas-edit', params: { id: tareaId } });
}; };
const btnClass = (view) => { // Removed btnClass as manual toggle buttons are removed
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) {
return `${baseClasses} text-white shadow-sm view-toggle-active-tareas`;
}
return `${baseClasses} bg-gray-200 text-gray-700 hover:bg-gray-300 focus:ring-gray-400`;
};
</script> </script>

View File

@@ -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: '<div data-testid="tabla-tareas"></div>',
},
}))
vi.mock('@/components/tareas/cardTarea.vue', () => ({
default: {
name: 'CardTarea',
props: ['tarea'], // Match actual props
emits: ['edit'],
template: '<div data-testid="card-tarea"></div>',
},
}))
// 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');
});
})
})