Struktura podle zasad: rozdeleni souboru, lint, testy, prisny TypeScript

Projekt srovnan se zasadami v D:\GitHubRepository\CLAUDE.md bez zmeny chovani.

Struktura: scripts/ (skripty konektoru) -> connectors/, src/scripts ->
src/runtime/scripts; src/index.ts jen startuje, novy src/app.ts s createApp();
routes/dashboard.ts a routes/settings.ts rozdeleny do slozek; openapi.ts
rozdelen na openapi/{index,helpers,components} a paths/* (98 cest overeno
shodnych); ticketStore, automationStore a services jsou fasady nad slozkami
data/tickets, data/automations a data/services/catalog. process.env se cte
jen v config.ts. Web: hooky v hooks/, sdilena ui/Table a ui/ServiceIcon,
surove inputy nahrazeny komponentami, sedm velkych souboru rozdeleno.

Nastroje: eslint (typescript-eslint, react-hooks v7), prettier, editorconfig,
nvmrc, .env.example, vitest; skripty lint, format, test. Lint je cisty bez
jedineho eslint-disable (nove hooky useLatest a useSyncFromSource, odvozeny
stav misto setState v effectu). noUncheckedIndexedAccess v obou tsconfig,
84 mist zuzeno bez non-null operatoru; odhalilo zalohu backoffu fronty pri
nule pokusu a Retry-After NaN pri max 0. Cely kod naformatovan prettierem.

Testy: 8 souboru, 105 testu (prava, viditelnost, podminky a opakovani
v executoru, redaktor tajemstvi, sitove guardy, migrace resitelu, tickety,
health a prihlaseni pres supertest). Testy odhalily dve chyby ve vyhodnoceni
podminek, obe opravene: chybejici castka se porovnavala jako nula a podminka
nad vystupem druheho kroku cetla hodnotu prvniho se stejnym nazvem.

Pojmenovane konstanty misto magickych hodnot, ctx.util.base64 pro skripty
konektoru, README a dokumentace aktualizovany vcetne znamych odchylek.
This commit is contained in:
JiriUhlir
2026-09-09 15:11:02 +02:00
parent 42b3da8303
commit 22dda2d139
312 changed files with 21085 additions and 15319 deletions
+556
View File
@@ -0,0 +1,556 @@
/**
* Vykonavani stromu kroku (src/runtime/executor.ts).
*
* Vyhodnoceni podminky neni exportovane, testuje se pres `runFlow`. Vnitrni
* kroky, skripty a konektory jsou nahrazene atrapami: strom se tu vykonava
* bez cizi sluzby a bez uloziste, aby slo overit jen rozhodovani executoru.
*/
import { beforeEach, describe, expect, test, vi } from 'vitest';
import type { StepContext, StepOutcome } from '../../src/runtime/builtinSteps.js';
import type { FlowStep } from '../../src/data/automationStore.js';
import type { ScriptRunResult } from '../../src/shared/scripts.js';
vi.mock('../../src/runtime/builtinSteps.js', () => ({ findBuiltinStep: vi.fn() }));
vi.mock('../../src/runtime/scripts/lookup.js', () => ({ scriptIdFor: vi.fn() }));
vi.mock('../../src/runtime/scripts/runner.js', () => ({ runScript: vi.fn() }));
vi.mock('../../src/data/connectorStore.js', () => ({
getConnector: vi.fn(),
defaultConnectorFor: vi.fn(),
}));
import { findBuiltinStep } from '../../src/runtime/builtinSteps.js';
import { scriptIdFor } from '../../src/runtime/scripts/lookup.js';
import { runScript } from '../../src/runtime/scripts/runner.js';
import { defaultConnectorFor, getConnector } from '../../src/data/connectorStore.js';
import { matchOf, rulesOf } from '../../src/data/automationStore.js';
import { runFlow, type RunContext, type RunOptions } from '../../src/runtime/executor.js';
type Handler = (
inputs: Record<string, string>,
context: StepContext,
) => Promise<StepOutcome> | StepOutcome;
type ConditionStep = Extract<FlowStep, { kind: 'condition' }>;
/** Vnitrni kroky sluzby `test`, ktere v testu existuji. */
const handlers: Record<string, Handler> = {};
function options(extra: Partial<RunOptions> = {}): RunOptions {
return { tenantId: 'tnt_a', ticketId: null, idempotencyKey: null, ...extra };
}
function condition(
rules: ConditionStep['rules'],
extra: Partial<Omit<ConditionStep, 'kind' | 'rules'>> = {},
): ConditionStep {
return { id: 'cond', kind: 'condition', rules, yes: [], no: [], ...extra };
}
function action(id: string, operationId = 'ok', inputs: Record<string, string> = {}): FlowStep {
return { id, kind: 'action', serviceId: 'test', operationId, inputs };
}
async function branchOf(step: ConditionStep, context: RunContext, extra: Partial<RunOptions> = {}) {
const result = await runFlow([step], context, options(extra));
return result.steps[0]?.branch;
}
function scriptResult(partial: Partial<ScriptRunResult>): ScriptRunResult {
return {
ok: true,
scriptId: 'svc/op',
outputs: {},
logs: [],
durationMs: 1,
httpCalls: 0,
error: null,
...partial,
};
}
beforeEach(() => {
vi.mocked(findBuiltinStep).mockReset();
vi.mocked(findBuiltinStep).mockImplementation((serviceId, operationId) =>
serviceId === 'test' ? handlers[operationId] : undefined,
);
vi.mocked(scriptIdFor).mockReset();
vi.mocked(scriptIdFor).mockReturnValue(undefined);
vi.mocked(runScript).mockReset();
vi.mocked(getConnector).mockReset();
vi.mocked(defaultConnectorFor).mockReset();
vi.mocked(defaultConnectorFor).mockResolvedValue(undefined);
handlers.ok = (inputs) => ({
ok: true,
summary: 'hotovo',
outputs: { echo: inputs.value ?? '' },
});
});
describe('podminky nad retezci', () => {
test('eq, neq, contains a startsWith bez ohledu na velikost pismen', async () => {
const context = { status: 'Open', subject: 'Reklamace faktury 2026' };
expect(
await branchOf(condition([{ fieldId: 'status', operator: 'eq', value: 'Open' }]), context),
).toBe('yes');
expect(
await branchOf(condition([{ fieldId: 'status', operator: 'eq', value: 'open' }]), context),
).toBe('no');
expect(
await branchOf(condition([{ fieldId: 'status', operator: 'neq', value: 'Closed' }]), context),
).toBe('yes');
expect(
await branchOf(
condition([{ fieldId: 'subject', operator: 'contains', value: 'FAKTURY' }]),
context,
),
).toBe('yes');
expect(
await branchOf(
condition([{ fieldId: 'subject', operator: 'startsWith', value: 'reklamace' }]),
context,
),
).toBe('yes');
expect(
await branchOf(
condition([{ fieldId: 'subject', operator: 'startsWith', value: 'faktury' }]),
context,
),
).toBe('no');
});
test('isEmpty plati pro prazdny text, null i chybejici hodnotu', async () => {
const empty = condition([{ fieldId: 'note', operator: 'isEmpty' }]);
expect(await branchOf(empty, { note: ' ' })).toBe('yes');
expect(await branchOf(empty, { note: null })).toBe('yes');
expect(await branchOf(empty, {})).toBe('yes');
expect(await branchOf(empty, { note: 'x' })).toBe('no');
expect(
await branchOf(condition([{ fieldId: 'note', operator: 'isNotEmpty' }]), { note: 'x' }),
).toBe('yes');
});
test('isTrue a isFalse rozumi i textovym podobam', async () => {
const isTrue = condition([{ fieldId: 'vip', operator: 'isTrue' }]);
const isFalse = condition([{ fieldId: 'vip', operator: 'isFalse' }]);
expect(await branchOf(isTrue, { vip: true })).toBe('yes');
expect(await branchOf(isTrue, { vip: '1' })).toBe('yes');
expect(await branchOf(isTrue, { vip: 'yes' })).toBe('no');
expect(await branchOf(isFalse, { vip: '0' })).toBe('yes');
expect(await branchOf(isFalse, {})).toBe('yes');
});
});
describe('podminky nad cisly a daty', () => {
test('gt, gte, lt, lte porovnavaji cisla, ne texty', async () => {
const context = { total: 1500, count: '9' };
expect(
await branchOf(condition([{ fieldId: 'total', operator: 'gt', value: '1000' }]), context),
).toBe('yes');
expect(
await branchOf(condition([{ fieldId: 'total', operator: 'lt', value: '1000' }]), context),
).toBe('no');
expect(
await branchOf(condition([{ fieldId: 'total', operator: 'gte', value: '1500' }]), context),
).toBe('yes');
expect(
await branchOf(condition([{ fieldId: 'total', operator: 'lte', value: '1499' }]), context),
).toBe('no');
// Textove by "9" > "10" platilo, ciselne ne.
expect(
await branchOf(condition([{ fieldId: 'count', operator: 'gt', value: '10' }]), context),
).toBe('no');
});
test('co neni cislo, se porovnat neda a podminka neplati', async () => {
expect(
await branchOf(condition([{ fieldId: 'total', operator: 'gt', value: '1' }]), {
total: 'hodne',
}),
).toBe('no');
expect(
await branchOf(condition([{ fieldId: 'total', operator: 'gte', value: '1' }]), {
total: '12,5',
}),
).toBe('no');
});
/*
* Chybejici nebo prazdna hodnota se v `ordered` (executor.ts:684) prevede
* pres `Number('')` na 0, takze "castka <= 1000" plati i pro castku,
* ktera nedorazila. Test zustava vypnuty, dokud se to v src neopravi.
*/
test('prazdna hodnota se pri gt/lt neporovnava jako nula', async () => {
expect(
await branchOf(condition([{ fieldId: 'total', operator: 'lte', value: '1000' }]), {}),
).toBe('no');
expect(
await branchOf(condition([{ fieldId: 'total', operator: 'lt', value: '1' }]), { total: '' }),
).toBe('no');
expect(
await branchOf(condition([{ fieldId: 'total', operator: 'gte', value: '0' }]), {
total: null,
}),
).toBe('no');
});
test('parametr typu datum se porovnava jako cas', async () => {
const context = { due: '2026-09-10' };
const typed = { fieldTypes: { due: 'date' as const } };
expect(
await branchOf(
condition([{ fieldId: 'due', operator: 'gt', value: '2026-09-01' }]),
context,
typed,
),
).toBe('yes');
expect(
await branchOf(
condition([{ fieldId: 'due', operator: 'lt', value: '2026-09-01' }]),
context,
typed,
),
).toBe('no');
expect(
await branchOf(
condition([{ fieldId: 'due', operator: 'gt', value: '2026-09-10T12:00:00Z' }]),
context,
typed,
),
).toBe('no');
expect(
await branchOf(
condition([{ fieldId: 'due', operator: 'eq', value: '2026-09-10' }]),
context,
typed,
),
).toBe('yes');
});
test('hodnoty, ktere vypadaji jako ISO datum, se porovnaji jako cas i bez typu', async () => {
const context = { createdAt: '2026-09-10T08:30:00.000Z' };
expect(
await branchOf(
condition([{ fieldId: 'createdAt', operator: 'gt', value: '2026-09-10' }]),
context,
),
).toBe('yes');
expect(
await branchOf(
condition([{ fieldId: 'createdAt', operator: 'lt', value: '2026-09-11' }]),
context,
),
).toBe('yes');
expect(
await branchOf(
condition([{ fieldId: 'createdAt', operator: 'gt', value: '2026-12-01' }]),
context,
),
).toBe('no');
});
});
describe('podminky nad seznamy a spojeni otazek', () => {
test('contains nad seznamem najde polozku', async () => {
const context = { tags: ['čeká na zabalení', 'vip'] };
expect(
await branchOf(condition([{ fieldId: 'tags', operator: 'contains', value: 'vip' }]), context),
).toBe('yes');
expect(
await branchOf(
condition([{ fieldId: 'tags', operator: 'contains', value: 'expres' }]),
context,
),
).toBe('no');
expect(
await branchOf(condition([{ fieldId: 'tags', operator: 'isEmpty' }]), { tags: [] }),
).toBe('no');
});
test('all vyzaduje vsechny otazky, any staci jedna', async () => {
const rules: ConditionStep['rules'] = [
{ fieldId: 'status', operator: 'eq', value: 'open' },
{ fieldId: 'priority', operator: 'eq', value: 'high' },
];
const context = { status: 'open', priority: 'low' };
expect(await branchOf(condition(rules), context)).toBe('no');
expect(await branchOf(condition(rules, { match: 'any' }), context)).toBe('yes');
});
test('parametr podle ID se najde pres jmeno i pres vystup kroku', async () => {
const names = { fieldNames: { f_9x1: 'callSid', 'st_kontakt.out_found': 'found' } };
expect(
await branchOf(
condition([{ fieldId: 'f_9x1', operator: 'eq', value: 'CA123' }]),
{ callSid: 'CA123' },
names,
),
).toBe('yes');
expect(
await branchOf(
condition([{ fieldId: 'st_kontakt.out_found', operator: 'isTrue' }]),
{ 'st_kontakt.found': true },
names,
),
).toBe('yes');
});
/*
* `conditionValue` (executor.ts:579-583) zkousi hole jmeno vystupu driv nez
* `krok.jmeno`, i kdyz komentar v kodu rika, ze presnejsi je to druhe.
* Hole jmeno navic drzi vystup **prvniho** kroku (publishOutputs ho
* neprepisuje), takze podminka nad druhym krokem se stejnym vystupem cte
* hodnotu z prvniho. Test zustava vypnuty, dokud se to v src neopravi.
*/
test('vystup kroku ma prednost pred holym jmenem z jineho kroku', async () => {
const names = { fieldNames: { 'st_b.out_found': 'found' } };
expect(
await branchOf(
condition([{ fieldId: 'st_b.out_found', operator: 'isTrue' }]),
{ 'st_a.found': false, found: false, 'st_b.found': true },
names,
),
).toBe('yes');
});
test('podminka bez otazky neplati a do logu jde, co chybelo', async () => {
const result = await runFlow([condition([])], {}, options());
expect(result.ok).toBe(true);
expect(result.steps[0]?.branch).toBe('no');
expect(result.steps[0]?.label).toBe('Podmínka bez otázky');
const missing = await runFlow(
[condition([{ fieldId: 'x', operator: 'eq', value: '1' }])],
{},
options(),
);
expect(missing.steps[0]?.detail).toContain('nedorazilo');
});
test('vykona se jen zvolena vetev', async () => {
const calls: string[] = [];
handlers.log = (inputs) => {
calls.push(inputs.value ?? '');
return { ok: true, summary: 'ok', outputs: {} };
};
const step = condition([{ fieldId: 'go', operator: 'isTrue' }], {
yes: [action('a', 'log', { value: 'yes' })],
no: [action('b', 'log', { value: 'no' })],
});
await runFlow([step], { go: 'true' }, options());
await runFlow([step], { go: 'false' }, options());
expect(calls).toEqual(['yes', 'no']);
});
});
describe('stara podoba podminky', () => {
test('rulesOf prevede jednu otazku na kroku a matchOf ma vychozi all', () => {
const legacy: ConditionStep = {
id: 'c',
kind: 'condition',
fieldId: 'status',
operator: 'eq',
value: 'open',
yes: [],
no: [],
};
expect(rulesOf(legacy)).toEqual([{ fieldId: 'status', operator: 'eq', value: 'open' }]);
expect(matchOf(legacy)).toBe('all');
// Nova podoba ma prednost, prazdny seznam otazek se bere jako stara podoba.
expect(rulesOf({ ...legacy, rules: [{ fieldId: 'a', operator: 'isEmpty' }] })).toEqual([
{ fieldId: 'a', operator: 'isEmpty' },
]);
expect(rulesOf({ ...legacy, rules: [] })).toHaveLength(1);
expect(rulesOf({ id: 'c', kind: 'condition', fieldId: 'x', yes: [], no: [] })).toEqual([]);
});
test('krok ulozeny ve stare podobe vetvi stejne jako novy', async () => {
const legacy: ConditionStep = {
id: 'c',
kind: 'condition',
fieldId: 'status',
operator: 'eq',
value: 'open',
yes: [],
no: [],
};
expect(await branchOf(legacy, { status: 'open' })).toBe('yes');
expect(await branchOf(legacy, { status: 'closed' })).toBe('no');
});
});
describe('stropy', () => {
test('strom nad MAX_STEPS se vubec nespusti', async () => {
const steps = Array.from({ length: 51 }, (_, index) => action(`s${index}`));
const result = await runFlow(steps, {}, options());
expect(result.ok).toBe(false);
expect(result.retryable).toBe(false);
expect(result.steps).toEqual([]);
expect(result.error).toContain('51');
expect(result.error).toContain('50');
const fifty = await runFlow(steps.slice(0, 50), {}, options());
expect(fifty.ok).toBe(true);
expect(fifty.steps).toHaveLength(50);
});
test('maxSteps ve volbach strop prepise a vetve podminek se pocitaji', async () => {
const nested = condition([{ fieldId: 'x', operator: 'isEmpty' }], {
yes: [action('a')],
no: [action('b')],
});
expect((await runFlow([nested], {}, options({ maxSteps: 2 }))).error).toContain('3');
expect((await runFlow([nested], {}, options({ maxSteps: 3 }))).ok).toBe(true);
});
test('smycka se meri staticky: maly strom smi vykonat vic nez MAX_STEPS kroku', async () => {
const items = Array.from({ length: 200 }, (_, index) => ({ n: index }));
const loop: FlowStep = {
id: 'loop',
kind: 'foreach',
path: 'items',
steps: [condition([{ fieldId: 'item.n', operator: 'gte', value: '0' }]), action('a')],
};
const result = await runFlow([loop], { items }, options());
expect(result.ok).toBe(true);
// Radek smycky plus dva kroky za kazdou polozku.
expect(result.steps).toHaveLength(1 + 2 * 200);
expect(result.context['loop.count']).toBe(200);
expect((result.context['loop.results'] as unknown[]).length).toBe(200);
});
test('MAX_ACTIONS zastavi beh, ktery vykonal tisic kroku', async () => {
const items = Array.from({ length: 200 }, (_, index) => ({ n: index }));
const loop: FlowStep = {
id: 'loop',
kind: 'foreach',
path: 'items',
steps: Array.from({ length: 5 }, (_, index) => ({
...condition([{ fieldId: 'item.n', operator: 'gte', value: '0' }]),
id: `c${index}`,
})),
};
const result = await runFlow([loop], { items }, options());
expect(result.ok).toBe(false);
expect(result.error).toContain('1000');
expect(result.steps).toHaveLength(1000);
expect(result.retryable).toBe(false);
});
test('seznam nad MAX_LOOP_ITEMS a hodnota, ktera neni seznam, beh zastavi bez opakovani', async () => {
const loop: FlowStep = { id: 'loop', kind: 'foreach', path: 'items', steps: [action('a')] };
const tooMany = await runFlow(
[loop],
{ items: Array.from({ length: 201 }, () => 1) },
options(),
);
expect(tooMany.ok).toBe(false);
expect(tooMany.retryable).toBe(false);
expect(tooMany.error).toContain('200');
const notList = await runFlow([loop], { items: 'text' }, options());
expect(notList.ok).toBe(false);
expect(notList.retryable).toBe(false);
expect(notList.steps[0]?.detail).toContain('string');
});
});
describe('vystupy a sablony', () => {
test('vystup kroku je pod jmenem kroku i pod holym jmenem, holé jmeno se neprepisuje', async () => {
handlers.emit = (inputs) => ({ ok: true, summary: 'ok', outputs: { id: inputs.value ?? '' } });
const steps = [
action('first', 'emit', { value: '{{seed}}' }),
action('second', 'emit', { value: '{{first.id}}-2' }),
];
const result = await runFlow(steps, { seed: 'S1' }, options());
expect(result.ok).toBe(true);
expect(result.context['first.id']).toBe('S1');
expect(result.context['second.id']).toBe('S1-2');
expect(result.context.id).toBe('S1');
expect(result.context.first).toEqual({ id: 'S1' });
});
});
describe('priznak retryable', () => {
test('vyjimka z vnitrniho kroku se neopakuje, krok muze opakovani vyslovne povolit', async () => {
handlers.boom = () => {
throw new Error('rozbite');
};
handlers.later = () => ({
ok: false,
summary: 'server nedostupny',
outputs: {},
retryable: true,
});
handlers.never = () => ({ ok: false, summary: 'spatne heslo', outputs: {} });
const boom = await runFlow([action('a', 'boom'), action('b')], {}, options());
expect(boom.ok).toBe(false);
expect(boom.retryable).toBe(false);
expect(boom.steps).toHaveLength(1);
expect(boom.steps[0]?.detail).toBe('rozbite');
expect((await runFlow([action('a', 'later')], {}, options())).retryable).toBe(true);
expect((await runFlow([action('a', 'never')], {}, options())).retryable).toBe(false);
});
test('operace bez skriptu konci hned a bez opakovani', async () => {
const result = await runFlow(
[{ id: 'x', kind: 'action', serviceId: 'cizi', operationId: 'op' }],
{},
options(),
);
expect(result.ok).toBe(false);
expect(result.retryable).toBe(false);
expect(result.steps[0]?.summary).toBe('operace nemá výkonnou část');
});
test('chyba skriptu se neopakuje, vypadek spojeni ano', async () => {
vi.mocked(scriptIdFor).mockReturnValue('svc/op');
const step: FlowStep = { id: 'x', kind: 'action', serviceId: 'svc', operationId: 'op' };
vi.mocked(runScript).mockResolvedValueOnce(
scriptResult({
ok: false,
error: { kind: 'internal', message: 'Skript selhal: TypeError', retryable: false },
}),
);
const scriptError = await runFlow([step], {}, options());
expect(scriptError.ok).toBe(false);
expect(scriptError.retryable).toBe(false);
expect(scriptError.steps[0]?.summary).toBe('Skript selhal: TypeError');
vi.mocked(runScript).mockResolvedValueOnce(
scriptResult({
ok: false,
error: { kind: 'timeout', message: 'Server neodpověděl v limitu.', retryable: true },
}),
);
expect((await runFlow([step], {}, options())).retryable).toBe(true);
vi.mocked(runScript).mockResolvedValueOnce(scriptResult({ outputs: { invoiceId: 42 } }));
const success = await runFlow([step], {}, options({ idempotencyKey: 'run-1' }));
expect(success.ok).toBe(true);
expect(success.context['x.invoiceId']).toBe(42);
expect(vi.mocked(runScript).mock.calls[2]?.[2]?.idempotencyKey).toBe('run-1:x');
});
test('nedostupne uloziste konektoru je docasna prekazka', async () => {
vi.mocked(scriptIdFor).mockReturnValue('svc/op');
vi.mocked(getConnector).mockRejectedValue(new Error('ECONNREFUSED'));
const step: FlowStep = {
id: 'x',
kind: 'action',
serviceId: 'svc',
operationId: 'op',
connectorId: 'con_1',
};
const result = await runFlow([step], {}, options());
expect(result.ok).toBe(false);
expect(result.retryable).toBe(true);
expect(result.steps[0]?.summary).toBe('napojení se nepodařilo načíst');
expect(runScript).not.toHaveBeenCalled();
});
});
+98
View File
@@ -0,0 +1,98 @@
/**
* Pomocne funkce skriptu a redakce tajemstvi (src/runtime/scripts/util.ts).
*/
import { describe, expect, test } from 'vitest';
import {
createRedactor,
parseBool,
parseNumber,
pick,
truncate,
} from '../../../src/runtime/scripts/util.js';
describe('createRedactor', () => {
const secret = 'p@ss w0rd/"x"';
const redact = createRedactor([secret, undefined, 'kratke', '12345']);
test('zakryje holou hodnotu i v hlavicce Bearer', () => {
expect(redact(`token=${secret};`)).toBe('token=***;');
expect(redact(`Authorization: Bearer ${secret}`)).toBe('Authorization: Bearer ***');
});
test('zakryje procentove kodovanou podobu v adrese', () => {
const encoded = encodeURIComponent(secret);
expect(encoded).not.toBe(secret);
expect(redact(`https://api.example.cz/?key=${encoded}&x=1`)).toBe(
'https://api.example.cz/?key=***&x=1',
);
});
test('zakryje podobu escapovanou v JSONu', () => {
const body = JSON.stringify({ error: `bad token ${secret}` });
expect(body).not.toContain(secret);
expect(redact(body)).toBe('{"error":"bad token ***"}');
});
test('hodnoty kratsi nez sest znaku a undefined se nezakryvaji', () => {
expect(redact('cislo 12345 a slovo kratke')).toBe('cislo 12345 a slovo ***');
expect(createRedactor([undefined, 'abc'])('abc abc')).toBe('abc abc');
});
});
describe('parseBool', () => {
test('rozumi ceskym i anglickym podobam', () => {
expect(parseBool('ano')).toBe(true);
expect(parseBool(' Yes ')).toBe(true);
expect(parseBool(1)).toBe(true);
expect(parseBool('ne')).toBe(false);
expect(parseBool('OFF')).toBe(false);
expect(parseBool(false)).toBe(false);
});
test('co neni ani ano, ani ne, je null', () => {
expect(parseBool('mozna')).toBeNull();
expect(parseBool('')).toBeNull();
expect(parseBool(undefined)).toBeNull();
});
});
describe('parseNumber', () => {
test('cislo s ceskou desetinnou carkou', () => {
expect(parseNumber('12,5')).toBe(12.5);
expect(parseNumber(' 1250.75 ')).toBe(1250.75);
expect(parseNumber(7)).toBe(7);
});
test('prazdno, text a nekonecno nejsou nula', () => {
expect(parseNumber('')).toBeNull();
expect(parseNumber('abc')).toBeNull();
expect(parseNumber(Number.NaN)).toBeNull();
expect(parseNumber(Number.POSITIVE_INFINITY)).toBeNull();
expect(parseNumber(true)).toBeNull();
expect(parseNumber(null)).toBeNull();
});
});
describe('pick', () => {
test('presny klic ma prednost, jinak bez ohledu na velikost pismen', () => {
expect(pick({ DocumentNumber: 'A', documentNumber: 'B' }, 'documentNumber')).toBe('B');
expect(pick({ DocumentNumber: 'A' }, 'documentNumber')).toBe('A');
// Presna shoda kterehokoliv jmena vitezi nad shodou bez ohledu na velikost pismen.
expect(pick({ Id: 1, name: 'x' }, 'id', 'name')).toBe('x');
expect(pick({ Id: 1 }, 'id', 'name')).toBe(1);
});
test('chybejici pole a nespravny vstup vraci undefined', () => {
expect(pick({ a: 1 }, 'b')).toBeUndefined();
expect(pick(null, 'a')).toBeUndefined();
expect(pick('text', 'length')).toBeUndefined();
});
});
describe('truncate', () => {
test('kratky text necha byt, dlouhy zkrati s poznamkou', () => {
expect(truncate('abc', 10)).toBe('abc');
expect(truncate('abcdefghij', 4)).toBe('abcd (zkráceno, celkem 10 znaků)');
});
});