fix: opravy generování QR kódů, zobrazení také na stránce objednání
CI / Generate TypeScript types (push) Successful in 14s
CI / Server unit tests (push) Successful in 21s
CI / Build server (push) Successful in 25s
CI / Build client (push) Successful in 34s
CI / Playwright E2E tests (push) Successful in 1m18s
CI / Build and push Docker image (push) Successful in 49s
CI / Notify (push) Successful in 3s
CI / Generate TypeScript types (push) Successful in 14s
CI / Server unit tests (push) Successful in 21s
CI / Build server (push) Successful in 25s
CI / Build client (push) Successful in 34s
CI / Playwright E2E tests (push) Successful in 1m18s
CI / Build and push Docker image (push) Successful in 49s
CI / Notify (push) Successful in 3s
This commit is contained in:
+13
-53
@@ -16,12 +16,12 @@ import Footer from './components/Footer';
|
|||||||
import { faArrowUpRightFromSquare, faBasketShopping, faChainBroken, faChevronLeft, faChevronRight, faGear, faMoneyBillTransfer, faSatelliteDish, faSearch, faTriangleExclamation } from '@fortawesome/free-solid-svg-icons';
|
import { faArrowUpRightFromSquare, faBasketShopping, faChainBroken, faChevronLeft, faChevronRight, faGear, faMoneyBillTransfer, faSatelliteDish, faSearch, faTriangleExclamation } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import Loader from './components/Loader';
|
import Loader from './components/Loader';
|
||||||
import { getHumanDateTime, isInTheFuture, formatDateString } from './Utils';
|
import { getHumanDateTime, isInTheFuture } from './Utils';
|
||||||
import NoteModal from './components/modals/NoteModal';
|
import NoteModal from './components/modals/NoteModal';
|
||||||
import ConfirmModal from './components/modals/ConfirmModal';
|
|
||||||
import PayForAllModal from './components/modals/PayForAllModal';
|
import PayForAllModal from './components/modals/PayForAllModal';
|
||||||
|
import PendingPayments from './components/PendingPayments';
|
||||||
import { useEasterEgg } from './context/eggs';
|
import { useEasterEgg } from './context/eggs';
|
||||||
import { ClientData, Food, MealSlot, PendingQr, PizzaOrder, DepartureTime, PizzaDayState, Restaurant, RestaurantDayMenu, RestaurantDayMenuMap, LunchChoice, LocationLunchChoicesMap, UserLunchChoice, PizzaVariant, getData, getEasterEggImage, addPizza, removePizza, updatePizzaDayNote, createPizzaDay, deletePizzaDay, lockPizzaDay, unlockPizzaDay, finishOrder, finishDelivery, addChoice, jdemeObed, removeChoices, removeChoice, updateNote, changeDepartureTime, setBuyer, dismissQr, generateQr } from '../../types';
|
import { ClientData, Food, MealSlot, PendingQr, PizzaOrder, DepartureTime, PizzaDayState, Restaurant, RestaurantDayMenu, RestaurantDayMenuMap, LunchChoice, LocationLunchChoicesMap, UserLunchChoice, PizzaVariant, getData, getEasterEggImage, addPizza, removePizza, updatePizzaDayNote, createPizzaDay, deletePizzaDay, lockPizzaDay, unlockPizzaDay, finishOrder, finishDelivery, addChoice, jdemeObed, removeChoices, removeChoice, updateNote, changeDepartureTime, setBuyer, generateQr } from '../../types';
|
||||||
import { getLunchChoiceName } from './enums';
|
import { getLunchChoiceName } from './enums';
|
||||||
// import FallingLeaves, { LEAF_PRESETS, LEAF_COLOR_THEMES } from './FallingLeaves';
|
// import FallingLeaves, { LEAF_PRESETS, LEAF_COLOR_THEMES } from './FallingLeaves';
|
||||||
// import './FallingLeaves.scss';
|
// import './FallingLeaves.scss';
|
||||||
@@ -78,7 +78,6 @@ function App() {
|
|||||||
const [dayIndex, setDayIndex] = useState<number>();
|
const [dayIndex, setDayIndex] = useState<number>();
|
||||||
const [loadingPizzaDay, setLoadingPizzaDay] = useState<boolean>(false);
|
const [loadingPizzaDay, setLoadingPizzaDay] = useState<boolean>(false);
|
||||||
const [noteModalOpen, setNoteModalOpen] = useState<boolean>(false);
|
const [noteModalOpen, setNoteModalOpen] = useState<boolean>(false);
|
||||||
const [dismissQrId, setDismissQrId] = useState<string | null>(null);
|
|
||||||
const [payForAllLocationKey, setPayForAllLocationKey] = useState<LunchChoice | null>(null);
|
const [payForAllLocationKey, setPayForAllLocationKey] = useState<LunchChoice | null>(null);
|
||||||
const [eggImage, setEggImage] = useState<Blob>();
|
const [eggImage, setEggImage] = useState<Blob>();
|
||||||
const eggRef = useRef<HTMLImageElement>(null);
|
const eggRef = useRef<HTMLImageElement>(null);
|
||||||
@@ -888,42 +887,21 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<PizzaOrderList state={data.pizzaDay.state!} orders={data.pizzaDay.orders!} onDelete={handlePizzaDelete} creator={data.pizzaDay.creator!} />
|
<PizzaOrderList state={data.pizzaDay.state!} orders={data.pizzaDay.orders!} onDelete={handlePizzaDelete} creator={data.pizzaDay.creator!} />
|
||||||
{
|
|
||||||
data.pizzaDay.state === PizzaDayState.DELIVERED && myOrder?.hasQr && (() => {
|
|
||||||
const pizzaQr = data.pendingQrs?.find(qr => qr.creator === data.pizzaDay?.creator);
|
|
||||||
return pizzaQr ? (
|
|
||||||
<div className='qr-code'>
|
|
||||||
<h3>QR platba</h3>
|
|
||||||
<img src={`/api/qr?login=${auth.login}&id=${pizzaQr.id}`} alt='QR kód' />
|
|
||||||
</div>
|
|
||||||
) : null;
|
|
||||||
})()
|
|
||||||
}
|
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
{data.pendingQrs && data.pendingQrs.length > 0 &&
|
<PendingPayments
|
||||||
<div className='pizza-section fade-in mt-4'>
|
pendingQrs={data.pendingQrs}
|
||||||
<h3>Nevyřízené platby</h3>
|
login={auth.login}
|
||||||
<p>Máte neuhrazené platby.</p>
|
onDismissed={async () => {
|
||||||
{data.pendingQrs.map(qr => (
|
const response = await getData({ query: { dayIndex } });
|
||||||
<div key={qr.id} className='qr-code mb-3'>
|
if (response.data) {
|
||||||
<p>
|
setData(response.data);
|
||||||
<strong>{formatDateString(qr.date)}</strong> — {qr.creator} ({qr.totalPrice / 100} Kč)
|
}
|
||||||
{qr.purpose && <><br /><span className="text-muted">{qr.purpose}</span></>}
|
}}
|
||||||
</p>
|
/>
|
||||||
<img src={`/api/qr?login=${auth.login}&id=${qr.id}`} alt='QR kód' />
|
|
||||||
<div className='mt-2'>
|
|
||||||
<Button variant="success" onClick={() => setDismissQrId(qr.id)}>
|
|
||||||
Zaplatil jsem
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</>
|
</>
|
||||||
</div>
|
</div>
|
||||||
{/* <FallingLeaves
|
{/* <FallingLeaves
|
||||||
@@ -932,24 +910,6 @@ function App() {
|
|||||||
/> */}
|
/> */}
|
||||||
<Footer />
|
<Footer />
|
||||||
<NoteModal isOpen={noteModalOpen} onClose={() => setNoteModalOpen(false)} onSave={saveNote} />
|
<NoteModal isOpen={noteModalOpen} onClose={() => setNoteModalOpen(false)} onSave={saveNote} />
|
||||||
<ConfirmModal
|
|
||||||
isOpen={dismissQrId !== null}
|
|
||||||
title="Potvrzení platby"
|
|
||||||
message="Opravdu jste zaplatili? QR kód bude odstraněn."
|
|
||||||
confirmLabel="Zaplatil jsem"
|
|
||||||
confirmVariant="success"
|
|
||||||
onClose={() => setDismissQrId(null)}
|
|
||||||
onConfirm={async () => {
|
|
||||||
if (!dismissQrId) return;
|
|
||||||
const id = dismissQrId;
|
|
||||||
setDismissQrId(null);
|
|
||||||
await dismissQr({ body: { id } });
|
|
||||||
const response = await getData({ query: { dayIndex } });
|
|
||||||
if (response.data) {
|
|
||||||
setData(response.data);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{payForAllLocationKey && data && (
|
{payForAllLocationKey && data && (
|
||||||
<PayForAllModal
|
<PayForAllModal
|
||||||
isOpen
|
isOpen
|
||||||
|
|||||||
@@ -109,4 +109,17 @@ export function getHumanDate(date: Date) {
|
|||||||
export function formatDateString(dateString: string): string {
|
export function formatDateString(dateString: string): string {
|
||||||
const [year, month, day] = dateString.split('-');
|
const [year, month, day] = dateString.split('-');
|
||||||
return `${day}.${month}.${year}`;
|
return `${day}.${month}.${year}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Očistí zprávu (účel platby) pro QR platbu – musí odpovídat serverové logice (qr.ts):
|
||||||
|
* transliteruje diakritiku na základní písmena (š→s, č→c, ...), odstraní znaky mimo
|
||||||
|
* ISO 8859-1 a hvězdičku (oddělovač polí v QR platbě) a ořízne na max. 60 znaků.
|
||||||
|
*/
|
||||||
|
export function sanitizeQrMessage(message: string): string {
|
||||||
|
const sanitized = message
|
||||||
|
.normalize('NFD').replace(/[\u0300-\u036f]/g, '') // diakritika → základní písmeno
|
||||||
|
.replace(/[^\x00-\xff]/g, '') // znaky mimo ISO 8859-1
|
||||||
|
.replace(/\*/g, ''); // '*' je v QR platbě oddělovač
|
||||||
|
return sanitized.length > 60 ? sanitized.substring(0, 60) : sanitized;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Button } from 'react-bootstrap';
|
||||||
|
import { PendingQr, dismissQr } from '../../../types';
|
||||||
|
import { formatDateString } from '../Utils';
|
||||||
|
import ConfirmModal from './modals/ConfirmModal';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
pendingQrs?: PendingQr[];
|
||||||
|
login?: string;
|
||||||
|
// Zavolá se po úspěšném potvrzení platby, aby si rodič mohl znovu načíst data
|
||||||
|
onDismissed?: () => void | Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sekce "Nevyřízené platby" – zobrazí QR kódy neuhrazených plateb přihlášeného uživatele
|
||||||
|
// včetně tlačítka "Zaplatil jsem" a potvrzovacího dialogu. Sdíleno hlavní stránkou i stránkou objednávek.
|
||||||
|
export default function PendingPayments({ pendingQrs, login, onDismissed }: Readonly<Props>) {
|
||||||
|
const [dismissQrId, setDismissQrId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
if (!pendingQrs || pendingQrs.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className='pizza-section fade-in mt-4'>
|
||||||
|
<h3>Nevyřízené platby</h3>
|
||||||
|
<p>Máte neuhrazené platby.</p>
|
||||||
|
{pendingQrs.map(qr => (
|
||||||
|
<div key={qr.id} className='qr-code mb-3'>
|
||||||
|
<p>
|
||||||
|
<strong>{formatDateString(qr.date)}</strong> — {qr.creator} ({qr.totalPrice / 100} Kč)
|
||||||
|
{qr.purpose && <><br /><span className="text-muted">{qr.purpose}</span></>}
|
||||||
|
</p>
|
||||||
|
<img src={`/api/qr?login=${login}&id=${qr.id}`} alt='QR kód' />
|
||||||
|
<div className='mt-2'>
|
||||||
|
<Button variant="success" onClick={() => setDismissQrId(qr.id)}>
|
||||||
|
Zaplatil jsem
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={dismissQrId !== null}
|
||||||
|
title="Potvrzení platby"
|
||||||
|
message="Opravdu jste zaplatili? QR kód bude odstraněn."
|
||||||
|
confirmLabel="Zaplatil jsem"
|
||||||
|
confirmVariant="success"
|
||||||
|
onClose={() => setDismissQrId(null)}
|
||||||
|
onConfirm={async () => {
|
||||||
|
if (!dismissQrId) return;
|
||||||
|
const id = dismissQrId;
|
||||||
|
setDismissQrId(null);
|
||||||
|
await dismissQr({ body: { id } });
|
||||||
|
await onDismissed?.();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Modal, Button, Form, Table, Alert } from "react-bootstrap";
|
import { Modal, Button, Form, Table, Alert } from "react-bootstrap";
|
||||||
import { generateQr, OrderGroup, OrderGroupMember, QrRecipient } from "../../../../types";
|
import { generateQr, OrderGroup, OrderGroupMember, QrRecipient } from "../../../../types";
|
||||||
|
import { sanitizeQrMessage } from "../../Utils";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -77,7 +78,7 @@ export default function PayForGroupModal({ isOpen, onClose, onSuccess, group, pa
|
|||||||
const note = d.member.note?.trim();
|
const note = d.member.note?.trim();
|
||||||
recipients.push({
|
recipients.push({
|
||||||
login: d.login,
|
login: d.login,
|
||||||
purpose: note ? note.replace(/[^\x00-\xff*]/g, '').replace(/\*/g, '').substring(0, 60) : `Objednávka ${group.name}`.substring(0, 60),
|
purpose: sanitizeQrMessage(note || `Objednávka ${group.name}`),
|
||||||
amount: total,
|
amount: total,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|||||||
import { faTrashCan } from '@fortawesome/free-regular-svg-icons';
|
import { faTrashCan } from '@fortawesome/free-regular-svg-icons';
|
||||||
import { faBasketShopping, faCircleCheck, faGear, faLock, faLockOpen, faSearch, faUserPlus } from '@fortawesome/free-solid-svg-icons';
|
import { faBasketShopping, faCircleCheck, faGear, faLock, faLockOpen, faSearch, faUserPlus } from '@fortawesome/free-solid-svg-icons';
|
||||||
import {
|
import {
|
||||||
ClientData, GroupState, MealSlot, OrderGroup, OrderGroupMember,
|
ClientData, GroupState, MealSlot, OrderGroup, OrderGroupMember, PendingQr,
|
||||||
getData, createGroup, deleteGroup, addGroupMember, removeGroupMember, updateGroupMember, setGroupState, updateGroupTimes,
|
getData, createGroup, deleteGroup, addGroupMember, removeGroupMember, updateGroupMember, setGroupState, updateGroupTimes,
|
||||||
} from '../../../types';
|
} from '../../../types';
|
||||||
import { EVENT_MESSAGE, SocketContext } from '../context/socket';
|
import { EVENT_MESSAGE, EVENT_PENDING_QR, SocketContext } from '../context/socket';
|
||||||
import { useAuth } from '../context/auth';
|
import { useAuth } from '../context/auth';
|
||||||
import { useSettings } from '../context/settings';
|
import { useSettings } from '../context/settings';
|
||||||
import Login from '../Login';
|
import Login from '../Login';
|
||||||
@@ -17,6 +17,7 @@ import Loader from '../components/Loader';
|
|||||||
import StoreAdminModal from '../components/modals/StoreAdminModal';
|
import StoreAdminModal from '../components/modals/StoreAdminModal';
|
||||||
import PayForGroupModal from '../components/modals/PayForGroupModal';
|
import PayForGroupModal from '../components/modals/PayForGroupModal';
|
||||||
import EditGroupFeesModal from '../components/modals/EditGroupFeesModal';
|
import EditGroupFeesModal from '../components/modals/EditGroupFeesModal';
|
||||||
|
import PendingPayments from '../components/PendingPayments';
|
||||||
|
|
||||||
const SLOT = MealSlot.EXTRA;
|
const SLOT = MealSlot.EXTRA;
|
||||||
const TIME_REGEX = /^([01]\d|2[0-3]):[0-5]\d$/;
|
const TIME_REGEX = /^([01]\d|2[0-3]):[0-5]\d$/;
|
||||||
@@ -70,7 +71,11 @@ export default function OrderGroupsPage() {
|
|||||||
stores: newData.stores ?? prev?.stores,
|
stores: newData.stores ?? prev?.stores,
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
return () => { socket.off(EVENT_MESSAGE); };
|
// Nová nevyřízená platba (QR kód) – připojíme do dat, aby se zobrazila i bez znovunačtení stránky
|
||||||
|
socket.on(EVENT_PENDING_QR, (pendingQr: PendingQr) => {
|
||||||
|
setData(prev => prev ? { ...prev, pendingQrs: [...(prev.pendingQrs ?? []), pendingQr] } : prev);
|
||||||
|
});
|
||||||
|
return () => { socket.off(EVENT_MESSAGE); socket.off(EVENT_PENDING_QR); };
|
||||||
}, [socket]);
|
}, [socket]);
|
||||||
|
|
||||||
const refresh = async (fn: () => Promise<any>): Promise<boolean> => {
|
const refresh = async (fn: () => Promise<any>): Promise<boolean> => {
|
||||||
@@ -545,6 +550,13 @@ export default function OrderGroupsPage() {
|
|||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
{/* Nevyřízené platby přihlášeného uživatele */}
|
||||||
|
<PendingPayments
|
||||||
|
pendingQrs={data.pendingQrs}
|
||||||
|
login={auth.login}
|
||||||
|
onDismissed={fetchData}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
[
|
||||||
|
"Zobrazení neuhrazených plateb i na stránce objednávek",
|
||||||
|
"Oprava duplicitního zobrazení QR kódu u Pizza day",
|
||||||
|
"Odstranění diakritiky v platebních QR kódech"
|
||||||
|
]
|
||||||
+19
-5
@@ -44,6 +44,24 @@ function createStorageKey(customerName: string, id: string): string {
|
|||||||
return `qr_${nameHash}_${id}`;
|
return `qr_${nameHash}_${id}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Očistí zprávu (účel platby) pro QR platbu:
|
||||||
|
* - transliteruje diakritiku na základní písmena (š→s, č→c, ř→r, ...)
|
||||||
|
* - odstraní zbylé znaky mimo ISO 8859-1
|
||||||
|
* - odstraní '*', který v QR platbě slouží jako oddělovač polí
|
||||||
|
* - ořízne na max. 60 znaků
|
||||||
|
*
|
||||||
|
* @param message původní zpráva
|
||||||
|
* @returns očištěná zpráva vhodná pro QR platbu
|
||||||
|
*/
|
||||||
|
export function sanitizeQrMessage(message: string): string {
|
||||||
|
const sanitized = message
|
||||||
|
.normalize('NFD').replace(/[\u0300-\u036f]/g, '') // diakritika → základní písmeno
|
||||||
|
.replace(/[^\x00-\xff]/g, '') // znaky mimo ISO 8859-1
|
||||||
|
.replace(/\*/g, ''); // '*' je v QR platbě oddělovač
|
||||||
|
return sanitized.length > 60 ? sanitized.substring(0, 60) : sanitized;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vygeneruje a uloží obrázek platebního QR kódu do storage (Redis/JSON).
|
* Vygeneruje a uloží obrázek platebního QR kódu do storage (Redis/JSON).
|
||||||
* Data přežijí redeploy — není třeba persistentní filesystém.
|
* Data přežijí redeploy — není třeba persistentní filesystém.
|
||||||
@@ -56,11 +74,7 @@ function createStorageKey(customerName: string, id: string): string {
|
|||||||
* @param id unikátní identifikátor (UUID) tohoto QR kódu
|
* @param id unikátní identifikátor (UUID) tohoto QR kódu
|
||||||
*/
|
*/
|
||||||
export async function generateQr(customerName: string, bankAccountNumber: string, bankAccountHolder: string, amount: number, message: string, id: string): Promise<void> {
|
export async function generateQr(customerName: string, bankAccountNumber: string, bankAccountHolder: string, amount: number, message: string, id: string): Promise<void> {
|
||||||
// Zpráva nesmí obsahovat '*' a znaky mimo ISO 8859-1; délka max. 60 znaků
|
message = sanitizeQrMessage(message);
|
||||||
message = message.replace(/[^\x00-\xff]/g, '').replace(/\*/g, '');
|
|
||||||
if (message.length > 60) {
|
|
||||||
message = message.substring(0, 60);
|
|
||||||
}
|
|
||||||
const payload = {
|
const payload = {
|
||||||
iban: convertBbanToIban(bankAccountNumber),
|
iban: convertBbanToIban(bankAccountNumber),
|
||||||
amount,
|
amount,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { convertBbanToIban } from '../qr';
|
import { convertBbanToIban, sanitizeQrMessage } from '../qr';
|
||||||
|
|
||||||
test('konverze BBAN s prefixem na IBAN', () => {
|
test('konverze BBAN s prefixem na IBAN', () => {
|
||||||
// Číslo účtu 19-2000145399/0800 (Česká spořitelna) → CZ6508000000192000145399
|
// Číslo účtu 19-2000145399/0800 (Česká spořitelna) → CZ6508000000192000145399
|
||||||
@@ -34,3 +34,26 @@ test('výsledek vždy začíná CZ', () => {
|
|||||||
expect(convertBbanToIban('100-2000145399/0300')).toMatch(/^CZ/);
|
expect(convertBbanToIban('100-2000145399/0300')).toMatch(/^CZ/);
|
||||||
expect(convertBbanToIban('2000145399/0100')).toMatch(/^CZ/);
|
expect(convertBbanToIban('2000145399/0100')).toMatch(/^CZ/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('sanitizace zprávy – diakritika se transliteruje na základní písmena', () => {
|
||||||
|
expect(sanitizeQrMessage('Pizza Šunková')).toBe('Pizza Sunkova');
|
||||||
|
expect(sanitizeQrMessage('čaj a káva')).toBe('caj a kava');
|
||||||
|
expect(sanitizeQrMessage('Žížala, řeřicha, ďábel')).toBe('Zizala, rericha, dabel');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sanitizace zprávy – hvězdička se odstraní', () => {
|
||||||
|
expect(sanitizeQrMessage('Pizza *akce* 1+1')).toBe('Pizza akce 1+1');
|
||||||
|
expect(sanitizeQrMessage('***')).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sanitizace zprávy – znaky mimo ISO 8859-1 se odstraní', () => {
|
||||||
|
// Emoji a CJK znaky nemají ASCII ekvivalent → zmizí, zbytek zůstane
|
||||||
|
expect(sanitizeQrMessage('Oběd 🍕 hotovo')).toBe('Obed hotovo');
|
||||||
|
// Znaky v rozsahu ISO 8859-1 (např. § ° é) zůstanou zachovány
|
||||||
|
expect(sanitizeQrMessage('Cena 100°C § café')).toBe('Cena 100°C § cafe');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sanitizace zprávy – ořez na 60 znaků', () => {
|
||||||
|
const long = 'a'.repeat(70);
|
||||||
|
expect(sanitizeQrMessage(long)).toHaveLength(60);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user