Terminplaner: Portal-Ansicht – Nachbesserungen (2/2)
- Annehmen mit Rueckfrage in der Seite (schliesst den Vertrag, schickt G02) - Offene Anfragen: alle bis Ende des Buchungszeitraums (Fenster zu je 62 Tagen, hoechstens 6 Aufrufe), passend zur Zahl auf Kachel und Banner - Meldung ins Bild geholt, Fehler mit Fokus; einzige Live-Region - Netzwerkfehler auf Deutsch; Antwort einer frueheren Website erscheint nie unter der neuen (Zaehler je Aufruf); Datumssprung nur mit echtem Tag - Ende um Mitternacht als 24:00, Statusfilter nach Eintragen auf Alle, abgeschaltet ohne Einstellung mit Export-Link, dunkles Datumsfeld - Playwright: Zeitzone New York/Berlin, ferne Anfragen, Projektwechsel, Meldung im Bild, Datumssprung, abgeschaltet ohne Einstellung Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9c07f6a8c5
commit
650919b022
10 files changed
+410
-73
No files matched your search
@@ -49,14 +49,14 @@ async function shot(page, name) {
|
||||
}
|
||||
const knopf = (loc, name) => loc.getByRole('button', { name }) // Teiltreffer: Knoepfe in Zeilen tragen den Kontext im aria-label
|
||||
const reihe = (page, text) => page.locator('li.tm-eintrag', { hasText: text })
|
||||
const meldung = (page) => page.locator('.tm-meldung:not([hidden])')
|
||||
const meldung = (page) => page.locator('.tm-meldung:not(:empty)')
|
||||
const pillen = (page) => page.locator('.tm-pille').allInnerTexts()
|
||||
const textVon = (loc) => loc.innerText()
|
||||
/** Kein Streifen rechts: die Seite passt in die Breite. */
|
||||
const passtInBreite = (page) => page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
|
||||
|
||||
// Berliner Wanduhr im Oktober 2026 (MESZ, bis 25.10.) als ISO-Zeit
|
||||
const utc = (tag, zeit) => new Date(`${tag}T${zeit}:00+02:00`).toISOString()
|
||||
// Berliner Wanduhr als ISO-Zeit: Sommerzeit (+02:00) bis 25.10.2026 und ab 28.03.2027, dazwischen +01:00
|
||||
const utc = (tag, zeit) => new Date(`${tag}T${zeit}:00${tag >= '2026-10-25' && tag < '2027-03-28' ? '+01:00' : '+02:00'}`).toISOString()
|
||||
|
||||
/** Die uebrigen Portal-Schnittstellen, die dashboard.html beim Laden braucht (wie test/ui/funktionen.ui.mjs). */
|
||||
function portalHilfe(p, json) {
|
||||
@@ -128,7 +128,13 @@ async function fixtureAntwort(fx, route, req, url, json) {
|
||||
const q = url.searchParams
|
||||
const m = req.method()
|
||||
if (p === '/api/funktionen') return json({ projects: fx.projekte, termine: { verfuegbar: fx.verfuegbar } })
|
||||
if (p === '/api/termine/einstellungen' && m === 'GET') return json(fx.daten())
|
||||
if (p === '/api/termine/einstellungen' && m === 'GET') {
|
||||
const projekt = q.get('projectId')
|
||||
if (fx.verzoegerung?.[projekt]) await new Promise((fertig) => { setTimeout(fertig, fx.verzoegerung[projekt]) })
|
||||
const daten = fx.daten()
|
||||
if (fx.ohneEinstellungFuer?.includes(projekt)) Object.assign(daten, { einstellung: null, version: 0 })
|
||||
return json(daten)
|
||||
}
|
||||
if (p === '/api/termine/einstellungen/vorlage' && m === 'POST') {
|
||||
fx.aufrufe.push(['vorlage', req.postDataJSON()])
|
||||
fx.einstellung = srv.einstellung()
|
||||
@@ -146,6 +152,7 @@ async function fixtureAntwort(fx, route, req, url, json) {
|
||||
const body = req.postDataJSON()
|
||||
fx.aufrufe.push(['aktion', aktion[1], body])
|
||||
const b = fx.buchungen.find((x) => x.id === aktion[1])
|
||||
if (b.gastName === 'Konflikt Test') return json({ error: 'Dieser Termin ist nicht mehr offen.', code: 'nicht_moeglich', fehler: { _: 'Dieser Termin ist nicht mehr offen.' } }, 409)
|
||||
if (body.aktion === 'annehmen') { b.status = 'bestaetigt'; b.angenommenAt = JETZT.toISOString() }
|
||||
if (body.aktion === 'absagen') Object.assign(b, { status: 'abgesagt', abgesagtVon: 'betrieb', absageGrund: body.grund })
|
||||
return json({ status: b.status })
|
||||
@@ -163,11 +170,11 @@ const hosts = new Set()
|
||||
* Eine Pruefung in eigenem Browser-Kontext. optionen: viewport/context (Playwright), fixture (Objekt aus neueFixture() ⇒ alle
|
||||
* Schnittstellen aus der Fixture; sonst die echten Router mit Portal-Cookie), reset (Optionen fuer srv.zuruecksetzen).
|
||||
*/
|
||||
async function check(name, fn, { context = {}, fixture = null, reset = {} } = {}) {
|
||||
async function check(name, fn, { context = {}, fixture = null, reset = {}, jetzt = JETZT } = {}) {
|
||||
if (process.env.NUR && !name.includes(process.env.NUR)) return
|
||||
await srv.zuruecksetzen(reset)
|
||||
const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 }, ...context })
|
||||
await ctx.clock.setFixedTime(JETZT)
|
||||
await ctx.clock.setFixedTime(jetzt)
|
||||
if (!fixture) await ctx.addCookies([await srv.portalCookie()])
|
||||
const page = await ctx.newPage()
|
||||
page.fx = fixture
|
||||
@@ -236,6 +243,7 @@ async function buche(o = {}) {
|
||||
}
|
||||
|
||||
const gleichZeit = (a, b) => new Date(a).getTime() === new Date(b).getTime() // der Nachbau speichert Datumswerte als +00:00
|
||||
const tagNach = (tag) => new Date(Date.parse(`${tag}T00:00:00Z`) + 86400000).toISOString().slice(0, 10)
|
||||
const mailsAn = (adresse) => srv.mails().filter((m) => m.to.some((t) => t.address === adresse))
|
||||
const buchungMit = (id) => srv.buchungen().find((b) => b.$id === id)
|
||||
|
||||
@@ -488,7 +496,7 @@ async function pruefungen() {
|
||||
await knopf(banner, 'Anfragen ansehen').click()
|
||||
await page.waitForFunction(() => document.querySelectorAll('li.tm-eintrag').length === 1) // die Liste der Tagesansicht steht noch bis die neue da ist
|
||||
eq(await textVon(page.locator('.tm-zeitraum')), 'Offene Anfragen', 'Titel Anfragen')
|
||||
eq(fx.aufrufe.at(-1)[1], { projectId: 'p1', von: '2026-10-05', bis: '2026-12-06', status: 'angefragt' }, 'Abfrage: ab heute, 62 Tage, nur Anfragen')
|
||||
eq(fx.aufrufe.at(-1)[1], { projectId: 'p1', von: '2026-10-05', bis: '2026-12-04', status: 'angefragt' }, 'Abfrage: ab heute bis Ende des Buchungszeitraums (60 Tage), nur Anfragen')
|
||||
eq(await page.locator('li.tm-eintrag').count(), 1, 'nur die Anfrage')
|
||||
const zeile = reihe(page, 'Lea Braun')
|
||||
// Details zeigen Kontakt und Nachricht als Text
|
||||
@@ -496,7 +504,20 @@ async function pruefungen() {
|
||||
const details = await textVon(zeile.locator('dl'))
|
||||
assert(details.includes('max@example.com') && details.includes('Hochzeit am 24.10., bitte mit Probetermin.') && details.includes('90 Minuten'), `Details: ${details}`)
|
||||
await shot(page, 'anfrage-offen')
|
||||
await knopf(zeile, 'Annehmen').click()
|
||||
// Annehmen schliesst den Vertrag und schickt die Bestaetigung: erst eine Rueckfrage in der Seite
|
||||
await knopf(zeile, 'Annehmen: Brautfrisur').click()
|
||||
const frage = zeile.locator('.tm-frage-box')
|
||||
await frage.waitFor()
|
||||
const ft = await textVon(frage)
|
||||
assert(ft.includes('Anfrage annehmen?') && ft.includes('Lea Braun bekommt eine Bestätigung per E-Mail.'), `Rückfrage: ${ft}`)
|
||||
eq(await page.evaluate(() => document.activeElement === document.querySelector('.tm-frage-box')), true, 'Fokus auf der Rückfrage, Enter bestätigt nichts')
|
||||
eq(fx.aufrufe.filter((a) => a[0] === 'aktion').length, 0, 'vor der Rückfrage nichts gesendet')
|
||||
await shot(page, 'anfrage-annehmen-frage')
|
||||
await knopf(frage, 'Abbrechen').click()
|
||||
eq(await zeile.locator('.tm-frage-box').count(), 0, 'Abbrechen schließt die Rückfrage')
|
||||
eq(fx.aufrufe.filter((a) => a[0] === 'aktion').length, 0, 'nach Abbrechen nichts gesendet')
|
||||
await knopf(zeile, 'Annehmen: Brautfrisur').click()
|
||||
await zeile.locator('.tm-frage-box').getByRole('button', { name: 'Annehmen', exact: true }).click()
|
||||
await meldung(page).waitFor()
|
||||
assert((await textVon(meldung(page))).startsWith('Anfrage angenommen.'), 'Meldung nach dem Annehmen')
|
||||
eq(fx.aufrufe.filter((a) => a[0] === 'aktion'), [['aktion', 'banfrage', { projectId: 'p1', aktion: 'annehmen' }]], 'POST annehmen')
|
||||
@@ -594,6 +615,151 @@ async function pruefungen() {
|
||||
eq(await page.evaluate(() => document.body.dataset.kbView), 'funktionen', 'zurueck in Funktionen')
|
||||
}, { fixture: Object.assign(neueFixture(), { buchungen: alleStatus() }), context: { viewport: { width: 390, height: 844 }, deviceScaleFactor: 2, isMobile: true, hasTouch: true } })
|
||||
|
||||
// ------------------------------------------------------------ Offene Anfragen: alle, auch weit in der Zukunft
|
||||
await check('fx_anfragen_ueber_mehrere_fenster', async (page) => {
|
||||
const fx = page.fx
|
||||
await oeffne(page, '#termine')
|
||||
await warteAnsicht(page)
|
||||
const banner = page.locator('.tm-box-info')
|
||||
await banner.getByText('3 Anfragen warten').waitFor()
|
||||
await knopf(banner, 'Anfragen ansehen').click()
|
||||
await page.waitForFunction(() => document.querySelectorAll('li.tm-eintrag').length === 3)
|
||||
// Zahl und Liste stimmen ueberein; die ferne Anfrage (107 Tage) steht dabei
|
||||
eq(await page.locator('li.tm-eintrag').evaluateAll((l) => l.map((e) => e.dataset.id)), ['banfrage', 'bfern1', 'bfern2'], 'alle drei Anfragen in Zeitfolge')
|
||||
assert((await textVon(reihe(page, 'Fern Eins'))).includes('Angefragt'), 'ferne Anfrage mit Status')
|
||||
// Abfragen: lueckenlose Fenster ab heute bis heute + Buchungszeitraum (180 Tage), jedes hoechstens 62 Tage nach seinem Beginn, nur Anfragen
|
||||
const tageZwischen = (a, b) => Math.round((Date.parse(`${b}T00:00:00Z`) - Date.parse(`${a}T00:00:00Z`)) / 86400000)
|
||||
const fenster = fx.aufrufe.filter((a) => a[0] === 'liste' && a[1].status === 'angefragt').map((a) => a[1]).sort((a, b) => (a.von < b.von ? -1 : 1))
|
||||
eq(fenster.length, 3, 'drei Aufrufe')
|
||||
eq(fenster[0].von, '2026-10-05', 'beginnt heute')
|
||||
eq(fenster.at(-1).bis, '2027-04-03', 'endet nach 180 Tagen')
|
||||
fenster.forEach((f, i) => {
|
||||
assert(tageZwischen(f.von, f.bis) <= 62, `Fenster ${i} zu lang: ${f.von}..${f.bis}`)
|
||||
if (i > 0) eq(tageZwischen(fenster[i - 1].bis, f.von), 1, `Fenster ${i} schließt lückenlos an`)
|
||||
})
|
||||
await shot(page, 'anfragen-fern')
|
||||
}, {
|
||||
fixture: (() => {
|
||||
const fx = neueFixture()
|
||||
fx.einstellung.regeln.horizontTage = 180
|
||||
fx.buchungen = [
|
||||
buchung('banfrage', '2026-10-05', '15:00', 90, { status: 'angefragt', leistung: 'Brautfrisur', gastName: 'Lea Braun' }),
|
||||
buchung('bfern1', '2027-01-20', '09:00', 90, { status: 'angefragt', leistung: 'Brautfrisur', gastName: 'Fern Eins' }),
|
||||
buchung('bfern2', '2027-03-10', '14:00', 90, { status: 'angefragt', leistung: 'Brautfrisur', gastName: 'Fern Zwei' }),
|
||||
]
|
||||
return fx
|
||||
})(),
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------ Website wechseln, Antwort der alten kommt spaet
|
||||
await check('fx_projektwechsel_antwort_kommt_spaet', async (page) => {
|
||||
await oeffne(page, '#termine')
|
||||
await page.locator('#termine-project').waitFor()
|
||||
// p1 (ohne Einstellung ⇒ Vorlagenauswahl) antwortet erst nach 1,5 s; in der Zwischenzeit zu p2 wechseln, das sofort antwortet
|
||||
await page.locator('#termine-project').selectOption('p2')
|
||||
await page.locator('.tm-reiterleiste').waitFor()
|
||||
await page.waitForTimeout(2000)
|
||||
eq(await page.locator('.tm-vorlagen').count(), 0, 'die späte Antwort von p1 erscheint nicht')
|
||||
eq(await page.locator('.tm-reiterleiste').count(), 1, 'p2 bleibt stehen')
|
||||
eq(await page.locator('#termine-project').inputValue(), 'p2', 'Auswahl p2')
|
||||
eq(await page.getByRole('tab', { selected: true }).innerText(), 'Termine', 'Reiter von p2')
|
||||
}, {
|
||||
fixture: (() => {
|
||||
const fx = neueFixture()
|
||||
fx.projekte = [
|
||||
{ projectId: 'p1', projectName: 'Langsame Website', liveUrl: '', features: { news: { state: 'off' }, termine: { state: 'active' } } },
|
||||
{ projectId: 'p2', projectName: 'Schnelle Website', liveUrl: '', features: { news: { state: 'off' }, termine: { state: 'active' } } },
|
||||
]
|
||||
fx.ohneEinstellungFuer = ['p1']
|
||||
fx.verzoegerung = { p1: 1500 }
|
||||
return fx
|
||||
})(),
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------ Abgeschaltet und nie eingerichtet
|
||||
await check('fx_abgeschaltet_ohne_einstellung', async (page) => {
|
||||
await oeffne(page, '#termine')
|
||||
await warteAnsicht(page)
|
||||
const hinweis = page.locator('.tm-box-warn')
|
||||
await hinweis.waitFor()
|
||||
assert((await textVon(hinweis)).includes('abgeschaltet'), 'Hinweis abgeschaltet')
|
||||
eq(await hinweis.locator('a').getAttribute('href'), '/api/termine/export.csv?projectId=p1', 'Export-Link auch ohne Einstellung')
|
||||
eq(await hinweis.locator('a').innerText(), 'exportieren', 'Linktext')
|
||||
eq(await page.locator('.tm-vorlagen').count(), 0, 'keine Vorlagenauswahl')
|
||||
eq(await page.locator('[data-werkzeug]').count(), 0, 'keine Schreib-Knöpfe')
|
||||
assert((await textVon(page.locator('.tm-info'))).includes('Keine Termine eingerichtet'), 'Info-Karte')
|
||||
}, {
|
||||
fixture: (() => {
|
||||
const fx = neueFixture()
|
||||
fx.projekte[0].features.termine = { state: 'disabled' }
|
||||
fx.einstellung = null
|
||||
return fx
|
||||
})(),
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------ Datumssprung mit Unsinn
|
||||
await check('fx_datumssprung_unsinn', async (page) => {
|
||||
await oeffne(page, '#termine')
|
||||
await warteAnsicht(page)
|
||||
await page.locator('li.tm-eintrag').first().waitFor()
|
||||
// fuenfstelliges Jahr: gueltiger Wert des Datumsfelds, aber kein Tag der Ansicht
|
||||
const wert = await page.locator('#tm-sprung').evaluate((el) => { el.value = '12345-01-01'; el.dispatchEvent(new Event('change', { bubbles: true })); return el.value })
|
||||
eq(wert, '12345-01-01', 'das Feld nimmt das Datum an')
|
||||
await page.waitForTimeout(200)
|
||||
eq(await textVon(page.locator('.tm-zeitraum')), 'Montag, 5. Oktober 2026', 'Ansicht bleibt stehen')
|
||||
await page.getByRole('button', { name: 'Weiter', exact: true }).click()
|
||||
eq(await textVon(page.locator('.tm-zeitraum')), 'Dienstag, 6. Oktober 2026', 'Ansicht reagiert weiter')
|
||||
// Datumsfeld im dunklen Farbschema und im Stil der anderen Felder
|
||||
eq(await page.locator('#tm-sprung').evaluate((el) => getComputedStyle(el).colorScheme), 'dark', 'color-scheme dark')
|
||||
}, { fixture: Object.assign(neueFixture(), { buchungen: alleStatus() }) })
|
||||
|
||||
// ------------------------------------------------------------ Rueckmeldung ist im Bild (390 px)
|
||||
await check('fx_meldung_im_bild', async (page) => {
|
||||
const fx = page.fx
|
||||
await oeffne(page, '#termine')
|
||||
await warteAnsicht(page)
|
||||
await page.getByRole('button', { name: 'Woche', exact: true }).click()
|
||||
await page.locator('li.tm-eintrag[data-id="bkonflikt"]').waitFor()
|
||||
const imBild = () => page.evaluate(() => {
|
||||
const m = document.querySelector('.tm-meldung:not(:empty)')
|
||||
if (!m) return 'keine Meldung'
|
||||
const r = m.getBoundingClientRect()
|
||||
const kopf = document.querySelector('.kb-header').getBoundingClientRect().bottom
|
||||
return r.top >= kopf - 1 && r.bottom <= window.innerHeight ? 'ja' : `nein (oben ${Math.round(r.top)}, unten ${Math.round(r.bottom)}, Kopf ${Math.round(kopf)}, Fenster ${window.innerHeight})`
|
||||
})
|
||||
// Erfolg: weit unten abgesagt ⇒ die Meldung (ueber den Reitern) wird ins Bild geholt
|
||||
const unten = page.locator('li.tm-eintrag[data-id="bvermerk"]')
|
||||
await unten.scrollIntoViewIfNeeded()
|
||||
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight))
|
||||
await knopf(unten, 'Absagen: Damenschnitt, 06.10.2026 um 16:00 Uhr').click()
|
||||
await unten.getByLabel('Grund').selectOption('Krankheit')
|
||||
await knopf(unten.locator('.tm-frage-box'), 'Termin absagen').click()
|
||||
await meldung(page).waitFor()
|
||||
eq(await imBild(), 'ja', 'Erfolgsmeldung im Bild')
|
||||
eq(fx.aufrufe.filter((a) => a[0] === 'aktion').length, 1, 'eine Absage gesendet')
|
||||
// Fehler: Meldung im Bild und mit Fokus (Screenreader, Tastatur)
|
||||
const konflikt = page.locator('li.tm-eintrag[data-id="bkonflikt"]')
|
||||
await konflikt.scrollIntoViewIfNeeded()
|
||||
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight))
|
||||
await knopf(konflikt, 'Absagen: Damenschnitt, 06.10.2026 um 17:00 Uhr').click()
|
||||
await konflikt.getByLabel('Grund').selectOption('Krankheit')
|
||||
await knopf(konflikt.locator('.tm-frage-box'), 'Termin absagen').click()
|
||||
await page.locator('.tm-meldung-fehler').waitFor()
|
||||
eq(await imBild(), 'ja', 'Fehlermeldung im Bild')
|
||||
eq(await page.evaluate(() => document.activeElement?.classList.contains('tm-meldung-fehler')), true, 'Fehlermeldung hat den Fokus')
|
||||
eq(await page.locator('.tm-meldung-fehler').getAttribute('role'), 'alert', 'role alert')
|
||||
// genau eine Live-Region: die Meldung; Liste und Titel melden nichts selbst
|
||||
eq(await page.locator('#termine-body [aria-live]:not([data-feld] *)').evaluateAll((l) => l.map((e) => e.className)), ['tm-meldung tm-meldung-fehler'], 'aria-live nur an der Meldung')
|
||||
await shot(page, '390-5-fehlermeldung')
|
||||
}, {
|
||||
fixture: (() => {
|
||||
const fx = neueFixture()
|
||||
fx.buchungen = [...alleStatus(), buchung('bkonflikt', '2026-10-06', '17:00', 45, { gastName: 'Konflikt Test' })]
|
||||
return fx
|
||||
})(),
|
||||
context: { viewport: { width: 390, height: 844 }, deviceScaleFactor: 2, isMobile: true, hasTouch: true },
|
||||
})
|
||||
|
||||
// ============================================================ Echter Server
|
||||
await check('echt_kachel_vorlage_und_liste', async (page) => {
|
||||
// aktiv, aber noch ohne Einstellung
|
||||
@@ -644,6 +810,8 @@ async function pruefungen() {
|
||||
// annehmen: G02 an die Anfragende
|
||||
const mailsVorher = mailsAn('lea@example.com').length
|
||||
await knopf(reihe(page, 'Lea Braun'), 'Annehmen: Brautfrisur, 13.10.2026 um 14:00 Uhr').click()
|
||||
eq(buchungMit(anfrage1.$id).status, 'angefragt', 'Rückfrage: noch nichts geändert')
|
||||
await reihe(page, 'Lea Braun').locator('.tm-frage-box').getByRole('button', { name: 'Annehmen', exact: true }).click()
|
||||
await meldung(page).waitFor()
|
||||
assert((await textVon(meldung(page))).includes('Lea Braun bekommt eine Bestätigung per E-Mail.'), 'Meldung annehmen')
|
||||
await srv.warteAufVersand()
|
||||
@@ -739,6 +907,8 @@ async function pruefungen() {
|
||||
eq([buchungMit('b_begonnen').gastName, buchungMit('b_begonnen').gastEmail], ['Gelöscht', ''], 'Server: Gastdaten geloescht')
|
||||
|
||||
// + Termin eintragen: Pflichtfelder, Feldfehler am Feld, dann Erfolg
|
||||
await page.getByRole('button', { name: 'Angefragt', exact: true }).click() // Statusfilter gesetzt: der neue Eintrag (bestaetigt) darf dadurch nicht verschwinden
|
||||
eq(await page.getByRole('button', { name: 'Angefragt', exact: true }).getAttribute('aria-pressed'), 'true', 'Filter Angefragt gesetzt')
|
||||
await knopf(page, '+ Termin eintragen').click()
|
||||
const form = page.getByRole('form', { name: 'Termin eintragen' })
|
||||
await form.waitFor()
|
||||
@@ -764,6 +934,7 @@ async function pruefungen() {
|
||||
// Ansicht springt auf den Tag des neuen Termins
|
||||
await page.locator(`li.tm-eintrag[data-id="${manuell.$id}"]`).waitFor()
|
||||
eq(await textVon(page.locator('.tm-zeitraum')), 'Dienstag, 6. Oktober 2026', 'Tag des Eintrags')
|
||||
eq(await page.getByRole('button', { name: 'Alle', exact: true }).getAttribute('aria-pressed'), 'true', 'Filter nach dem Eintragen wieder auf Alle')
|
||||
eq(await page.locator('.tm-formular').count(), 0, 'Formular zu')
|
||||
assert((await textVon(page.locator(`li.tm-eintrag[data-id="${manuell.$id}"]`))).includes('von dir eingetragen'), 'Handeintrag gekennzeichnet')
|
||||
|
||||
@@ -864,6 +1035,42 @@ async function pruefungen() {
|
||||
eq(await passtInBreite(page), true, 'echt: Sperre ohne Streifen')
|
||||
await shot(page, '390-7-echt-zeit-sperren')
|
||||
}, { context: { viewport: { width: 390, height: 844 }, deviceScaleFactor: 2, isMobile: true, hasTouch: true } })
|
||||
|
||||
// ------------------------------------------------------------ Zeitzone des Browsers ist egal: Berliner Tage und Uhrzeiten
|
||||
// Browser-Uhr 2026-10-05T23:30Z: in Berlin schon Dienstag 01:30, in New York noch Montag 19:30. Drei Termine rund um Mitternacht.
|
||||
for (const [name, zone] of [['echt_zeitzone_new_york', 'America/New_York'], ['echt_zeitzone_berlin', 'Europe/Berlin']]) {
|
||||
await check(name, async (page) => {
|
||||
const doc = (id, tag, von, bis, gast) => ({
|
||||
$id: id, featureId: srv.FEATURE, customerId: srv.KUNDE, projectId: srv.PROJEKT, leistungId: S_HERREN, leistungName: 'Herrenschnitt', ressourceId: R_BEN, ressourceName: 'Ben',
|
||||
startsAt: utc(tag, von), endsAt: bis === '24:00' ? utc(tagNach(tag), '00:00') : utc(tag, bis), blockEndsAt: bis === '24:00' ? utc(tagNach(tag), '00:00') : utc(tag, bis),
|
||||
status: 'bestaetigt', quelle: 'online', gastName: gast, gastEmail: 'gast@example.com', gastTelefon: '', gastNachricht: '', gastZusatz: '', erinnerung: false,
|
||||
createdAt: JETZT.toISOString(), updatedAt: JETZT.toISOString(), preisCent: 2800, preisArt: 'fest', buchungsart: 'sofort', verbraucher: true, zahlart: 'vorort', zahlStatus: 'keine', icsSequenz: 0,
|
||||
})
|
||||
srv.aw.seed('termineBookings', doc('b_spaet', '2026-10-05', '23:30', '24:00', 'Spät Abend'))
|
||||
srv.aw.seed('termineBookings', doc('b_nacht', '2026-10-06', '00:30', '01:00', 'Nacht Eins'))
|
||||
srv.aw.seed('termineBookings', doc('b_frueh', '2026-10-06', '09:00', '09:30', 'Früh Neun'))
|
||||
await oeffne(page, '#termine')
|
||||
await warteAnsicht(page)
|
||||
eq(await page.evaluate(() => Intl.DateTimeFormat().resolvedOptions().timeZone), zone, 'Zeitzone des Browsers')
|
||||
// heute ist in Berlin Dienstag, auch wenn es in New York noch Montag ist
|
||||
eq(await textVon(page.locator('.tm-zeitraum')), 'Dienstag, 6. Oktober 2026', 'Titel: Berliner Tag')
|
||||
await page.locator('li.tm-eintrag[data-id="b_frueh"]').waitFor()
|
||||
eq(await page.locator('li.tm-eintrag').evaluateAll((l) => l.map((e) => e.dataset.id)), ['b_nacht', 'b_frueh'], 'Dienstag: die beiden Termine')
|
||||
const zeiten = await page.locator('li.tm-eintrag .tm-zeit').allInnerTexts()
|
||||
eq(zeiten, ['00:30–01:00', '09:00–09:30'], 'Uhrzeiten wie in Berlin')
|
||||
// ein Tag zurueck: der Termin bis Mitternacht endet um 24:00
|
||||
await page.getByRole('button', { name: 'Zurück', exact: true }).click()
|
||||
await page.locator('li.tm-eintrag[data-id="b_spaet"]').waitFor()
|
||||
eq(await textVon(page.locator('.tm-zeitraum')), 'Montag, 5. Oktober 2026', 'ein Tag zurück')
|
||||
eq(await page.locator('li.tm-eintrag .tm-zeit').allInnerTexts(), ['23:30–24:00'], 'Ende um Mitternacht als 24:00')
|
||||
// Woche: Ueberschriften „Heute“ und „Gestern“ nach Berliner Datum
|
||||
await page.getByRole('button', { name: 'Heute', exact: true }).click()
|
||||
await page.getByRole('button', { name: 'Woche', exact: true }).click()
|
||||
await page.locator('li.tm-eintrag[data-id="b_spaet"]').waitFor()
|
||||
eq(await page.locator('.tm-tag-kopf').allInnerTexts().then((l) => l.slice(0, 2)), ['Gestern · Montag, 5. Oktober', 'Heute · Dienstag, 6. Oktober'], 'Gestern und Heute nach Berliner Datum')
|
||||
await shot(page, name === 'echt_zeitzone_new_york' ? 'echt-new-york' : 'echt-berlin')
|
||||
}, { context: { timezoneId: zone }, jetzt: new Date('2026-10-05T23:30:00Z') })
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Lauf
|
||||
|
||||
Reference in new issue
Block a user