Files
doormilxpress_astryx/tests/lib/doormileFormat.test.js
2026-08-27 13:40:48 +05:30

313 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { renderHook, act } from '@testing-library/react';
import { currency, number, km, percentOf, orDash, matchesQuery, useDebouncedValue, exportRows } from '@/lib/doormileFormat';
/**
* `xlsx` is imported on demand inside exportRows, so it is mocked at the module
* boundary. The export path itself — column declaration to sheet rows to the
* written filename — is exactly what these tests verify, so none of that is
* mocked away.
*/
jest.mock('xlsx', () => ({
__esModule: true,
utils: {
json_to_sheet: jest.fn(() => ({ sheet: true })),
book_new: jest.fn(() => ({ book: true })),
book_append_sheet: jest.fn()
},
writeFile: jest.fn()
}));
import * as XLSX from 'xlsx';
/**
* Shared display formatting. The reason it lives in one module is that a number
* reading ₹1,240 on Orders and 1240 on Reports looks like two different figures
* for the same money — so these tests pin the exact rendering contract, and in
* particular that 0 is a real value while blank/NaN is a dash.
*/
describe('doormileFormat', () => {
describe('currency', () => {
it('should render an amount in rupees with Indian digit grouping', () => {
expect(currency(1240)).toMatch(/^₹\s?1,240\.00$/);
});
it('should group in lakhs, not thousands', () => {
// 12,34,567 — not 1,234,567. The console is an Indian ops tool.
expect(currency(1234567)).toMatch(/12,34,567\.00$/);
});
it('should render zero as a real amount, not a dash', () => {
// A ₹0 delivery fee is a fact; an em dash reads as "unknown".
expect(currency(0)).toMatch(/^₹\s?0\.00$/);
});
it('should accept a numeric string, as the API returns for money columns', () => {
expect(currency('1240')).toMatch(/1,240\.00$/);
});
it('should cap the fraction at two digits', () => {
expect(currency(10.999)).toMatch(/^₹\s?11$|^₹\s?11\.00$/);
expect(currency(10.994)).toMatch(/10\.99$/);
});
it('should render a negative amount rather than dropping the sign', () => {
expect(currency(-500)).toMatch(/500\.00$/);
expect(currency(-500)).toMatch(/-|−|\(/);
});
it.each([undefined, 'abc', NaN, Infinity, {}])(
'should fall back to an em dash for the unusable value %p',
(value) => {
expect(currency(value)).toBe('—');
}
);
it('should fall back to an em dash for a null amount', () => {
// A nullable money column means UNKNOWN, not zero. Rendering ₹0.00 asserts
// the delivery earned nothing, which is the exact confusion the module's
// own "0 is a real amount" comment exists to prevent.
expect(currency(null)).toBe('—');
});
it('should fall back to an em dash for an empty-string amount', () => {
expect(currency('')).toBe('—');
});
it('should honour a caller-supplied fallback', () => {
expect(currency('abc', 'n/a')).toBe('n/a');
});
});
describe('number', () => {
it('should render a count with Indian grouping', () => {
expect(number(1234567)).toBe('12,34,567');
});
it('should render zero as 0', () => {
expect(number(0)).toBe('0');
});
it.each([undefined, 'abc', NaN, Infinity])('should fall back to an em dash for %p', (value) => {
expect(number(value)).toBe('—');
});
it('should fall back to an em dash for a null count', () => {
// Same unknown-vs-zero distinction as `currency`.
expect(number(null)).toBe('—');
});
it('should fall back to an em dash for an empty-string count', () => {
expect(number('')).toBe('—');
});
});
describe('km', () => {
it('should render a distance to exactly one decimal place', () => {
expect(km(12)).toBe('12.0 km');
expect(km(12.34)).toBe('12.3 km');
expect(km(12.36)).toBe('12.4 km');
});
it('should render a zero distance rather than a dash', () => {
expect(km(0)).toBe('0.0 km');
});
it.each([undefined, 'abc', NaN, Infinity])('should fall back to an em dash for %p', (value) => {
expect(km(value)).toBe('—');
});
it('should fall back to an em dash for a null distance', () => {
// "0.0 km" for an unknown distance is worse than a dash: it reads as a
// measured zero-distance trip.
expect(km(null)).toBe('—');
});
});
describe('percentOf', () => {
it('should return a whole-number percentage', () => {
expect(percentOf(25, 100)).toBe(25);
expect(percentOf(1, 3)).toBe(33);
expect(percentOf(2, 3)).toBe(67);
});
it('should return 0 — not Infinity or NaN — when the total is zero', () => {
// This feeds progress bars and chip counts; NaN% renders as garbage.
expect(percentOf(5, 0)).toBe(0);
});
it.each([
[null, 100],
[10, null],
['abc', 100],
[10, 'abc'],
[undefined, undefined]
])('should return 0 for the non-numeric pair (%p, %p)', (value, total) => {
expect(percentOf(value, total)).toBe(0);
});
it('should allow a percentage above 100 rather than clamping silently', () => {
expect(percentOf(150, 100)).toBe(150);
});
});
describe('orDash', () => {
it.each([null, undefined, '', ' '])('should render the blank value %p as an em dash', (value) => {
expect(orDash(value)).toBe('—');
});
it('should trim surrounding whitespace off a real value', () => {
expect(orDash(' Bengaluru ')).toBe('Bengaluru');
});
it('should render 0 and false as text, since they are real values', () => {
expect(orDash(0)).toBe('0');
expect(orDash(false)).toBe('false');
});
});
describe('matchesQuery', () => {
const row = { customername: 'Anita Rao', city: 'Bengaluru', phone: '9876543210', notes: null };
it('should match a case-insensitive substring on any listed field', () => {
expect(matchesQuery(row, ['customername', 'city'], 'anita')).toBe(true);
expect(matchesQuery(row, ['customername', 'city'], 'BENGAL')).toBe(true);
});
it('should not match a field that was not listed', () => {
expect(matchesQuery(row, ['city'], 'anita')).toBe(false);
});
it('should support a derived field expressed as a function', () => {
expect(matchesQuery(row, [(r) => `${r.customername} ${r.city}`], 'rao beng')).toBe(true);
});
it.each(['', ' ', null, undefined])(
'should treat the blank query %p as "match everything"',
(query) => {
expect(matchesQuery(row, ['customername'], query)).toBe(true);
}
);
it('should trim the query before matching', () => {
expect(matchesQuery(row, ['city'], ' bengaluru ')).toBe(true);
});
it('should match against a numeric field coerced to text', () => {
expect(matchesQuery({ orderid: 4021 }, ['orderid'], '402')).toBe(true);
});
it('should skip null/undefined/empty fields instead of matching them', () => {
expect(matchesQuery(row, ['notes'], 'null')).toBe(false);
expect(matchesQuery(row, ['missing'], 'undefined')).toBe(false);
});
it('should return false for a row with no matching field rather than throwing', () => {
expect(matchesQuery(null, ['customername'], 'anita')).toBe(false);
expect(matchesQuery(row, [], 'anita')).toBe(false);
});
});
describe('useDebouncedValue', () => {
beforeEach(() => jest.useFakeTimers());
afterEach(() => jest.useRealTimers());
it('should return the initial value immediately', () => {
const { result } = renderHook(() => useDebouncedValue('a', 250));
expect(result.current).toBe('a');
});
it('should not settle until the delay has fully elapsed', () => {
const { result, rerender } = renderHook(({ value }) => useDebouncedValue(value, 250), {
initialProps: { value: 'a' }
});
rerender({ value: 'ab' });
act(() => {
jest.advanceTimersByTime(249);
});
expect(result.current).toBe('a');
act(() => {
jest.advanceTimersByTime(1);
});
expect(result.current).toBe('ab');
});
it('should collapse a burst of keystrokes into a single settled value', () => {
const { result, rerender } = renderHook(({ value }) => useDebouncedValue(value, 250), {
initialProps: { value: '' }
});
['b', 'be', 'ben', 'beng'].forEach((value) => {
rerender({ value });
act(() => {
jest.advanceTimersByTime(100);
});
});
expect(result.current).toBe('');
act(() => {
jest.advanceTimersByTime(250);
});
expect(result.current).toBe('beng');
});
it('should clear its pending timer on unmount', () => {
const clearSpy = jest.spyOn(global, 'clearTimeout');
const { unmount } = renderHook(() => useDebouncedValue('a', 250));
unmount();
expect(clearSpy).toHaveBeenCalled();
});
});
describe('exportRows', () => {
it('should build one sheet record per row using the column headers', async () => {
const rows = [
{ orderid: 1, customername: 'Anita', amount: 240 },
{ orderid: 2, customername: 'Vikram', amount: 90 }
];
const columns = [
{ key: 'orderid', header: 'Order ID' },
{ key: 'customername', header: 'Customer' }
];
await exportRows(rows, columns, 'orders-2026-08-27');
expect(XLSX.utils.json_to_sheet).toHaveBeenCalledWith([
{ 'Order ID': 1, Customer: 'Anita' },
{ 'Order ID': 2, Customer: 'Vikram' }
]);
expect(XLSX.writeFile).toHaveBeenCalledWith({ book: true }, 'orders-2026-08-27.xlsx');
});
it('should prefer a column value() accessor over the raw key', async () => {
await exportRows([{ kms: 12.345 }], [{ key: 'kms', header: 'Distance', value: (r) => `${r.kms.toFixed(1)} km` }], 'x');
expect(XLSX.utils.json_to_sheet).toHaveBeenCalledWith([{ Distance: '12.3 km' }]);
});
it('should write a blank cell — not the word null — for a missing value', async () => {
await exportRows([{ a: null, b: undefined }], [{ key: 'a', header: 'A' }, { key: 'b', header: 'B' }], 'x');
expect(XLSX.utils.json_to_sheet).toHaveBeenCalledWith([{ A: '', B: '' }]);
});
it('should preserve a zero value rather than blanking it', async () => {
await exportRows([{ amount: 0 }], [{ key: 'amount', header: 'Amount' }], 'x');
expect(XLSX.utils.json_to_sheet).toHaveBeenCalledWith([{ Amount: 0 }]);
});
it.each([[[]], [null], [undefined]])(
'should still produce a (empty) workbook when rows is %p',
async (rows) => {
await exportRows(rows, [{ key: 'a', header: 'A' }], 'empty');
expect(XLSX.utils.json_to_sheet).toHaveBeenCalledWith([]);
expect(XLSX.writeFile).toHaveBeenCalledWith(expect.anything(), 'empty.xlsx');
}
);
it('should name the sheet consistently so downstream parsers can find it', async () => {
await exportRows([{ a: 1 }], [{ key: 'a', header: 'A' }], 'x');
expect(XLSX.utils.book_append_sheet).toHaveBeenCalledWith({ book: true }, { sheet: true }, 'Export');
});
});
});