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.
- package/dist/react/components/ConfirmDialog.d.ts +4 -1
- package/dist/react/components/ConfirmDialog.d.ts.map +1 -1
- package/dist/react/components/ConfirmDialog.js +2 -2
- package/dist/react/components/ConfirmDialog.js.map +1 -1
- package/dist/react/components/DataTable/DataTable.d.ts.map +1 -1
- package/dist/react/components/DataTable/DataTable.js +5 -1
- package/dist/react/components/DataTable/DataTable.js.map +1 -1
- package/dist/react/components/PromptDialog.d.ts +4 -1
- package/dist/react/components/PromptDialog.d.ts.map +1 -1
- package/dist/react/components/PromptDialog.js +2 -2
- package/dist/react/components/PromptDialog.js.map +1 -1
- package/dist/react/file-tree/FileTree.d.ts +26 -1
- package/dist/react/file-tree/FileTree.d.ts.map +1 -1
- package/dist/react/file-tree/FileTree.js +182 -40
- package/dist/react/file-tree/FileTree.js.map +1 -1
- package/dist/scripts/guardrailsEnforce.d.ts +11 -0
- package/dist/scripts/guardrailsEnforce.d.ts.map +1 -1
- package/dist/testing/index.d.ts +3 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +4 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/objectLeak.d.ts +23 -0
- package/dist/testing/objectLeak.d.ts.map +1 -0
- package/dist/testing/objectLeak.js +37 -0
- package/dist/testing/objectLeak.js.map +1 -0
- package/package.json +5 -5
- package/scripts/guardrails-baseline.json +4 -1
- package/scripts/guardrailsEnforce.spec.ts +163 -6
- package/scripts/guardrailsEnforce.ts +365 -40
- package/scripts/paths.ts +1 -1
- package/src/react/components/ConfirmDialog.tsx +5 -0
- package/src/react/components/DataTable/DataTable.spec.tsx +16 -0
- package/src/react/components/DataTable/DataTable.tsx +5 -1
- package/src/react/components/PromptDialog.spec.tsx +29 -1
- package/src/react/components/PromptDialog.tsx +5 -0
- package/src/react/file-tree/FileTree.spec.tsx +205 -0
- package/src/react/file-tree/FileTree.tsx +220 -38
- package/src/testing/index.ts +3 -0
- package/src/testing/objectLeak.spec.ts +21 -0
- 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
|
-
|
|
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.
|
|
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
|
+
});
|