From 41fd2e259ec1adc73a87007897fa8174949be5e9 Mon Sep 17 00:00:00 2001 From: Martin Berka Date: Mon, 26 Oct 2026 08:59:29 +0100 Subject: [PATCH] =?UTF-8?q?fix:=20na=C4=8Dten=C3=AD=20TechTower=20p=C5=99i?= =?UTF-8?q?=20star=C3=A9=20nab=C3=ADdce?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/changelogs/2026-10-26.json | 4 + server/src/restaurants.ts | 54 +++- .../fixtures/techtower-2026-10-stale.html | 305 ++++++++++++++++++ server/src/tests/scrapers.test.ts | 48 ++- 4 files changed, 393 insertions(+), 18 deletions(-) create mode 100644 server/changelogs/2026-10-26.json create mode 100644 server/src/tests/fixtures/techtower-2026-10-stale.html diff --git a/server/changelogs/2026-10-26.json b/server/changelogs/2026-10-26.json new file mode 100644 index 0000000..cb82da5 --- /dev/null +++ b/server/changelogs/2026-10-26.json @@ -0,0 +1,4 @@ +[ + "Správné načtení jídelního lístku TechTower i v případě, že podnik na svém webu ještě nepřepsal nabídku z minulého týdne", + "Mnoho nových vzhledů (v Nastavení)" +] diff --git a/server/src/restaurants.ts b/server/src/restaurants.ts index 9a3aa55..8d89b67 100644 --- a/server/src/restaurants.ts +++ b/server/src/restaurants.ts @@ -381,6 +381,11 @@ export const getMenuTechTower = async (firstDayOfWeek: Date, mock: boolean = fal // podle
na jednotlivé řádky. Řádek s názvem dne přepne aktuální den, ostatní řádky // jsou položky. Zvládne tak i den a první položku ve stejném

. if (fifthTry) { + // Za týdnem z hlavičky může následovat menu "Následující týden", které se ukládá zvlášť. + // Zaměstnanci často zapomenou stránku včas přepsat, takže v hlavičce ještě visí minulý + // týden a aktuální menu je právě pod "Následující týden" (viz finalizeTechTower). + const nextWeek: Food[][] = []; + let target = result; let parsing = false; let currentDayIndex = 0; let container = $(font).parent().parent(); @@ -395,27 +400,34 @@ export const getMenuTechTower = async (firstDayOfWeek: Date, mock: boolean = fal .filter(line => line.length > 0); for (const text of lines) { const lower = text.toLocaleLowerCase(); - // Za aktuálním týdnem může následovat menu na další týden - to již nezpracováváme if (parsing && lower.startsWith('následující týden')) { - return false; + if (target === nextWeek) { + return false; + } + target = nextWeek; + parsing = false; + continue; } if (DAYS_IN_WEEK.includes(lower)) { - if (result[DAYS_IN_WEEK.indexOf(lower)]) { + if (target[DAYS_IN_WEEK.indexOf(lower)]) { // Opakující se den = začátek dalšího týdne - return false; + if (target === nextWeek) { + return false; + } + target = nextWeek; } currentDayIndex = DAYS_IN_WEEK.indexOf(lower); parsing = true; - result[currentDayIndex] ??= []; + target[currentDayIndex] ??= []; continue; } if (!parsing) { continue; } - result[currentDayIndex].push(buildFoodItem(text)); + target[currentDayIndex].push(buildFoodItem(text)); } }); - return finalizeTechTower(result, $(font).text().trim(), firstDayOfWeek); + return finalizeTechTower(result, $(font).text().trim(), firstDayOfWeek, nextWeek); } // Čtvrtý pokus (detekce): thirdTry našel , ale nový formát má každý den v jednom

@@ -483,22 +495,30 @@ export const getMenuTechTower = async (firstDayOfWeek: Date, mock: boolean = fal * Datum v hlavičce nemusí být přesně pondělí (např. je-li pondělí svátek, začíná hlavička * úterým), proto stačí, aby první nalezené datum padlo do libovolného dne požadovaného týdne. * Toleruje i mezery za tečkami (formát "7. 7.-10. 7."). + * + * Pokud hlavička odpovídá předchozímu týdnu a stránka obsahuje i menu "Následující týden", + * vrátí se to, protože jde o menu požadovaného týdne, jen stránka ještě nebyla přepsána. */ -const finalizeTechTower = (result: Food[][], headerText: string, firstDayOfWeek: Date): Food[][] => { +const finalizeTechTower = (result: Food[][], headerText: string, firstDayOfWeek: Date, nextWeek: Food[][] = []): Food[][] => { const dateMatch = headerText.match(/(\d{1,2})\.\s*(\d{1,2})\./); if (dateMatch) { const foundDay = parseInt(dateMatch[1]); const foundMonth = parseInt(dateMatch[2]) - 1; // JS months are 0-based - let matchesWeek = false; - for (let d = 0; d < 7; d++) { - const day = new Date(firstDayOfWeek); - day.setDate(firstDayOfWeek.getDate() + d); - if (day.getDate() === foundDay && day.getMonth() === foundMonth) { - matchesWeek = true; - break; + // Vrátí true, pokud datum z hlavičky padne do týdne začínajícího posunem offsetDays od firstDayOfWeek + const matchesWeek = (offsetDays: number): boolean => { + for (let d = 0; d < 7; d++) { + const day = new Date(firstDayOfWeek); + day.setDate(firstDayOfWeek.getDate() + offsetDays + d); + if (day.getDate() === foundDay && day.getMonth() === foundMonth) { + return true; + } + } + return false; + }; + if (!matchesWeek(0)) { + if (nextWeek.some(day => day?.length) && matchesWeek(-7)) { + return nextWeek; } - } - if (!matchesWeek) { throw new StaleWeekError(result); } } diff --git a/server/src/tests/fixtures/techtower-2026-10-stale.html b/server/src/tests/fixtures/techtower-2026-10-stale.html new file mode 100644 index 0000000..248a200 --- /dev/null +++ b/server/src/tests/fixtures/techtower-2026-10-stale.html @@ -0,0 +1,305 @@ + + + + + Restaurace :: Equifarm + + + + + + + + + + + +

+
+
+
+ + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+

Restaurace 

+
+
+
+
+
+
+

Otevírací doba

po – pá 10.00 - 14.00

obědy 10.00 - 13:45

+
+

Jídelní lístek 19. 10.– 23. 10.

Pondělí

Krémová česnečka s krutony 1,7 — 45 Kč

Uzené kuřecí stehno, máslové brambory s petrželí 7 — 140 Kč

Špagety s česnekem, olivami, sušenými rajčaty a parmezánem 1,7

Kuřecí kousky v sýrové omáčce, jasmínová rýže 1,7

Vepřová panenka s pepřovou omáčkou, hranolky, bylinková majonéza 1,3,7

Úterý

Fazolová s uzeninou 1 — 45 Kč

Hovězí guláš s čerstvým křenem, houskový knedlík 1,3,7 — 140 Kč

Grilovaný hermelín s brusinkami, polníčkový salát a cherry rajčata 7

Přírodní kuřecí kapsa se šunkou a sýrem, jasmínová rýže 1,7

Šašlik z vepřové krkovice, pečené grenaille se slaninou a cibulí, česneková majonéza 1,3,7

Středa

Kuřecí vývar se zeleninou, masem a těstovinami 1,3,7,9 — 45 Kč

Smažený květák s bramborem a tatarskou omáčkou 1,3,7 — 140 Kč

Dýňové curry s kokosovým mlékem a cizrnou, jasmínová rýže 1,7

Tagliatelle Pomodoro, masové kuličky s bazalkou 1,7

Kachní roláda, chlupaté knedlíky se špekem, silná omáčka 1,3,7

Čtvrtek

Kulajda s vejcem 1,7 — 45 Kč

Výpečky, špenát, houskový knedlík 1,3,7 — 140 Kč

Maková buchta se švestkami, vanilková omáčka 1,3,7

Pečené kuřecí stehno na cibulce, kari rýže 1

Duroc krkovice sous-vide s omáčkou velouté, špecle, grilovaná cuketa 1,7

Pátek

Hovězí vývar se zeleninou, masem a játrovou rýží 1,3,7,9 — 45 Kč

Kuřecí rizoto se zeleninou, sypané sýrem 1,7 — 140 Kč

Smažené čínské nudle se zeleninou a vejcem 1,3,6,11

Americký hot dog s pikantní klobásou, čedarová omáčka, restovaná slanina s cibulí 1,7

Hovězí líčka na červeném víně, máslová bramborová kaše 1,7,9


Následující týden

Pondělí

Zeleninová s bulgurem 1,9 — 45 Kč

Smažený rybí filet, brambory, citron 1,3,4,7 — 140 Kč

Gnocchi s vlašskými ořechy a gorgonzolou 1,7

Grilovaný kuřecí supreme, pečená kořenová zelenina s červenou řepou 1

Steak z vepřové pečeně Duroc, hořčicovo-medová omáčka, hranolky 1,7

Úterý

Česnečka se slaninou, krutony a sýrem 1,7 — 45 Kč

Kuřecí paličky na paprice, těstoviny 1,7 — 140 Kč

Grilovaný oštiepok, salát s černými fazolemi 7

Trhaný hovězí krk, kedlubnové ragú, bramborový knedlík s cibulkou 1,3,7

Losos na japonský způsob, jasmínová rýže, barevný sezam, nakládaný zázvor 1,4,6,11

Středa

SVÁTEK

Čtvrtek

Batátový krém s chilli 1,7 — 45 Kč

Halušky s uzeným masem a restovanou cibulkou 1 — 140 Kč

Hruškový štrúdl s rozinkovou omáčkou, sypaný perníkem 1,3,7

Husí játra na slanině s čerstvou majoránkou, jasmínová rýže 1,7

Bylinkový karbanátek, bramborová kaše 1,3,7

Pátek

Hovězí vývar se zeleninou a masem, vaječné kapání 1,3,7,9 — 45 Kč

Krůtí guláš s houskovým knedlíkem 1,3,7 — 140 Kč

Lasagne se zeleninou, rajčaty a ricottou 1,7

Hovězí burger se sázeným vejcem, cibulové chutney, hranolky 1,3,7

Kachní prso sous-vide se silnou omáčkou, nádivka s jedlými kaštany a petrželkou 1,3,7

+
+

+
+

www.equifarm.cz

techtower@equifarm.cz

+420 608 180 311

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+
+ © 2022 Equifarm, s.r.o. Všechna práva vyhrazena. +
+
+ +Vytvořeno službou Webnode + + + Cookies + + + + +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+ +
+ + + + + + + diff --git a/server/src/tests/scrapers.test.ts b/server/src/tests/scrapers.test.ts index b862051..de99793 100644 --- a/server/src/tests/scrapers.test.ts +++ b/server/src/tests/scrapers.test.ts @@ -161,8 +161,54 @@ describe('TechTower parser – nový formát 10/2026', () => { } }); + test('pro následující týden vrátí menu z části "Následující týden"', async () => { + const menu = await getMenuTechTower(new Date('2026-10-12')); + expect(menu.flat().some(f => f.name.startsWith('Hrášková'))).toBe(true); + expect(menu.flat().some(f => f.name === 'Mexická fazolačka s kukuřicí a masem')).toBe(false); + }); + test('hodí StaleWeekError pro jiný týden', async () => { - await expect(getMenuTechTower(new Date('2026-10-12'))).rejects.toBeInstanceOf(StaleWeekError); + await expect(getMenuTechTower(new Date('2026-10-19'))).rejects.toBeInstanceOf(StaleWeekError); + await expect(getMenuTechTower(new Date('2026-09-28'))).rejects.toBeInstanceOf(StaleWeekError); + }); +}); + +describe('TechTower parser – neaktualizovaná hlavička (aktuální týden pod "Následující týden")', () => { + // Pondělí 26.10.2026, hlavička stránky je stále "Jídelní lístek 19. 10.– 23. 10." + const MONDAY_2026_10_26 = new Date('2026-10-26'); + + beforeEach(() => { + mockedAxios.get = jest.fn().mockResolvedValue({ data: loadFixture('techtower-2026-10-stale.html') }); + }); + + test('vrátí menu aktuálního týdne bez StaleWeekError', async () => { + const menu = await getMenuTechTower(MONDAY_2026_10_26); + expect(menu).toHaveLength(5); + expect(menu[0][0].name).toBe('Zeleninová s bulgurem'); + expect(menu[0][0].isSoup).toBe(true); + expect(menu[4][1].name).toBe('Krůtí guláš s houskovým knedlíkem'); + expect(menu.flat().some(f => f.name.startsWith('Krémová česnečka'))).toBe(false); + }); + + test('svátek je naparsován jako samostatná položka', async () => { + const menu = await getMenuTechTower(MONDAY_2026_10_26); + expect(menu[2]).toHaveLength(1); + expect(menu[2][0].name).toBe('SVÁTEK'); + }); + + test('pro týden z hlavičky vrátí menu z hlavičky', async () => { + const menu = await getMenuTechTower(new Date('2026-10-19')); + expect(menu[0][0].name).toBe('Krémová česnečka s krutony'); + }); + + test('pro vzdálenější týden hodí StaleWeekError s daty z hlavičky', async () => { + expect.assertions(2); + try { + await getMenuTechTower(new Date('2026-11-02')); + } catch (e) { + expect(e).toBeInstanceOf(StaleWeekError); + expect((e as StaleWeekError).food[0][0].name).toBe('Krémová česnečka s krutony'); + } }); });