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