diff --git a/src/automats/orderAutomat.js b/src/automats/orderAutomat.js index 4e2caf9..2b45c2f 100644 --- a/src/automats/orderAutomat.js +++ b/src/automats/orderAutomat.js @@ -49,6 +49,7 @@ import authAutomat from './authAutomat'; const initialState = { items: [], + filter: '', total: 0, offset: 0, limit: 20, @@ -58,61 +59,83 @@ const initialState = { let automat; let inflightLoad = null; +const FETCH_PAGE_SIZE = 100; + +// MARK: pages through GET /api/order until every order is collected +async function fetchAllOrders() { + let offset = 0; + let total = Infinity; + const collected = []; + while (offset === 0 || collected.length < total) { + const res = await orderApi.list({ offset, limit: FETCH_PAGE_SIZE }); + if (offset === 0) total = Number(res?.total ?? res?.orders?.length ?? 0); + const page = Array.isArray(res?.orders) ? res.orders : []; + collected.push(...page); + offset += page.length; + if (page.length === 0) break; + } + return collected; +} + +const applyAll = (orders) => ({ + items: orders, + total: orders.length, + offset: 0, + limit: Math.max(orders.length, 1), + status: 'ready', + error: null, +}); + +/** + * Client-side filter over number, external reference and (resolved) customer name. + * @param {Order[]} items + * @param {string} filter + * @param {(order: Order) => string} [nameOf] Customer name resolver. + */ +export function filterOrders(items, filter, nameOf) { + const query = String(filter ?? '').trim().toLowerCase(); + if (!query) return items; + return items.filter((o) => + [o.number, o.externalNumber, nameOf?.(o)] + .some((v) => String(v ?? '').toLowerCase().includes(query))); +} const actions = { + /** Sets the active search filter. */ + filter(query) { + automat.setState({ filter: String(query ?? '') }); + }, + /** - * Loads paginated orders, deduplicating concurrent callers. + * Loads all orders, deduplicating concurrent callers. * - * @param {{ offset?: number, limit?: number }} [params] * @returns {Promise} */ - load({ offset, limit } = {}) { - const currentOffset = offset ?? automat.state.offset ?? 0; - const currentLimit = limit ?? automat.state.limit ?? 20; - + load() { if (inflightLoad) return inflightLoad; - inflightLoad = (async () => { - automat.setState({ status: 'loading', error: null }); - try { - const res = await orderApi.list({ offset: currentOffset, limit: currentLimit }); - const orders = res?.orders ?? []; - automat.setState({ - items: orders, - total: Number(res?.total ?? orders.length), - offset: Number(res?.offset ?? currentOffset), - limit: Number(res?.limit ?? currentLimit), - status: 'ready', - error: null, - }); + automat.setState({ status: 'loading', error: null }); + inflightLoad = fetchAllOrders() + .then((orders) => { + automat.setState(applyAll(orders)); return orders; - } catch (error) { + }) + .catch((error) => { automat.setState({ status: 'failed', error }); throw error; - } finally { - inflightLoad = null; - } - })(); + }) + .finally(() => { inflightLoad = null; }); return inflightLoad; }, /** - * Silently re-fetches the current page of orders without flashing loading state. + * Silently re-fetches all orders without flashing loading state. * * @returns {Promise} */ async reload() { - const { offset, limit } = automat.state; try { - const res = await orderApi.list({ offset, limit }); - const orders = res?.orders ?? []; - automat.setState({ - items: orders, - total: Number(res?.total ?? orders.length), - offset: Number(res?.offset ?? offset), - limit: Number(res?.limit ?? limit), - status: 'ready', - error: null, - }); + const orders = await fetchAllOrders(); + automat.setState(applyAll(orders)); return orders; } catch (error) { automat.setState({ error }); @@ -120,37 +143,6 @@ const actions = { } }, - /** - * Navigates to a 1-based page using formula: offset = (page - 1) * limit - * - * @param {number} page 1-based page number. - * @returns {Promise} - */ - setPage(page) { - const limit = automat.state.limit || 20; - const offset = Math.max(0, (page - 1) * limit); - return actions.load({ offset, limit }); - }, - - /** - * Sets pagination offset directly. - * - * @param {number} offset 0-based offset. - * @returns {Promise} - */ - setOffset(offset) { - return actions.load({ offset, limit: automat.state.limit }); - }, - - /** - * Sets page limit (records to return) and resets offset to 0. - * - * @param {number} limit Records per page (1..100). - * @returns {Promise} - */ - setLimit(limit) { - return actions.load({ offset: 0, limit }); - }, /** * Ensures orders are loaded once the selected company is known. diff --git a/src/components/manifest/OrderList.jsx b/src/components/manifest/OrderList.jsx index 0795e8f..b0e2cbf 100644 --- a/src/components/manifest/OrderList.jsx +++ b/src/components/manifest/OrderList.jsx @@ -1,33 +1,23 @@ -import { Fragment, PureComponent } from 'react'; +import { PureComponent, createRef } from 'react'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import Chip from '@mui/material/Chip'; import CircularProgress from '@mui/material/CircularProgress'; -import Collapse from '@mui/material/Collapse'; import IconButton from '@mui/material/IconButton'; -import Stack from '@mui/material/Stack'; -import Table from '@mui/material/Table'; -import TableBody from '@mui/material/TableBody'; -import TableCell from '@mui/material/TableCell'; -import TableContainer from '@mui/material/TableContainer'; -import TableFooter from '@mui/material/TableFooter'; -import TableHead from '@mui/material/TableHead'; -import TableRow from '@mui/material/TableRow'; +import ListItemButton from '@mui/material/ListItemButton'; import Typography from '@mui/material/Typography'; import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined'; -import EditOutlinedIcon from '@mui/icons-material/EditOutlined'; -import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; -import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; import { enqueueSnackbar } from 'notistack'; -import BackendError from '../BackendError'; +import SingleLineTextEdit from 'dashgui/SingleLineTextEdit'; +import FillSpace from 'dashgui/FillSpace'; import { PanelTile, PanelTileGrid } from 'dashgui/panels'; -import orderAutomat from '../../automats/orderAutomat'; +import BackendError from '../BackendError'; +import orderAutomat, { filterOrders } from '../../automats/orderAutomat'; import { order as orderApi } from '../../api'; import { backendErrorFrom } from '../../utils/backendError'; import { notifyError } from '../../utils/notify'; import { formatPrice } from '../../utils/price'; import withNavigate from '../withNavigate'; -import ResultsPagination from '../article/ResultsPagination'; import { formatDate, loadParties, @@ -36,532 +26,295 @@ import { partyDisplayName, } from './manifestConfig'; -function tintDivider(color, amount = 0.15) { - if (typeof color !== 'string') return 'divider'; - const m = color.trim().match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i); - if (!m) return 'divider'; - - let hex = m[1]; - if (hex.length === 3) { - hex = hex - .split('') - .map((c) => c + c) - .join(''); - } - const num = parseInt(hex, 16); - const channels = [16, 8, 0].map((shift) => { - const c = (num >> shift) & 0xff; - return Math.max(0, Math.round(c * (1 - amount))); - }); - const toHex = (v) => v.toString(16).padStart(2, '0'); - return `#${channels.map(toHex).join('')}`; -} - -function calculateOrderTotal(lines) { - if (!Array.isArray(lines) || lines.length === 0) return 0; +// MARK: pure helpers +function orderGrossTotal(lines) { + if (!Array.isArray(lines)) return 0; return lines.reduce((acc, line) => { - const amount = Number(line.amount) || 0; - const net = Number(line.net) || 0; - const rebate = Number(line.rebate) || 0; - const lineTotal = amount * net * (1 - rebate / 100); - return acc + lineTotal; + const net = (Number(line.amount) || 0) * (Number(line.net) || 0) * (1 - (Number(line.rebate) || 0) / 100); + return acc + net * (1 + (Number(line.taxrate) || 0) / 100); }, 0); } -function calculateOrderGrossTotal(lines) { - if (!Array.isArray(lines) || lines.length === 0) return 0; - return lines.reduce((acc, line) => { - const amount = Number(line.amount) || 0; - const net = Number(line.net) || 0; - const rebate = Number(line.rebate) || 0; - const taxrate = Number(line.taxrate) || 0; - const lineNet = amount * net * (1 - rebate / 100); - const lineGross = lineNet * (1 + taxrate / 100); - return acc + lineGross; - }, 0); -} +const cfg = manifestConfig('order'); -export class OrderList extends PureComponent { - constructor(props) { - super(props); - this.cfg = manifestConfig('order'); - const orderState = orderAutomat.state; - this.state = { - status: orderState.status === 'ready' ? 'ready' : 'loading', - error: orderState.error, - orders: orderState.items ?? [], - total: orderState.total ?? 0, - offset: orderState.offset ?? 0, - limit: orderState.limit ?? 20, - partiesById: {}, - busyId: null, - expandedIds: new Set(), - }; - this._cancelled = false; - this.handleRetry = this.handleRetry.bind(this); - this.handleCreate = this.handleCreate.bind(this); - this.handleEdit = this.handleEdit.bind(this); - this.handleDelete = this.handleDelete.bind(this); - this.handlePageChange = this.handlePageChange.bind(this); - this.toggleExpand = this.toggleExpand.bind(this); - } +const rowSx = { + alignItems: 'center', + height: 64, + minHeight: 64, + maxHeight: 64, + boxSizing: 'border-box', + py: 0, + gap: 1.5, +}; - componentDidMount() { - this._cancelled = false; - this.unsubOrder = orderAutomat.subscribe(this, (state) => ({ - orders: state.items ?? [], - total: state.total ?? 0, - offset: state.offset ?? 0, - limit: state.limit ?? 20, - status: state.status, - error: state.error, - })); +const ellipsisSx = { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }; - void this.loadPartiesAndOrders(); - } - - componentWillUnmount() { - this._cancelled = true; - this.unsubOrder?.(); - } - - async loadPartiesAndOrders() { - try { - const parties = await loadParties('order'); - if (this._cancelled) return; - this.setState({ - partiesById: this.cfg.partiesByIdFromList(parties), - }); - } catch { - // Non-fatal, party names can fall back to IDs - } - - try { - await orderAutomat.actions.ensureLoaded(); - } catch (err) { - if (this._cancelled) return; - this.setState({ status: 'failed', error: backendErrorFrom(err) }); - } - } - - async handleRetry() { - this.setState({ status: 'loading', error: null }); - try { - await orderAutomat.actions.reload(); - } catch (err) { - if (this._cancelled) return; - this.setState({ status: 'failed', error: backendErrorFrom(err) }); - } - } - - go(to) { - const { navigate } = this.props; - if (typeof navigate === 'function') { - navigate(to); - return; - } - window.location.assign(to); - } - - handleCreate() { - this.go(this.cfg.createPath); - } - - handleEdit(order) { - this.go(this.cfg.editPath(order.id)); - } - - async handleDelete(order) { - if (this.state.busyId != null) return; - if (!window.confirm(this.cfg.confirmDelete(order.number))) return; - - this.setState({ busyId: order.id }); - try { - await orderApi.delete(order.id); - await orderAutomat.actions.reload(); - if (this._cancelled) return; - this.setState({ busyId: null }); - enqueueSnackbar(this.cfg.deletedMsg, { variant: 'success' }); - } catch (err) { - if (this._cancelled) return; - notifyError(err); - this.setState({ busyId: null }); - } - } - - handlePageChange(page) { - // Formula: Next Offset: (page - 1) * limit - void orderAutomat.actions.setPage(page); - } - - toggleExpand(orderId) { - this.setState((prev) => { - const next = new Set(prev.expandedIds); - if (next.has(orderId)) { - next.delete(orderId); - } else { - next.add(orderId); - } - return { expandedIds: next }; - }); - } +/** + * Ambient context for `OrderRow`: FillSpace renders rows as bare elements + * (``), so the list publishes callbacks and the party map + * here on every render. + */ +const rowContext = { partiesById: {}, busyId: null, onOpen: null, onDelete: null }; +// MARK: one row rendered by FillSpace +class OrderRow extends PureComponent { partyName(partyId) { if (partyId == null) return '–'; - const party = this.state.partiesById[partyId]; + const party = rowContext.partiesById[partyId]; return partyDisplayName('order', party) || `Kunde #${partyId}`; } - renderLoading() { - return ( - - - - ); - } + render() { + const { item: order } = this.props; - renderLineItems(order, borderColor) { - const lines = order.lines ?? []; - if (lines.length === 0) { + // Data-free probe (FillSpace measures the row height with `item` undefined). + if (!order) { return ( - - - Keine Positionen vorhanden. - - + +   + ); } - const netTotal = calculateOrderTotal(lines); - const grossTotal = calculateOrderGrossTotal(lines); + const { busyId, onOpen, onDelete } = rowContext; + const lines = order.lines ?? []; + const gross = orderGrossTotal(lines); return ( - - - - - - Pos - - SKU - Artikel - - Menge - - - Netto - - - Brutto - - - Rabatt - - - MwSt. - - - Gesamt netto - - - Gesamt brutto - - - - - {lines.map((line, idx) => { - const amount = Number(line.amount) || 0; - const net = Number(line.net) || 0; - const taxrate = Number(line.taxrate) || 0; - const gross = net * (1 + taxrate / 100); - const rebate = Number(line.rebate) || 0; - const lineNet = amount * net * (1 - rebate / 100); - const lineGross = lineNet * (1 + taxrate / 100); - const puText = Number(line.pu) > 1 ? ` (VPE ${line.pu})` : ''; + onOpen?.(order)}> + + + {order.number} + + + {numberDisplay(order.externalNumber)} + + - return ( - - - {idx + 1} - - - {line.sku || '–'} - - - - {line.name || '–'} - - {line.comment ? ( - - {line.comment} - - ) : null} - - - {amount} - {puText} - - - {formatPrice(net)} - - - {formatPrice(gross)} - - - {rebate > 0 ? `${rebate}%` : '–'} - - - {line.taxrate != null ? `${line.taxrate}%` : '–'} - - - {formatPrice(lineNet)} - - - {formatPrice(lineGross)} - - - ); - })} - - - td': { borderTop: '2px solid', borderColor, fontWeight: 700 } }}> - - Gesamtsumme: - - - {formatPrice(netTotal)} - - - {formatPrice(grossTotal)} - - - -
-
+ + + {this.partyName(order.entityId)} + + + {cfg.dateLabel} {formatDate(order.date)} + + + + 0 ? 'text.primary' : 'text.disabled', + }} + /> + + + {gross > 0 ? formatPrice(gross) : '—'} + + + { + e.stopPropagation(); + onDelete?.(order); + }} + onMouseDown={(e) => e.stopPropagation()} + sx={{ width: 40, height: 40, flexShrink: 0 }} + > + + + ); } +} + +// MARK: search + create bar +class OrderSearchField extends PureComponent { + constructor(props) { + super(props); + this.state = { input: orderAutomat.state.filter ?? '' }; + } + + componentDidMount() { + this.unsub = orderAutomat.subscribe(this, (state) => ({ input: state.filter })); + } + + componentWillUnmount() { + this.unsub?.(); + } render() { - const { status, error, orders, total, offset, limit, busyId, expandedIds } = this.state; - const cfg = this.cfg; - - if (status === 'loading') return this.renderLoading(); - if (status === 'failed') { - return ( - - ); - } - - const borderColor = tintDivider(cfg.listColor); - - // Client Pagination Formula: - // Current Page: Math.floor(offset / limit) + 1 - // Total Pages: Math.ceil(total / limit) - const currentPage = Math.floor(offset / limit) + 1; - const pageCount = Math.max(1, Math.ceil(total / limit)); - const showPagination = total > limit; - + const { inputRef, onCreate } = this.props; return ( - - - {/* Create Header Button */} - - - - - {/* Empty State */} - {orders.length === 0 ? ( - - {cfg.emptyList} - - ) : ( - - - - - - Nummer - Referenz - {cfg.dateLabel} - {cfg.partyLabel} - Positionen - - - - - {orders.map((order) => { - const isExpanded = expandedIds.has(order.id); - const lines = order.lines ?? []; - const netTotal = calculateOrderTotal(lines); - const grossTotal = calculateOrderGrossTotal(lines); - const hasLines = lines.length > 0; - - return ( - - this.handleEdit(order)} - > - {/* Expand / Collapse toggle */} - { - e.stopPropagation(); - if (hasLines) this.toggleExpand(order.id); - }} - > - {hasLines ? ( - - {isExpanded ? ( - - ) : ( - - )} - - ) : null} - - - {/* Number */} - {order.number} - - {/* Reference / External Number */} - {numberDisplay(order.externalNumber)} - - {/* Date */} - {formatDate(order.date)} - - {/* Customer */} - {this.partyName(order.entityId)} - - {/* Lines summary */} - - - - {grossTotal > 0 ? ( - - - {formatPrice(grossTotal)} brutto - - - {formatPrice(netTotal)} netto - - - ) : netTotal > 0 ? ( - - {formatPrice(netTotal)} - - ) : null} - - - - {/* Row Action buttons */} - e.stopPropagation()} - > - - this.handleEdit(order)} - > - - - this.handleDelete(order)} - > - - - - - - - {/* Collapsible Line Details Row */} - {hasLines && ( - - - - - {this.renderLineItems(order, borderColor)} - - - - - )} - - ); - })} - -
-
- )} - - {/* Pagination Controls */} - {showPagination ? ( - - this.handlePageChange(page)} - /> - - ) : null} -
-
+ + orderAutomat.actions.filter(next)} + /> + +
); } } +// MARK: list page +export class OrderList extends PureComponent { + constructor(props) { + super(props); + const s = orderAutomat.state; + this.state = { + status: s.status === 'ready' ? 'ready' : 'loading', + error: s.error ? backendErrorFrom(s.error) : null, + items: s.items ?? [], + filter: s.filter ?? '', + partiesById: {}, + busyId: null, + }; + this.searchRef = createRef(); + this._cancelled = false; + this.handleRetry = this.handleRetry.bind(this); + this.handleCreate = this.handleCreate.bind(this); + this.handleOpen = this.handleOpen.bind(this); + this.handleDelete = this.handleDelete.bind(this); + this.nameOf = this.nameOf.bind(this); + } + + componentDidMount() { + this._cancelled = false; + this.unsub = orderAutomat.subscribe(this, (state) => ({ + items: state.items ?? [], + filter: state.filter ?? '', + status: state.status, + error: state.error ? backendErrorFrom(state.error) : null, + })); + void this.loadAll(); + } + + componentWillUnmount() { + this._cancelled = true; + this.unsub?.(); + } + + async loadAll() { + try { + const parties = await loadParties('order'); + if (!this._cancelled) this.setState({ partiesById: cfg.partiesByIdFromList(parties) }); + } catch { + // Non-fatal: names fall back to ids. + } + try { + await orderAutomat.actions.ensureLoaded(); + } catch { + // Surfaced through automat state. + } + } + + nameOf(order) { + const party = this.state.partiesById[order.entityId]; + return partyDisplayName('order', party); + } + + handleRetry() { + void orderAutomat.actions.load().catch(() => {}); + } + + handleCreate() { + this.props.navigate(cfg.createPath); + } + + handleOpen(order) { + this.props.navigate(cfg.editPath(order.id)); + } + + async handleDelete(order) { + if (this.state.busyId != null) return; + if (!window.confirm(cfg.confirmDelete(order.number))) return; + this.setState({ busyId: order.id }); + try { + await orderApi.delete(order.id); + await orderAutomat.actions.reload(); + enqueueSnackbar(cfg.deletedMsg, { variant: 'success' }); + } catch (err) { + notifyError(err); + } finally { + if (!this._cancelled) this.setState({ busyId: null }); + } + } + + render() { + const { status, error, items, filter, partiesById, busyId } = this.state; + + if (status === 'loading' || status === 'idle') { + return ( + + + + ); + } + if (status === 'failed') { + return ; + } + + rowContext.partiesById = partiesById; + rowContext.busyId = busyId; + rowContext.onOpen = this.handleOpen; + rowContext.onDelete = this.handleDelete; + + const rows = filterOrders(items, filter, this.nameOf); + + return ( + <> + + + + {items.length === 0 ? ( + + {cfg.emptyList} + + ) : ( + + )} + + + + ); + } +} + export default withNavigate(OrderList); diff --git a/src/routes/artikel.jsx b/src/routes/artikel.jsx index c820eda..994418b 100644 --- a/src/routes/artikel.jsx +++ b/src/routes/artikel.jsx @@ -645,7 +645,6 @@ class ArtikelSearchPage extends Component { onMouseDown={(e) => e.preventDefault()} onClick={() => { const next = selected ? [] : [id]; - this.setState({ categoryFilterOpen: false }); this.updateParams(input, buildFilter(next), 0); }} sx={{ pl: 2 + depth * 1.5 }} diff --git a/src/routes/artikel/marken.jsx b/src/routes/artikel/marken.jsx index 67220a2..d28ebea 100644 --- a/src/routes/artikel/marken.jsx +++ b/src/routes/artikel/marken.jsx @@ -110,6 +110,7 @@ class ArtikelMarkenPage extends PureComponent { status: brandAutomat.state.status, error: brandAutomat.state.error ? backendErrorFrom({ message: brandAutomat.state.error }) : null, brands: brandAutomat.state.filteredBrands ?? brandAutomat.state.brands, + totalCount: brandAutomat.state.brands?.length ?? 0, }; this.searchRef = createRef(); this.handleRetry = this.handleRetry.bind(this); @@ -120,6 +121,7 @@ class ArtikelMarkenPage extends PureComponent { this.unsub = brandAutomat.subscribe(this, (state) => ({ brands: state.filteredBrands, + totalCount: state.brands?.length ?? 0, status: state.status, error: state.error ? backendErrorFrom({ message: state.error }) : null, })); @@ -159,6 +161,7 @@ class ArtikelMarkenPage extends PureComponent { minPageSize={8} minHeight={0} items={brands} + totalCount={this.state.totalCount} renderItem={ArtikelMarke} /> diff --git a/src/routes/artikel/merkmale.jsx b/src/routes/artikel/merkmale.jsx index ca8c52a..6289ef2 100644 --- a/src/routes/artikel/merkmale.jsx +++ b/src/routes/artikel/merkmale.jsx @@ -499,6 +499,7 @@ class ArtikelMerkmalePage extends PureComponent { ) : ( { const build = spawn('npx', ['vite', 'build'], { cwd: FRONT_ROOT, - env: { ...process.env }, + env: { ...process.env, NODE_ENV: 'production' }, stdio: ['ignore', 'pipe', 'pipe'], }); build.stdout.on('data', () => process.stdout.write('[build] ')); @@ -208,7 +208,7 @@ async function main() { const server = startProc( 'npx', ['vite', 'preview', '--port', String(VITE_PORT), '--strictPort'], - { RIG_API_TARGET: `http://localhost:${DUMMY_PORT}` }, + { RIG_API_TARGET: `http://localhost:${DUMMY_PORT}`, NODE_ENV: 'production' }, 'preview', ); await waitForPort(VITE_PORT); @@ -248,6 +248,7 @@ async function main() { height: viewport.height, settleMs: SETTLE_MS, dialog: route.dialog, + icons: route.icons, }); const kb = (buf.length / 1024).toFixed(0); diff --git a/test/cdp.mjs b/test/cdp.mjs index 4df44ec..01a99a5 100644 --- a/test/cdp.mjs +++ b/test/cdp.mjs @@ -161,7 +161,7 @@ async function httpJson(url, options = {}) { * Opens a new page target on the browser, navigates, waits for the app root * to contain content + network to quiet down, captures a PNG. */ -export async function capturePage(debugPort, url, file, { width, height, settleMs = 6000, dialog }) { +export async function capturePage(debugPort, url, file, { width, height, settleMs = 6000, dialog, icons }) { // Chrome ≥136 requires PUT for /json/new. const target = await httpJson(`http://127.0.0.1:${debugPort}/json/new?${encodeURIComponent(url)}`, { method: 'PUT', @@ -257,6 +257,55 @@ export async function capturePage(debugPort, url, file, { width, height, settleM } } + // Icon sheet: replace the page by a white sheet with every distinct icon + // (inline SVG) found on it, drawn in black. + if (icons) { + await session.send('Runtime.evaluate', { + expression: `(() => { + const seen = new Map(); + for (const svg of document.querySelectorAll('svg')) { + const r = svg.getBoundingClientRect(); + if (r.width > 64 || r.height > 64) continue; // skip illustrations / spinners + if (svg.closest('[role="progressbar"]')) continue; + const key = svg.getAttribute('data-testid') || svg.innerHTML; + if (!svg.innerHTML || seen.has(key)) continue; + seen.set(key, svg); + } + const style = document.createElement('style'); + style.textContent = \` + #icon-sheet{position:absolute;inset:0;background:#fff;color:#000;padding:24px; + display:flex;flex-wrap:wrap;gap:20px;align-content:flex-start;font:11px sans-serif} + #icon-sheet .cell{width:144px;display:flex;flex-direction:column;align-items:center;gap:6px;text-align:center;word-break:break-all} + #icon-sheet svg{width:144px;height:144px;color:#000;fill:#000} + #icon-sheet svg *{fill:#000;color:#000} + #icon-sheet svg [stroke]:not([stroke=none]){stroke:#000} + \`; + document.head.appendChild(style); + document.getElementById('root').style.display = 'none'; + const sheet = document.createElement('div'); + sheet.id = 'icon-sheet'; + const sorted = [...seen.entries()].sort((a, b) => String(a[0]).localeCompare(String(b[0]))); + for (const [key, svg] of sorted) { + const cell = document.createElement('div'); + cell.className = 'cell'; + const clone = svg.cloneNode(true); + clone.removeAttribute('class'); + clone.removeAttribute('style'); + clone.removeAttribute('width'); + clone.removeAttribute('height'); + cell.appendChild(clone); + const label = document.createElement('span'); + label.textContent = svg.getAttribute('data-testid') || ''; + cell.appendChild(label); + sheet.appendChild(cell); + } + document.body.style.background = '#fff'; + document.body.appendChild(sheet); + })()`, + }); + await wait(300); + } + // Determinism: hide carets and scrollbars. await session.send('Runtime.evaluate', { expression: `(() => { const s = document.createElement('style'); diff --git a/test/routes.mjs b/test/routes.mjs index 4588067..1ec2bfb 100644 --- a/test/routes.mjs +++ b/test/routes.mjs @@ -20,6 +20,8 @@ export const ROUTES = [ { name: 'vorlagen', path: '/artikel/vorlagen' }, { name: 'merkmale', path: '/artikel/merkmale' }, { name: 'artikel-detail', path: '/artikel/ART-000001' }, + // Same page, but the shot is a white sheet with every icon on it in black. + { name: 'artikel-detail-icons', path: '/artikel/ART-000001', icons: true }, { name: 'lieferung', path: '/lieferung' }, { name: 'lieferung-neu', path: '/lieferung/neu' }, { name: 'lieferanten', path: '/lieferung/lieferanten' }, diff --git a/test/screenshots/reference/artikel-detail-icons.1280x960.png b/test/screenshots/reference/artikel-detail-icons.1280x960.png new file mode 100644 index 0000000..516561d Binary files /dev/null and b/test/screenshots/reference/artikel-detail-icons.1280x960.png differ diff --git a/test/screenshots/reference/artikel-detail-icons.1920x1200.png b/test/screenshots/reference/artikel-detail-icons.1920x1200.png new file mode 100644 index 0000000..7d25789 Binary files /dev/null and b/test/screenshots/reference/artikel-detail-icons.1920x1200.png differ diff --git a/test/screenshots/reference/artikel-detail.1280x960.png b/test/screenshots/reference/artikel-detail.1280x960.png index b056d2a..bc10022 100644 Binary files a/test/screenshots/reference/artikel-detail.1280x960.png and b/test/screenshots/reference/artikel-detail.1280x960.png differ diff --git a/test/screenshots/reference/artikel-detail.1920x1200.png b/test/screenshots/reference/artikel-detail.1920x1200.png index b4dd5ba..303be87 100644 Binary files a/test/screenshots/reference/artikel-detail.1920x1200.png and b/test/screenshots/reference/artikel-detail.1920x1200.png differ diff --git a/test/screenshots/reference/artikel.1280x960.png b/test/screenshots/reference/artikel.1280x960.png index d828d24..874d924 100644 Binary files a/test/screenshots/reference/artikel.1280x960.png and b/test/screenshots/reference/artikel.1280x960.png differ diff --git a/test/screenshots/reference/artikel.1920x1200.png b/test/screenshots/reference/artikel.1920x1200.png index 057a95c..7f25af7 100644 Binary files a/test/screenshots/reference/artikel.1920x1200.png and b/test/screenshots/reference/artikel.1920x1200.png differ diff --git a/test/screenshots/reference/bestellung.1280x960.png b/test/screenshots/reference/bestellung.1280x960.png index 1e11d64..8a51d94 100644 Binary files a/test/screenshots/reference/bestellung.1280x960.png and b/test/screenshots/reference/bestellung.1280x960.png differ diff --git a/test/screenshots/reference/bestellung.1920x1200.png b/test/screenshots/reference/bestellung.1920x1200.png index 4d46b3b..d2c0bb9 100644 Binary files a/test/screenshots/reference/bestellung.1920x1200.png and b/test/screenshots/reference/bestellung.1920x1200.png differ diff --git a/test/screenshots/reference/dialog-settings-drucker.1280x960.png b/test/screenshots/reference/dialog-settings-drucker.1280x960.png index 93265fa..f326516 100644 Binary files a/test/screenshots/reference/dialog-settings-drucker.1280x960.png and b/test/screenshots/reference/dialog-settings-drucker.1280x960.png differ diff --git a/test/screenshots/reference/dialog-settings-drucker.1920x1200.png b/test/screenshots/reference/dialog-settings-drucker.1920x1200.png index 8881c11..d2ac603 100644 Binary files a/test/screenshots/reference/dialog-settings-drucker.1920x1200.png and b/test/screenshots/reference/dialog-settings-drucker.1920x1200.png differ diff --git a/test/screenshots/reference/dialog-settings-einstellungen.1280x960.png b/test/screenshots/reference/dialog-settings-einstellungen.1280x960.png index a632911..ceb060b 100644 Binary files a/test/screenshots/reference/dialog-settings-einstellungen.1280x960.png and b/test/screenshots/reference/dialog-settings-einstellungen.1280x960.png differ diff --git a/test/screenshots/reference/dialog-settings-einstellungen.1920x1200.png b/test/screenshots/reference/dialog-settings-einstellungen.1920x1200.png index 4213623..85dc10f 100644 Binary files a/test/screenshots/reference/dialog-settings-einstellungen.1920x1200.png and b/test/screenshots/reference/dialog-settings-einstellungen.1920x1200.png differ diff --git a/test/screenshots/reference/dialog-settings-importexport.1280x960.png b/test/screenshots/reference/dialog-settings-importexport.1280x960.png index 069ef8a..f7c3f81 100644 Binary files a/test/screenshots/reference/dialog-settings-importexport.1280x960.png and b/test/screenshots/reference/dialog-settings-importexport.1280x960.png differ diff --git a/test/screenshots/reference/dialog-settings-importexport.1920x1200.png b/test/screenshots/reference/dialog-settings-importexport.1920x1200.png index 7fdb98b..8e4cee4 100644 Binary files a/test/screenshots/reference/dialog-settings-importexport.1920x1200.png and b/test/screenshots/reference/dialog-settings-importexport.1920x1200.png differ diff --git a/test/screenshots/reference/dialog-settings-nummernkreise.1280x960.png b/test/screenshots/reference/dialog-settings-nummernkreise.1280x960.png index 254e22e..a2f7161 100644 Binary files a/test/screenshots/reference/dialog-settings-nummernkreise.1280x960.png and b/test/screenshots/reference/dialog-settings-nummernkreise.1280x960.png differ diff --git a/test/screenshots/reference/dialog-settings-nummernkreise.1920x1200.png b/test/screenshots/reference/dialog-settings-nummernkreise.1920x1200.png index 70b5bd7..252021f 100644 Binary files a/test/screenshots/reference/dialog-settings-nummernkreise.1920x1200.png and b/test/screenshots/reference/dialog-settings-nummernkreise.1920x1200.png differ diff --git a/test/screenshots/reference/dialog-settings-posterminal.1280x960.png b/test/screenshots/reference/dialog-settings-posterminal.1280x960.png index 7abbd49..7e8aec7 100644 Binary files a/test/screenshots/reference/dialog-settings-posterminal.1280x960.png and b/test/screenshots/reference/dialog-settings-posterminal.1280x960.png differ diff --git a/test/screenshots/reference/dialog-settings-posterminal.1920x1200.png b/test/screenshots/reference/dialog-settings-posterminal.1920x1200.png index b9809b9..feda611 100644 Binary files a/test/screenshots/reference/dialog-settings-posterminal.1920x1200.png and b/test/screenshots/reference/dialog-settings-posterminal.1920x1200.png differ diff --git a/test/screenshots/reference/dialog-settings-unternehmen.1280x960.png b/test/screenshots/reference/dialog-settings-unternehmen.1280x960.png index e5d5683..37ae30d 100644 Binary files a/test/screenshots/reference/dialog-settings-unternehmen.1280x960.png and b/test/screenshots/reference/dialog-settings-unternehmen.1280x960.png differ diff --git a/test/screenshots/reference/dialog-settings-unternehmen.1920x1200.png b/test/screenshots/reference/dialog-settings-unternehmen.1920x1200.png index ec650e5..344a905 100644 Binary files a/test/screenshots/reference/dialog-settings-unternehmen.1920x1200.png and b/test/screenshots/reference/dialog-settings-unternehmen.1920x1200.png differ diff --git a/test/screenshots/reference/gruppen.1280x960.png b/test/screenshots/reference/gruppen.1280x960.png index 6938ca8..533f555 100644 Binary files a/test/screenshots/reference/gruppen.1280x960.png and b/test/screenshots/reference/gruppen.1280x960.png differ diff --git a/test/screenshots/reference/gruppen.1920x1200.png b/test/screenshots/reference/gruppen.1920x1200.png index 63bf6f4..f17992b 100644 Binary files a/test/screenshots/reference/gruppen.1920x1200.png and b/test/screenshots/reference/gruppen.1920x1200.png differ diff --git a/test/screenshots/reference/lager.1280x960.png b/test/screenshots/reference/lager.1280x960.png index 09b2c86..55f5984 100644 Binary files a/test/screenshots/reference/lager.1280x960.png and b/test/screenshots/reference/lager.1280x960.png differ diff --git a/test/screenshots/reference/lager.1920x1200.png b/test/screenshots/reference/lager.1920x1200.png index 5089c26..87d17e1 100644 Binary files a/test/screenshots/reference/lager.1920x1200.png and b/test/screenshots/reference/lager.1920x1200.png differ diff --git a/test/screenshots/reference/marken.1280x960.png b/test/screenshots/reference/marken.1280x960.png index da41e89..7439ab5 100644 Binary files a/test/screenshots/reference/marken.1280x960.png and b/test/screenshots/reference/marken.1280x960.png differ diff --git a/test/screenshots/reference/marken.1920x1200.png b/test/screenshots/reference/marken.1920x1200.png index 94ce48b..c3bcfd6 100644 Binary files a/test/screenshots/reference/marken.1920x1200.png and b/test/screenshots/reference/marken.1920x1200.png differ diff --git a/test/screenshots/reference/merkmale.1280x960.png b/test/screenshots/reference/merkmale.1280x960.png index 3b4cd30..004823b 100644 Binary files a/test/screenshots/reference/merkmale.1280x960.png and b/test/screenshots/reference/merkmale.1280x960.png differ diff --git a/test/screenshots/reference/merkmale.1920x1200.png b/test/screenshots/reference/merkmale.1920x1200.png index 5010064..7b4ae7c 100644 Binary files a/test/screenshots/reference/merkmale.1920x1200.png and b/test/screenshots/reference/merkmale.1920x1200.png differ