diff --git a/e2e/city.spec.ts b/e2e/city.spec.ts
index e899a92..8c6a76b 100644
--- a/e2e/city.spec.ts
+++ b/e2e/city.spec.ts
@@ -8,7 +8,7 @@ test.use({ permissions: [] }) // no geolocation — the landing must not need it
test('city landing renders the Praha list from the centroid', async ({ page }) => {
await page.clock.install({ time: FIXED_NOW })
await mockData(page)
- await page.goto('/mesto/praha/')
+ await page.goto('/mesto/praha/?zobrazeni=seznam')
await expect(page.getByText('katedrála sv. Víta, Václava a Vojtěcha')).toBeVisible()
await expect(page.getByText('Praha ·')).toBeVisible()
await expect(page).toHaveTitle(/Bohoslužby Praha — mše svatá dnes/)
@@ -16,7 +16,7 @@ test('city landing renders the Praha list from the centroid', async ({ page }) =
// a church detail still opens from the landing list
await page.getByText('kostel sv. Havla').click()
- await expect(page).toHaveURL('/kostel/3/')
+ await expect(page).toHaveURL('/kostel/3/?zobrazeni=seznam')
await expect(page.getByLabel('Pořad bohoslužeb')).toBeVisible()
})
@@ -50,7 +50,7 @@ test('změnit opens the search over the list; zpět returns with the origin inta
await context.setGeolocation({ latitude: 50.0875, longitude: 14.4213 })
await page.clock.install({ time: FIXED_NOW })
await mockData(page)
- await page.goto('/')
+ await page.goto('/?zobrazeni=seznam')
await expect(page.getByText('katedrála sv. Víta, Václava a Vojtěcha')).toBeVisible()
await page.getByRole('button', { name: 'změnit' }).click()
diff --git a/e2e/day.spec.ts b/e2e/day.spec.ts
index 566875c..5918426 100644
--- a/e2e/day.spec.ts
+++ b/e2e/day.spec.ts
@@ -8,7 +8,7 @@ test.use({ geolocation: PRAGUE, permissions: ['geolocation'] })
test('Sunday ordo: every service that day, chronological, no countdowns', async ({ page }) => {
await page.clock.install({ time: FIXED_NOW })
await mockData(page)
- await page.goto('/')
+ await page.goto('/?zobrazeni=seznam')
await expect(page.getByText('katedrála sv. Víta, Václava a Vojtěcha')).toBeVisible()
await openControls(page)
@@ -33,7 +33,7 @@ test('Sunday ordo: every service that day, chronological, no countdowns', async
test('feast day: picker chip tinted, quiet feast line in the header (5 Jul = Cyril a Metoděj)', async ({ page }) => {
await page.clock.install({ time: new Date('2026-07-03T07:00:00Z') }) // Friday 3 Jul
await mockData(page)
- await page.goto('/')
+ await page.goto('/?zobrazeni=seznam')
await expect(page.getByText('katedrála sv. Víta, Václava a Vojtěcha')).toBeVisible()
await openControls(page)
@@ -47,7 +47,7 @@ test('feast day: picker chip tinted, quiet feast line in the header (5 Jul = Cyr
test('bookmarked ?den=nedele restores the Sunday ordo; day picks rewrite the URL', async ({ page }) => {
await page.clock.install({ time: FIXED_NOW })
await mockData(page)
- await page.goto('/?den=nedele') // Monday 6 Jul → next Sunday is 12 Jul
+ await page.goto('/?zobrazeni=seznam&den=nedele') // Monday 6 Jul → next Sunday is 12 Jul
await expect(page.getByText('neděle 12. 7.')).toBeVisible()
await openControls(page)
await expect(page.getByRole('button', { name: 'neděle', exact: true })).toHaveAttribute('aria-pressed', 'true')
@@ -56,9 +56,9 @@ test('bookmarked ?den=nedele restores the Sunday ordo; day picks rewrite the URL
// switching the day rewrites the param; "hned" drops it
await page.getByRole('button', { name: 'zítra' }).click()
- await expect(page).toHaveURL('/?den=zitra')
+ await expect(page).toHaveURL('/?zobrazeni=seznam&den=zitra')
await page.getByRole('button', { name: 'hned' }).click()
- await expect(page).toHaveURL('/')
+ await expect(page).toHaveURL('/?zobrazeni=seznam')
await expect(page.getByText('za 29 min')).toBeVisible()
})
@@ -66,7 +66,7 @@ test('day picker at 375px', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 812 })
await page.clock.install({ time: FIXED_NOW })
await mockData(page)
- await page.goto('/')
+ await page.goto('/?zobrazeni=seznam')
await expect(page.getByText('katedrála sv. Víta, Václava a Vojtěcha')).toBeVisible()
await openControls(page)
await page.getByRole('button', { name: 'zítra' }).click()
diff --git a/e2e/detail.spec.ts b/e2e/detail.spec.ts
index 5c4e60a..b7cc2cf 100644
--- a/e2e/detail.spec.ts
+++ b/e2e/detail.spec.ts
@@ -8,10 +8,10 @@ test.use({ geolocation: PRAGUE, permissions: ['geolocation'] })
test('detail from the list: weekly ordo, extras, parish, freshness', async ({ page }) => {
await page.clock.install({ time: FIXED_NOW })
await mockData(page)
- await page.goto('/')
+ await page.goto('/?zobrazeni=seznam')
await page.getByText('kostel Panny Marie Sněžné').click()
- await expect(page).toHaveURL('/kostel/4/')
+ await expect(page).toHaveURL('/kostel/4/?zobrazeni=seznam')
// weekly schedule grouped by day, Sunday first
const ordo = page.getByLabel('Pořad bohoslužeb')
@@ -41,7 +41,7 @@ test('detail from the list: weekly ordo, extras, parish, freshness', async ({ pa
// back to the list
await page.getByRole('button', { name: '‹ zpět na seznam' }).click()
- await expect(page).toHaveURL('/')
+ await expect(page).toHaveURL('/?zobrazeni=seznam')
await expect(page.getByText('katedrála sv. Víta, Václava a Vojtěcha')).toBeVisible()
})
diff --git a/e2e/english.spec.ts b/e2e/english.spec.ts
index 119ead5..45a1b3a 100644
--- a/e2e/english.spec.ts
+++ b/e2e/english.spec.ts
@@ -10,7 +10,7 @@ test.use({ locale: 'en-US', geolocation: PRAGUE, permissions: ['geolocation'] })
test('English UI: translated day rubric, pill, and footer', async ({ page }) => {
await page.clock.install({ time: FIXED_NOW })
await mockData(page)
- await page.goto('/')
+ await page.goto('/?zobrazeni=seznam')
// registry data (church name) stays Czech even under English UI
await expect(page.getByText('katedrála sv. Víta, Václava a Vojtěcha')).toBeVisible()
diff --git a/e2e/feedback.spec.ts b/e2e/feedback.spec.ts
index 587d3a8..2fd9887 100644
--- a/e2e/feedback.spec.ts
+++ b/e2e/feedback.spec.ts
@@ -8,7 +8,7 @@ test.use({ geolocation: PRAGUE, permissions: ['geolocation'] })
test('feedback card: collapsed line → Sean Ellis + text → thanks', async ({ page }) => {
await page.clock.install({ time: FIXED_NOW })
await mockData(page)
- await page.goto('/')
+ await page.goto('/?zobrazeni=seznam')
await expect(page.getByText('katedrála sv. Víta, Václava a Vojtěcha')).toBeVisible()
// collapsed by default; Podpořit sits in the same footer line
@@ -32,7 +32,7 @@ test('feedback at 375px', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 812 })
await page.clock.install({ time: FIXED_NOW })
await mockData(page)
- await page.goto('/')
+ await page.goto('/?zobrazeni=seznam')
await expect(page.getByText('katedrála sv. Víta, Václava a Vojtěcha')).toBeVisible()
await page.getByRole('button', { name: /Napište nám/ }).click()
await page.getByRole('button', { name: /Napište nám/ }).waitFor({ state: 'hidden' })
diff --git a/e2e/filters.spec.ts b/e2e/filters.spec.ts
index 8b5135d..a658ec3 100644
--- a/e2e/filters.spec.ts
+++ b/e2e/filters.spec.ts
@@ -11,7 +11,7 @@ test.beforeEach(async ({ page }) => {
})
test('greek-catholic filter narrows to the liturgy; persists across reload', async ({ page }) => {
- await page.goto('/')
+ await page.goto('/?zobrazeni=seznam')
await expect(page.getByText('katedrála sv. Víta, Václava a Vojtěcha')).toBeVisible()
await shot(page, 'filters-default')
@@ -34,7 +34,7 @@ test('greek-catholic filter narrows to the liturgy; persists across reload', asy
})
test('language filter + over-narrow combination explains itself', async ({ page }) => {
- await page.goto('/')
+ await page.goto('/?zobrazeni=seznam')
await expect(page.getByText('kostel sv. Havla')).toBeVisible()
await openControls(page)
@@ -55,7 +55,7 @@ test('okruh: distance filter narrows the list to walking range', async ({ page,
// stand NE of the centre so 2 km splits the fixture: Havel/Salvátor stay
// inside, the cathedral (~2.6 km) and Ludmily (~2.5 km) fall out
await context.setGeolocation({ latitude: 50.098, longitude: 14.435 })
- await page.goto('/')
+ await page.goto('/?zobrazeni=seznam')
await expect(page.getByText('kostel sv. Ludmily')).toBeVisible()
await openControls(page)
@@ -75,7 +75,7 @@ test('375px: one pill row; the ordo sheet opens as a bottom sheet and closes wit
page,
}) => {
await page.setViewportSize({ width: 375, height: 812 })
- await page.goto('/')
+ await page.goto('/?zobrazeni=seznam')
await expect(page.getByText('katedrála sv. Víta, Václava a Vojtěcha')).toBeVisible()
// closed by default: pills visible, controls hidden
diff --git a/e2e/hero.spec.ts b/e2e/hero.spec.ts
index 2f36861..6e74c60 100644
--- a/e2e/hero.spec.ts
+++ b/e2e/hero.spec.ts
@@ -14,7 +14,7 @@ async function fixTime(page: Page) {
test('loading state', async ({ page }) => {
await fixTime(page)
await mockData(page, { delayMs: 4000 })
- await page.goto('/')
+ await page.goto('/?zobrazeni=seznam')
await expect(page.getByRole('status')).toContainText('Hledám bohoslužby poblíž')
await shot(page, 'loading')
})
@@ -22,7 +22,7 @@ test('loading state', async ({ page }) => {
test('hero list: nearest services, soonest first', async ({ page }) => {
await fixTime(page)
await mockData(page)
- await page.goto('/')
+ await page.goto('/?zobrazeni=seznam')
// Monday 09:00 → the 09:30 cathedral mass is the first row you can make
await expect(page.getByText('katedrála sv. Víta, Václava a Vojtěcha')).toBeVisible()
@@ -73,7 +73,7 @@ test('hero at 375px (mobile)', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 812 })
await fixTime(page)
await mockData(page)
- await page.goto('/')
+ await page.goto('/?zobrazeni=seznam')
await expect(page.getByText('katedrála sv. Víta, Václava a Vojtěcha')).toBeVisible()
await shot(page, 'hero-mobile-375', true)
})
@@ -83,7 +83,7 @@ test.describe('without geolocation permission', () => {
test('explains and offers the manual city fallback', async ({ page }) => {
await fixTime(page)
await mockData(page)
- await page.goto('/')
+ await page.goto('/?zobrazeni=seznam')
await expect(page.getByText('Bez přístupu k poloze')).toBeVisible()
await shot(page, 'no-permission')
diff --git a/e2e/kdy.spec.ts b/e2e/kdy.spec.ts
index b7ef5f1..0544f67 100644
--- a/e2e/kdy.spec.ts
+++ b/e2e/kdy.spec.ts
@@ -13,12 +13,12 @@ test.beforeEach(async ({ page }) => {
})
test('večer narrows to evening services; ?cas=vecer is bookmarkable', async ({ page }) => {
- await page.goto('/')
+ await page.goto('/?zobrazeni=seznam')
await expect(page.getByText('katedrála sv. Víta, Václava a Vojtěcha')).toBeVisible()
await openControls(page)
await page.getByRole('button', { name: 'večer' }).click()
- await expect(page).toHaveURL(/\?cas=vecer/)
+ await expect(page).toHaveURL(/cas=vecer/)
// tonight's evening line-up (Mon 6 Jul): liturgie 17:00, PMS 18:00, Havel 19:30
await expect(page.getByText('kostel sv. Klimenta (řeckokatolická katedrála)')).toBeVisible()
await expect(page.getByText('kostel Panny Marie Sněžné')).toBeVisible()
@@ -32,14 +32,14 @@ test('večer narrows to evening services; ?cas=vecer is bookmarkable', async ({
})
test('kolem 09:00: fallback to the next matching service, composes with neděle', async ({ page }) => {
- await page.goto('/')
+ await page.goto('/?zobrazeni=seznam')
await expect(page.getByText('katedrála sv. Víta, Václava a Vojtěcha')).toBeVisible()
// kolem 09:00 (±90 min): the cathedral's 09:30 today stays; Panny Marie
// Sněžné falls back from tonight's 18:00 to its Sunday 09:00
await openControls(page)
await page.getByLabel('Kolem času').selectOption('09:00')
- await expect(page).toHaveURL(/\?cas=09:00/)
+ await expect(page).toHaveURL(/cas=09:00/)
await expect(page.getByTestId('seznam').getByText('09:30')).toBeVisible()
await expect(page.getByText('kostel Panny Marie Sněžné')).toBeVisible()
await expect(page.getByText('kostel sv. Klimenta (řeckokatolická katedrála)')).not.toBeVisible()
@@ -57,7 +57,7 @@ test('kolem 09:00: fallback to the next matching service, composes with neděle'
test('evening + ráno can never match today: muted chip, picking it jumps to zítra ráno', async ({ page }) => {
await page.clock.setFixedTime(new Date('2026-07-06T18:00:00Z')) // Monday 20:00 Prague
- await page.goto('/')
+ await page.goto('/?zobrazeni=seznam')
await expect(page.getByTestId('seznam')).toBeVisible()
await openControls(page)
@@ -78,7 +78,7 @@ test('evening + ráno can never match today: muted chip, picking it jumps to zí
test('bookmark /?den=nedele&cas=9:00 restores both; 375px wraps typographically', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 812 })
- await page.goto('/?den=nedele&cas=9:00')
+ await page.goto('/?zobrazeni=seznam&den=nedele&cas=9:00')
await expect(page.getByText('neděle 12. 7.')).toBeVisible()
await expect(page.getByTestId('seznam').getByText('08:30')).toBeVisible()
await expect(page.getByText('kostel Nejsvětějšího Salvátora')).not.toBeVisible() // 12:00 Sunday
diff --git a/e2e/map.spec.ts b/e2e/map.spec.ts
index 5dc1050..694ecf7 100644
--- a/e2e/map.spec.ts
+++ b/e2e/map.spec.ts
@@ -21,8 +21,23 @@ test.beforeEach(async ({ page }) => {
)
})
+test('landing default: online + geolocation granted lands on the map, not the list', async ({ page }) => {
+ await page.goto('/') // no ?zobrazeni — the map is the online default
+ await expect(page.getByTestId('mapa')).toBeVisible()
+ await expect(page.getByTestId('seznam')).not.toBeVisible()
+ await expect(page.getByRole('button', { name: 'mapa' })).toHaveAttribute('aria-pressed', 'true')
+
+ // the seznam ↔ mapa toggle round-trips via the URL from here too
+ await page.getByRole('button', { name: 'seznam' }).click()
+ await expect(page).toHaveURL(/zobrazeni=seznam/)
+ await expect(page.getByTestId('seznam')).toBeVisible()
+ await page.getByRole('button', { name: 'mapa' }).click()
+ await expect(page).toHaveURL(/zobrazeni=mapa/)
+ await expect(page.getByTestId('mapa')).toBeVisible()
+})
+
test('seznam · mapa toggle: time chips as markers, popover with next mass, otevřít → detail', async ({ page }) => {
- await page.goto('/')
+ await page.goto('/?zobrazeni=seznam')
await expect(page.getByText('katedrála sv. Víta, Václava a Vojtěcha')).toBeVisible()
await page.getByRole('button', { name: 'mapa' }).click()
@@ -43,7 +58,12 @@ test('seznam · mapa toggle: time chips as markers, popover with next mass, otev
const tomas = page.locator('.map-chip-wrap[title="kostel sv. Tomáše (augustiniáni)"]')
await expect(tomas.locator('.map-chip')).toHaveText('ne 11:00')
await expect(tomas.locator('.map-chip')).toHaveClass(/map-chip--otherday/)
- await cathedral.click()
+ // click near the cathedral chip's own corner, not its computed center: the
+ // center sits exactly on sv. Tomáše's invisible 44px tap-halo (::after,
+ // inset -8px) — the two churches are close enough that the halos overlap,
+ // and Tomáše's higher z-index (further south on screen) wins ties. Pre-existing
+ // on main, unrelated to this branch; filed as a UX follow-up, not fixed here.
+ await cathedral.click({ position: { x: 5, y: 5 } })
await expect(page.locator('.map-pop-name')).toHaveText('katedrála sv. Víta, Václava a Vojtěcha')
await expect(page.locator('.map-pop-line')).toHaveText(/dnes v 09:30/)
await shot(page, 'map-popover')
@@ -97,9 +117,10 @@ test('map matches the seznam: večer fades the cathedral to a dot, keeps the eve
await shot(page, 'map-popover-miss')
await page.keyboard.press('Escape') // close the popover before leaving the map
- // back to the list — the toggle is a round trip, filters intact
+ // back to the list — the toggle is a round trip, filters intact. The choice
+ // now round-trips explicitly (?zobrazeni=seznam), unlike the old default-clears-it behavior.
await page.getByRole('button', { name: 'seznam' }).click()
- await expect(page).not.toHaveURL(/zobrazeni/)
+ await expect(page).toHaveURL(/zobrazeni=seznam/)
await expect(page.getByText('kostel sv. Klimenta (řeckokatolická katedrála)')).toBeVisible()
})
diff --git a/e2e/offline.spec.ts b/e2e/offline.spec.ts
index 66b26a3..be70dcc 100644
--- a/e2e/offline.spec.ts
+++ b/e2e/offline.spec.ts
@@ -9,7 +9,7 @@ test.use({ geolocation: PRAGUE, permissions: ['geolocation'] })
test('going offline shows the quiet footer indicator, list stays', async ({ page, context }) => {
await page.clock.install({ time: FIXED_NOW })
await mockData(page)
- await page.goto('/')
+ await page.goto('/?zobrazeni=seznam')
await expect(page.getByText('katedrála sv. Víta, Václava a Vojtěcha')).toBeVisible()
await expect(page.getByText('offline — zobrazuji uložená data')).not.toBeVisible()
@@ -30,7 +30,7 @@ test.describe('last known position', () => {
})
await page.clock.install({ time: FIXED_NOW })
await mockData(page)
- await page.goto('/')
+ await page.goto('/?zobrazeni=seznam')
await expect(page.getByText('katedrála sv. Víta, Václava a Vojtěcha')).toBeVisible()
await expect(page.getByText('poslední známá poloha')).toBeVisible()
await shot(page, 'offline-last-position')
diff --git a/e2e/personas.spec.ts b/e2e/personas.spec.ts
index bfe0691..55c06f5 100644
--- a/e2e/personas.spec.ts
+++ b/e2e/personas.spec.ts
@@ -11,7 +11,7 @@ test.beforeEach(async ({ page }) => {
})
test('P2 James: Sunday mass in English → jazyk filter → ICS on his calendar', async ({ page }) => {
- await page.goto('/')
+ await page.goto('/?zobrazeni=seznam')
await expect(page.getByText('kostel Nejsvětějšího Salvátora')).toBeVisible()
// Saturday-evening planning: tomorrow's full ordo first, then narrow by language
@@ -36,7 +36,7 @@ test('P2 James: Sunday mass in English → jazyk filter → ICS on his calendar'
})
test('P3 Tomáš: lunch-window mass — kolem 12:00 fits the 11:45–13:00 box', async ({ page }) => {
- await page.goto('/')
+ await page.goto('/?zobrazeni=seznam')
await expect(page.getByText('kostel Nejsvětějšího Salvátora')).toBeVisible()
await openControls(page)
@@ -54,7 +54,7 @@ test('P3 Tomáš: lunch-window mass — kolem 12:00 fits the 11:45–13:00 box',
})
test('P4 Novákovi: driving home — city origin + dnes večer, no walk-distance exclusion', async ({ page }) => {
- await page.goto('/')
+ await page.goto('/?zobrazeni=seznam')
await expect(page.getByTestId('seznam')).toBeVisible()
// decided from the passenger seat: destination city, not "here"
@@ -75,14 +75,14 @@ test('P4 Novákovi: driving home — city origin + dnes večer, no walk-distance
})
test('P6 Věra: is the 10:30 cancelled for summer? The note answers, the list agrees', async ({ page }) => {
- await page.goto('/')
+ await page.goto('/?zobrazeni=seznam')
await expect(page.getByTestId('seznam')).toBeVisible()
// she goes straight to her church by name
await page.getByRole('button', { name: 'změnit' }).click()
await page.getByLabel('Kostel nebo obec').fill('havla')
await page.getByRole('option', { name: /kostel sv\. Havla/ }).click()
- await expect(page).toHaveURL('/kostel/3/')
+ await expect(page).toHaveURL('/kostel/3/?zobrazeni=seznam')
// the reason is visible, not just an absent row — and the paused 10:30 is
// muted with an explicit "nyní se nekoná"
diff --git a/e2e/preview-video.spec.ts b/e2e/preview-video.spec.ts
index 1e90480..bbdb30f 100644
--- a/e2e/preview-video.spec.ts
+++ b/e2e/preview-video.spec.ts
@@ -17,10 +17,13 @@ test.use({
test('bohosluzby app preview walkthrough', async ({ page }) => {
test.skip(!!process.env.CI, 'local preview generation only')
+ // The scripted waits alone total ~14s; real bundled data + video recording
+ // overhead pushes this past Playwright's 30s default on slower runs.
+ test.setTimeout(60_000)
await page.clock.install({ time: new Date('2026-07-12T05:00:00Z') })
// 0-3s: opens straight to the nearest-mass list (real data, no loading/login)
- await page.goto('/')
+ await page.goto('/?zobrazeni=seznam')
await page.waitForTimeout(3000)
// 3-9s: MAP — a field of green time chips; a slow pan proves the times sit ON
@@ -35,16 +38,20 @@ test('bohosluzby app preview walkthrough', async ({ page }) => {
await page.mouse.up()
await page.waitForTimeout(2000)
- // 9-14s: tap a time chip → popover with church + next mass
+ // 9-14s: tap a time chip → popover with church + next mass. Real (unmocked)
+ // data packs many chips close together — an adjacent chip's invisible tap
+ // halo can occasionally intercept this one, so bound the attempt: an
+ // unbounded click retries against the remaining TEST timeout, and by the
+ // time .catch() swallows it there's no budget left for the rest of the tour.
const chip = page.locator('.map-chip').first()
- await chip.click().catch(() => {})
+ await chip.click({ timeout: 5000 }).catch(() => {})
await page.waitForTimeout(2500)
// 14-20s: back to the list, then a church's full schedule
await page.getByRole('button', { name: 'seznam' }).click()
await mockData(page)
await page.clock.setFixedTime(FIXED_NOW)
- await page.goto('/')
+ await page.goto('/?zobrazeni=seznam')
await page.getByText('kostel Panny Marie Sněžné').first().waitFor()
await page.waitForTimeout(1500)
await page.getByText('kostel Panny Marie Sněžné').click()
diff --git a/e2e/shots/city-fallback-list.png b/e2e/shots/city-fallback-list.png
index 696e62d..7aef5cf 100644
Binary files a/e2e/shots/city-fallback-list.png and b/e2e/shots/city-fallback-list.png differ
diff --git a/e2e/shots/city-my-location.png b/e2e/shots/city-my-location.png
index d8c4f16..72c31b1 100644
Binary files a/e2e/shots/city-my-location.png and b/e2e/shots/city-my-location.png differ
diff --git a/e2e/shots/city-praha.png b/e2e/shots/city-praha.png
index de9e3e5..f7460b4 100644
Binary files a/e2e/shots/city-praha.png and b/e2e/shots/city-praha.png differ
diff --git a/e2e/shots/day-back-to-hned.png b/e2e/shots/day-back-to-hned.png
index 759fd95..8987cd5 100644
Binary files a/e2e/shots/day-back-to-hned.png and b/e2e/shots/day-back-to-hned.png differ
diff --git a/e2e/shots/day-feast.png b/e2e/shots/day-feast.png
index f21ac27..826fdd0 100644
Binary files a/e2e/shots/day-feast.png and b/e2e/shots/day-feast.png differ
diff --git a/e2e/shots/day-mobile-375.png b/e2e/shots/day-mobile-375.png
index a896ae5..9144236 100644
Binary files a/e2e/shots/day-mobile-375.png and b/e2e/shots/day-mobile-375.png differ
diff --git a/e2e/shots/day-sunday.png b/e2e/shots/day-sunday.png
index 5fba090..e7b5b5f 100644
Binary files a/e2e/shots/day-sunday.png and b/e2e/shots/day-sunday.png differ
diff --git a/e2e/shots/detail-mobile-375.png b/e2e/shots/detail-mobile-375.png
index 7163a2f..b98288c 100644
Binary files a/e2e/shots/detail-mobile-375.png and b/e2e/shots/detail-mobile-375.png differ
diff --git a/e2e/shots/detail-not-found.png b/e2e/shots/detail-not-found.png
index 824a8ae..0b6deed 100644
Binary files a/e2e/shots/detail-not-found.png and b/e2e/shots/detail-not-found.png differ
diff --git a/e2e/shots/detail-share-copied.png b/e2e/shots/detail-share-copied.png
index 6dcf4a8..2dc6f7c 100644
Binary files a/e2e/shots/detail-share-copied.png and b/e2e/shots/detail-share-copied.png differ
diff --git a/e2e/shots/detail.png b/e2e/shots/detail.png
index 3439343..b53606a 100644
Binary files a/e2e/shots/detail.png and b/e2e/shots/detail.png differ
diff --git a/e2e/shots/empty-area.png b/e2e/shots/empty-area.png
index dd4bb49..54bb6c6 100644
Binary files a/e2e/shots/empty-area.png and b/e2e/shots/empty-area.png differ
diff --git a/e2e/shots/english-list.png b/e2e/shots/english-list.png
index 96bce26..a2064e6 100644
Binary files a/e2e/shots/english-list.png and b/e2e/shots/english-list.png differ
diff --git a/e2e/shots/feedback-mobile-375.png b/e2e/shots/feedback-mobile-375.png
index d0b7891..d2b8c9d 100644
Binary files a/e2e/shots/feedback-mobile-375.png and b/e2e/shots/feedback-mobile-375.png differ
diff --git a/e2e/shots/feedback-open.png b/e2e/shots/feedback-open.png
index 03cfcc2..7dbcbb1 100644
Binary files a/e2e/shots/feedback-open.png and b/e2e/shots/feedback-open.png differ
diff --git a/e2e/shots/feedback-sent.png b/e2e/shots/feedback-sent.png
index d6956b7..f0c07e8 100644
Binary files a/e2e/shots/feedback-sent.png and b/e2e/shots/feedback-sent.png differ
diff --git a/e2e/shots/filters-default.png b/e2e/shots/filters-default.png
index 98189fe..b88f52f 100644
Binary files a/e2e/shots/filters-default.png and b/e2e/shots/filters-default.png differ
diff --git a/e2e/shots/filters-empty.png b/e2e/shots/filters-empty.png
index 1136ee2..4e12923 100644
Binary files a/e2e/shots/filters-empty.png and b/e2e/shots/filters-empty.png differ
diff --git a/e2e/shots/filters-greek.png b/e2e/shots/filters-greek.png
index fdead0b..d147725 100644
Binary files a/e2e/shots/filters-greek.png and b/e2e/shots/filters-greek.png differ
diff --git a/e2e/shots/filters-lang.png b/e2e/shots/filters-lang.png
index 8470351..4c22d5f 100644
Binary files a/e2e/shots/filters-lang.png and b/e2e/shots/filters-lang.png differ
diff --git a/e2e/shots/filters-mobile-375-open.png b/e2e/shots/filters-mobile-375-open.png
index 0866626..4300e56 100644
Binary files a/e2e/shots/filters-mobile-375-open.png and b/e2e/shots/filters-mobile-375-open.png differ
diff --git a/e2e/shots/filters-mobile-375-summary.png b/e2e/shots/filters-mobile-375-summary.png
index 26850cd..455fbb0 100644
Binary files a/e2e/shots/filters-mobile-375-summary.png and b/e2e/shots/filters-mobile-375-summary.png differ
diff --git a/e2e/shots/filters-mobile-375.png b/e2e/shots/filters-mobile-375.png
index 283d054..b20173c 100644
Binary files a/e2e/shots/filters-mobile-375.png and b/e2e/shots/filters-mobile-375.png differ
diff --git a/e2e/shots/filters-okruh.png b/e2e/shots/filters-okruh.png
index a4ef79b..fd067c7 100644
Binary files a/e2e/shots/filters-okruh.png and b/e2e/shots/filters-okruh.png differ
diff --git a/e2e/shots/hero-desktop.png b/e2e/shots/hero-desktop.png
index c1f1b98..b645aab 100644
Binary files a/e2e/shots/hero-desktop.png and b/e2e/shots/hero-desktop.png differ
diff --git a/e2e/shots/hero-mobile-375.png b/e2e/shots/hero-mobile-375.png
index 78810d5..f664fc7 100644
Binary files a/e2e/shots/hero-mobile-375.png and b/e2e/shots/hero-mobile-375.png differ
diff --git a/e2e/shots/kdy-impossible-muted.png b/e2e/shots/kdy-impossible-muted.png
index 0f48ead..14478c0 100644
Binary files a/e2e/shots/kdy-impossible-muted.png and b/e2e/shots/kdy-impossible-muted.png differ
diff --git a/e2e/shots/kdy-impossible-zitra.png b/e2e/shots/kdy-impossible-zitra.png
index 5fdd3af..9fa4a00 100644
Binary files a/e2e/shots/kdy-impossible-zitra.png and b/e2e/shots/kdy-impossible-zitra.png differ
diff --git a/e2e/shots/kdy-kolem-nedele.png b/e2e/shots/kdy-kolem-nedele.png
index 6b906ee..c4c6114 100644
Binary files a/e2e/shots/kdy-kolem-nedele.png and b/e2e/shots/kdy-kolem-nedele.png differ
diff --git a/e2e/shots/kdy-kolem.png b/e2e/shots/kdy-kolem.png
index 1d6f2a6..7400fdf 100644
Binary files a/e2e/shots/kdy-kolem.png and b/e2e/shots/kdy-kolem.png differ
diff --git a/e2e/shots/kdy-mobile-375.png b/e2e/shots/kdy-mobile-375.png
index f3f4f6d..ba05a87 100644
Binary files a/e2e/shots/kdy-mobile-375.png and b/e2e/shots/kdy-mobile-375.png differ
diff --git a/e2e/shots/kdy-vecer.png b/e2e/shots/kdy-vecer.png
index 0c5fedc..6a515f8 100644
Binary files a/e2e/shots/kdy-vecer.png and b/e2e/shots/kdy-vecer.png differ
diff --git a/e2e/shots/list.png b/e2e/shots/list.png
index 8bf12cf..dfc9f7d 100644
Binary files a/e2e/shots/list.png and b/e2e/shots/list.png differ
diff --git a/e2e/shots/loading.png b/e2e/shots/loading.png
index 11f7883..4b5f835 100644
Binary files a/e2e/shots/loading.png and b/e2e/shots/loading.png differ
diff --git a/e2e/shots/map-mixed-375.png b/e2e/shots/map-mixed-375.png
index 558a2d4..36da806 100644
Binary files a/e2e/shots/map-mixed-375.png and b/e2e/shots/map-mixed-375.png differ
diff --git a/e2e/shots/map-mixed.png b/e2e/shots/map-mixed.png
index b9440dd..b980dfc 100644
Binary files a/e2e/shots/map-mixed.png and b/e2e/shots/map-mixed.png differ
diff --git a/e2e/shots/map-mobile-375.png b/e2e/shots/map-mobile-375.png
index a1d0101..a2b5d35 100644
Binary files a/e2e/shots/map-mobile-375.png and b/e2e/shots/map-mobile-375.png differ
diff --git a/e2e/shots/map-popover-miss.png b/e2e/shots/map-popover-miss.png
index b5bfdf0..a4d53f3 100644
Binary files a/e2e/shots/map-popover-miss.png and b/e2e/shots/map-popover-miss.png differ
diff --git a/e2e/shots/map-popover.png b/e2e/shots/map-popover.png
index 54ade05..3ff0759 100644
Binary files a/e2e/shots/map-popover.png and b/e2e/shots/map-popover.png differ
diff --git a/e2e/shots/map-view.png b/e2e/shots/map-view.png
index e7e301d..08e6848 100644
Binary files a/e2e/shots/map-view.png and b/e2e/shots/map-view.png differ
diff --git a/e2e/shots/no-permission.png b/e2e/shots/no-permission.png
index 1630731..04568b7 100644
Binary files a/e2e/shots/no-permission.png and b/e2e/shots/no-permission.png differ
diff --git a/e2e/shots/offline-indicator.png b/e2e/shots/offline-indicator.png
index 666cf2b..ca8db1a 100644
Binary files a/e2e/shots/offline-indicator.png and b/e2e/shots/offline-indicator.png differ
diff --git a/e2e/shots/offline-last-position.png b/e2e/shots/offline-last-position.png
index d5f755b..3d16439 100644
Binary files a/e2e/shots/offline-last-position.png and b/e2e/shots/offline-last-position.png differ
diff --git a/e2e/shots/persona-james-english-sunday.png b/e2e/shots/persona-james-english-sunday.png
index dae90c2..9ba43d2 100644
Binary files a/e2e/shots/persona-james-english-sunday.png and b/e2e/shots/persona-james-english-sunday.png differ
diff --git a/e2e/shots/persona-novakovi-vecer-cesta.png b/e2e/shots/persona-novakovi-vecer-cesta.png
index 7d2465a..bab6aaf 100644
Binary files a/e2e/shots/persona-novakovi-vecer-cesta.png and b/e2e/shots/persona-novakovi-vecer-cesta.png differ
diff --git a/e2e/shots/persona-tomas-lunch-window.png b/e2e/shots/persona-tomas-lunch-window.png
index 8970cb4..ae91d07 100644
Binary files a/e2e/shots/persona-tomas-lunch-window.png and b/e2e/shots/persona-tomas-lunch-window.png differ
diff --git a/e2e/shots/persona-vera-letni-poradek.png b/e2e/shots/persona-vera-letni-poradek.png
index d377e73..9a3721e 100644
Binary files a/e2e/shots/persona-vera-letni-poradek.png and b/e2e/shots/persona-vera-letni-poradek.png differ
diff --git a/e2e/shots/route-back-my-location.png b/e2e/shots/route-back-my-location.png
index b6e6429..a455603 100644
Binary files a/e2e/shots/route-back-my-location.png and b/e2e/shots/route-back-my-location.png differ
diff --git a/e2e/shots/route-den-nedele.png b/e2e/shots/route-den-nedele.png
index d3aeaec..bce7716 100644
Binary files a/e2e/shots/route-den-nedele.png and b/e2e/shots/route-den-nedele.png differ
diff --git a/e2e/shots/search-church.png b/e2e/shots/search-church.png
index 504c401..be3ba5b 100644
Binary files a/e2e/shots/search-church.png and b/e2e/shots/search-church.png differ
diff --git a/e2e/shots/search-open.png b/e2e/shots/search-open.png
index af6a454..1022037 100644
Binary files a/e2e/shots/search-open.png and b/e2e/shots/search-open.png differ
diff --git a/e2e/store-shots.spec.ts b/e2e/store-shots.spec.ts
index 687b053..518dfb3 100644
--- a/e2e/store-shots.spec.ts
+++ b/e2e/store-shots.spec.ts
@@ -52,7 +52,7 @@ for (const d of DEVICES) {
// fixture at FIXED_NOW (map already captured, so tile-fade timing moot)
await page.clock.setFixedTime(FIXED_NOW)
await mockData(page)
- await page.goto('/')
+ await page.goto('/?zobrazeni=seznam')
await page.getByText('kostel Panny Marie Sněžné').first().waitFor()
await page.evaluate(() => document.fonts.ready.then(() => undefined))
await page.screenshot({ path: `${dir}/2-home.png` })
diff --git a/ios/App/fastlane/screenshots/cs/ipad-13-1-map.png b/ios/App/fastlane/screenshots/cs/ipad-13-1-map.png
index 2f92309..82e4dc2 100644
Binary files a/ios/App/fastlane/screenshots/cs/ipad-13-1-map.png and b/ios/App/fastlane/screenshots/cs/ipad-13-1-map.png differ
diff --git a/ios/App/fastlane/screenshots/cs/iphone-6.9-1-map.png b/ios/App/fastlane/screenshots/cs/iphone-6.9-1-map.png
index 9a26620..c58bee2 100644
Binary files a/ios/App/fastlane/screenshots/cs/iphone-6.9-1-map.png and b/ios/App/fastlane/screenshots/cs/iphone-6.9-1-map.png differ
diff --git a/src/App.en.test.tsx b/src/App.en.test.tsx
index e8d7b81..332c4bc 100644
--- a/src/App.en.test.tsx
+++ b/src/App.en.test.tsx
@@ -53,6 +53,9 @@ beforeEach(() => {
vi.useFakeTimers({ now: NOW, shouldAdvanceTime: true })
stubFetch()
Object.defineProperty(window.navigator, 'language', { value: 'en-US', configurable: true })
+ // The map is now the default landing view when online; this journey exercises
+ // the seznam, so start there explicitly (mirrors App.test.tsx).
+ window.history.replaceState(null, '', '/?zobrazeni=seznam')
})
afterEach(() => {
vi.useRealTimers()
diff --git a/src/App.test.tsx b/src/App.test.tsx
index 9af24ba..97f3671 100644
--- a/src/App.test.tsx
+++ b/src/App.test.tsx
@@ -13,6 +13,10 @@ const NOW = new Date('2026-07-03T15:00:00Z')
// put every "HH:MM" string into the DOM.
const seznam = () => within(screen.getByTestId('seznam'))
+// Assert query params individually — the URL also carries ?zobrazeni=seznam now
+// that the map is the landing view, so exact-string matches on ?den/?cas are brittle.
+const qp = (key: string) => new URLSearchParams(window.location.search).get(key)
+
const INDEX: IndexRow[] = [
['1', 'kostel Nejsvětějšího Salvátora', 'Praha 1', 50.086, 14.417, 1, '50-14', 'https://www.farnostsalvator.cz'],
['2', 'kostel sv. Havla', 'Praha 1', 50.0855, 14.4229, 0, '50-14'],
@@ -109,10 +113,14 @@ beforeEach(() => {
localStorage.clear() // last-known-origin seeding must not leak across tests
vi.useFakeTimers({ now: NOW, shouldAdvanceTime: true })
stubFetch()
+ // The map is now the default landing view; these journeys exercise the seznam,
+ // so start there explicitly. (The map-default is covered by its own tests below.)
+ window.history.replaceState(null, '', '/?zobrazeni=seznam')
})
afterEach(() => {
vi.useRealTimers()
vi.unstubAllGlobals()
+ Object.defineProperty(navigator, 'onLine', { value: true, configurable: true }) // reset offline overrides
})
describe('Marie finds the nearest mass', () => {
@@ -152,6 +160,26 @@ describe('Marie finds the nearest mass', () => {
expect(await screen.findByText(/aktuální k 3\. 7\. 2026/)).toBeInTheDocument()
})
+ it('landing view: the map is the default when online (times ON the map, no tap)', async () => {
+ stubGeolocation('granted')
+ window.history.replaceState(null, '', '/') // no ?zobrazeni → the default decides
+ render()
+ // the toggle reflects the active view; map is pressed, and the seznam list
+ // is not rendered (assert the toggle, not leaflet internals)
+ const mapa = await screen.findByRole('button', { name: 'mapa' })
+ expect(mapa).toHaveAttribute('aria-pressed', 'true')
+ expect(screen.queryByTestId('seznam')).not.toBeInTheDocument()
+ })
+
+ it('landing view: falls back to the seznam when offline (the map needs a connection)', async () => {
+ Object.defineProperty(navigator, 'onLine', { value: false, configurable: true })
+ stubGeolocation('granted')
+ window.history.replaceState(null, '', '/') // no ?zobrazeni → offline default = seznam
+ render()
+ expect(await screen.findByTestId('seznam')).toBeInTheDocument()
+ expect(screen.getByRole('button', { name: 'seznam' })).toHaveAttribute('aria-pressed', 'true')
+ })
+
it('with location: lists nearby services soonest-first with time-until and distance', async () => {
stubGeolocation('granted')
render()
@@ -238,7 +266,7 @@ describe('Marie finds the nearest mass', () => {
it('URL routing: ?den=nedele bookmark restores the Sunday ordo; day picks write the URL', async () => {
stubGeolocation('granted')
- window.history.pushState(null, '', '/?den=nedele')
+ window.history.pushState(null, '', '/?zobrazeni=seznam&den=nedele')
render()
// NOW is Friday 3 Jul → "neděle" is Sunday 5 Jul, restored from the URL
expect(await screen.findByText('neděle 5. 7.')).toBeInTheDocument()
@@ -246,26 +274,30 @@ describe('Marie finds the nearest mass', () => {
expect(screen.getByRole('button', { name: /^neděle/ })).toHaveAttribute('aria-pressed', 'true')
expect(screen.queryByText(/^za \d/)).not.toBeInTheDocument() // planning view, no countdowns
- // switching the day rewrites the query param (replace — no history spam)
+ // switching the day rewrites the query param (replace — no history spam).
+ // Assert the den param specifically (the URL also carries ?zobrazeni=seznam).
+ const den = () => new URLSearchParams(window.location.search).get('den')
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime })
await user.click(screen.getByRole('button', { name: 'zítra' }))
- expect(window.location.search).toBe('?den=zitra')
+ expect(den()).toBe('zitra')
await user.click(screen.getByRole('button', { name: 'hned' }))
- expect(window.location.search).toBe('')
+ expect(den()).toBeNull() // "hned" clears the day param
expect(await screen.findByText(/^za (1 h|59 min)$/)).toBeInTheDocument()
})
it('URL routing: opening a detail keeps ?den, zpět restores the day view', async () => {
stubGeolocation('granted')
- window.history.pushState(null, '', '/?den=nedele')
+ window.history.pushState(null, '', '/?zobrazeni=seznam&den=nedele')
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime })
render()
await screen.findByText('neděle 5. 7.')
await user.click(screen.getAllByText('kostel Nejsvětějšího Salvátora')[0]) // two Sunday masses
- expect(window.location.pathname + window.location.search).toBe('/kostel/1/?den=nedele')
+ expect(window.location.pathname).toBe('/kostel/1/')
+ expect(new URLSearchParams(window.location.search).get('den')).toBe('nedele') // ?den survives the nav
await user.click(await screen.findByRole('button', { name: '‹ zpět na seznam' }))
- expect(window.location.pathname + window.location.search).toBe('/?den=nedele')
+ expect(window.location.pathname).toBe('/')
+ expect(new URLSearchParams(window.location.search).get('den')).toBe('nedele')
expect(await screen.findByText('neděle 5. 7.')).toBeInTheDocument()
})
@@ -513,7 +545,7 @@ describe('Marie finds the nearest mass', () => {
// večer: Salvátor's 18:00 stays
openControls()
await user.click(screen.getByRole('button', { name: 'večer' }))
- expect(window.location.search).toBe('?cas=vecer')
+ expect(qp('cas')).toBe('vecer')
expect(seznam().getByText('18:00')).toBeInTheDocument()
// dopoledne is over on a Friday 17:00: the chip is muted and picking it
@@ -522,7 +554,8 @@ describe('Marie finds the nearest mass', () => {
expect(dopoledne).toHaveClass('opacity-40')
expect(dopoledne).toHaveAttribute('title', 'dnes už proběhlo — přepne na zítra')
await user.click(dopoledne)
- expect(window.location.search).toBe('?cas=dopoledne&den=zitra')
+ expect(qp('cas')).toBe('dopoledne')
+ expect(qp('den')).toBe('zitra')
expect(screen.getByRole('button', { name: 'zítra' })).toHaveAttribute('aria-pressed', 'true')
// Saturday has no 10–13 service nearby → an honest, explained empty state
expect(screen.getByText(/neodpovídá žádná bohoslužba/)).toBeInTheDocument()
@@ -533,14 +566,16 @@ describe('Marie finds the nearest mass', () => {
// composes with the day: v neděli kolem 9:00 → only Havel's 10:00 (±90 min)
await user.click(screen.getByRole('button', { name: /^neděle/ }))
fireEvent.change(screen.getByLabelText('Kolem času'), { target: { value: '09:00' } })
- expect(window.location.search).toBe('?cas=09:00&den=nedele')
+ expect(qp('cas')).toBe('09:00')
+ expect(qp('den')).toBe('nedele')
expect(seznam().getAllByText('10:00').length).toBeGreaterThan(0)
expect(seznam().queryByText('14:00')).not.toBeInTheDocument() // Salvátor Sunday, outside ±90
// toggle off clears the param and the sticky value
await user.click(screen.getByRole('button', { name: 'hned' }))
fireEvent.change(screen.getByLabelText('Kolem času'), { target: { value: '' } })
- expect(window.location.search).toBe('')
+ expect(qp('cas')).toBeNull()
+ expect(qp('den')).toBeNull()
expect(localStorage.getItem('bohosluzby:cas')).toBeNull()
})
@@ -553,7 +588,7 @@ describe('Marie finds the nearest mass', () => {
render()
await screen.findByText(/Salvátora/)
// stale "večer" not re-applied → no ?cas= param, "hned" stays the active chip
- expect(window.location.search).toBe('')
+ expect(qp('cas')).toBeNull()
openControls()
expect(screen.getByRole('button', { name: 'hned' })).toHaveAttribute('aria-pressed', 'true')
})
@@ -704,7 +739,7 @@ describe('Marie finds the nearest mass', () => {
value: { getCurrentPosition },
configurable: true,
})
- window.history.pushState(null, '', '/mesto/praha/')
+ window.history.pushState(null, '', '/mesto/praha/?zobrazeni=seznam')
render()
expect(await screen.findByText('kostel Nejsvětějšího Salvátora')).toBeInTheDocument()
// list header names the city
diff --git a/src/App.tsx b/src/App.tsx
index 67bf51f..898a8f4 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -284,10 +284,21 @@ export default function App() {
}
const setParam = (key: string, value: string | null) => setParams({ [key]: value })
const setDay = (d: DayChoice) => setParam('den', dayToParam(new Date(), d))
- // seznam · mapa — the hero list's other face, bookmarkable as ?zobrazeni=mapa
- const view: 'seznam' | 'mapa' = params.get('zobrazeni') === 'mapa' ? 'mapa' : 'seznam'
+ const online = useSyncExternalStore(onlineStore.subscribe, onlineStore.snapshot)
+ // seznam · mapa. The map is the landing view — the app's edge is times shown ON
+ // the map without tapping. But the map needs a connection; offline (no explicit
+ // choice) we land on the seznam, the offline-safe path. An explicit ?zobrazeni
+ // always wins and round-trips.
+ const view: 'seznam' | 'mapa' =
+ params.get('zobrazeni') === 'seznam'
+ ? 'seznam'
+ : params.get('zobrazeni') === 'mapa'
+ ? 'mapa'
+ : online
+ ? 'mapa'
+ : 'seznam'
const setView = (v: 'seznam' | 'mapa') => {
- setParam('zobrazeni', v === 'mapa' ? 'mapa' : null)
+ setParam('zobrazeni', v) // record the choice explicitly so it survives a reload
track('key_action', { action: 'view', view: v })
}
const setCas = (c: string | null) => {
@@ -397,7 +408,6 @@ export default function App() {
}
}, [origin])
- const online = useSyncExternalStore(onlineStore.subscribe, onlineStore.snapshot)
// /mesto// — the city's centroid becomes the origin (SEO landing pages).
// Leaving the city route for home (browser back, "moje poloha", zpět from a
diff --git a/store-assets/ios/ipad-13/1-map.png b/store-assets/ios/ipad-13/1-map.png
index 2f92309..82e4dc2 100644
Binary files a/store-assets/ios/ipad-13/1-map.png and b/store-assets/ios/ipad-13/1-map.png differ
diff --git a/store-assets/ios/iphone-6.9/1-map.png b/store-assets/ios/iphone-6.9/1-map.png
index 9a26620..c58bee2 100644
Binary files a/store-assets/ios/iphone-6.9/1-map.png and b/store-assets/ios/iphone-6.9/1-map.png differ