Compare commits
14 Commits
b6c7c66027
...
v1.4.19
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
467e48e1da | ||
|
|
566410eea5 | ||
|
|
37afc2957b | ||
|
|
b1763135aa | ||
|
|
98020f0b8f | ||
|
|
c2e3282131 | ||
|
|
7a82cb06db | ||
|
|
d89b080da5 | ||
|
|
d80863a169 | ||
|
|
ae79c58d30 | ||
|
|
a0ef6e631e | ||
|
|
d895a5fb7c | ||
|
|
fd765a74c0 | ||
|
|
1f184fab8b |
@@ -48,6 +48,11 @@ trigger: always_on
|
|||||||
- **Browser**: Allowed for documentation and safe browsing. No automated logins without permission.
|
- **Browser**: Allowed for documentation and safe browsing. No automated logins without permission.
|
||||||
- **Terminal**: No `rm -rf`. Run tests (`pytest` etc.) after logic changes.
|
- **Terminal**: No `rm -rf`. Run tests (`pytest` etc.) after logic changes.
|
||||||
|
|
||||||
|
## 7. Mandatory Testing Policy 🧪
|
||||||
|
**CRITICAL: No logic or UI fix is complete without a corresponding automated test.**
|
||||||
|
- If you fix a regression or implement a new UI feature, you **MUST** write or update a test in `tests/test_dom.js` or `test_logic.js`.
|
||||||
|
- Refactoring MUST include verifying that no click listeners drop out. This guarantees that features like modal toggles stay functional.
|
||||||
|
|
||||||
## 7. Requirements-Konsistenz 📋
|
## 7. Requirements-Konsistenz 📋
|
||||||
Alle umgesetzten Anforderungen müssen mit `REQUIREMENTS.md` übereinstimmen.
|
Alle umgesetzten Anforderungen müssen mit `REQUIREMENTS.md` übereinstimmen.
|
||||||
1. **Vor der Umsetzung prüfen**: Passt die neue Anforderung zu den bestehenden Requirements?
|
1. **Vor der Umsetzung prüfen**: Passt die neue Anforderung zu den bestehenden Requirements?
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|||||||
DIST_DIR="$SCRIPT_DIR/dist"
|
DIST_DIR="$SCRIPT_DIR/dist"
|
||||||
CSS_FILE="$SCRIPT_DIR/style.css"
|
CSS_FILE="$SCRIPT_DIR/style.css"
|
||||||
JS_FILE="$SCRIPT_DIR/kantine.js"
|
JS_FILE="$SCRIPT_DIR/kantine.js"
|
||||||
|
FAVICON_FILE="$SCRIPT_DIR/favicon.svg"
|
||||||
|
|
||||||
# === VERSION ===
|
# === VERSION ===
|
||||||
if [ -f "$SCRIPT_DIR/version.txt" ]; then
|
if [ -f "$SCRIPT_DIR/version.txt" ]; then
|
||||||
@@ -23,6 +24,11 @@ echo "=== Kantine Bookmarklet Builder ($VERSION) ==="
|
|||||||
# Check files exist
|
# Check files exist
|
||||||
if [ ! -f "$CSS_FILE" ]; then echo "ERROR: $CSS_FILE not found"; exit 1; fi
|
if [ ! -f "$CSS_FILE" ]; then echo "ERROR: $CSS_FILE not found"; exit 1; fi
|
||||||
if [ ! -f "$JS_FILE" ]; then echo "ERROR: $JS_FILE not found"; exit 1; fi
|
if [ ! -f "$JS_FILE" ]; then echo "ERROR: $JS_FILE not found"; exit 1; fi
|
||||||
|
if [ ! -f "$FAVICON_FILE" ]; then echo "ERROR: $FAVICON_FILE not found"; exit 1; fi
|
||||||
|
|
||||||
|
# Generate favicon Base64 data URI
|
||||||
|
FAVICON_B64=$(base64 -w0 "$FAVICON_FILE")
|
||||||
|
FAVICON_URI="data:image/svg+xml;base64,${FAVICON_B64}"
|
||||||
|
|
||||||
CSS_CONTENT=$(cat "$CSS_FILE")
|
CSS_CONTENT=$(cat "$CSS_FILE")
|
||||||
|
|
||||||
@@ -101,6 +107,7 @@ cat > "$DIST_DIR/install.html" << INSTALLEOF
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Kantine Wrapper Installer ($VERSION)</title>
|
<title>Kantine Wrapper Installer ($VERSION)</title>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="$FAVICON_URI">
|
||||||
<style>
|
<style>
|
||||||
body { font-family: 'Inter', sans-serif; max-width: 600px; margin: 40px auto; padding: 20px; background: #1a1a2e; color: #eee; }
|
body { font-family: 'Inter', sans-serif; max-width: 600px; margin: 40px auto; padding: 20px; background: #1a1a2e; color: #eee; }
|
||||||
h1 { color: #029AA8; } /* Knapp Teal */
|
h1 { color: #029AA8; } /* Knapp Teal */
|
||||||
@@ -258,6 +265,14 @@ if [ $LOGIC_EXIT -ne 0 ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo "=== Running DOM Interaction Tests ==="
|
||||||
|
node "$SCRIPT_DIR/tests/test_dom.js"
|
||||||
|
DOM_EXIT=$?
|
||||||
|
if [ $DOM_EXIT -ne 0 ]; then
|
||||||
|
echo "❌ DOM UI tests FAILED! Regressions detected."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
echo "=== Running Build Tests ==="
|
echo "=== Running Build Tests ==="
|
||||||
python3 "$SCRIPT_DIR/test_build.py"
|
python3 "$SCRIPT_DIR/test_build.py"
|
||||||
TEST_EXIT=$?
|
TEST_EXIT=$?
|
||||||
|
|||||||
24
changelog.md
24
changelog.md
@@ -1,3 +1,27 @@
|
|||||||
|
## v1.4.19
|
||||||
|
- 🎨 **Feature**: Eigenes Favicon für die Installer-Seite hinzugefügt (Dreieck + Gabel & Messer). Wird beim Drag & Drop in die Lesezeichenleiste als Icon übernommen.
|
||||||
|
|
||||||
|
## v1.4.18
|
||||||
|
- 🧪 **Testing**: Die automatische DOM-Testing Suite (`test_dom.js`) wurde massiv ausgebaut. Sie prüft nun neben der Alarmglocke und den Highlights auch systematisch alle anderen UI-Komponenten (Login-Modal, History-Modal, Versionen-Modal, Theme-Toggle, und Navigation Tabs) auf korrekte Event-Listener-Bindungen, um Regressionen (tote Buttons) endgültig auszuschließen.
|
||||||
|
|
||||||
|
## v1.4.17
|
||||||
|
- 🐛 **Bugfix**: Regression behoben: Der "Persönliche Highlights" (Stern-Button) Dialog öffnet sich nun wieder korrekt.
|
||||||
|
- 🧪 **Testing**: Es wurde ein initialer UI-Testing-Hook (`test_dom.js` mit `jsdom`) in die Build-Pipeline integriert, um kritische DOM Event-Listener Regressionen (wie den Highlights-Button und die Alarmglocke) automatisch zu preventen.
|
||||||
|
|
||||||
|
## v1.4.16
|
||||||
|
- ⚡ **Feature**: Ein Button "Lokalen Cache leeren" wurde zum Versionen-Menü hinzugefügt, um bei hartnäckigen lokalen Fehlern alle Caches und Sessions bereinigen zu können, ohne die Entwicklertools (F12) des Browsers bemühen zu müssen.
|
||||||
|
|
||||||
|
## v1.4.15
|
||||||
|
- 🧹 **Bugfix**: In der Vergangenheit gesetzte Alarme/Flags wurden nicht zuverlässig gelöscht. Dies ist nun behoben, sodass verfallene Menüs nach 10:00 Uhr bzw. an vergangenen Tagen automatisch aus dem Tracker verschwinden.
|
||||||
|
|
||||||
|
## v1.4.14
|
||||||
|
- 🐛 **Bugfix**: Alarmglocke versteckt sich jetzt zuverlässig auch auf Endgeräten mit CSS Konflikten
|
||||||
|
- 🚀 **Feature**: Sofortige API-Aktualisierung (Refresh) bei Aktivierung eines Menüalarms
|
||||||
|
- ⚡ **Optimierung**: "Unbekannt" im letzten Refresh-Zeitpunkt wird abgefangen und zeigt initial "gerade eben"
|
||||||
|
|
||||||
|
## v1.4.13 (2026-02-24)
|
||||||
|
- **Fix**: Die Farben der Glocke funktionieren nun verlässlich, da CSS-Variablen durch direkte Hex-Codes ersetzt wurden.
|
||||||
|
|
||||||
## v1.4.12 (2026-02-24)
|
## v1.4.12 (2026-02-24)
|
||||||
- **Fix**: Das Glocken-Icon sollte nun endgültig versteckt bleiben, wenn keine Benachrichtigungen aktiv sind (CSS-Kollision mit `.hidden` behoben).
|
- **Fix**: Das Glocken-Icon sollte nun endgültig versteckt bleiben, wenn keine Benachrichtigungen aktiv sind (CSS-Kollision mit `.hidden` behoben).
|
||||||
|
|
||||||
|
|||||||
15
debug_test.js
Executable file
15
debug_test.js
Executable file
@@ -0,0 +1,15 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const jsCode = fs.readFileSync('kantine.js', 'utf8').replace('(function () {', '').replace(/}\)\(\);$/, '');
|
||||||
|
try {
|
||||||
|
const vm = require('vm');
|
||||||
|
new vm.Script(jsCode);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e.message);
|
||||||
|
const lines = jsCode.split('\n');
|
||||||
|
console.error("Around line", e.loc?.line);
|
||||||
|
if(e.loc?.line) {
|
||||||
|
console.log(lines[e.loc.line - 2]);
|
||||||
|
console.log(lines[e.loc.line - 1]);
|
||||||
|
console.log(lines[e.loc.line]);
|
||||||
|
}
|
||||||
|
}
|
||||||
2
dist/bookmarklet-payload.js
vendored
2
dist/bookmarklet-payload.js
vendored
File diff suppressed because one or more lines are too long
2
dist/bookmarklet.txt
vendored
2
dist/bookmarklet.txt
vendored
File diff suppressed because one or more lines are too long
42
dist/install.html
vendored
42
dist/install.html
vendored
File diff suppressed because one or more lines are too long
141
dist/kantine-standalone.html
vendored
141
dist/kantine-standalone.html
vendored
@@ -2021,7 +2021,7 @@ body {
|
|||||||
<div class="brand">
|
<div class="brand">
|
||||||
<span class="material-icons-round logo-icon">restaurant_menu</span>
|
<span class="material-icons-round logo-icon">restaurant_menu</span>
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<h1>Kantinen Übersicht <small class="version-tag" style="font-size: 0.6em; opacity: 0.7; font-weight: 400; cursor: pointer;" title="Klick für Versionsmenü">v1.4.12</small></h1>
|
<h1>Kantinen Übersicht <small class="version-tag" style="font-size: 0.6em; opacity: 0.7; font-weight: 400; cursor: pointer;" title="Klick für Versionsmenü">v1.4.19</small></h1>
|
||||||
<div id="last-updated-subtitle" class="subtitle"></div>
|
<div id="last-updated-subtitle" class="subtitle"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-group" style="margin-left: 1rem;">
|
<div class="nav-group" style="margin-left: 1rem;">
|
||||||
@@ -2163,7 +2163,7 @@ body {
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div style="margin-bottom: 1rem;">
|
<div style="margin-bottom: 1rem;">
|
||||||
<strong>Aktuell:</strong> <span id="version-current">v1.4.12</span>
|
<strong>Aktuell:</strong> <span id="version-current">v1.4.19</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="dev-toggle">
|
<div class="dev-toggle">
|
||||||
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;">
|
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;">
|
||||||
@@ -2181,6 +2181,9 @@ body {
|
|||||||
<a href="https://github.com/TauNeutrino/kantine-overview/discussions/categories/ideas" target="_blank" rel="noopener noreferrer" style="color: var(--primary-color); text-decoration: none; display: flex; align-items: center; gap: 0.5rem;" title="Schlage ein neues Feature auf GitHub vor">
|
<a href="https://github.com/TauNeutrino/kantine-overview/discussions/categories/ideas" target="_blank" rel="noopener noreferrer" style="color: var(--primary-color); text-decoration: none; display: flex; align-items: center; gap: 0.5rem;" title="Schlage ein neues Feature auf GitHub vor">
|
||||||
<span class="material-icons-round" style="font-size: 1.2em;">lightbulb</span> Feature vorschlagen
|
<span class="material-icons-round" style="font-size: 1.2em;">lightbulb</span> Feature vorschlagen
|
||||||
</a>
|
</a>
|
||||||
|
<button id="btn-clear-cache" style="background: none; border: none; padding: 0; color: var(--error-color); text-decoration: none; display: flex; align-items: center; gap: 0.5rem; cursor: pointer; text-align: left; font-size: inherit; font-family: inherit;" title="Löscht alle lokalen Daten & erzwingt einen Neuladen">
|
||||||
|
<span class="material-icons-round" style="font-size: 1.2em;">delete_forever</span> Lokalen Cache leeren
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2228,6 +2231,18 @@ body {
|
|||||||
const historyModal = document.getElementById('history-modal');
|
const historyModal = document.getElementById('history-modal');
|
||||||
const btnHistoryClose = document.getElementById('btn-history-close');
|
const btnHistoryClose = document.getElementById('btn-history-close');
|
||||||
|
|
||||||
|
if (btnHighlights) {
|
||||||
|
btnHighlights.addEventListener('click', () => {
|
||||||
|
highlightsModal.classList.remove('hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (btnHighlightsClose) {
|
||||||
|
btnHighlightsClose.addEventListener('click', () => {
|
||||||
|
highlightsModal.classList.add('hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
btnHistory.addEventListener('click', () => {
|
btnHistory.addEventListener('click', () => {
|
||||||
if (!authToken) {
|
if (!authToken) {
|
||||||
loginModal.classList.remove('hidden');
|
loginModal.classList.remove('hidden');
|
||||||
@@ -2265,6 +2280,17 @@ body {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const btnClearCache = document.getElementById('btn-clear-cache');
|
||||||
|
if (btnClearCache) {
|
||||||
|
btnClearCache.addEventListener('click', () => {
|
||||||
|
if (confirm('Möchtest du wirklich alle lokalen Daten (inkl. Login-Session, Cache und Einstellungen) löschen? Die Seite wird danach neu geladen.')) {
|
||||||
|
localStorage.clear();
|
||||||
|
sessionStorage.clear();
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
window.addEventListener('click', (e) => {
|
window.addEventListener('click', (e) => {
|
||||||
if (e.target === versionModal) versionModal.classList.add('hidden');
|
if (e.target === versionModal) versionModal.classList.add('hidden');
|
||||||
});
|
});
|
||||||
@@ -2889,6 +2915,67 @@ body {
|
|||||||
localStorage.setItem('kantine_flags', JSON.stringify([...userFlags]));
|
localStorage.setItem('kantine_flags', JSON.stringify([...userFlags]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function refreshFlaggedItems() {
|
||||||
|
if (userFlags.size === 0) return;
|
||||||
|
const token = authToken || GUEST_TOKEN;
|
||||||
|
const datesToFetch = new Set();
|
||||||
|
|
||||||
|
for (const flagId of userFlags) {
|
||||||
|
const [dateStr] = flagId.split('_');
|
||||||
|
datesToFetch.add(dateStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
let updated = false;
|
||||||
|
for (const dateStr of datesToFetch) {
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`${API_BASE}/venues/${VENUE_ID}/menu/${MENU_ID}/${dateStr}/`, {
|
||||||
|
headers: apiHeaders(token)
|
||||||
|
});
|
||||||
|
if (!resp.ok) continue;
|
||||||
|
const data = await resp.json();
|
||||||
|
const menuGroups = data.results || [];
|
||||||
|
let dayItems = [];
|
||||||
|
for (const group of menuGroups) {
|
||||||
|
if (group.items && Array.isArray(group.items)) {
|
||||||
|
dayItems = dayItems.concat(group.items);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update allWeeks in memory
|
||||||
|
for (let week of allWeeks) {
|
||||||
|
if (!week.days) continue;
|
||||||
|
let dayObj = week.days.find(d => d.date === dateStr);
|
||||||
|
if (dayObj) {
|
||||||
|
dayObj.items = dayItems.map(item => {
|
||||||
|
const isUnlimited = item.amount_tracking === false;
|
||||||
|
const hasStock = parseInt(item.available_amount) > 0;
|
||||||
|
return {
|
||||||
|
id: `${dateStr}_${item.id}`,
|
||||||
|
articleId: item.id,
|
||||||
|
name: item.name || 'Unknown',
|
||||||
|
description: item.description || '',
|
||||||
|
price: parseFloat(item.price) || 0,
|
||||||
|
available: isUnlimited || hasStock,
|
||||||
|
availableAmount: parseInt(item.available_amount) || 0,
|
||||||
|
amountTracking: item.amount_tracking !== false
|
||||||
|
};
|
||||||
|
});
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error refreshing flag date', dateStr, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updated) {
|
||||||
|
saveMenuCache();
|
||||||
|
updateLastUpdatedTime(new Date().toISOString());
|
||||||
|
updateAlarmBell();
|
||||||
|
renderVisibleWeeks();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function updateAlarmBell() {
|
function updateAlarmBell() {
|
||||||
const bellBtn = document.getElementById('alarm-bell');
|
const bellBtn = document.getElementById('alarm-bell');
|
||||||
const bellIcon = document.getElementById('alarm-bell-icon');
|
const bellIcon = document.getElementById('alarm-bell-icon');
|
||||||
@@ -2896,12 +2983,14 @@ body {
|
|||||||
|
|
||||||
if (userFlags.size === 0) {
|
if (userFlags.size === 0) {
|
||||||
bellBtn.classList.add('hidden');
|
bellBtn.classList.add('hidden');
|
||||||
|
bellBtn.style.display = 'none';
|
||||||
bellIcon.style.color = 'var(--text-secondary)';
|
bellIcon.style.color = 'var(--text-secondary)';
|
||||||
bellIcon.style.textShadow = 'none';
|
bellIcon.style.textShadow = 'none';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
bellBtn.classList.remove('hidden');
|
bellBtn.classList.remove('hidden');
|
||||||
|
bellBtn.style.display = 'inline-flex';
|
||||||
|
|
||||||
// Check if any flagged item is available
|
// Check if any flagged item is available
|
||||||
let anyAvailable = false;
|
let anyAvailable = false;
|
||||||
@@ -2920,33 +3009,40 @@ body {
|
|||||||
if (anyAvailable) break;
|
if (anyAvailable) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastUpdatedStr = localStorage.getItem('kantine_last_updated');
|
let lastUpdatedStr = localStorage.getItem('kantine_last_updated');
|
||||||
let timeStr = 'Unbekannt';
|
let timeStr = 'gerade eben'; // Fallback instead of Unbekannt
|
||||||
if (lastUpdatedStr) {
|
if (!lastUpdatedStr) {
|
||||||
|
lastUpdatedStr = new Date().toISOString();
|
||||||
|
localStorage.setItem('kantine_last_updated', lastUpdatedStr);
|
||||||
|
}
|
||||||
|
|
||||||
const lastUpdated = new Date(lastUpdatedStr);
|
const lastUpdated = new Date(lastUpdatedStr);
|
||||||
const diffMs = Date.now() - lastUpdated.getTime();
|
const diffMs = Date.now() - lastUpdated.getTime();
|
||||||
const diffMins = Math.floor(diffMs / 60000);
|
const diffMins = Math.floor(diffMs / 60000);
|
||||||
if (diffMins < 60) timeStr = `vor ${diffMins} Min.`;
|
if (diffMins < 1) timeStr = 'gerade eben';
|
||||||
|
else if (diffMins < 60) timeStr = `vor ${diffMins} Min.`;
|
||||||
else timeStr = `vor ${Math.floor(diffMins / 60)} Std.`;
|
else timeStr = `vor ${Math.floor(diffMins / 60)} Std.`;
|
||||||
}
|
|
||||||
bellBtn.title = `Zuletzt geprüft: ${timeStr}`;
|
bellBtn.title = `Zuletzt geprüft: ${timeStr}`;
|
||||||
|
|
||||||
if (anyAvailable) {
|
if (anyAvailable) {
|
||||||
bellIcon.style.color = 'var(--success-color)';
|
bellIcon.style.color = '#10b981'; // green / success
|
||||||
bellIcon.style.textShadow = '0 0 10px rgba(16, 185, 129, 0.4)';
|
bellIcon.style.textShadow = '0 0 10px rgba(16, 185, 129, 0.4)';
|
||||||
} else {
|
} else {
|
||||||
bellIcon.style.color = 'var(--warning-color)';
|
bellIcon.style.color = '#f59e0b'; // yellow / warning
|
||||||
bellIcon.style.textShadow = '0 0 10px rgba(245, 158, 11, 0.4)';
|
bellIcon.style.textShadow = '0 0 10px rgba(245, 158, 11, 0.4)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleFlag(date, articleId, name, cutoff) {
|
function toggleFlag(date, articleId, name, cutoff) {
|
||||||
const id = `${date}_${articleId}`;
|
const id = `${date}_${articleId}`;
|
||||||
|
let flagAdded = false;
|
||||||
if (userFlags.has(id)) {
|
if (userFlags.has(id)) {
|
||||||
userFlags.delete(id);
|
userFlags.delete(id);
|
||||||
showToast(`Flag entfernt für ${name}`, 'success');
|
showToast(`Flag entfernt für ${name}`, 'success');
|
||||||
} else {
|
} else {
|
||||||
userFlags.add(id);
|
userFlags.add(id);
|
||||||
|
flagAdded = true;
|
||||||
showToast(`Benachrichtigung aktiviert für ${name}`, 'success');
|
showToast(`Benachrichtigung aktiviert für ${name}`, 'success');
|
||||||
if (Notification.permission === 'default') {
|
if (Notification.permission === 'default') {
|
||||||
Notification.requestPermission();
|
Notification.requestPermission();
|
||||||
@@ -2955,17 +3051,36 @@ body {
|
|||||||
saveFlags();
|
saveFlags();
|
||||||
updateAlarmBell();
|
updateAlarmBell();
|
||||||
renderVisibleWeeks();
|
renderVisibleWeeks();
|
||||||
|
|
||||||
|
if (flagAdded) {
|
||||||
|
refreshFlaggedItems();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FR-019: Auto-remove flags whose cutoff has passed
|
// FR-019: Auto-remove flags whose cutoff has passed
|
||||||
function cleanupExpiredFlags() {
|
function cleanupExpiredFlags() {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
const todayStr = now.toISOString().split('T')[0]; // Format: YYYY-MM-DD
|
||||||
let changed = false;
|
let changed = false;
|
||||||
|
|
||||||
for (const flagId of [...userFlags]) {
|
for (const flagId of [...userFlags]) {
|
||||||
const [date] = flagId.split('_');
|
const [dateStr] = flagId.split('_'); // Format usually is YYYY-MM-DD
|
||||||
const cutoff = new Date(date);
|
|
||||||
|
// If the flag's date string is entirely in the past (before today)
|
||||||
|
// or if it's today but past the 10:00 cutoff time
|
||||||
|
let isExpired = false;
|
||||||
|
|
||||||
|
if (dateStr < todayStr) {
|
||||||
|
isExpired = true;
|
||||||
|
} else if (dateStr === todayStr) {
|
||||||
|
const cutoff = new Date(dateStr);
|
||||||
cutoff.setHours(10, 0, 0, 0); // Standard cutoff 10:00
|
cutoff.setHours(10, 0, 0, 0); // Standard cutoff 10:00
|
||||||
if (now >= cutoff) {
|
if (now >= cutoff) {
|
||||||
|
isExpired = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isExpired) {
|
||||||
userFlags.delete(flagId);
|
userFlags.delete(flagId);
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
@@ -3895,7 +4010,7 @@ body {
|
|||||||
|
|
||||||
// Periodic update check (runs on init + every hour)
|
// Periodic update check (runs on init + every hour)
|
||||||
async function checkForUpdates() {
|
async function checkForUpdates() {
|
||||||
const currentVersion = 'v1.4.12';
|
const currentVersion = 'v1.4.19';
|
||||||
const devMode = localStorage.getItem('kantine_dev_mode') === 'true';
|
const devMode = localStorage.getItem('kantine_dev_mode') === 'true';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -3936,7 +4051,7 @@ body {
|
|||||||
const modal = document.getElementById('version-modal');
|
const modal = document.getElementById('version-modal');
|
||||||
const container = document.getElementById('version-list-container');
|
const container = document.getElementById('version-list-container');
|
||||||
const devToggle = document.getElementById('dev-mode-toggle');
|
const devToggle = document.getElementById('dev-mode-toggle');
|
||||||
const currentVersion = 'v1.4.12';
|
const currentVersion = 'v1.4.19';
|
||||||
|
|
||||||
if (!modal) return;
|
if (!modal) return;
|
||||||
modal.classList.remove('hidden');
|
modal.classList.remove('hidden');
|
||||||
|
|||||||
29
favicon.svg
Executable file
29
favicon.svg
Executable file
@@ -0,0 +1,29 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
|
||||||
|
<!-- Fork (left, with gap to triangle) -->
|
||||||
|
<g transform="translate(2, 10)">
|
||||||
|
<!-- Tines -->
|
||||||
|
<rect x="1" y="0" width="1.8" height="16" rx="0.9" fill="#333"/>
|
||||||
|
<rect x="4.6" y="0" width="1.8" height="16" rx="0.9" fill="#333"/>
|
||||||
|
<rect x="8.2" y="0" width="1.8" height="16" rx="0.9" fill="#333"/>
|
||||||
|
<!-- Connector -->
|
||||||
|
<rect x="1" y="14" width="9" height="3.5" rx="1.5" fill="#333"/>
|
||||||
|
<!-- Handle -->
|
||||||
|
<rect x="3.5" y="16.5" width="4" height="24" rx="2" fill="#333"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Triangle (center, equilateral aspect ratio ~1:0.866) -->
|
||||||
|
<!-- Equilateral: base=28, height=24.25 => keeps proper ratio -->
|
||||||
|
<polygon points="32,8 47,48 17,48" fill="none" stroke="#333" stroke-width="4" stroke-linejoin="round"/>
|
||||||
|
|
||||||
|
<!-- Knife (right, with gap to triangle) -->
|
||||||
|
<g transform="translate(50, 10)">
|
||||||
|
<!-- Blade (slight curve) -->
|
||||||
|
<path d="M3,0 C3,0 3,0 3,0 L3,17 L10,14 C10,6 7,0 3,0 Z" fill="#333"/>
|
||||||
|
<!-- Spine -->
|
||||||
|
<rect x="1.5" y="0" width="2" height="18" rx="1" fill="#333"/>
|
||||||
|
<!-- Bolster -->
|
||||||
|
<rect x="1.5" y="16.5" width="8.5" height="3.5" rx="1.2" fill="#333"/>
|
||||||
|
<!-- Handle -->
|
||||||
|
<rect x="3.5" y="19" width="4" height="22" rx="2" fill="#333"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
133
kantine.js
133
kantine.js
@@ -231,6 +231,9 @@
|
|||||||
<a href="https://github.com/TauNeutrino/kantine-overview/discussions/categories/ideas" target="_blank" rel="noopener noreferrer" style="color: var(--primary-color); text-decoration: none; display: flex; align-items: center; gap: 0.5rem;" title="Schlage ein neues Feature auf GitHub vor">
|
<a href="https://github.com/TauNeutrino/kantine-overview/discussions/categories/ideas" target="_blank" rel="noopener noreferrer" style="color: var(--primary-color); text-decoration: none; display: flex; align-items: center; gap: 0.5rem;" title="Schlage ein neues Feature auf GitHub vor">
|
||||||
<span class="material-icons-round" style="font-size: 1.2em;">lightbulb</span> Feature vorschlagen
|
<span class="material-icons-round" style="font-size: 1.2em;">lightbulb</span> Feature vorschlagen
|
||||||
</a>
|
</a>
|
||||||
|
<button id="btn-clear-cache" style="background: none; border: none; padding: 0; color: var(--error-color); text-decoration: none; display: flex; align-items: center; gap: 0.5rem; cursor: pointer; text-align: left; font-size: inherit; font-family: inherit;" title="Löscht alle lokalen Daten & erzwingt einen Neuladen">
|
||||||
|
<span class="material-icons-round" style="font-size: 1.2em;">delete_forever</span> Lokalen Cache leeren
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -278,6 +281,18 @@
|
|||||||
const historyModal = document.getElementById('history-modal');
|
const historyModal = document.getElementById('history-modal');
|
||||||
const btnHistoryClose = document.getElementById('btn-history-close');
|
const btnHistoryClose = document.getElementById('btn-history-close');
|
||||||
|
|
||||||
|
if (btnHighlights) {
|
||||||
|
btnHighlights.addEventListener('click', () => {
|
||||||
|
highlightsModal.classList.remove('hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (btnHighlightsClose) {
|
||||||
|
btnHighlightsClose.addEventListener('click', () => {
|
||||||
|
highlightsModal.classList.add('hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
btnHistory.addEventListener('click', () => {
|
btnHistory.addEventListener('click', () => {
|
||||||
if (!authToken) {
|
if (!authToken) {
|
||||||
loginModal.classList.remove('hidden');
|
loginModal.classList.remove('hidden');
|
||||||
@@ -315,6 +330,17 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const btnClearCache = document.getElementById('btn-clear-cache');
|
||||||
|
if (btnClearCache) {
|
||||||
|
btnClearCache.addEventListener('click', () => {
|
||||||
|
if (confirm('Möchtest du wirklich alle lokalen Daten (inkl. Login-Session, Cache und Einstellungen) löschen? Die Seite wird danach neu geladen.')) {
|
||||||
|
localStorage.clear();
|
||||||
|
sessionStorage.clear();
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
window.addEventListener('click', (e) => {
|
window.addEventListener('click', (e) => {
|
||||||
if (e.target === versionModal) versionModal.classList.add('hidden');
|
if (e.target === versionModal) versionModal.classList.add('hidden');
|
||||||
});
|
});
|
||||||
@@ -939,6 +965,67 @@
|
|||||||
localStorage.setItem('kantine_flags', JSON.stringify([...userFlags]));
|
localStorage.setItem('kantine_flags', JSON.stringify([...userFlags]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function refreshFlaggedItems() {
|
||||||
|
if (userFlags.size === 0) return;
|
||||||
|
const token = authToken || GUEST_TOKEN;
|
||||||
|
const datesToFetch = new Set();
|
||||||
|
|
||||||
|
for (const flagId of userFlags) {
|
||||||
|
const [dateStr] = flagId.split('_');
|
||||||
|
datesToFetch.add(dateStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
let updated = false;
|
||||||
|
for (const dateStr of datesToFetch) {
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`${API_BASE}/venues/${VENUE_ID}/menu/${MENU_ID}/${dateStr}/`, {
|
||||||
|
headers: apiHeaders(token)
|
||||||
|
});
|
||||||
|
if (!resp.ok) continue;
|
||||||
|
const data = await resp.json();
|
||||||
|
const menuGroups = data.results || [];
|
||||||
|
let dayItems = [];
|
||||||
|
for (const group of menuGroups) {
|
||||||
|
if (group.items && Array.isArray(group.items)) {
|
||||||
|
dayItems = dayItems.concat(group.items);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update allWeeks in memory
|
||||||
|
for (let week of allWeeks) {
|
||||||
|
if (!week.days) continue;
|
||||||
|
let dayObj = week.days.find(d => d.date === dateStr);
|
||||||
|
if (dayObj) {
|
||||||
|
dayObj.items = dayItems.map(item => {
|
||||||
|
const isUnlimited = item.amount_tracking === false;
|
||||||
|
const hasStock = parseInt(item.available_amount) > 0;
|
||||||
|
return {
|
||||||
|
id: `${dateStr}_${item.id}`,
|
||||||
|
articleId: item.id,
|
||||||
|
name: item.name || 'Unknown',
|
||||||
|
description: item.description || '',
|
||||||
|
price: parseFloat(item.price) || 0,
|
||||||
|
available: isUnlimited || hasStock,
|
||||||
|
availableAmount: parseInt(item.available_amount) || 0,
|
||||||
|
amountTracking: item.amount_tracking !== false
|
||||||
|
};
|
||||||
|
});
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error refreshing flag date', dateStr, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updated) {
|
||||||
|
saveMenuCache();
|
||||||
|
updateLastUpdatedTime(new Date().toISOString());
|
||||||
|
updateAlarmBell();
|
||||||
|
renderVisibleWeeks();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function updateAlarmBell() {
|
function updateAlarmBell() {
|
||||||
const bellBtn = document.getElementById('alarm-bell');
|
const bellBtn = document.getElementById('alarm-bell');
|
||||||
const bellIcon = document.getElementById('alarm-bell-icon');
|
const bellIcon = document.getElementById('alarm-bell-icon');
|
||||||
@@ -946,12 +1033,14 @@
|
|||||||
|
|
||||||
if (userFlags.size === 0) {
|
if (userFlags.size === 0) {
|
||||||
bellBtn.classList.add('hidden');
|
bellBtn.classList.add('hidden');
|
||||||
|
bellBtn.style.display = 'none';
|
||||||
bellIcon.style.color = 'var(--text-secondary)';
|
bellIcon.style.color = 'var(--text-secondary)';
|
||||||
bellIcon.style.textShadow = 'none';
|
bellIcon.style.textShadow = 'none';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
bellBtn.classList.remove('hidden');
|
bellBtn.classList.remove('hidden');
|
||||||
|
bellBtn.style.display = 'inline-flex';
|
||||||
|
|
||||||
// Check if any flagged item is available
|
// Check if any flagged item is available
|
||||||
let anyAvailable = false;
|
let anyAvailable = false;
|
||||||
@@ -970,33 +1059,40 @@
|
|||||||
if (anyAvailable) break;
|
if (anyAvailable) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastUpdatedStr = localStorage.getItem('kantine_last_updated');
|
let lastUpdatedStr = localStorage.getItem('kantine_last_updated');
|
||||||
let timeStr = 'Unbekannt';
|
let timeStr = 'gerade eben'; // Fallback instead of Unbekannt
|
||||||
if (lastUpdatedStr) {
|
if (!lastUpdatedStr) {
|
||||||
|
lastUpdatedStr = new Date().toISOString();
|
||||||
|
localStorage.setItem('kantine_last_updated', lastUpdatedStr);
|
||||||
|
}
|
||||||
|
|
||||||
const lastUpdated = new Date(lastUpdatedStr);
|
const lastUpdated = new Date(lastUpdatedStr);
|
||||||
const diffMs = Date.now() - lastUpdated.getTime();
|
const diffMs = Date.now() - lastUpdated.getTime();
|
||||||
const diffMins = Math.floor(diffMs / 60000);
|
const diffMins = Math.floor(diffMs / 60000);
|
||||||
if (diffMins < 60) timeStr = `vor ${diffMins} Min.`;
|
if (diffMins < 1) timeStr = 'gerade eben';
|
||||||
|
else if (diffMins < 60) timeStr = `vor ${diffMins} Min.`;
|
||||||
else timeStr = `vor ${Math.floor(diffMins / 60)} Std.`;
|
else timeStr = `vor ${Math.floor(diffMins / 60)} Std.`;
|
||||||
}
|
|
||||||
bellBtn.title = `Zuletzt geprüft: ${timeStr}`;
|
bellBtn.title = `Zuletzt geprüft: ${timeStr}`;
|
||||||
|
|
||||||
if (anyAvailable) {
|
if (anyAvailable) {
|
||||||
bellIcon.style.color = 'var(--success-color)';
|
bellIcon.style.color = '#10b981'; // green / success
|
||||||
bellIcon.style.textShadow = '0 0 10px rgba(16, 185, 129, 0.4)';
|
bellIcon.style.textShadow = '0 0 10px rgba(16, 185, 129, 0.4)';
|
||||||
} else {
|
} else {
|
||||||
bellIcon.style.color = 'var(--warning-color)';
|
bellIcon.style.color = '#f59e0b'; // yellow / warning
|
||||||
bellIcon.style.textShadow = '0 0 10px rgba(245, 158, 11, 0.4)';
|
bellIcon.style.textShadow = '0 0 10px rgba(245, 158, 11, 0.4)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleFlag(date, articleId, name, cutoff) {
|
function toggleFlag(date, articleId, name, cutoff) {
|
||||||
const id = `${date}_${articleId}`;
|
const id = `${date}_${articleId}`;
|
||||||
|
let flagAdded = false;
|
||||||
if (userFlags.has(id)) {
|
if (userFlags.has(id)) {
|
||||||
userFlags.delete(id);
|
userFlags.delete(id);
|
||||||
showToast(`Flag entfernt für ${name}`, 'success');
|
showToast(`Flag entfernt für ${name}`, 'success');
|
||||||
} else {
|
} else {
|
||||||
userFlags.add(id);
|
userFlags.add(id);
|
||||||
|
flagAdded = true;
|
||||||
showToast(`Benachrichtigung aktiviert für ${name}`, 'success');
|
showToast(`Benachrichtigung aktiviert für ${name}`, 'success');
|
||||||
if (Notification.permission === 'default') {
|
if (Notification.permission === 'default') {
|
||||||
Notification.requestPermission();
|
Notification.requestPermission();
|
||||||
@@ -1005,17 +1101,36 @@
|
|||||||
saveFlags();
|
saveFlags();
|
||||||
updateAlarmBell();
|
updateAlarmBell();
|
||||||
renderVisibleWeeks();
|
renderVisibleWeeks();
|
||||||
|
|
||||||
|
if (flagAdded) {
|
||||||
|
refreshFlaggedItems();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FR-019: Auto-remove flags whose cutoff has passed
|
// FR-019: Auto-remove flags whose cutoff has passed
|
||||||
function cleanupExpiredFlags() {
|
function cleanupExpiredFlags() {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
const todayStr = now.toISOString().split('T')[0]; // Format: YYYY-MM-DD
|
||||||
let changed = false;
|
let changed = false;
|
||||||
|
|
||||||
for (const flagId of [...userFlags]) {
|
for (const flagId of [...userFlags]) {
|
||||||
const [date] = flagId.split('_');
|
const [dateStr] = flagId.split('_'); // Format usually is YYYY-MM-DD
|
||||||
const cutoff = new Date(date);
|
|
||||||
|
// If the flag's date string is entirely in the past (before today)
|
||||||
|
// or if it's today but past the 10:00 cutoff time
|
||||||
|
let isExpired = false;
|
||||||
|
|
||||||
|
if (dateStr < todayStr) {
|
||||||
|
isExpired = true;
|
||||||
|
} else if (dateStr === todayStr) {
|
||||||
|
const cutoff = new Date(dateStr);
|
||||||
cutoff.setHours(10, 0, 0, 0); // Standard cutoff 10:00
|
cutoff.setHours(10, 0, 0, 0); // Standard cutoff 10:00
|
||||||
if (now >= cutoff) {
|
if (now >= cutoff) {
|
||||||
|
isExpired = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isExpired) {
|
||||||
userFlags.delete(flagId);
|
userFlags.delete(flagId);
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
|||||||
17
syntax_check.js
Executable file
17
syntax_check.js
Executable file
@@ -0,0 +1,17 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const jsCode = fs.readFileSync('kantine.js', 'utf8')
|
||||||
|
.replace('(function () {', '')
|
||||||
|
.replace('})();', '')
|
||||||
|
.replace('if (window.__KANTINE_LOADED) return;', '');
|
||||||
|
const testCode = `
|
||||||
|
console.log("TEST");
|
||||||
|
`;
|
||||||
|
const code = jsCode + '\n' + testCode;
|
||||||
|
try {
|
||||||
|
const vm = require('vm');
|
||||||
|
new vm.Script(code);
|
||||||
|
} catch (e) {
|
||||||
|
if(e.stack) {
|
||||||
|
console.log("Syntax error at:", e.stack.split('\n').slice(0,3).join('\n'));
|
||||||
|
}
|
||||||
|
}
|
||||||
184
tests/test_dom.js
Executable file
184
tests/test_dom.js
Executable file
@@ -0,0 +1,184 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
fs.writeFileSync('trace.log', '');
|
||||||
|
function log(m) { fs.appendFileSync('trace.log', m + '\n'); }
|
||||||
|
|
||||||
|
log("Initializing JSDOM...");
|
||||||
|
const jsdom = require('jsdom');
|
||||||
|
const { JSDOM } = jsdom;
|
||||||
|
|
||||||
|
log("Reading html...");
|
||||||
|
const html = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
.hidden { display: none !important; }
|
||||||
|
.icon-btn { display: inline-flex; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<button id="alarm-bell" class="icon-btn hidden">
|
||||||
|
<span id="alarm-bell-icon" style="color:var(--text-secondary);"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Mocks for Highlights Feature -->
|
||||||
|
<button id="btn-highlights">Highlights</button>
|
||||||
|
<div id="highlights-modal" class="modal hidden">
|
||||||
|
<button id="btn-highlights-close">Close</button>
|
||||||
|
<input id="tag-input" type="text" />
|
||||||
|
<button id="btn-add-tag">Add</button>
|
||||||
|
<ul id="tags-list"></ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mocks for Login Modal -->
|
||||||
|
<button id="btn-login-open">Login</button>
|
||||||
|
<div id="login-modal" class="modal hidden">
|
||||||
|
<button id="btn-login-close">Close</button>
|
||||||
|
<form id="login-form"></form>
|
||||||
|
<div id="login-error" class="hidden"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mocks for History Modal -->
|
||||||
|
<button id="btn-history">History</button>
|
||||||
|
<div id="history-modal" class="modal hidden">
|
||||||
|
<button id="btn-history-close">Close</button>
|
||||||
|
<div id="history-loading" class="hidden"></div>
|
||||||
|
<div id="history-content"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mocks for Version Modal -->
|
||||||
|
<span class="version-tag">v1.4.17</span>
|
||||||
|
<div id="version-modal" class="modal hidden">
|
||||||
|
<button id="btn-version-close">Close</button>
|
||||||
|
<button id="btn-clear-cache">Clear</button>
|
||||||
|
<span id="version-current"></span>
|
||||||
|
<div id="version-list-container"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mocks for Theme Toggle -->
|
||||||
|
<button id="theme-toggle"><span class="theme-icon">light_mode</span></button>
|
||||||
|
|
||||||
|
<!-- Mocks for Navigation Tabs -->
|
||||||
|
<button id="btn-this-week" class="active">This Week</button>
|
||||||
|
<button id="btn-next-week">Next Week</button>
|
||||||
|
|
||||||
|
<button id="btn-refresh">Refresh</button>
|
||||||
|
<button id="btn-logout">Logout</button>
|
||||||
|
<div class="order-history-header">Header</div>
|
||||||
|
<button id="btn-error-redirect">Error Redirect</button>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
log("Reading file jsCode...");
|
||||||
|
const jsCode = fs.readFileSync('kantine.js', 'utf8')
|
||||||
|
.replace('(function () {', '')
|
||||||
|
.replace('})();', '')
|
||||||
|
.replace('if (window.__KANTINE_LOADED) return;', '')
|
||||||
|
.replace('window.location.reload();', 'window.__RELOAD_CALLED = true;');
|
||||||
|
|
||||||
|
log("Instantiating JSDOM...");
|
||||||
|
const dom = new JSDOM(html, { runScripts: "dangerously", url: "http://localhost/" });
|
||||||
|
log("JSDOM dom created...");
|
||||||
|
global.window = dom.window;
|
||||||
|
global.document = window.document;
|
||||||
|
global.localStorage = { getItem: () => '[]', setItem: () => { } };
|
||||||
|
global.sessionStorage = { getItem: () => null };
|
||||||
|
|
||||||
|
global.showToast = () => { };
|
||||||
|
global.saveFlags = () => { };
|
||||||
|
global.renderVisibleWeeks = () => { };
|
||||||
|
// Mock missing browser features if needed
|
||||||
|
global.Notification = { permission: 'default', requestPermission: () => { } };
|
||||||
|
global.window.matchMedia = () => ({ matches: false, addListener: () => { }, removeListener: () => { } });
|
||||||
|
global.fetch = () => Promise.resolve({ ok: true, json: () => Promise.resolve({ results: [] }) });
|
||||||
|
global.window.fetch = global.fetch;
|
||||||
|
|
||||||
|
log("Before eval...");
|
||||||
|
const testCode = `
|
||||||
|
console.log("--- Testing Alarm Bell ---");
|
||||||
|
// Add flag
|
||||||
|
userFlags.add('2026-02-24_123'); updateAlarmBell();
|
||||||
|
if (document.getElementById('alarm-bell').className.includes('hidden')) throw new Error("Bell should be visible");
|
||||||
|
|
||||||
|
// Remove flag
|
||||||
|
userFlags.delete('2026-02-24_123'); updateAlarmBell();
|
||||||
|
if (!document.getElementById('alarm-bell').className.includes('hidden')) throw new Error("Bell should be hidden");
|
||||||
|
|
||||||
|
console.log("✅ Alarm Bell Test Passed");
|
||||||
|
|
||||||
|
console.log("--- Testing Highlights Modal ---");
|
||||||
|
// First, verify initial state
|
||||||
|
const hlModal = document.getElementById('highlights-modal');
|
||||||
|
if (!hlModal.classList.contains('hidden')) throw new Error("Highlights modal should be hidden initially");
|
||||||
|
|
||||||
|
// Click to open
|
||||||
|
document.getElementById('btn-highlights').click();
|
||||||
|
if (hlModal.classList.contains('hidden')) throw new Error("Highlights modal did not open upon clicking btn-highlights!");
|
||||||
|
|
||||||
|
// Click to close
|
||||||
|
document.getElementById('btn-highlights-close').click();
|
||||||
|
if (!hlModal.classList.contains('hidden')) throw new Error("Highlights modal did not close upon clicking btn-highlights-close!");
|
||||||
|
|
||||||
|
console.log("✅ Highlights Modal Test Passed");
|
||||||
|
|
||||||
|
console.log("--- Testing Login Modal ---");
|
||||||
|
const loginModal = document.getElementById('login-modal');
|
||||||
|
document.getElementById('btn-login-open').click();
|
||||||
|
if (loginModal.classList.contains('hidden')) throw new Error("Login modal should open");
|
||||||
|
document.getElementById('btn-login-close').click();
|
||||||
|
if (!loginModal.classList.contains('hidden')) throw new Error("Login modal should close");
|
||||||
|
console.log("✅ Login Modal Test Passed");
|
||||||
|
|
||||||
|
console.log("--- Testing History Modal ---");
|
||||||
|
// We need authToken to be truthy to open history modal
|
||||||
|
authToken = "fake_token";
|
||||||
|
const historyModal = document.getElementById('history-modal');
|
||||||
|
document.getElementById('btn-history').click();
|
||||||
|
if (historyModal.classList.contains('hidden')) throw new Error("History modal should open");
|
||||||
|
document.getElementById('btn-history-close').click();
|
||||||
|
if (!historyModal.classList.contains('hidden')) throw new Error("History modal should close");
|
||||||
|
console.log("✅ History Modal Test Passed");
|
||||||
|
|
||||||
|
console.log("--- Testing Version Modal ---");
|
||||||
|
const versionModal = document.getElementById('version-modal');
|
||||||
|
document.querySelector('.version-tag').click();
|
||||||
|
if (versionModal.classList.contains('hidden')) throw new Error("Version modal should open");
|
||||||
|
document.getElementById('btn-version-close').click();
|
||||||
|
if (!versionModal.classList.contains('hidden')) throw new Error("Version modal should close");
|
||||||
|
console.log("✅ Version Modal Test Passed");
|
||||||
|
|
||||||
|
console.log("--- Testing Theme Toggle ---");
|
||||||
|
const themeBtn = document.getElementById('theme-toggle');
|
||||||
|
const initialTheme = document.documentElement.getAttribute('data-theme');
|
||||||
|
themeBtn.click();
|
||||||
|
const newTheme = document.documentElement.getAttribute('data-theme');
|
||||||
|
if (initialTheme === newTheme) throw new Error("Theme did not toggle");
|
||||||
|
console.log("✅ Theme Toggle Test Passed");
|
||||||
|
|
||||||
|
console.log("--- Testing Navigation Tabs ---");
|
||||||
|
const btnThis = document.getElementById('btn-this-week');
|
||||||
|
const btnNext = document.getElementById('btn-next-week');
|
||||||
|
btnNext.click();
|
||||||
|
if (!btnNext.classList.contains('active') || btnThis.classList.contains('active')) throw new Error("Next week tab not active");
|
||||||
|
btnThis.click();
|
||||||
|
if (!btnThis.classList.contains('active') || btnNext.classList.contains('active')) throw new Error("This week tab not active");
|
||||||
|
console.log("✅ Navigation Tabs Test Passed");
|
||||||
|
|
||||||
|
console.log("--- Testing Clear Cache Button ---");
|
||||||
|
// Mock confirm directly inside evaluated JSDOM context
|
||||||
|
window.confirm = () => true;
|
||||||
|
document.getElementById('btn-clear-cache').click();
|
||||||
|
if (!window.__RELOAD_CALLED) throw new Error("Clear cache did not reload the page");
|
||||||
|
console.log("✅ Clear Cache Button Test Passed");
|
||||||
|
|
||||||
|
window.__TEST_PASSED = true;
|
||||||
|
`;
|
||||||
|
|
||||||
|
dom.window.eval(jsCode + "\n" + testCode);
|
||||||
|
|
||||||
|
if (!dom.window.__TEST_PASSED) {
|
||||||
|
throw new Error("Tests failed to reach completion inside JSDOM.");
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
@@ -1 +1 @@
|
|||||||
v1.4.12
|
v1.4.19
|
||||||
|
|||||||
Reference in New Issue
Block a user