diff --git a/templates/index.html b/templates/index.html index 6d8bc3e..69ec6b9 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1119,7 +1119,7 @@

Edit Pomodoro

pomodoros_until_long_break: 4, always_use_short_break: false, tick_sound_enabled: true, - timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + timezone: 'auto', date_format: 'us', auto_start_after_break: false, tick_sound_during_breaks: false, @@ -1130,6 +1130,19 @@

Edit Pomodoro

timer_snap_interval: 60 // seconds: 1, 30, or 60 }; + // Global app timezone - always use this instead of browser timezone + // Updated when settings load or timezone setting changes + let appTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + + // Update appTimezone from settings (call after settings load or timezone change) + function updateAppTimezone() { + if (settings.timezone && settings.timezone !== 'auto') { + appTimezone = settings.timezone; + } else { + appTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + } + } + // Bell sound using Web Audio API let audioContext = null; function playBellSound() { @@ -1245,12 +1258,12 @@

Edit Pomodoro

}); } - // Date formatting helper + // Date formatting helper - uses appTimezone function formatDate(date, format) { const d = new Date(date); - const day = d.getDate(); - const month = d.getMonth() + 1; - const year = d.getFullYear(); + const day = parseInt(d.toLocaleString('en-US', { day: 'numeric', timeZone: appTimezone })); + const month = parseInt(d.toLocaleString('en-US', { month: 'numeric', timeZone: appTimezone })); + const year = parseInt(d.toLocaleString('en-US', { year: 'numeric', timeZone: appTimezone })); const fmt = format || getEffectiveDateFormat(); if (fmt === 'eu') { @@ -1265,13 +1278,11 @@

Edit Pomodoro

function formatDateWithDay(date, format) { const d = new Date(date); - const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; - const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; - const dayName = dayNames[d.getDay()]; - const day = d.getDate(); - const month = d.getMonth() + 1; - const monthName = monthNames[d.getMonth()]; - const year = d.getFullYear(); + const dayName = d.toLocaleString('en-US', { weekday: 'short', timeZone: appTimezone }); + const day = parseInt(d.toLocaleString('en-US', { day: 'numeric', timeZone: appTimezone })); + const month = parseInt(d.toLocaleString('en-US', { month: 'numeric', timeZone: appTimezone })); + const monthName = d.toLocaleString('en-US', { month: 'short', timeZone: appTimezone }); + const year = parseInt(d.toLocaleString('en-US', { year: 'numeric', timeZone: appTimezone })); const fmt = format || getEffectiveDateFormat(); if (fmt === 'eu') { @@ -1284,6 +1295,27 @@

Edit Pomodoro

} } + // Format date with day name in a specific timezone + function formatDateWithDayTz(date, tz) { + const d = new Date(date); + // Extract components in the specified timezone + const dayName = d.toLocaleString('en-US', { weekday: 'short', timeZone: tz }); + const day = parseInt(d.toLocaleString('en-US', { day: 'numeric', timeZone: tz })); + const month = parseInt(d.toLocaleString('en-US', { month: 'numeric', timeZone: tz })); + const monthName = d.toLocaleString('en-US', { month: 'short', timeZone: tz }); + const year = parseInt(d.toLocaleString('en-US', { year: 'numeric', timeZone: tz })); + const fmt = getEffectiveDateFormat(); + + if (fmt === 'eu') { + return `${dayName}, ${day} ${monthName}`; + } else if (fmt === 'iso') { + return `${dayName}, ${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`; + } else { + // US format (default) + return `${dayName}, ${monthName} ${day}`; + } + } + // Clock functions // Derive date format from timezone region @@ -1314,7 +1346,7 @@

Edit Pomodoro

// Get effective date format (resolves 'auto' to actual format) function getEffectiveDateFormat() { if (settings.date_format === 'auto') { - return getDateFormatForTimezone(settings.timezone); + return getDateFormatForTimezone(getEffectiveTimezone()); } return settings.date_format || 'us'; } @@ -1322,7 +1354,7 @@

Edit Pomodoro

// Get effective clock format (resolves 'auto' based on timezone) function getEffectiveClockFormat() { if (settings.clock_format === 'auto') { - const dateFormat = getDateFormatForTimezone(settings.timezone); + const dateFormat = getDateFormatForTimezone(getEffectiveTimezone()); return dateFormat === 'us' ? '12' : '24'; } return settings.clock_format || '12'; @@ -1331,7 +1363,7 @@

Edit Pomodoro

// Get effective period labels (resolves 'auto' based on timezone) function getEffectivePeriodLabels() { if (settings.period_labels === 'auto') { - const dateFormat = getDateFormatForTimezone(settings.timezone); + const dateFormat = getDateFormatForTimezone(getEffectiveTimezone()); return dateFormat === 'us' ? 'ampm' : 'morning'; } return settings.period_labels || 'ampm'; @@ -1341,12 +1373,18 @@

Edit Pomodoro

const select = document.getElementById('timezone'); const browserTz = Intl.DateTimeFormat().resolvedOptions().timeZone; - // Add browser's timezone first if not in list + // Add "Automatic" option first with detected timezone + const autoOption = document.createElement('option'); + autoOption.value = 'auto'; + autoOption.textContent = `Automatic (Currently ${browserTz} detected from browser)`; + select.appendChild(autoOption); + + // Add browser's timezone if not in the standard list const inList = TIMEZONES.some(tz => tz.value === browserTz); if (!inList) { const option = document.createElement('option'); option.value = browserTz; - option.textContent = `${browserTz} (Local)`; + option.textContent = browserTz; select.appendChild(option); } @@ -1354,17 +1392,20 @@

Edit Pomodoro

const option = document.createElement('option'); option.value = tz.value; option.textContent = tz.label; - if (tz.value === browserTz) { - option.textContent += ' (Local)'; - } select.appendChild(option); }); - select.value = settings.timezone || browserTz; + // Default to 'auto' if no timezone set, otherwise use saved setting + select.value = settings.timezone || 'auto'; + } + + // Get the effective timezone - returns the global appTimezone + function getEffectiveTimezone() { + return appTimezone; } function updateClock() { - const tz = settings.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone; + const tz = getEffectiveTimezone(); const now = new Date(); // Check for date change (midnight crossing or returning from background) @@ -1419,20 +1460,17 @@

Edit Pomodoro

} async function loadWeeklyOverview() { - const tz = settings.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone; + const tz = appTimezone; const now = new Date(); // US = Sunday first, EU/ISO = Monday first const sundayFirst = getEffectiveDateFormat() === 'us'; - // Get start of current week + // Get start of current week (use browser timezone for calculation, then convert) const startOfWeek = new Date(now); if (sundayFirst) { - // Sunday first: go back to Sunday (getDay() = 0 for Sunday) startOfWeek.setDate(now.getDate() - now.getDay()); } else { - // Monday first: go back to Monday - // (getDay() + 6) % 7 gives days since Monday (Mon=0, Tue=1, ..., Sun=6) const daysSinceMonday = (now.getDay() + 6) % 7; startOfWeek.setDate(now.getDate() - daysSinceMonday); } @@ -1453,10 +1491,9 @@

Edit Pomodoro

weekLabel.textContent = 'Last Week'; } else { // Format date range - const startMonth = startOfWeek.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); - const endDate = new Date(endOfWeek); - endDate.setDate(endDate.getDate() - 1); // End of week is exclusive, so subtract 1 - const endMonth = endDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); + const startMonth = startOfWeek.toLocaleDateString('en-US', { month: 'short', day: 'numeric', timeZone: tz }); + const endDate = new Date(endOfWeek.getTime() - 24 * 60 * 60 * 1000); // End of week is exclusive, so subtract 1 day + const endMonth = endDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric', timeZone: tz }); weekLabel.textContent = `${startMonth} - ${endMonth}`; } @@ -1483,11 +1520,10 @@

Edit Pomodoro

let html = ''; for (let i = 0; i < 7; i++) { - const dayDate = new Date(startOfWeek); - dayDate.setDate(startOfWeek.getDate() + i); + const dayDate = new Date(startOfWeek.getTime() + i * 24 * 60 * 60 * 1000); const dayStr = dayDate.toLocaleDateString('en-CA', { timeZone: tz }); const isToday = dayStr === todayStr; - const dayNum = dayDate.getDate(); + const dayNum = parseInt(dayDate.toLocaleString('en-US', { day: 'numeric', timeZone: tz })); // Filter pomodoros for this day const dayPomos = pomodoros.filter(p => { @@ -1495,13 +1531,13 @@

Edit Pomodoro

return pomoDate.toLocaleDateString('en-CA', { timeZone: tz }) === dayStr; }); - // Split into AM/PM (noon = 12:00) + // Split into AM/PM (noon = 12:00) using configured timezone const amPomos = dayPomos.filter(p => { - const h = new Date(p.start_time).getHours(); + const h = parseInt(new Date(p.start_time).toLocaleString('en-US', { hour: 'numeric', hour12: false, timeZone: tz })); return h < 12; }); const pmPomos = dayPomos.filter(p => { - const h = new Date(p.start_time).getHours(); + const h = parseInt(new Date(p.start_time).toLocaleString('en-US', { hour: 'numeric', hour12: false, timeZone: tz })); return h >= 12; }); @@ -1680,8 +1716,59 @@

Edit Pomodoro

} } + // Helper: Create a Date object for a specific time in the configured timezone + // When user enters "12:00" in the app, we want that to mean 12:00 in appTimezone + function createDateInTimezone(dayStr, timeStr, tz) { + const browserTz = Intl.DateTimeFormat().resolvedOptions().timeZone; + + // If app timezone matches browser timezone, no conversion needed + if (tz === browserTz) { + return new Date(dayStr + 'T' + timeStr + ':00'); + } + + // Different timezones: we need to find what UTC time corresponds to + // the given local time in the target timezone + // + // Strategy: Create date in browser TZ, then figure out the offset difference + const browserDate = new Date(dayStr + 'T' + timeStr + ':00'); + + // Get what time it would be in target TZ at this UTC moment + const targetTimeStr = browserDate.toLocaleString('en-US', { + timeZone: tz, + year: 'numeric', month: '2-digit', day: '2-digit', + hour: '2-digit', minute: '2-digit', second: '2-digit', + hour12: false + }); + + // Get what time it is in browser TZ at this UTC moment + const browserTimeStr = browserDate.toLocaleString('en-US', { + timeZone: browserTz, + year: 'numeric', month: '2-digit', day: '2-digit', + hour: '2-digit', minute: '2-digit', second: '2-digit', + hour12: false + }); + + // Parse both times to get the offset in minutes + const parseTime = (str) => { + const [datePart, timePart] = str.split(', '); + const [m, d, y] = datePart.split('/').map(Number); + const [h, min, s] = timePart.split(':').map(Number); + return new Date(y, m-1, d, h, min, s).getTime(); + }; + + const targetMs = parseTime(targetTimeStr); + const browserMs = parseTime(browserTimeStr); + const offsetMs = browserMs - targetMs; + + // Adjust: if user wants 12:00 in target TZ, and target is 6 hours behind browser, + // we need to ADD 6 hours to the browser time + return new Date(browserDate.getTime() + offsetMs); + } + // Add a pomodoro to the next available slot in morning or afternoon async function addPomoToNextSlot(dayStr, isMorning) { + const tz = getEffectiveTimezone(); + // Get the selected duration from settings const duration = settings[`timer_preset_${selectedPreset}`] || 25; @@ -1699,8 +1786,9 @@

Edit Pomodoro

const endMin = isMorning ? 0 : workEndMin; // Fetch existing pomodoros for that day to find free slots - const dayStart = new Date(dayStr + 'T00:00:00'); - const dayEnd = new Date(dayStr + 'T23:59:59'); + const dayStartTime = String(workStartHour).padStart(2, '0') + ':' + String(workStartMin).padStart(2, '0'); + const dayStart = createDateInTimezone(dayStr, '00:00', tz); + const dayEnd = createDateInTimezone(dayStr, '23:59', tz); try { const existingPomos = await Storage.getPomodoros(dayStart.toISOString(), dayEnd.toISOString()); @@ -1719,7 +1807,8 @@

Edit Pomodoro

for (let timeMinutes = startTimeMinutes; timeMinutes < endTimeMinutes; timeMinutes += 30) { const hour = Math.floor(timeMinutes / 60); const min = timeMinutes % 60; - const slotStart = new Date(dayStr + 'T' + String(hour).padStart(2, '0') + ':' + String(min).padStart(2, '0') + ':00'); + const timeStr = String(hour).padStart(2, '0') + ':' + String(min).padStart(2, '0'); + const slotStart = createDateInTimezone(dayStr, timeStr, tz); const slotEnd = new Date(slotStart.getTime() + duration * 60 * 1000); // Check if this slot overlaps with any existing pomodoro @@ -1728,7 +1817,7 @@

Edit Pomodoro

); if (!isOccupied) { - freeSlotTime = String(hour).padStart(2, '0') + ':' + String(min).padStart(2, '0'); + freeSlotTime = timeStr; break; } } @@ -2452,6 +2541,7 @@

Edit Pomodoro

// History async function loadHistory() { const list = document.getElementById('history-list'); + const tz = getEffectiveTimezone(); try { const pomodoros = await Storage.getPomodoros(); @@ -2460,10 +2550,10 @@

Edit Pomodoro

return; } - // Group by date + // Group by date (using configured timezone) const grouped = {}; pomodoros.forEach(p => { - const date = formatDateWithDay(p.start_time); + const date = formatDateWithDayTz(p.start_time, tz); if (!grouped[date]) grouped[date] = []; grouped[date].push(p); }); @@ -2475,7 +2565,7 @@

Edit Pomodoro

html += `

${date}

`; items.forEach(p => { const color = TYPE_COLORS[p.type] || '#6b7280'; - const time = new Date(p.start_time).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' }); + const time = new Date(p.start_time).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', timeZone: tz }); const safeId = escapeHtml(p.id); html += `
@@ -2549,16 +2639,18 @@

Edit Pomodoro

let existingPomodoros = []; // Cache for conflict checking - // Get default start time based on working hours + // Get default start time based on working hours (uses configured timezone) function getDefaultStartTime(now) { + const tz = getEffectiveTimezone(); const workStart = settings.working_hours_start || '08:00'; const workEnd = settings.working_hours_end || '17:00'; const [startHour, startMin] = workStart.split(':').map(Number); const [endHour, endMin] = workEnd.split(':').map(Number); - const currentHour = now.getHours(); - const currentMin = now.getMinutes(); + // Get current time in configured timezone + const currentHour = parseInt(now.toLocaleString('en-US', { hour: 'numeric', hour12: false, timeZone: tz })); + const currentMin = parseInt(now.toLocaleString('en-US', { minute: 'numeric', timeZone: tz })); const currentTimeMinutes = currentHour * 60 + currentMin; const workStartMinutes = startHour * 60 + startMin; const workEndMinutes = endHour * 60 + endMin; @@ -2571,7 +2663,7 @@

Edit Pomodoro

return '12:00'; } else { // During working hours: use current time snapped to half hour - return formatTimeSnapped(now); + return formatTimeSnappedTz(now, tz); } } @@ -2736,6 +2828,7 @@

Edit Pomodoro

async function addManualPomodoro() { try { + const tz = getEffectiveTimezone(); const name = document.getElementById('add-name').value.trim(); const type = document.getElementById('add-type').value; const date = document.getElementById('add-date').value; @@ -2749,7 +2842,8 @@

Edit Pomodoro

return; } - const startTime = new Date(`${date}T${time}`); + // Create start time in the configured timezone, not browser timezone + const startTime = createDateInTimezone(date, time, tz); const slots = []; let currentStart = startTime; @@ -2809,6 +2903,15 @@

Edit Pomodoro

return `${String(hours % 24).padStart(2, '0')}:${String(snappedMinutes % 60).padStart(2, '0')}`; } + // Format time as HH:MM with 30-minute snapping in a specific timezone + function formatTimeSnappedTz(date, tz) { + const minutes = parseInt(date.toLocaleString('en-US', { minute: 'numeric', timeZone: tz })); + const snappedMinutes = snapTo30Minutes(minutes); + let hours = parseInt(date.toLocaleString('en-US', { hour: 'numeric', hour12: false, timeZone: tz })); + if (snappedMinutes >= 60) hours = (hours + 1) % 24; + return `${String(hours).padStart(2, '0')}:${String(snappedMinutes % 60).padStart(2, '0')}`; + } + async function showEditModal(id) { // Fetch all pomodoros to find the one to edit allPomodoros = await Storage.getPomodoros(); @@ -3212,11 +3315,12 @@

Edit Pomodoro

totalSeconds = remainingSeconds; updateTimerDisplay(); - // Timezone (default to browser timezone) + // Timezone (default to 'auto' which uses browser timezone) if (!settings.timezone) { - settings.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + settings.timezone = 'auto'; } document.getElementById('timezone').value = settings.timezone; + updateAppTimezone(); // Update global appTimezone updateClock(); // Date format (default to auto) @@ -3378,6 +3482,7 @@

Edit Pomodoro

// Timezone dropdown - auto-save and refresh views (auto settings derive from timezone) document.getElementById('timezone').addEventListener('change', e => { settings.timezone = e.target.value; + updateAppTimezone(); // Update global appTimezone updateClock(); // Reinitialize pickers and views (auto settings will use new timezone) diff --git a/tests/test_timezone.js b/tests/test_timezone.js new file mode 100644 index 0000000..8176aac --- /dev/null +++ b/tests/test_timezone.js @@ -0,0 +1,308 @@ +/** + * Automated timezone handling tests using Puppeteer + * + * Tests that entries are created and displayed correctly when + * browser timezone differs from configured app timezone. + * + * Run with: node tests/test_timezone.js + * Requires: Running acquacotta-dev container on localhost:5000 + */ + +const puppeteer = require('puppeteer'); + +const APP_URL = 'http://localhost:5000'; + +// Test results tracking +let passed = 0; +let failed = 0; +const results = []; + +function log(msg) { + console.log(`[TEST] ${msg}`); +} + +function assert(condition, testName) { + if (condition) { + passed++; + results.push({ name: testName, pass: true }); + log(`✓ ${testName}`); + } else { + failed++; + results.push({ name: testName, pass: false }); + log(`✗ ${testName}`); + } +} + +async function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +async function runTests() { + log('Starting timezone tests...'); + + const browser = await puppeteer.launch({ + headless: true, + args: ['--no-sandbox', '--disable-setuid-sandbox'] + }); + + try { + // Test 1: Browser timezone emulation and Automatic option + await testAutomaticTimezone(browser); + + // Test 2: Different timezones - PM entry appears in PM section + await testPMEntryWithDifferentTimezones(browser); + + // Test 3: Timer start/stop creates entry with correct timezone + await testTimerWithDifferentTimezones(browser); + + // Test 4: Same timezone - entries display correctly + await testSameTimezone(browser); + + } catch (e) { + log(`Test error: ${e.message}`); + console.error(e); + } finally { + await browser.close(); + } + + // Summary + console.log('\n' + '='.repeat(50)); + log(`Results: ${passed} passed, ${failed} failed`); + process.exit(failed > 0 ? 1 : 0); +} + +async function testAutomaticTimezone(browser) { + log('\n--- Test: Automatic timezone option ---'); + + const page = await browser.newPage(); + + // Emulate Brussels timezone + await page.emulateTimezone('Europe/Brussels'); + await page.goto(APP_URL, { waitUntil: 'networkidle0' }); + await sleep(1000); + + // Open settings (click nav button with data-view="settings") + await page.click('nav button[data-view="settings"]'); + await sleep(500); + + // Check the Automatic option text contains Brussels + const automaticOption = await page.$eval( + '#timezone option[value="auto"]', + el => el.textContent + ); + + assert( + automaticOption.includes('Europe/Brussels'), + 'Automatic option shows detected browser timezone (Brussels)' + ); + + // Change emulated timezone and refresh + await page.close(); + + const page2 = await browser.newPage(); + await page2.emulateTimezone('America/New_York'); + await page2.goto(APP_URL, { waitUntil: 'networkidle0' }); + await sleep(1000); + + await page2.click('nav button[data-view="settings"]'); + await sleep(500); + + const automaticOption2 = await page2.$eval( + '#timezone option[value="auto"]', + el => el.textContent + ); + + assert( + automaticOption2.includes('America/New_York'), + 'Automatic option shows detected browser timezone (New York)' + ); + + await page2.close(); +} + +async function testPMEntryWithDifferentTimezones(browser) { + log('\n--- Test: PM entry with different timezones ---'); + + const page = await browser.newPage(); + + // Browser is Brussels, app will be set to New York + await page.emulateTimezone('Europe/Brussels'); + await page.goto(APP_URL, { waitUntil: 'networkidle0' }); + await sleep(1000); + + // Open settings and set timezone to America/New_York + await page.click('nav button[data-view="settings"]'); + await sleep(500); + + await page.select('#timezone', 'America/New_York'); + await sleep(300); + + // Go back to timer view to see week grid + await page.click('nav button[data-view="timer"]'); + await sleep(500); + + // Find and click a PM + button (afternoon section) + // Use evaluate to click since the button might be small + const clicked = await page.evaluate(() => { + const pmSections = document.querySelectorAll('.week-day-section:nth-child(2)'); + for (const section of pmSections) { + const label = section.querySelector('.week-day-section-label'); + if (label && (label.textContent.includes('PM') || label.textContent.includes('Afternoon'))) { + const btn = section.querySelector('.week-section-add-btn'); + if (btn) { + btn.click(); + return true; + } + } + } + return false; + }); + + if (clicked) { + await sleep(500); + + // Modal should be open - check that time is in PM range (12:00 or later) + const timeValue = await page.$eval('#add-time', el => el.value); + const hour = parseInt(timeValue.split(':')[0]); + + assert( + hour >= 12, + `PM + button sets time to PM (got ${timeValue})` + ); + + // Submit the entry + await page.click('#add-submit'); + await sleep(1000); + + // Check that entry appears in PM section (not AM) + const pmHasEntry = await page.evaluate(() => { + const sections = document.querySelectorAll('.week-day-section'); + for (const section of sections) { + const label = section.querySelector('.week-day-section-label'); + if (label && (label.textContent.includes('PM') || label.textContent.includes('Afternoon'))) { + // Check if section has pomodoro entries (not just "—") + const content = section.textContent; + return !content.includes('—') || section.querySelectorAll('.week-pomo').length > 0; + } + } + return false; + }); + + assert( + pmHasEntry, + 'Entry appears in PM section after adding via PM + button' + ); + } else { + log('Warning: Could not find PM + button'); + } + + await page.close(); +} + +async function testTimerWithDifferentTimezones(browser) { + log('\n--- Test: Timer start/stop with different timezones ---'); + + const page = await browser.newPage(); + + // Browser is Tokyo, app will be set to Los Angeles (big difference) + await page.emulateTimezone('Asia/Tokyo'); + await page.goto(APP_URL, { waitUntil: 'networkidle0' }); + await sleep(1000); + + // Set timezone to America/Los_Angeles + await page.click('nav button[data-view="settings"]'); + await sleep(500); + await page.select('#timezone', 'America/Los_Angeles'); + await sleep(300); + + // Go back to timer view + await page.click('nav button[data-view="timer"]'); + await sleep(500); + + // Get the current displayed time (should be in LA timezone) + const clockTime = await page.$eval('#clock-time', el => el.textContent); + log(`Clock shows: ${clockTime} (should be LA time)`); + + // Start the timer + await page.click('#btn-start'); + await sleep(1000); + + // Stop the timer immediately + await page.click('#btn-stop'); + await sleep(1000); + + // Check that an entry was created + // Navigate to history to verify + await page.click('nav button[data-view="history"]'); + await sleep(1000); + + const historyContent = await page.$eval('#history-list', el => el.textContent); + + // Should have at least one entry (might say "No entries" if empty) + const hasEntry = !historyContent.includes('No entries'); + + assert( + hasEntry, + 'Timer creates entry when stopped' + ); + + await page.close(); +} + +async function testSameTimezone(browser) { + log('\n--- Test: Same timezone (browser = app) ---'); + + const page = await browser.newPage(); + + // Both browser and app will be Brussels + await page.emulateTimezone('Europe/Brussels'); + await page.goto(APP_URL, { waitUntil: 'networkidle0' }); + await sleep(1000); + + // Set timezone to Brussels explicitly + await page.click('nav button[data-view="settings"]'); + await sleep(500); + await page.select('#timezone', 'Europe/Brussels'); + await sleep(300); + + // Go back to timer view + await page.click('nav button[data-view="timer"]'); + await sleep(500); + + // Add a PM entry using evaluate + const clicked = await page.evaluate(() => { + const sections = document.querySelectorAll('.week-day-section'); + for (const section of sections) { + const label = section.querySelector('.week-day-section-label'); + if (label && (label.textContent.includes('PM') || label.textContent.includes('Afternoon'))) { + const btn = section.querySelector('.week-section-add-btn'); + if (btn) { + btn.click(); + return true; + } + } + } + return false; + }); + + if (clicked) { + await sleep(500); + + const timeValue = await page.$eval('#add-time', el => el.value); + const hour = parseInt(timeValue.split(':')[0]); + + assert( + hour >= 12, + `Same TZ: PM + button sets time to PM (got ${timeValue})` + ); + + // Cancel instead of adding (to avoid polluting data) + await page.click('#add-modal .modal-actions .secondary'); + await sleep(300); + } + + await page.close(); +} + +// Run the tests +runTests();