feat: spin the wheel, objednavani scrape menu, historie cen
CI / Generate TypeScript types (push) Successful in 11s
CI / Server unit tests (push) Successful in 25s
CI / Build server (push) Successful in 29s
CI / Build client (push) Successful in 39s
CI / Playwright E2E tests (push) Successful in 1m52s
CI / Build and push Docker image (push) Successful in 51s
CI / Notify (push) Successful in 2s
CI / Generate TypeScript types (push) Successful in 11s
CI / Server unit tests (push) Successful in 25s
CI / Build server (push) Successful in 29s
CI / Build client (push) Successful in 39s
CI / Playwright E2E tests (push) Successful in 1m52s
CI / Build and push Docker image (push) Successful in 51s
CI / Notify (push) Successful in 2s
This commit is contained in:
35 files changed
+3979
-15
No files matched your search
@@ -0,0 +1,39 @@
|
||||
import axios from 'axios';
|
||||
import { resetMemoryStorage } from '../storage/memory';
|
||||
import { getConfettiCharacters, getConfettiImage } from '../confetti';
|
||||
|
||||
jest.mock('axios');
|
||||
const mockedAxios = axios as jest.Mocked<typeof axios>;
|
||||
|
||||
beforeEach(() => {
|
||||
resetMemoryStorage();
|
||||
mockedAxios.get.mockReset();
|
||||
});
|
||||
|
||||
test('postavy odkazují jen na cachující proxy serveru', () => {
|
||||
const images = getConfettiCharacters().flatMap(c => c.images);
|
||||
expect(images.length).toBeGreaterThan(0);
|
||||
expect(images.every(u => /^\/api\/confetti\/image\/[0-9a-f]{16}$/.test(u))).toBe(true);
|
||||
// ID jsou stabilní (hash URL) a unikátní
|
||||
expect(new Set(images).size).toBe(images.length);
|
||||
});
|
||||
|
||||
const idAt = (i: number) => getConfettiCharacters().flatMap(c => c.images)[i].split('/').pop()!;
|
||||
|
||||
test('obrázek stáhne jen jednou, pak ho vrací ze storage', async () => {
|
||||
mockedAxios.get.mockResolvedValue({ data: Buffer.from('PNGDATA'), headers: { 'content-type': 'image/png' } });
|
||||
const first = await getConfettiImage(idAt(0));
|
||||
const second = await getConfettiImage(idAt(0));
|
||||
expect(first?.contentType).toBe('image/png');
|
||||
expect(second?.data.toString()).toBe('PNGDATA');
|
||||
expect(mockedAxios.get).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('neznámé ID ani ne-obrázek nevrátí', async () => {
|
||||
expect(await getConfettiImage('ffffffffffffffff')).toBeUndefined();
|
||||
mockedAxios.get.mockResolvedValue({ data: Buffer.from('<html>'), headers: { 'content-type': 'text/html' } });
|
||||
expect(await getConfettiImage(idAt(1))).toBeUndefined();
|
||||
// po selhání se hned znovu nestahuje
|
||||
expect(await getConfettiImage(idAt(1))).toBeUndefined();
|
||||
expect(mockedAxios.get).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -0,0 +1,315 @@
|
||||
import axios from 'axios';
|
||||
import { resetMemoryStorage } from '../storage/memory';
|
||||
import getStorage from '../storage';
|
||||
import { diffMenus, fetchBoltMenu, fetchWoltMenu, getStoreMenu, getStoreMenuHistory, getStoreMenuStatus, parseBoltProviderId, parseWoltSlug, refreshStoreMenu, renameStoreMenus, resetProviderThrottle } from '../storeMenus';
|
||||
|
||||
jest.mock('axios');
|
||||
const mockedAxios = axios as jest.Mocked<typeof axios>;
|
||||
|
||||
const BOLT_URL = 'https://food.bolt.eu/cs-cz/427-plzen/p/27744-new-york-burger-chicken/';
|
||||
const WOLT_URL = 'https://wolt.com/cs/cze/pilsen/restaurant/new-york-burger-chicken';
|
||||
|
||||
/** Odpověď Bolt getMenuCategories ve zkrácené podobě (plochá mapa položek). */
|
||||
const boltResponse = () => ({
|
||||
data: {
|
||||
code: 0,
|
||||
message: 'OK',
|
||||
data: {
|
||||
items: {
|
||||
1: { type: 'menu', id: 1, parent_id: null },
|
||||
10: { type: 'category', id: 10, parent_id: 1, index: 1, name: { value: 'Burgery 🍔' } },
|
||||
11: { type: 'category', id: 11, parent_id: 1, index: 2, name: { value: 'Přílohy 🍟' } },
|
||||
12: { type: 'category', id: 12, parent_id: 1, index: 0, name: { value: 'Polévky' } },
|
||||
100: { type: 'dish', id: 100, parent_id: 10, index: 1, name: { value: 'Big Cheeseburger ' }, price: { value: 189 }, availability: 'in_stock' },
|
||||
101: { type: 'dish', id: 101, parent_id: 10, index: 0, name: { value: 'Chicken Burger' }, price: { value: 169 }, availability: 'in_stock' },
|
||||
102: { type: 'dish', id: 102, parent_id: 10, index: 2, name: { value: 'Vyprodaný burger' }, price: { value: 1 }, availability: 'out_of_stock' },
|
||||
103: { type: 'dish', id: 103, parent_id: 11, index: 0, name: { value: 'Hranolky' }, price: { value: 59 }, availability: 'in_stock' },
|
||||
104: { type: 'dish', id: 104, parent_id: 12, index: 0, name: { value: 'Gulášovka' }, price: { value: 79 }, availability: 'in_stock' },
|
||||
13: { type: 'category', id: 13, parent_id: 1, index: 3, name: { value: 'Nealko' } },
|
||||
105: { type: 'dish', id: 105, parent_id: 13, index: 0, name: { value: 'Red Bull 0.25' }, price: { value: 69 }, availability: 'in_stock' },
|
||||
106: { type: 'dish', id: 106, parent_id: 10, index: 3, name: { value: 'Půlená pizza (příplatek)' }, price: { value: 20 }, availability: 'in_stock' },
|
||||
107: { type: 'dish', id: 107, parent_id: 10, index: 4, name: { value: 'DRŠŤKOVÁ' }, price: { value: 89 }, availability: 'in_stock' },
|
||||
200: { type: 'option_select', id: 200, parent_id: 100, name: { value: 'Extra sýr' } },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/** Odpověď Wolt assortment ve zkrácené podobě (ceny v haléřích). */
|
||||
const woltResponse = () => ({
|
||||
data: {
|
||||
categories: [
|
||||
{ name: '🥩 BEEF BURGER', item_ids: ['a', 'b'], subcategories: [] },
|
||||
{ name: '🥤 Nápoje', item_ids: ['c'] },
|
||||
{ name: 'Menu', item_ids: [], subcategories: [{ name: 'Tortilly', item_ids: ['d', 'e'] }] },
|
||||
],
|
||||
items: [
|
||||
{ id: 'a', name: 'Cheeseburger', price: 18900, disabled_info: null },
|
||||
{ id: 'b', name: 'Nedostupný burger', price: 100, disabled_info: { reason: 'sold out' } },
|
||||
{ id: 'c', name: 'Cola', price: 4900, disabled_info: null },
|
||||
{ id: 'd', name: 'Kuřecí tortilla', price: 15950, disabled_info: null },
|
||||
{ id: 'e', name: 'Kuřecí polévka', price: 6900, disabled_info: null },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
resetMemoryStorage();
|
||||
resetProviderThrottle();
|
||||
mockedAxios.get.mockReset();
|
||||
mockedAxios.isAxiosError.mockImplementation((e: any) => !!e?.isAxiosError);
|
||||
});
|
||||
|
||||
/** Chyba axiosu s HTTP 429 (omezení počtu dotazů). */
|
||||
const tooManyRequests = (retryAfter?: string) =>
|
||||
Object.assign(new Error('Request failed with status code 429'), {
|
||||
isAxiosError: true,
|
||||
response: { status: 429, headers: retryAfter ? { 'retry-after': retryAfter } : {} },
|
||||
});
|
||||
|
||||
describe('parsování URL', () => {
|
||||
test('ID podniku z Bolt URL', () => {
|
||||
expect(parseBoltProviderId(BOLT_URL)).toBe('27744');
|
||||
expect(parseBoltProviderId(WOLT_URL)).toBeUndefined();
|
||||
expect(parseBoltProviderId('nesmysl')).toBeUndefined();
|
||||
});
|
||||
|
||||
test('slug podniku z Wolt URL', () => {
|
||||
expect(parseWoltSlug(WOLT_URL)).toBe('new-york-burger-chicken');
|
||||
expect(parseWoltSlug(BOLT_URL)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchBoltMenu', () => {
|
||||
test('vrátí dostupné položky dle pořadí v nabídce, hlavní jídla označí (ne přílohy, nápoje, příplatky, polévky)', async () => {
|
||||
mockedAxios.get.mockResolvedValue(boltResponse());
|
||||
const dishes = await fetchBoltMenu('27744');
|
||||
expect(dishes).toEqual([
|
||||
{ name: 'Gulášovka', price: 79, category: 'Polévky', isMain: false },
|
||||
{ name: 'Chicken Burger', price: 169, category: 'Burgery 🍔', isMain: true },
|
||||
{ name: 'Big Cheeseburger', price: 189, category: 'Burgery 🍔', isMain: true },
|
||||
{ name: 'Půlená pizza (příplatek)', price: 20, category: 'Burgery 🍔', isMain: false },
|
||||
{ name: 'DRŠŤKOVÁ', price: 89, category: 'Burgery 🍔', isMain: false },
|
||||
{ name: 'Hranolky', price: 59, category: 'Přílohy 🍟', isMain: false },
|
||||
{ name: 'Red Bull 0.25', price: 69, category: 'Nealko', isMain: false },
|
||||
]);
|
||||
expect(mockedAxios.get).toHaveBeenCalledWith(expect.stringContaining('getMenuCategories'),
|
||||
expect.objectContaining({ params: expect.objectContaining({ provider_id: '27744' }) }));
|
||||
});
|
||||
|
||||
test('chybový kód Bolt API vyhodí výjimku', async () => {
|
||||
mockedAxios.get.mockResolvedValue({ data: { code: 702, message: 'INVALID_REQUEST' } });
|
||||
await expect(fetchBoltMenu('1')).rejects.toThrow('702');
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchWoltMenu', () => {
|
||||
test('vrátí dostupné položky včetně podkategorií, ceny v Kč, hlavní jídla označí', async () => {
|
||||
mockedAxios.get.mockResolvedValue(woltResponse());
|
||||
const dishes = await fetchWoltMenu('new-york-burger-chicken');
|
||||
expect(dishes).toEqual([
|
||||
{ name: 'Cheeseburger', price: 189, category: '🥩 BEEF BURGER', isMain: true },
|
||||
{ name: 'Cola', price: 49, category: '🥤 Nápoje', isMain: false },
|
||||
{ name: 'Kuřecí tortilla', price: 160, category: 'Menu / Tortilly', isMain: true },
|
||||
{ name: 'Kuřecí polévka', price: 69, category: 'Menu / Tortilly', isMain: false },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
const DAY = 24 * 60 * 60 * 1000;
|
||||
const ADMIN_PW = 'testadmin';
|
||||
|
||||
describe('getStoreMenu', () => {
|
||||
beforeEach(async () => {
|
||||
process.env.ADMIN_PASSWORD = ADMIN_PW;
|
||||
await getStorage().setData('stores', [{ name: 'New York', urls: [BOLT_URL, WOLT_URL] }, { name: 'Bez URL' }]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
delete process.env.ADMIN_PASSWORD;
|
||||
});
|
||||
|
||||
test('stáhne Bolt jen jednou — uložená nabídka nemá expiraci', async () => {
|
||||
jest.useFakeTimers({ now: new Date('2026-09-01T10:00:00Z'), doNotFake: ['nextTick', 'setImmediate'] });
|
||||
mockedAxios.get.mockResolvedValue(boltResponse());
|
||||
const menu = await getStoreMenu('new york');
|
||||
expect(menu.source).toBe('bolt');
|
||||
expect(menu.dishes.filter(d => d.isMain)).toHaveLength(2);
|
||||
jest.setSystemTime(new Date('2026-12-01T10:00:00Z'));
|
||||
await getStoreMenu('New York');
|
||||
expect(mockedAxios.get).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('při selhání Boltu použije Wolt', async () => {
|
||||
mockedAxios.get
|
||||
.mockRejectedValueOnce(new Error('timeout'))
|
||||
.mockResolvedValueOnce(woltResponse());
|
||||
const menu = await getStoreMenu('New York');
|
||||
expect(menu.source).toBe('wolt');
|
||||
expect(menu.dishes.filter(d => d.isMain).map(d => d.name)).toEqual(['Cheeseburger', 'Kuřecí tortilla']);
|
||||
const [status] = await getStoreMenuStatus();
|
||||
expect(status.providers).toEqual([
|
||||
expect.objectContaining({ provider: 'bolt', state: 'failed', failures: 1, lastError: 'timeout' }),
|
||||
expect.objectContaining({ provider: 'wolt', state: 'ok', dishCount: 2 }),
|
||||
]);
|
||||
});
|
||||
|
||||
test('po selhání zkouší nejdřív za 3 dny a po 2 neúspěších službu zablokuje', async () => {
|
||||
jest.useFakeTimers({ now: new Date('2026-09-01T10:00:00Z'), doNotFake: ['nextTick', 'setImmediate'] });
|
||||
await getStorage().setData('stores', [{ name: 'Jen Bolt', urls: [BOLT_URL] }]);
|
||||
mockedAxios.get.mockRejectedValue(new Error('HTTP 500'));
|
||||
|
||||
await getStoreMenu('Jen Bolt');
|
||||
expect(mockedAxios.get).toHaveBeenCalledTimes(1);
|
||||
jest.setSystemTime(Date.now() + 2 * DAY);
|
||||
await getStoreMenu('Jen Bolt'); // příliš brzy — bez pokusu
|
||||
expect(mockedAxios.get).toHaveBeenCalledTimes(1);
|
||||
jest.setSystemTime(Date.now() + 1 * DAY);
|
||||
await getStoreMenu('Jen Bolt'); // druhý pokus → zablokováno
|
||||
expect(mockedAxios.get).toHaveBeenCalledTimes(2);
|
||||
jest.setSystemTime(Date.now() + 30 * DAY);
|
||||
const menu = await getStoreMenu('Jen Bolt');
|
||||
expect(mockedAxios.get).toHaveBeenCalledTimes(2);
|
||||
expect(menu.source).toBe('none');
|
||||
const [status] = await getStoreMenuStatus();
|
||||
expect(status.providers[0]).toEqual(expect.objectContaining({ state: 'blocked', failures: 2, lastError: 'HTTP 500' }));
|
||||
});
|
||||
|
||||
test('HTTP 429 se nepočítá jako selhání a službu dočasně pozastaví', async () => {
|
||||
jest.useFakeTimers({ now: new Date('2026-09-01T10:00:00Z'), doNotFake: ['nextTick', 'setImmediate'] });
|
||||
await getStorage().setData('stores', [{ name: 'Jen Bolt', urls: [BOLT_URL] }, { name: 'Jiný Bolt', urls: ['https://food.bolt.eu/cs-cz/427-plzen/p/2-jiny/'] }]);
|
||||
mockedAxios.get.mockRejectedValueOnce(tooManyRequests('120'));
|
||||
|
||||
await getStoreMenu('Jen Bolt');
|
||||
// Během pauzy se na Bolt vůbec nedotazujeme — ani pro jiný obchod
|
||||
await getStoreMenu('Jiný Bolt');
|
||||
expect(mockedAxios.get).toHaveBeenCalledTimes(1);
|
||||
const status = await getStoreMenuStatus();
|
||||
expect(status[0].providers[0]).toEqual(expect.objectContaining({ state: 'pending', failures: 0 }));
|
||||
expect(status[0].providers[0].lastError).toContain('429');
|
||||
|
||||
// Po uplynutí Retry-After se hned zkusí znovu (žádný 3denní odklad)
|
||||
jest.setSystemTime(Date.now() + 121 * 1000);
|
||||
mockedAxios.get.mockResolvedValue(boltResponse());
|
||||
const menu = await getStoreMenu('Jen Bolt');
|
||||
expect(menu.source).toBe('bolt');
|
||||
});
|
||||
|
||||
test('ruční obnovení stáhne i zablokovanou službu a zablokování zruší', async () => {
|
||||
await getStorage().setData('store_menus', {
|
||||
'New York': { bolt: { url: BOLT_URL, failures: 2, blocked: true, lastError: 'x', lastAttemptAt: new Date().toISOString() } },
|
||||
});
|
||||
mockedAxios.get.mockResolvedValue(boltResponse());
|
||||
const info = await refreshStoreMenu('new york', 'bolt', ADMIN_PW);
|
||||
expect(info.providers[0]).toEqual(expect.objectContaining({ provider: 'bolt', state: 'ok', dishCount: 2, failures: 0 }));
|
||||
expect(info.providers[0].lastError).toBeUndefined();
|
||||
});
|
||||
|
||||
test('neúspěšné ruční obnovení ponechá dříve staženou nabídku', async () => {
|
||||
mockedAxios.get.mockResolvedValueOnce(boltResponse()).mockRejectedValueOnce(new Error('HTTP 403'));
|
||||
await getStoreMenu('New York');
|
||||
const info = await refreshStoreMenu('New York', 'bolt', ADMIN_PW);
|
||||
expect(info.providers[0]).toEqual(expect.objectContaining({ state: 'ok', dishCount: 2, failures: 1, lastError: 'HTTP 403' }));
|
||||
});
|
||||
|
||||
test('ruční obnovení vyžaduje heslo a URL služby', async () => {
|
||||
await expect(refreshStoreMenu('New York', 'bolt', 'spatne')).rejects.toThrow('UNAUTHORIZED');
|
||||
await expect(refreshStoreMenu('Bez URL', 'wolt', ADMIN_PW)).rejects.toThrow('Obchod nemá URL');
|
||||
});
|
||||
|
||||
test('změna URL služby zahodí uloženou nabídku', async () => {
|
||||
await getStorage().setData('store_menus', {
|
||||
'New York': { bolt: { url: 'https://food.bolt.eu/cs-cz/427-plzen/p/1-stary/', dishes: [{ name: 'Staré' }], failures: 0, blocked: false } },
|
||||
});
|
||||
mockedAxios.get.mockResolvedValue(boltResponse());
|
||||
const menu = await getStoreMenu('New York');
|
||||
expect(menu.dishes.filter(d => d.isMain).map(d => d.name)).toEqual(['Chicken Burger', 'Big Cheeseburger']);
|
||||
});
|
||||
|
||||
test('přejmenování obchodu přenese uloženou nabídku', async () => {
|
||||
await getStorage().setData('store_menus', { 'New York': { bolt: { url: BOLT_URL, dishes: [{ name: 'Burger' }], failures: 0, blocked: false } } });
|
||||
await renameStoreMenus('new york', 'NY Burger');
|
||||
expect(await getStorage().getData('store_menus')).toEqual({ 'NY Burger': expect.any(Object) });
|
||||
});
|
||||
|
||||
test('bez stažitelné nabídky použije historii objednávek', async () => {
|
||||
await getStorage().setData('2026-09-01_extra', {
|
||||
groups: [{ id: 'g', name: 'Bez URL', creatorLogin: 'a', state: 'delivered', members: { a: { note: 'Kebab box' } } }],
|
||||
});
|
||||
const menu = await getStoreMenu('Bez URL');
|
||||
expect(menu).toEqual({ store: 'Bez URL', source: 'history', dishes: [{ name: 'Kebab box', isMain: true }] });
|
||||
expect(mockedAxios.get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('neznámý obchod vyhodí chybu', async () => {
|
||||
await expect(getStoreMenu('Neexistuje')).rejects.toThrow('Obchod nebyl nalezen');
|
||||
});
|
||||
});
|
||||
|
||||
describe('historie cen', () => {
|
||||
beforeEach(async () => {
|
||||
process.env.ADMIN_PASSWORD = ADMIN_PW;
|
||||
await getStorage().setData('stores', [{ name: 'New York', urls: [BOLT_URL] }]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env.ADMIN_PASSWORD;
|
||||
});
|
||||
|
||||
test('diffMenus najde změněné ceny, nové i odebrané položky', () => {
|
||||
expect(diffMenus(
|
||||
[{ name: 'Burger', price: 150 }, { name: 'Hranolky', price: 59 }, { name: 'Salát', price: 99 }],
|
||||
[{ name: 'burger', price: 165 }, { name: 'Hranolky', price: 59 }, { name: 'Wrap', price: 129 }],
|
||||
)).toEqual([
|
||||
{ name: 'burger', before: 150, after: 165 },
|
||||
{ name: 'Wrap', after: 129 },
|
||||
{ name: 'Salát', before: 99 },
|
||||
]);
|
||||
});
|
||||
|
||||
test('první stažení uloží výchozí stav, další jen změny (beze změn nic)', async () => {
|
||||
const changed = boltResponse();
|
||||
changed.data.data.items[100 as any].price = { value: 199 };
|
||||
|
||||
mockedAxios.get.mockResolvedValueOnce(boltResponse()).mockResolvedValueOnce(boltResponse()).mockResolvedValueOnce(changed);
|
||||
await getStoreMenu('New York');
|
||||
await refreshStoreMenu('New York', 'bolt', ADMIN_PW); // beze změny
|
||||
await refreshStoreMenu('New York', 'bolt', ADMIN_PW); // Big Cheeseburger 189 → 199
|
||||
|
||||
const history = await getStoreMenuHistory('new york');
|
||||
expect(history.store).toBe('New York');
|
||||
const entries = history.providers[0].entries;
|
||||
expect(entries).toHaveLength(2);
|
||||
expect(entries[0].changes).toEqual([{ name: 'Big Cheeseburger', before: 189, after: 199 }]);
|
||||
expect(entries[1]).toEqual(expect.objectContaining({ initial: true, itemCount: 7, changes: [] }));
|
||||
});
|
||||
|
||||
test('neúspěšné stažení historii nemění', async () => {
|
||||
mockedAxios.get.mockRejectedValue(new Error('HTTP 500'));
|
||||
await getStoreMenu('New York');
|
||||
expect((await getStoreMenuHistory('New York')).providers).toEqual([]);
|
||||
});
|
||||
|
||||
test('nabídka stažená před zavedením historie se zobrazí jako výchozí verze', async () => {
|
||||
await getStorage().setData('store_menus', {
|
||||
'New York': { bolt: { url: BOLT_URL, dishes: [{ name: 'Burger', price: 150 }], failures: 0, blocked: false, lastSuccessAt: '2026-09-01T10:00:00.000Z' } },
|
||||
});
|
||||
const history = await getStoreMenuHistory('New York');
|
||||
expect(history.providers).toEqual([
|
||||
{ provider: 'bolt', entries: [{ at: '2026-09-01T10:00:00.000Z', initial: true, itemCount: 1, changes: [] }] },
|
||||
]);
|
||||
});
|
||||
|
||||
test('první změna u nabídky stažené před zavedením historie zachová výchozí verzi', async () => {
|
||||
await getStorage().setData('store_menus', {
|
||||
'New York': { bolt: { url: BOLT_URL, dishes: [{ name: 'Chicken Burger', price: 150, isMain: true }], failures: 0, blocked: false, lastSuccessAt: '2026-09-01T10:00:00.000Z' } },
|
||||
});
|
||||
mockedAxios.get.mockResolvedValue(boltResponse());
|
||||
await refreshStoreMenu('New York', 'bolt', ADMIN_PW);
|
||||
const entries = (await getStoreMenuHistory('New York')).providers[0].entries;
|
||||
expect(entries).toHaveLength(2);
|
||||
expect(entries[0].changes).toContainEqual({ name: 'Chicken Burger', before: 150, after: 169 });
|
||||
expect(entries[1]).toEqual({ at: '2026-09-01T10:00:00.000Z', initial: true, itemCount: 1, changes: [] });
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { resetMemoryStorage } from '../storage/memory';
|
||||
import getStorage from '../storage';
|
||||
import { getStores, addStore, updateStore, removeStore } from '../stores';
|
||||
import { getStores, addStore, updateStore, removeStore, getStoreDishes } from '../stores';
|
||||
|
||||
const ADMIN_PW = 'testadmin';
|
||||
|
||||
@@ -213,3 +213,27 @@ describe('removeStore', () => {
|
||||
expect(names(stores)).toContain('McDonald\'s');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStoreDishes', () => {
|
||||
/** Uloží skupinové objednávky pro daný den ve formátu `YYYY-MM-DD_extra`. */
|
||||
const saveGroups = (date: string, groups: { name: string, notes: string[] }[]) =>
|
||||
getStorage().setData(`${date}_extra`, {
|
||||
groups: groups.map((g, gi) => ({
|
||||
id: `${date}-${gi}`, name: g.name, creatorLogin: 'a', state: 'delivered',
|
||||
members: Object.fromEntries(g.notes.map((note, i) => [`user${i}`, { note }])),
|
||||
})),
|
||||
});
|
||||
|
||||
test('vrátí jídla dle četnosti, bez duplicit, polévek a čísel položek', async () => {
|
||||
await getStorage().setData('stores', [{ name: 'China Express' }, { name: 'Everest' }]);
|
||||
await saveGroups('2026-09-01', [{ name: 'China Express', notes: ['28. Vepřové v pekingské omáčce', 'Kuře na medu', 'Polévka Pho'] }]);
|
||||
await saveGroups('2026-09-02', [{ name: 'china express', notes: ['kuře na medu', ' ', '⬇️ (to samé)', 'Hovězí se zeleninou'] }]);
|
||||
await saveGroups('2026-09-03', [{ name: 'Neznámý podnik', notes: ['Burger'] }]);
|
||||
|
||||
const result = await getStoreDishes();
|
||||
expect(result).toEqual([
|
||||
{ store: 'China Express', dishes: ['Kuře na medu', 'Hovězí se zeleninou', 'Vepřové v pekingské omáčce'] },
|
||||
{ store: 'Everest', dishes: [] },
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user