cursedbelt 5.30.0 β†’ 5.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/react/components/ConfirmDialog.d.ts +4 -1
  2. package/dist/react/components/ConfirmDialog.d.ts.map +1 -1
  3. package/dist/react/components/ConfirmDialog.js +2 -2
  4. package/dist/react/components/ConfirmDialog.js.map +1 -1
  5. package/dist/react/components/DataTable/DataTable.d.ts.map +1 -1
  6. package/dist/react/components/DataTable/DataTable.js +5 -1
  7. package/dist/react/components/DataTable/DataTable.js.map +1 -1
  8. package/dist/react/components/PromptDialog.d.ts +4 -1
  9. package/dist/react/components/PromptDialog.d.ts.map +1 -1
  10. package/dist/react/components/PromptDialog.js +2 -2
  11. package/dist/react/components/PromptDialog.js.map +1 -1
  12. package/dist/react/file-tree/FileTree.d.ts +26 -1
  13. package/dist/react/file-tree/FileTree.d.ts.map +1 -1
  14. package/dist/react/file-tree/FileTree.js +182 -40
  15. package/dist/react/file-tree/FileTree.js.map +1 -1
  16. package/dist/scripts/guardrailsEnforce.d.ts +11 -0
  17. package/dist/scripts/guardrailsEnforce.d.ts.map +1 -1
  18. package/dist/testing/index.d.ts +3 -0
  19. package/dist/testing/index.d.ts.map +1 -0
  20. package/dist/testing/index.js +4 -0
  21. package/dist/testing/index.js.map +1 -0
  22. package/dist/testing/objectLeak.d.ts +23 -0
  23. package/dist/testing/objectLeak.d.ts.map +1 -0
  24. package/dist/testing/objectLeak.js +37 -0
  25. package/dist/testing/objectLeak.js.map +1 -0
  26. package/package.json +5 -5
  27. package/scripts/guardrails-baseline.json +4 -1
  28. package/scripts/guardrailsEnforce.spec.ts +163 -6
  29. package/scripts/guardrailsEnforce.ts +365 -40
  30. package/scripts/paths.ts +1 -1
  31. package/src/react/components/ConfirmDialog.tsx +5 -0
  32. package/src/react/components/DataTable/DataTable.spec.tsx +16 -0
  33. package/src/react/components/DataTable/DataTable.tsx +5 -1
  34. package/src/react/components/PromptDialog.spec.tsx +29 -1
  35. package/src/react/components/PromptDialog.tsx +5 -0
  36. package/src/react/file-tree/FileTree.spec.tsx +205 -0
  37. package/src/react/file-tree/FileTree.tsx +220 -38
  38. package/src/testing/index.ts +3 -0
  39. package/src/testing/objectLeak.spec.ts +21 -0
  40. package/src/testing/objectLeak.ts +39 -0
@@ -536,3 +536,19 @@ describe('TableViewToggle', () => {
536
536
  expect(container.textContent).toContain('Cards');
537
537
  });
538
538
  });
539
+
540
+ // forge task 2396: hiding the pager used to leave the paging on, so row 51 and beyond were
541
+ // simply not there β€” with no control to reach them.
542
+ describe('hidePagination shows every row', () => {
543
+ const MANY: Person[] = Array.from({ length: 60 }, (_, i) => ({ name: `p${String(i).padStart(2, '0')}`, age: i }));
544
+
545
+ test('60 rows with no pager render 60 rows', () => {
546
+ const { container } = render(<DataTable columns={COLUMNS} data={MANY} hidePagination />);
547
+ expect(bodyRows(container).length).toBe(60);
548
+ });
549
+
550
+ test('with the pager, the first page is still 50', () => {
551
+ const { container } = render(<DataTable columns={COLUMNS} data={MANY} />);
552
+ expect(bodyRows(container).length).toBe(50);
553
+ });
554
+ });
@@ -1270,7 +1270,11 @@ export function DataTable<TRow extends Record<string, unknown>>({
1270
1270
  getFilteredRowModel: serverSide || serverWindow ? undefined : getFilteredRowModel(),
1271
1271
  // πŸ”΄ `virtualEnabled` too, not only `serverSide`. See the note where the flag is
1272
1272
  // computed: a paginated virtualizer sizes its spacer from one PAGE.
1273
- getPaginationRowModel: serverSide || virtualEnabled ? undefined : getPaginationRowModel(),
1273
+ // πŸ”΄ And `hidePagination` (forge task 2396): hiding the pager used to leave the paging ON,
1274
+ // so a table past `pageSize` rows (50) silently showed its first page with no control to
1275
+ // reach the rest β€” binary-server's run history and station's metric tables among them.
1276
+ // A table with no pager shows every row; that is what "no pager" means.
1277
+ getPaginationRowModel: serverSide || paginationHidden ? undefined : getPaginationRowModel(),
1274
1278
  // TanStack's built-in auto-reset fires `resetPageIndex()` on EVERY `data`
1275
1279
  // identity change (see `getCoreRowModel`'s memo deps), not just on filter/sort
1276
1280
  // changes β€” any live-polling table (this one included: rows are re-derived
@@ -1,5 +1,6 @@
1
1
  import { afterEach, describe, expect, test } from 'bun:test';
2
2
  import { cleanup, fireEvent, render, waitFor } from '@testing-library/react';
3
+ import { confirm } from '../confirm.js';
3
4
  import { prompt } from '../prompt.js';
4
5
  import { PromptDialog } from './PromptDialog.js';
5
6
 
@@ -64,7 +65,7 @@ describe('PromptDialog', () => {
64
65
  }}
65
66
  />,
66
67
  );
67
- fireEvent.click(body().querySelector('[data-cb-cancel]') as HTMLElement);
68
+ fireEvent.keyDown(dialogs()[0] as HTMLElement, { key: 'Escape' });
68
69
  expect(cancelled).toBe(1);
69
70
  expect(submitted).toBe(0);
70
71
  });
@@ -116,3 +117,30 @@ describe('prompt() imperative', () => {
116
117
  expect(await result).toBeNull();
117
118
  });
118
119
  });
120
+
121
+ // The dialogs portal to <body>, outside any subtree an app scopes the belt's preflight under
122
+ // (binary-server's admin: `cb-scope`), so the class has to reach the content box itself.
123
+ describe('className reaches the portalled dialog (forge task 2380)', () => {
124
+ const cancelButton = () =>
125
+ waitFor(() => {
126
+ const b = body().querySelector('[data-cb-cancel]') as HTMLButtonElement | null;
127
+ if (!b) throw new Error('not yet');
128
+ return b;
129
+ });
130
+
131
+ test('prompt({ className }) puts it on the dialog', async () => {
132
+ const typed = prompt({ title: 'Type it', className: 'cb-scope' });
133
+ const cancel = await cancelButton();
134
+ expect(cancel.closest('[role="dialog"]')?.classList.contains('cb-scope')).toBe(true);
135
+ fireEvent.click(cancel);
136
+ expect(await typed).toBeNull();
137
+ });
138
+
139
+ test('confirm({ className }) puts it on the dialog', async () => {
140
+ const asked = confirm({ title: 'Sure?', className: 'cb-scope' });
141
+ const cancel = await cancelButton();
142
+ expect(cancel.closest('[role="dialog"], [role="alertdialog"]')?.classList.contains('cb-scope')).toBe(true);
143
+ fireEvent.click(cancel);
144
+ expect(await asked).toBe(false);
145
+ });
146
+ });
@@ -16,6 +16,9 @@ export interface PromptDialogProps {
16
16
  cancelLabel?: string;
17
17
  /** Spinner on the confirm button while an async `onSubmit` runs (caller-controlled). */
18
18
  loading?: boolean;
19
+ /** Added to the dialog's content box β€” an app that scopes the belt's preflight under a class
20
+ * (binary-server's admin: `cb-scope`) passes it here, because the dialog portals to <body>. */
21
+ className?: string;
19
22
  }
20
23
 
21
24
  /**
@@ -37,6 +40,7 @@ export function PromptDialog({
37
40
  confirmLabel = 'OK',
38
41
  cancelLabel = 'Cancel',
39
42
  loading = false,
43
+ className,
40
44
  }: PromptDialogProps) {
41
45
  const [value, setValue] = useState(defaultValue);
42
46
  const inputRef = useRef<HTMLInputElement>(null);
@@ -58,6 +62,7 @@ export function PromptDialog({
58
62
  'rounded-xl border border-border bg-popover p-6 text-popover-foreground shadow-overlay',
59
63
  'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
60
64
  'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
65
+ className,
61
66
  )}
62
67
  onOpenAutoFocus={(e) => {
63
68
  e.preventDefault();
@@ -1553,6 +1553,140 @@ describe('the clipboard', () => {
1553
1553
  expect(cutEvent.defaultPrevented).toBe(false);
1554
1554
  expect(cut.container.querySelectorAll('[data-cut]')).toHaveLength(0);
1555
1555
  });
1556
+
1557
+ /**
1558
+ * Task 2371 β€” Finder has ONE clipboard, so the newest copy wins. ⌘C a row, later βŒ˜β‡§4 + ⌘C a
1559
+ * screenshot, then ⌘V on a row: the screenshot must reach the consumer's paste (desk files
1560
+ * it into the folder), not be swallowed by a row copy held since.
1561
+ */
1562
+ describe('πŸ”΄ the newest copy wins', () => {
1563
+ const written: string[] = [];
1564
+ let restore: () => void = () => undefined;
1565
+ const withSystemClipboard = () => {
1566
+ const had = Object.getOwnPropertyDescriptor(navigator, 'clipboard');
1567
+ Object.defineProperty(navigator, 'clipboard', {
1568
+ configurable: true,
1569
+ value: {
1570
+ writeText: async (text: string) => {
1571
+ written.push(text);
1572
+ },
1573
+ },
1574
+ });
1575
+ restore = () => {
1576
+ if (had) Object.defineProperty(navigator, 'clipboard', had);
1577
+ else delete (navigator as { clipboard?: unknown }).clipboard;
1578
+ };
1579
+ };
1580
+ afterEach(() => {
1581
+ restore();
1582
+ written.length = 0;
1583
+ });
1584
+
1585
+ /** A consumer around the tree that takes pasted files, the way desk's LibraryTree does. */
1586
+ const hosted = (props: Partial<React.ComponentProps<typeof FileTree>>, heard: string[]) =>
1587
+ render(
1588
+ <div
1589
+ onPaste={(event) => {
1590
+ for (const file of Array.from(event.clipboardData.files)) heard.push(file.name);
1591
+ }}
1592
+ >
1593
+ <FileTree nodes={TREE} selectedId={null} onSelect={() => undefined} {...props} />
1594
+ </div>,
1595
+ );
1596
+
1597
+ const pressV = (name: string) => {
1598
+ const row = focusRow(name);
1599
+ const key = new KeyboardEvent('keydown', {
1600
+ key: 'v',
1601
+ metaKey: true,
1602
+ bubbles: true,
1603
+ cancelable: true,
1604
+ });
1605
+ row.dispatchEvent(key);
1606
+ return { row, key };
1607
+ };
1608
+
1609
+ const shot = new File([new Uint8Array([137, 80, 78, 71])], 'Pasted image.png', {
1610
+ type: 'image/png',
1611
+ });
1612
+ const clipboardWith = (text: string, files: File[] = []) => ({
1613
+ files,
1614
+ items: files.map(() => ({ kind: 'file' })),
1615
+ getData: (type: string) => (type === 'text/plain' ? text : ''),
1616
+ });
1617
+
1618
+ test('⌘C puts the rows’ names on the system clipboard as well', async () => {
1619
+ withSystemClipboard();
1620
+ tree({ selectedIds: ['dinos', 'travels'], onSelectionChange: () => undefined, onCopy: () => undefined });
1621
+ fireEvent.keyDown(focusRow('Dinos'), { key: 'c', metaKey: true });
1622
+ await act(async () => undefined);
1623
+ expect(written).toEqual(['Dinos\nTravels']);
1624
+ });
1625
+
1626
+ test('πŸ”΄ a screenshot copied after a row ⌘C reaches the consumer, and the row copy is put down', async () => {
1627
+ withSystemClipboard();
1628
+ const copied: unknown[] = [];
1629
+ const heard: string[] = [];
1630
+ hosted({ onCopy: (ids) => copied.push(ids) }, heard);
1631
+ fireEvent.keyDown(focusRow('Dinos'), { key: 'c', metaKey: true });
1632
+ await act(async () => undefined);
1633
+
1634
+ const { row, key } = pressV('Simpsons');
1635
+ // Left to the browser, or no `paste` event would fire at all.
1636
+ expect(key.defaultPrevented).toBe(false);
1637
+ fireEvent.paste(row, { clipboardData: clipboardWith('', [shot]) });
1638
+ await act(async () => new Promise((done) => setTimeout(done, 5)));
1639
+ expect(heard).toEqual(['Pasted image.png']);
1640
+ expect(copied).toEqual([]);
1641
+
1642
+ // Put down, not merely skipped once: the next ⌘V is not the tree's either.
1643
+ const again = pressV('screencaps');
1644
+ await act(async () => new Promise((done) => setTimeout(done, 5)));
1645
+ expect(again.key.defaultPrevented).toBe(false);
1646
+ expect(copied).toEqual([]);
1647
+ });
1648
+
1649
+ test('πŸ”΄ a held ⌘X is put down by newer text the same way β€” desk’s tree has cut, not copy', async () => {
1650
+ withSystemClipboard();
1651
+ const moved: unknown[] = [];
1652
+ const { container } = hosted({ onMove: (id) => moved.push(id) }, []);
1653
+ fireEvent.keyDown(focusRow('Dinos'), { key: 'x', metaKey: true });
1654
+ await act(async () => undefined);
1655
+ const { row } = pressV('Simpsons');
1656
+ fireEvent.paste(row, { clipboardData: clipboardWith('something copied since') });
1657
+ await act(async () => new Promise((done) => setTimeout(done, 5)));
1658
+ expect(moved).toEqual([]);
1659
+ expect(container.querySelectorAll('[data-cut]')).toHaveLength(0);
1660
+ });
1661
+
1662
+ test('πŸ”΄ the rows’ own names still on the clipboard mean the rows ARE newest β€” pasted, and the consumer never hears it', async () => {
1663
+ withSystemClipboard();
1664
+ const copied: Array<[readonly string[], unknown]> = [];
1665
+ const heard: string[] = [];
1666
+ hosted({ onCopy: (ids, drop) => copied.push([ids, drop]) }, heard);
1667
+ fireEvent.keyDown(focusRow('Dinos'), { key: 'c', metaKey: true });
1668
+ await act(async () => undefined);
1669
+ const { row } = pressV('Simpsons');
1670
+ const paste = fireEvent.paste(row, { clipboardData: clipboardWith('Dinos') });
1671
+ // `fireEvent` returns false when the event was cancelled.
1672
+ expect(paste).toBe(false);
1673
+ await act(async () => new Promise((done) => setTimeout(done, 5)));
1674
+ // Once β€” the fallback timer must not paste the same keypress a second time.
1675
+ expect(copied).toEqual([[['dinos'], { kind: 'into', parentId: 'simpsons' }]]);
1676
+ expect(heard).toEqual([]);
1677
+ });
1678
+
1679
+ test('an engine that sends no `paste` to a row still pastes the rows, from the keypress', async () => {
1680
+ withSystemClipboard();
1681
+ const copied: unknown[] = [];
1682
+ tree({ onCopy: (ids) => copied.push(ids) });
1683
+ fireEvent.keyDown(focusRow('Dinos'), { key: 'c', metaKey: true });
1684
+ await act(async () => undefined);
1685
+ pressV('Simpsons');
1686
+ await act(async () => new Promise((done) => setTimeout(done, 5)));
1687
+ expect(copied).toEqual([['dinos']]);
1688
+ });
1689
+ });
1556
1690
  });
1557
1691
 
1558
1692
  /**
@@ -2046,3 +2180,74 @@ describe('every row carries its id', () => {
2046
2180
  ]);
2047
2181
  });
2048
2182
  });
2183
+
2184
+ /*
2185
+ * `onDropOutside` β€” rows carried out of the tree onto something drawn beside it.
2186
+ *
2187
+ * happy-dom lays nothing out (see the header), so the tree's own box is given one here and
2188
+ * every row stays a zero rect: that is enough to tell "released outside the tree" from
2189
+ * "released inside it", which is the whole decision this prop adds. Where inside a drop
2190
+ * lands is the model's arithmetic, tested in `fileTreeModel.spec.ts`.
2191
+ */
2192
+ describe('onDropOutside β€” a drag let go beside the tree', () => {
2193
+ const boxTree = (container: HTMLElement) => {
2194
+ const root = container.querySelector('.cbft') as HTMLElement;
2195
+ root.getBoundingClientRect = () =>
2196
+ ({ left: 0, top: 0, right: 200, bottom: 400, width: 200, height: 400, x: 0, y: 0 }) as DOMRect;
2197
+ };
2198
+ const dragDinos = (to: { x: number; y: number }) => {
2199
+ const row = screen.getByText('Dinos').closest('[role="treeitem"]') as HTMLElement;
2200
+ fireEvent.pointerDown(row, { button: 0, clientX: 40, clientY: 30, pointerId: 1, pointerType: 'mouse' });
2201
+ fireEvent.pointerMove(row, { clientX: to.x, clientY: to.y, pointerId: 1, pointerType: 'mouse' });
2202
+ return row;
2203
+ };
2204
+
2205
+ test('released outside, the rows and the point go to the consumer β€” and nothing moves', () => {
2206
+ const onDropOutside = jest.fn();
2207
+ const onMove = jest.fn();
2208
+ const { container } = tree({ onMove, onDropOutside });
2209
+ boxTree(container);
2210
+ const row = dragDinos({ x: 500, y: 60 });
2211
+ expect(container.querySelector('.cbft-chip-where')?.textContent).toBe('Drop to add it here');
2212
+ fireEvent.pointerUp(row, { clientX: 500, clientY: 60, pointerId: 1, pointerType: 'mouse' });
2213
+ expect(onDropOutside).toHaveBeenCalledTimes(1);
2214
+ expect(onDropOutside.mock.calls[0]).toEqual([['dinos'], { clientX: 500, clientY: 60 }]);
2215
+ expect(onMove).not.toHaveBeenCalled();
2216
+ });
2217
+
2218
+ test('released inside, it is a rearrange as it always was β€” the consumer hears nothing', () => {
2219
+ const onDropOutside = jest.fn();
2220
+ const { container } = tree({ onMove: () => undefined, onDropOutside });
2221
+ boxTree(container);
2222
+ const row = dragDinos({ x: 100, y: 60 });
2223
+ fireEvent.pointerUp(row, { clientX: 100, clientY: 60, pointerId: 1, pointerType: 'mouse' });
2224
+ expect(onDropOutside).not.toHaveBeenCalled();
2225
+ });
2226
+
2227
+ test('describeDropOutside names the place, and null refuses it β€” the rows go back', () => {
2228
+ const onDropOutside = jest.fn();
2229
+ const { container } = tree({
2230
+ onDropOutside,
2231
+ describeDropOutside: ({ clientX }) => (clientX > 400 ? 'Onto the canvas' : null),
2232
+ });
2233
+ boxTree(container);
2234
+ const row = dragDinos({ x: 300, y: 60 });
2235
+ const chip = () => container.querySelector('.cbft-chip') as HTMLElement;
2236
+ expect(chip().getAttribute('data-refused')).toBe('true');
2237
+ expect(chip().textContent).toContain('Nothing here takes it');
2238
+ fireEvent.pointerUp(row, { clientX: 300, clientY: 60, pointerId: 1, pointerType: 'mouse' });
2239
+ expect(onDropOutside).not.toHaveBeenCalled();
2240
+
2241
+ const again = dragDinos({ x: 600, y: 60 });
2242
+ expect(chip().textContent).toContain('Onto the canvas');
2243
+ fireEvent.pointerUp(again, { clientX: 600, clientY: 60, pointerId: 1, pointerType: 'mouse' });
2244
+ expect(onDropOutside).toHaveBeenCalledTimes(1);
2245
+ });
2246
+
2247
+ test('without it, a tree that cannot be rearranged starts no drag at all', () => {
2248
+ const { container } = tree();
2249
+ boxTree(container);
2250
+ dragDinos({ x: 500, y: 60 });
2251
+ expect(container.querySelector('.cbft-chip')).toBeNull();
2252
+ });
2253
+ });