Verbessere die Handhabung von Datumsbereichen in der Worklog-Abfrage und füge Fehlerbehandlung für Statistikabfragen in den Komponenten hinzu.
All checks were successful
Deploy production / deploy (push) Successful in 42s
All checks were successful
Deploy production / deploy (push) Successful in 42s
This commit is contained in:
@@ -367,7 +367,7 @@ class WorklogRepository {
|
|||||||
where: {
|
where: {
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
tstamp: {
|
tstamp: {
|
||||||
[Op.between]: [startKey, endKey]
|
[Op.between]: [startDate, endDate]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
order: [['tstamp', 'ASC']],
|
order: [['tstamp', 'ASC']],
|
||||||
@@ -438,7 +438,7 @@ class WorklogRepository {
|
|||||||
where: {
|
where: {
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
tstamp: {
|
tstamp: {
|
||||||
[Op.between]: [startKey, endKey]
|
[Op.between]: [startDate, endDate]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
order: [['tstamp', 'ASC']] // Aufsteigend, damit Start vor Stop kommt
|
order: [['tstamp', 'ASC']] // Aufsteigend, damit Start vor Stop kommt
|
||||||
|
|||||||
@@ -101,3 +101,74 @@ test('Status: keine Fehlzeit für kranken Montag, Pausen angerechnet, laufende W
|
|||||||
assert.equal(stats.adjustedEndTodayWeek, '16:30');
|
assert.equal(stats.adjustedEndTodayWeek, '16:30');
|
||||||
assert.equal(stats.totalOvertime, '+5:49');
|
assert.equal(stats.totalOvertime, '+5:49');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Repository nicht ersetzen: Genau dort muss die Datumsabfrage funktionieren.
|
||||||
|
const { Op } = require('sequelize');
|
||||||
|
const weekController = require('../src/controllers/WeekOverviewController');
|
||||||
|
const timeController = require('../src/controllers/TimeEntryController');
|
||||||
|
const actualFindByDateRange = repository.findByDateRange;
|
||||||
|
|
||||||
|
function responseRecorder() {
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
status(code) { this.statusCode = code; return this; },
|
||||||
|
json(body) { this.body = body; return this; }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupRepositoryFlow(t) {
|
||||||
|
setupWeek(t);
|
||||||
|
mock(t, repository, 'findByDateRange', actualFindByDateRange);
|
||||||
|
mock(t, database, 'getModels', () => ({
|
||||||
|
Worklog: {
|
||||||
|
findAll: async options => {
|
||||||
|
assert.equal(options.where.user_id, 1);
|
||||||
|
const range = options.where.tstamp[Op.between];
|
||||||
|
assert.equal(range[0].getTime(), new Date('2026-10-26T00:00:00').getTime());
|
||||||
|
assert.equal(range[1].getTime(), new Date('2026-11-01T23:59:59.999').getTime());
|
||||||
|
return [{ id: 1, state: 'start work', tstamp: '2026-10-27 08:00:00' }];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
User: { findByPk: async () => ({ daily_hours: 8 }) },
|
||||||
|
Timewish: { findOne: async () => null, findAll: async () => [] }
|
||||||
|
}));
|
||||||
|
mock(t, service, '_calculateTotalOvertime', async () => ({ minutes: 349, formatted: '+5:49' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
test('Wochen-API durchläuft echte Repository-Abfrage und liefert aktuelle Woche', async t => {
|
||||||
|
setupRepositoryFlow(t);
|
||||||
|
const res = responseRecorder();
|
||||||
|
await weekController.getWeekOverview({ user: { userId: 1 }, query: { weekOffset: '0' } }, res);
|
||||||
|
assert.equal(res.statusCode, 200);
|
||||||
|
assert.equal(res.body.data.weekStart, '2026-10-26');
|
||||||
|
assert.equal(res.body.data.weekEnd, '2026-11-01');
|
||||||
|
assert.equal(res.body.data.days[0].sick.hours, 8);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Statistik-API durchläuft echte Repository-Abfrage und liefert Werte statt Fehlerobjekt', async t => {
|
||||||
|
setupRepositoryFlow(t);
|
||||||
|
const res = responseRecorder();
|
||||||
|
await timeController.getStats({ user: { userId: 1 } }, res);
|
||||||
|
assert.equal(res.statusCode, 200);
|
||||||
|
assert.equal(res.body.overtime, '+0:00');
|
||||||
|
assert.equal(res.body.nonWorkingHours, '8:00');
|
||||||
|
assert.match(res.body.open, /^\d{2}:\d{2}:\d{2}$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Repository: Arbeitszeitpaare verwenden den vollständigen Datumsbereich', async t => {
|
||||||
|
const start = new Date('2026-10-26T00:00:00');
|
||||||
|
const end = new Date('2026-11-01T23:59:59.999');
|
||||||
|
let queried = false;
|
||||||
|
mock(t, database, 'getModels', () => ({ Worklog: { findAll: async options => {
|
||||||
|
queried = true;
|
||||||
|
assert.deepEqual(options.where.tstamp[Op.between], [start, end]);
|
||||||
|
return [
|
||||||
|
{ id: 1, state: 'start work', tstamp: '2026-10-27 08:00:00' },
|
||||||
|
{ id: 2, state: 'stop work', tstamp: '2026-10-27 16:00:00', relatedTo_id: 1 }
|
||||||
|
];
|
||||||
|
} } }));
|
||||||
|
const pairs = await repository.findPairsByUserInDateRange(1, start, end);
|
||||||
|
assert.equal(queried, true);
|
||||||
|
assert.equal(pairs.length, 1);
|
||||||
|
assert.equal(pairs[0].end_id, 2);
|
||||||
|
});
|
||||||
|
|||||||
@@ -23,6 +23,10 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="statsError" class="stats-error" role="alert">
|
||||||
|
{{ statsError }}
|
||||||
|
<button class="btn btn-small" @click="refreshStatusData" :disabled="loading">Erneut versuchen</button>
|
||||||
|
</div>
|
||||||
<div class="status-grid">
|
<div class="status-grid">
|
||||||
<div class="row" v-for="(value, key) in displayRows" :key="key" :class="{ 'heading-row': value === null }">
|
<div class="row" v-for="(value, key) in displayRows" :key="key" :class="{ 'heading-row': value === null }">
|
||||||
<span class="label">{{ key }}{{ value === null ? '' : ':' }}</span>
|
<span class="label">{{ key }}{{ value === null ? '' : ':' }}</span>
|
||||||
@@ -44,6 +48,7 @@ const API_URL = API_BASE_URL
|
|||||||
const timeStore = useTimeStore()
|
const timeStore = useTimeStore()
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const stats = ref({})
|
const stats = ref({})
|
||||||
|
const statsError = ref('')
|
||||||
const currentState = ref(null) // 'null', 'start work', 'start pause', 'stop pause'
|
const currentState = ref(null) // 'null', 'start work', 'start pause', 'stop pause'
|
||||||
const isReady = ref(false)
|
const isReady = ref(false)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -61,7 +66,9 @@ let dataFetchInterval = null // Daten vom Server laden
|
|||||||
let displayUpdateInterval = null // Anzeige aktualisieren
|
let displayUpdateInterval = null // Anzeige aktualisieren
|
||||||
|
|
||||||
const fetchStats = async () => {
|
const fetchStats = async () => {
|
||||||
|
try {
|
||||||
const data = await timeStore.fetchStats()
|
const data = await timeStore.fetchStats()
|
||||||
|
statsError.value = ''
|
||||||
stats.value = data || {}
|
stats.value = data || {}
|
||||||
|
|
||||||
// Hole vom Server berechnete Arbeitszeit, offene Zeit und Timestamp
|
// Hole vom Server berechnete Arbeitszeit, offene Zeit und Timestamp
|
||||||
@@ -90,7 +97,12 @@ const fetchStats = async () => {
|
|||||||
|
|
||||||
updateCurrentlyWorkedTime()
|
updateCurrentlyWorkedTime()
|
||||||
updateOpenTime()
|
updateOpenTime()
|
||||||
|
} catch (error) {
|
||||||
|
statsError.value = 'Statistiken konnten nicht geladen werden.' +
|
||||||
|
(serverTimestamp.value ? ' Angezeigte Werte können veraltet sein.' : '')
|
||||||
|
} finally {
|
||||||
isReady.value = true
|
isReady.value = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Lädt Zustand, laufende Zeiten und Tages-Stats neu (z. B. nach Stempeln oder Zeitkorrektur). */
|
/** Lädt Zustand, laufende Zeiten und Tages-Stats neu (z. B. nach Stempeln oder Zeitkorrektur). */
|
||||||
@@ -419,8 +431,8 @@ const displayRows = computed(() => {
|
|||||||
|
|
||||||
// Füge andere Stats hinzu
|
// Füge andere Stats hinzu
|
||||||
const map = [
|
const map = [
|
||||||
['overtime', 'overtime'], // Label wird dynamisch gesetzt
|
['Überstunden (Woche)', 'overtime'], // Label wird dynamisch gesetzt
|
||||||
['totalOvertime', 'totalOvertime'], // Label wird dynamisch gesetzt
|
['Überstunden (Gesamt)', 'totalOvertime'], // Label wird dynamisch gesetzt
|
||||||
['Wochenarbeitszeit', 'weekWorktime'],
|
['Wochenarbeitszeit', 'weekWorktime'],
|
||||||
['Arbeitsfreie Stunden', 'nonWorkingHours'],
|
['Arbeitsfreie Stunden', 'nonWorkingHours'],
|
||||||
['Offen für Woche', 'openForWeek'],
|
['Offen für Woche', 'openForWeek'],
|
||||||
@@ -554,6 +566,12 @@ const displayRows = computed(() => {
|
|||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stats-error {
|
||||||
|
color: #842029;
|
||||||
|
padding: 8px 0;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.status-grid {
|
.status-grid {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
column-gap: 16px;
|
column-gap: 16px;
|
||||||
|
|||||||
@@ -106,10 +106,13 @@ export const useTimeStore = defineStore('time', () => {
|
|||||||
const fetchStats = async () => {
|
const fetchStats = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetchWithAuth(`${API_URL}/time-entries/stats/summary`)
|
const response = await fetchWithAuth(`${API_URL}/time-entries/stats/summary`)
|
||||||
return await response.json()
|
if (!response.ok) throw new Error('Statistiken konnten nicht geladen werden')
|
||||||
|
const data = await response.json()
|
||||||
|
if (data.error) throw new Error(data.error)
|
||||||
|
return data
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Laden der Statistiken:', error)
|
console.error('Fehler beim Laden der Statistiken:', error)
|
||||||
return {}
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,12 @@
|
|||||||
<button @click="nextWeek" class="btn btn-secondary">Nächste Woche →</button>
|
<button @click="nextWeek" class="btn btn-secondary">Nächste Woche →</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class="week-table">
|
<div v-if="loadError" class="load-error" role="alert">
|
||||||
|
{{ loadError }}
|
||||||
|
<button @click="loadWeekData()" class="btn btn-secondary">Erneut versuchen</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table v-if="weekData" class="week-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Tag</th>
|
<th>Tag</th>
|
||||||
@@ -202,6 +207,8 @@ const authStore = useAuthStore()
|
|||||||
|
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const weekData = ref(null)
|
const weekData = ref(null)
|
||||||
|
const loadError = ref('')
|
||||||
|
let latestRequest = 0
|
||||||
const weekOffset = ref(0)
|
const weekOffset = ref(0)
|
||||||
let refreshInterval = null
|
let refreshInterval = null
|
||||||
|
|
||||||
@@ -216,7 +223,13 @@ const weekDays = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const weekRange = computed(() => {
|
const weekRange = computed(() => {
|
||||||
if (!weekData.value) return ''
|
if (!weekData.value) {
|
||||||
|
const monday = new Date();
|
||||||
|
monday.setDate(monday.getDate() + (monday.getDay() === 0 ? -6 : 1 - monday.getDay()) + weekOffset.value * 7);
|
||||||
|
const sunday = new Date(monday);
|
||||||
|
sunday.setDate(sunday.getDate() + 6);
|
||||||
|
return `${formatDate(monday)} - ${formatDate(sunday)}`;
|
||||||
|
}
|
||||||
|
|
||||||
const start = formatDate(weekData.value.weekStart)
|
const start = formatDate(weekData.value.weekStart)
|
||||||
const end = formatDate(weekData.value.weekEnd)
|
const end = formatDate(weekData.value.weekEnd)
|
||||||
@@ -238,10 +251,12 @@ const formatDate = (dateString) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const loadWeekData = async (background = false) => {
|
const loadWeekData = async (background = false) => {
|
||||||
|
const requestId = ++latestRequest
|
||||||
|
const requestedOffset = weekOffset.value
|
||||||
try {
|
try {
|
||||||
if (!background) loading.value = true
|
if (!background) loading.value = true
|
||||||
|
|
||||||
const response = await fetch(`${API_URL}/week-overview?weekOffset=${weekOffset.value}`, {
|
const response = await fetch(`${API_URL}/week-overview?weekOffset=${requestedOffset}`, {
|
||||||
cache: 'no-store',
|
cache: 'no-store',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${authStore.token}`,
|
'Authorization': `Bearer ${authStore.token}`,
|
||||||
@@ -255,17 +270,20 @@ const loadWeekData = async (background = false) => {
|
|||||||
|
|
||||||
const result = await response.json()
|
const result = await response.json()
|
||||||
|
|
||||||
if (result.success) {
|
if (requestId !== latestRequest) return
|
||||||
|
if (result.success && result.data?.weekStart && Array.isArray(result.data.days)) {
|
||||||
weekData.value = result.data
|
weekData.value = result.data
|
||||||
|
loadError.value = ''
|
||||||
} else {
|
} else {
|
||||||
throw new Error(result.error || 'Unbekannter Fehler')
|
throw new Error(result.error || 'Unbekannter Fehler')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Laden der Wochenübersicht:', error)
|
console.error('Fehler beim Laden der Wochenübersicht:', error)
|
||||||
// Fallback zu Dummy-Daten bei Fehlern
|
if (requestId !== latestRequest) return
|
||||||
weekData.value = getDummyData()
|
weekData.value = null
|
||||||
|
loadError.value = 'Die Wochenübersicht konnte nicht geladen werden.'
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
if (requestId === latestRequest) loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,101 +297,6 @@ const nextWeek = () => {
|
|||||||
loadWeekData()
|
loadWeekData()
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDummyData = () => {
|
|
||||||
return {
|
|
||||||
weekStart: '2025-01-13',
|
|
||||||
weekEnd: '2025-01-19',
|
|
||||||
weekOffset: weekOffset.value,
|
|
||||||
weekTotal: '40:00',
|
|
||||||
days: [
|
|
||||||
{
|
|
||||||
date: '2025-01-13',
|
|
||||||
name: 'Montag',
|
|
||||||
workTime: '08:00 - 16:30',
|
|
||||||
pauses: ['12:00 - 12:30'],
|
|
||||||
totalWorkTime: '8:30',
|
|
||||||
pauseTimes: ['0:30'],
|
|
||||||
netWorkTime: '8:00',
|
|
||||||
status: 'complete',
|
|
||||||
statusText: 'Abgeschlossen',
|
|
||||||
isToday: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: '2025-01-14',
|
|
||||||
name: 'Dienstag',
|
|
||||||
workTime: '08:15 - 17:00',
|
|
||||||
pauses: ['12:00 - 12:30', '14:00 - 14:30'],
|
|
||||||
totalWorkTime: '8:45',
|
|
||||||
pauseTimes: ['0:30', '0:30'],
|
|
||||||
netWorkTime: '7:45',
|
|
||||||
status: 'complete',
|
|
||||||
statusText: 'Abgeschlossen',
|
|
||||||
isToday: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: '2025-01-15',
|
|
||||||
name: 'Mittwoch',
|
|
||||||
workTime: '08:00 - 16:00',
|
|
||||||
pauses: ['12:00 - 12:30'],
|
|
||||||
totalWorkTime: '8:00',
|
|
||||||
pauseTimes: ['0:30'],
|
|
||||||
netWorkTime: '7:30',
|
|
||||||
status: 'complete',
|
|
||||||
statusText: 'Abgeschlossen',
|
|
||||||
isToday: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: '2025-01-16',
|
|
||||||
name: 'Donnerstag',
|
|
||||||
workTime: '08:30 - 17:30',
|
|
||||||
pauses: ['12:00 - 12:30', '15:00 - 15:15'],
|
|
||||||
totalWorkTime: '9:00',
|
|
||||||
pauseTimes: ['0:30', '0:15'],
|
|
||||||
netWorkTime: '8:15',
|
|
||||||
status: 'complete',
|
|
||||||
statusText: 'Abgeschlossen',
|
|
||||||
isToday: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: '2025-01-17',
|
|
||||||
name: 'Freitag',
|
|
||||||
workTime: '08:00 - 15:00',
|
|
||||||
pauses: ['12:00 - 12:30'],
|
|
||||||
totalWorkTime: '7:00',
|
|
||||||
pauseTimes: ['0:30'],
|
|
||||||
netWorkTime: '6:30',
|
|
||||||
status: 'complete',
|
|
||||||
statusText: 'Abgeschlossen',
|
|
||||||
isToday: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: '2025-01-18',
|
|
||||||
name: 'Samstag',
|
|
||||||
workTime: null,
|
|
||||||
pauses: [],
|
|
||||||
totalWorkTime: null,
|
|
||||||
pauseTimes: [],
|
|
||||||
netWorkTime: null,
|
|
||||||
status: 'weekend',
|
|
||||||
statusText: 'Wochenende',
|
|
||||||
isToday: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: '2025-01-19',
|
|
||||||
name: 'Sonntag',
|
|
||||||
workTime: null,
|
|
||||||
pauses: [],
|
|
||||||
totalWorkTime: null,
|
|
||||||
pauseTimes: [],
|
|
||||||
netWorkTime: null,
|
|
||||||
status: 'weekend',
|
|
||||||
statusText: 'Wochenende',
|
|
||||||
isToday: false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleWorklogUpdate = () => {
|
const handleWorklogUpdate = () => {
|
||||||
console.log('Worklog aktualisiert, lade Wochendaten neu...')
|
console.log('Worklog aktualisiert, lade Wochendaten neu...')
|
||||||
loadWeekData()
|
loadWeekData()
|
||||||
@@ -418,6 +341,13 @@ onBeforeUnmount(() => {
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.load-error {
|
||||||
|
padding: 1rem;
|
||||||
|
color: #842029;
|
||||||
|
background: #f8d7da;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
.loading {
|
.loading {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
|
|||||||
107
frontend/test/load-errors.test.cjs
Normal file
107
frontend/test/load-errors.test.cjs
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
process.env.TZ = 'Europe/Berlin';
|
||||||
|
const { test } = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const { readFileSync } = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
const vm = require('node:vm');
|
||||||
|
const { parse, compileScript } = require('@vue/compiler-sfc');
|
||||||
|
const { transformSync } = require('esbuild');
|
||||||
|
const vue = require('vue');
|
||||||
|
|
||||||
|
// Execute the real Vue setup and store code, with HTTP responses controlled by
|
||||||
|
// each test. No mounted app or browser globals are needed for these load paths.
|
||||||
|
function load(relativePath, dependencies = {}) {
|
||||||
|
const filename = path.resolve(__dirname, '..', relativePath);
|
||||||
|
let source = readFileSync(filename, 'utf8');
|
||||||
|
if (filename.endsWith('.vue')) {
|
||||||
|
source = compileScript(parse(source).descriptor, { id: 'load-error-test' }).content;
|
||||||
|
}
|
||||||
|
const code = transformSync(source, { format: 'cjs', loader: 'js' }).code;
|
||||||
|
const module = { exports: {} };
|
||||||
|
vm.runInThisContext(`(function(require, module, exports) { ${code}\n})`, { filename })(id => {
|
||||||
|
if (id in dependencies) return dependencies[id];
|
||||||
|
if (id === 'vue') return { ...vue, onMounted() {}, onBeforeUnmount() {} };
|
||||||
|
if (id === 'pinia') return { defineStore: (_name, setup) => setup };
|
||||||
|
if (id === '@/config/api') return { API_BASE_URL: '/api' };
|
||||||
|
if (id.endsWith('authStore')) return {
|
||||||
|
useAuthStore: () => ({ token: 'test-token', getAuthHeaders: () => ({}), clearAuth() {} })
|
||||||
|
};
|
||||||
|
throw new Error(`Unexpected import: ${id}`);
|
||||||
|
}, module, module.exports);
|
||||||
|
return module.exports;
|
||||||
|
}
|
||||||
|
|
||||||
|
function component(file, dependencies) {
|
||||||
|
return load(file, dependencies).default.setup({}, { expose() {} });
|
||||||
|
}
|
||||||
|
|
||||||
|
const ok = data => ({ ok: true, json: async () => data });
|
||||||
|
|
||||||
|
test('Wochenansicht fordert beim Öffnen aktuelle Woche an', async t => {
|
||||||
|
let requestedUrl;
|
||||||
|
t.mock.method(global, 'fetch', async url => {
|
||||||
|
requestedUrl = url;
|
||||||
|
return ok({ success: true, data: { weekStart: '2026-10-26', weekEnd: '2026-11-01', days: [] } });
|
||||||
|
});
|
||||||
|
const page = component('src/views/WeekOverview.vue');
|
||||||
|
await page.loadWeekData();
|
||||||
|
assert.equal(requestedUrl, '/api/week-overview?weekOffset=0');
|
||||||
|
assert.equal(page.weekRange.value, '26.10.2026 - 01.11.2026');
|
||||||
|
assert.equal(page.loadError.value, '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Wochenansicht zeigt bei HTTP 500 Fehler und aktuelle Wochengrenzen statt Beispieldaten', async t => {
|
||||||
|
const RealDate = Date;
|
||||||
|
t.mock.method(global, 'Date', class extends RealDate {
|
||||||
|
constructor(...args) { super(...(args.length ? args : ['2026-10-27T14:00:00'])); }
|
||||||
|
});
|
||||||
|
t.mock.method(console, 'error', () => {});
|
||||||
|
t.mock.method(global, 'fetch', async () => ({ ok: false, status: 500 }));
|
||||||
|
const page = component('src/views/WeekOverview.vue');
|
||||||
|
await page.loadWeekData();
|
||||||
|
assert.equal(page.weekData.value, null);
|
||||||
|
assert.deepEqual(page.weekDays.value, []);
|
||||||
|
assert.equal(page.weekRange.value, '26.10.2026 - 01.11.2026');
|
||||||
|
assert.match(page.loadError.value, /nicht geladen/);
|
||||||
|
assert.equal(page.loading.value, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Ältere Wochenantwort überschreibt die zuletzt ausgewählte Woche nicht', async t => {
|
||||||
|
let finishOldRequest;
|
||||||
|
t.mock.method(global, 'fetch', url => url.endsWith('=0')
|
||||||
|
? new Promise(resolve => { finishOldRequest = resolve; })
|
||||||
|
: Promise.resolve(ok({ success: true, data: { weekStart: '2026-11-02', weekEnd: '2026-11-08', days: [] } })));
|
||||||
|
const page = component('src/views/WeekOverview.vue');
|
||||||
|
const oldRequest = page.loadWeekData();
|
||||||
|
page.weekOffset.value = 1;
|
||||||
|
await page.loadWeekData();
|
||||||
|
finishOldRequest(ok({ success: true, data: { weekStart: '2026-10-26', weekEnd: '2026-11-01', days: [] } }));
|
||||||
|
await oldRequest;
|
||||||
|
assert.equal(page.weekRange.value, '02.11.2026 - 08.11.2026');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Statistikfehler wird angezeigt, gültige Werte bleiben erhalten und Wiederholen funktioniert', async t => {
|
||||||
|
t.mock.method(console, 'error', () => {});
|
||||||
|
const stats = {
|
||||||
|
timestamp: new Date().toISOString(), currentlyWorked: '03:53:53', open: '03:06:07',
|
||||||
|
overtime: '+0:00', totalOvertime: '+5:49', weekWorktime: '3:53', nonWorkingHours: '8:00'
|
||||||
|
};
|
||||||
|
let failing = true;
|
||||||
|
t.mock.method(global, 'fetch', async () => failing ? { ok: false, status: 500 } : ok(stats));
|
||||||
|
const store = load('src/stores/timeStore.js');
|
||||||
|
const box = component('src/components/StatusBox.vue', { '../stores/timeStore': store });
|
||||||
|
await box.fetchStats();
|
||||||
|
assert.match(box.statsError.value, /nicht geladen/);
|
||||||
|
assert.equal(box.isReady.value, true);
|
||||||
|
assert.equal(box.displayRows.value['Überstunden (Woche)'], '—');
|
||||||
|
assert.equal('overtime' in box.displayRows.value, false);
|
||||||
|
assert.equal('totalOvertime' in box.displayRows.value, false);
|
||||||
|
failing = false;
|
||||||
|
await box.fetchStats();
|
||||||
|
assert.equal(box.statsError.value, '');
|
||||||
|
assert.equal(box.displayRows.value['Überstunden (Gesamt)'], '+5:49 h');
|
||||||
|
failing = true;
|
||||||
|
await box.fetchStats();
|
||||||
|
assert.match(box.statsError.value, /veraltet/);
|
||||||
|
assert.equal(box.displayRows.value['Überstunden (Gesamt)'], '+5:49 h');
|
||||||
|
});
|
||||||
@@ -13,7 +13,8 @@
|
|||||||
"dev:frontend": "cd frontend && npm run dev",
|
"dev:frontend": "cd frontend && npm run dev",
|
||||||
"dev": "concurrently \"npm run dev:backend\" \"npm run dev:frontend\"",
|
"dev": "concurrently \"npm run dev:backend\" \"npm run dev:frontend\"",
|
||||||
"start:backend": "cd backend && npm start",
|
"start:backend": "cd backend && npm start",
|
||||||
"build:frontend": "cd frontend && npm run build"
|
"build:frontend": "cd frontend && npm run build",
|
||||||
|
"test": "node --test backend/test/*.test.js frontend/test/*.test.cjs"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"timeclock",
|
"timeclock",
|
||||||
|
|||||||
Reference in New Issue
Block a user