wrolpi/app/src/components/inventory/InventoryPrint.test.js
Roland 947ee01b25
Add CSV/PDF export and a search filter to Inventory
New Export tab with two client-side formats (no new dependencies):
- CSV: the raw items table (RFC-4180 quoting, UTF-8 BOM for Excel), generated
  entirely in the browser.  The download anchor is attached to the DOM and the
  object URL is revoked on the next tick for cross-browser robustness.
- PDF: a printable full-inventory table followed by a group-by summary table,
  via the browser's print dialog ("Save as PDF").  The summary's grouping is
  chosen in the Export tab.

Search filter: a box on its own line under the inventory selector filters the
active inventory across every column's displayed value (case-insensitive;
whitespace-separated terms are AND-ed).  It narrows the whole view — Items
table, Summary aggregation, and Export/PDF — while edits/adds in the table
still operate on the full inventory, so filtering never drops data.

Summary/export grouping:
- The group-by aggregation is extracted into a shared summarize.js (field-role
  detection + count-aware totals via units.js + calories), now the single
  source of truth for the Summary tab, the PDF summary, and the ration
  calculator.  findCaloriesKey/findCountKey moved here from RationCalculator
  (re-exported for back-compat).
- The Sum selector now offers number and calories fields in addition to
  quantity: number/calories are summed as a plain column total, while quantity
  stays count-aware and unit-compacted and the dedicated Calories column stays
  count-aware.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 11:06:38 -06:00

64 lines
3 KiB
JavaScript

import React from "react";
import {render} from "@testing-library/react";
import {InventoryPrint} from "./InventoryPrint";
const FIELDS = [
{key: 'name', label: 'Name', type: 'text', order: 0},
{key: 'category', label: 'Category', type: 'select', order: 1},
{key: 'item_size', label: 'Size', type: 'quantity', unit: 'lb', order: 2},
{key: 'count', label: 'Count', type: 'number', order: 3},
{key: 'calories', label: 'kcal', type: 'calories', order: 4},
];
const ITEMS = [
{id: 1, name: 'White Rice', category: 'grains', item_size: '30', item_size_unit: 'lb', count: '3', calories: '100'},
{id: 2, name: 'Oats', category: 'grains', item_size: '10', item_size_unit: 'lb', count: '2', calories: '50'},
{id: 3, name: 'Beans', category: 'legumes', item_size: '25', item_size_unit: 'lb', count: '1'},
];
describe('InventoryPrint', () => {
const tables = (container) => [...container.querySelectorAll('table')];
test('renders the full items table and a summary grouped by the chosen field', () => {
const {container} = render(
<InventoryPrint name='Food Storage' fields={FIELDS} items={ITEMS}
groupKey='category' sumKey='item_size'/>);
expect(container.querySelector('h1').textContent).toBe('Food Storage');
expect(container.querySelector('.inventory-print-meta').textContent).toMatch(/3 items/);
// Two tables: items, then summary.
const [itemsTable, summaryTable] = tables(container);
expect(itemsTable.querySelectorAll('tbody tr').length).toBe(3);
// Summary section header + grouped rows.
expect(container.querySelector('h2').textContent).toBe('Summary by Category');
const summaryRows = [...summaryTable.querySelectorAll('tbody tr')]
.map(tr => [...tr.querySelectorAll('td')].map(td => td.textContent));
// grains: 2 items, 110 lb, 400 kcal; legumes: 1 item, 25 lb, no calories.
expect(summaryRows).toEqual([
['grains', '2', '110 lb', '400 kcal'],
['legumes', '1', '25 lb', '—'],
]);
});
test('summing a number field shows a plain column total', () => {
const {container} = render(
<InventoryPrint name='Food Storage' fields={FIELDS} items={ITEMS}
groupKey='category' sumKey='count'/>);
const summaryRows = [...tables(container)[1].querySelectorAll('tbody tr')]
.map(tr => [...tr.querySelectorAll('td')].map(td => td.textContent));
// grains: 2 items, count 3+2=5; legumes: 1 item, count 1.
expect(summaryRows).toEqual([
['grains', '2', '5', '400 kcal'],
['legumes', '1', '1', '—'],
]);
});
test('omits the summary when there is no group key', () => {
const {container} = render(
<InventoryPrint name='X' fields={FIELDS} items={ITEMS} groupKey={undefined} sumKey='item_size'/>);
expect(container.querySelector('h2')).toBeNull();
expect(tables(container).length).toBe(1);
});
});