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

@@ -78,7 +78,7 @@
<!-- Per-Module Color Settings -->
<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>
<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">
<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)"
@@ -89,12 +89,20 @@
<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)]">
</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>
</section>
<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>
<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">
<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)"
@@ -105,12 +113,20 @@
<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)]">
</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>
</section>
<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>
<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">
<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)"
@@ -121,12 +137,20 @@
<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)]">
</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>
</section>
<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>
<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">
<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)"
@@ -137,12 +161,20 @@
<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)]">
</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>
</section>
<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>
<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">
<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)"
@@ -153,6 +185,14 @@
<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)]">
</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>
</section>
</div>

View File

@@ -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();
});
});

View File

@@ -7,15 +7,7 @@
</button>
</header>
<div class="mb-6 flex justify-end items-center space-x-3">
<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>
<!-- Removed manual view toggle buttons -->
<div v-if="isLoading" class="loading-message">
Cargando asistencias...
@@ -60,18 +52,21 @@
<script setup>
import { ref, computed, onMounted } from 'vue';
import { useAsistenciasStore } from '../../stores/useAsistencias';
import { useUi } from '../../stores/useUi'; // Import useUi
import { useRouter } from 'vue-router';
import TablaAsistencias from '../../components/asistencias/tablaAsistencias.vue';
import CardAsistencia from '../../components/asistencias/cardAsistencia.vue';
const asistenciasStore = useAsistenciasStore();
const ui = useUi(); // Access the ui store
const router = useRouter();
const isLoading = ref(true);
const errorLoading = ref(false);
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);
@@ -100,13 +95,7 @@ const handleEditAsistencia = (asistenciaId) => {
router.push({ name: 'asistencias-edit', params: { id: asistenciaId } });
};
const btnClass = (view) => {
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`;
};
// Removed btnClass as manual toggle buttons are removed
</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>
<!-- selector de vista -->
<div class="mb-6 flex justify-end items-center space-x-3">
<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>
<!-- Removed manual view toggle buttons -->
<!-- contenido -->
<div>
@@ -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<string | null>(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 () => {

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>
</header>
<div class="mb-6 flex justify-end items-center space-x-3">
<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>
<!-- Removed manual view toggle buttons -->
<div v-if="isLoading" class="loading-message">
Cargando planillas...
@@ -60,18 +52,21 @@
<script setup>
import { ref, computed, onMounted } from 'vue';
import { usePlanillasStore } from '../../stores/usePlanillas';
import { useUi } from '../../stores/useUi'; // Import useUi
import { useRouter } from 'vue-router';
import TablaPlanillas from '../../components/planillas/tablaPlanillas.vue'; // Corrected path
import CardPlanilla from '../../components/planillas/cardPlanilla.vue';
const planillasStore = usePlanillasStore();
const ui = useUi(); // Access the ui store
const router = useRouter();
const isLoading = ref(true); // Set to true initially
const errorLoading = ref(false);
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
const planillasList = computed(() => planillasStore.planillas);
@@ -106,13 +101,7 @@ const handleEditPlanilla = (planillaId) => {
router.push({ name: 'planillas-edit', params: { id: planillaId } });
};
const btnClass = (view) => {
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`;
};
// Removed btnClass as manual toggle buttons are removed
</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>
</header>
<div class="mb-6 flex justify-end items-center space-x-3">
<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>
<!-- Removed manual view toggle buttons -->
<div v-if="isLoading" class="loading-message">
Cargando tareas...
@@ -60,18 +52,21 @@
<script setup>
import { ref, computed, onMounted } from 'vue';
import { useTareasStore } from '../../stores/useTareas';
import { useUi } from '../../stores/useUi'; // Import useUi
import { useRouter } from 'vue-router';
import TablaTareas from '../../components/tareas/tablaTareas.vue';
import CardTarea from '../../components/tareas/cardTarea.vue';
const tareasStore = useTareasStore();
const ui = useUi(); // Access the ui store
const router = useRouter();
const isLoading = ref(true);
const errorLoading = ref(false);
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);
@@ -100,13 +95,7 @@ const handleEditTarea = (tareaId) => {
router.push({ name: 'tareas-edit', params: { id: tareaId } });
};
const btnClass = (view) => {
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`;
};
// Removed btnClass as manual toggle buttons are removed
</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');
});
})
})