cursedbelt-core 1.0.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/LICENSE +15 -0
- package/dist/client/wsWire.d.ts +49 -0
- package/dist/client/wsWire.js +57 -0
- package/dist/config/appConfig.d.ts +361 -0
- package/dist/config/appConfig.js +308 -0
- package/dist/core/activity/index.d.ts +8 -0
- package/dist/core/activity/index.js +7 -0
- package/dist/core/activity/model.d.ts +115 -0
- package/dist/core/activity/model.js +63 -0
- package/dist/core/analytics/index.d.ts +18 -0
- package/dist/core/analytics/index.js +18 -0
- package/dist/core/analytics/range.d.ts +186 -0
- package/dist/core/analytics/range.js +271 -0
- package/dist/core/analytics/schedule.d.ts +301 -0
- package/dist/core/analytics/schedule.js +372 -0
- package/dist/core/category/index.d.ts +3 -0
- package/dist/core/category/index.js +6 -0
- package/dist/core/category/memoryStore.d.ts +21 -0
- package/dist/core/category/memoryStore.js +97 -0
- package/dist/core/category/tree.d.ts +26 -0
- package/dist/core/category/tree.js +113 -0
- package/dist/core/category/types.d.ts +48 -0
- package/dist/core/category/types.js +8 -0
- package/dist/core/color.d.ts +14 -0
- package/dist/core/color.js +32 -0
- package/dist/core/colorScheme/colorSchemeStore.d.ts +70 -0
- package/dist/core/colorScheme/colorSchemeStore.js +123 -0
- package/dist/core/colorScheme/index.d.ts +7 -0
- package/dist/core/colorScheme/index.js +7 -0
- package/dist/core/domainOwners.d.ts +19 -0
- package/dist/core/domainOwners.js +15 -0
- package/dist/core/download.d.ts +14 -0
- package/dist/core/download.js +37 -0
- package/dist/core/events/ccEventBus.d.ts +44 -0
- package/dist/core/events/ccEventBus.js +70 -0
- package/dist/core/file-tree/fileTreeModel.d.ts +276 -0
- package/dist/core/file-tree/fileTreeModel.js +312 -0
- package/dist/core/folder-tree/actions.d.ts +84 -0
- package/dist/core/folder-tree/actions.js +119 -0
- package/dist/core/folder-tree/index.d.ts +3 -0
- package/dist/core/folder-tree/index.js +20 -0
- package/dist/core/folder-tree/path.d.ts +76 -0
- package/dist/core/folder-tree/path.js +146 -0
- package/dist/core/folder-tree/tree.d.ts +86 -0
- package/dist/core/folder-tree/tree.js +184 -0
- package/dist/core/layout/index.d.ts +1 -0
- package/dist/core/layout/index.js +7 -0
- package/dist/core/master-lock/index.d.ts +14 -0
- package/dist/core/master-lock/index.js +14 -0
- package/dist/core/master-lock/kdf.d.ts +63 -0
- package/dist/core/master-lock/kdf.js +117 -0
- package/dist/core/master-lock/policy.d.ts +32 -0
- package/dist/core/master-lock/policy.js +52 -0
- package/dist/core/master-lock/presence.d.ts +52 -0
- package/dist/core/master-lock/presence.js +61 -0
- package/dist/core/master-lock/wire.d.ts +139 -0
- package/dist/core/master-lock/wire.js +81 -0
- package/dist/core/media/bgRemoval.d.ts +27 -0
- package/dist/core/media/bgRemoval.js +101 -0
- package/dist/core/media/ffmpeg.d.ts +57 -0
- package/dist/core/media/ffmpeg.js +126 -0
- package/dist/core/media/format.d.ts +20 -0
- package/dist/core/media/format.js +54 -0
- package/dist/core/media/index.d.ts +12 -0
- package/dist/core/media/index.js +18 -0
- package/dist/core/media/mediaItem.d.ts +15 -0
- package/dist/core/media/mediaItem.js +34 -0
- package/dist/core/media/renderPlan.d.ts +63 -0
- package/dist/core/media/renderPlan.js +232 -0
- package/dist/core/media/segment.d.ts +79 -0
- package/dist/core/media/segment.js +247 -0
- package/dist/core/media/size.d.ts +1 -0
- package/dist/core/media/size.js +3 -0
- package/dist/core/media/trim.d.ts +21 -0
- package/dist/core/media/trim.js +40 -0
- package/dist/core/media/types.d.ts +148 -0
- package/dist/core/media/types.js +11 -0
- package/dist/core/media/uploadLimits.d.ts +7 -0
- package/dist/core/media/uploadLimits.js +16 -0
- package/dist/core/media/validate.d.ts +9 -0
- package/dist/core/media/validate.js +121 -0
- package/dist/core/media/videoCuts.d.ts +152 -0
- package/dist/core/media/videoCuts.js +284 -0
- package/dist/core/navigation/breadcrumbCompat.d.ts +24 -0
- package/dist/core/navigation/breadcrumbCompat.js +21 -0
- package/dist/core/navigation/coverageTest.d.ts +24 -0
- package/dist/core/navigation/coverageTest.js +48 -0
- package/dist/core/navigation/index.d.ts +6 -0
- package/dist/core/navigation/index.js +10 -0
- package/dist/core/navigation/migrateNavPreferences.d.ts +55 -0
- package/dist/core/navigation/migrateNavPreferences.js +63 -0
- package/dist/core/navigation/navigationModel.d.ts +165 -0
- package/dist/core/navigation/navigationModel.js +14 -0
- package/dist/core/navigation/navigationSelectors.d.ts +63 -0
- package/dist/core/navigation/navigationSelectors.js +167 -0
- package/dist/core/navigation/searchCatalogue.d.ts +23 -0
- package/dist/core/navigation/searchCatalogue.js +76 -0
- package/dist/core/net/reconnectSchedule.d.ts +46 -0
- package/dist/core/net/reconnectSchedule.js +38 -0
- package/dist/core/palette/ShortcutRegistry.d.ts +33 -0
- package/dist/core/palette/ShortcutRegistry.js +52 -0
- package/dist/core/palette/fuzzyRank.d.ts +18 -0
- package/dist/core/palette/fuzzyRank.js +73 -0
- package/dist/core/palette/hotkeys.d.ts +28 -0
- package/dist/core/palette/hotkeys.js +118 -0
- package/dist/core/palette/index.d.ts +3 -0
- package/dist/core/palette/index.js +8 -0
- package/dist/core/schema/projection.d.ts +84 -0
- package/dist/core/schema/projection.js +184 -0
- package/dist/core/session/claims.d.ts +145 -0
- package/dist/core/session/claims.js +157 -0
- package/dist/core/sharing/index.d.ts +5 -0
- package/dist/core/sharing/index.js +4 -0
- package/dist/core/sharing/model.d.ts +195 -0
- package/dist/core/sharing/model.js +150 -0
- package/dist/core/site/siteCopy.d.ts +92 -0
- package/dist/core/site/siteCopy.js +91 -0
- package/dist/core/slots.d.ts +22 -0
- package/dist/core/slots.js +31 -0
- package/dist/core/styles/constructableSheet.d.ts +51 -0
- package/dist/core/styles/constructableSheet.js +80 -0
- package/dist/core/tokens/registry.d.ts +119 -0
- package/dist/core/tokens/registry.js +447 -0
- package/dist/core/uiScale/index.d.ts +8 -0
- package/dist/core/uiScale/index.js +8 -0
- package/dist/core/uiScale/scale.d.ts +36 -0
- package/dist/core/uiScale/scale.js +47 -0
- package/dist/core/uiScale/uiScaleStore.d.ts +47 -0
- package/dist/core/uiScale/uiScaleStore.js +63 -0
- package/dist/core/wire/__fixtures__/rowsMarshalWorker.d.ts +1 -0
- package/dist/core/wire/__fixtures__/rowsMarshalWorker.js +23 -0
- package/dist/core/wire/index.d.ts +17 -0
- package/dist/core/wire/index.js +17 -0
- package/dist/core/wire/ingress.d.ts +44 -0
- package/dist/core/wire/ingress.js +134 -0
- package/dist/core/wire/marshalWorker.d.ts +70 -0
- package/dist/core/wire/marshalWorker.js +273 -0
- package/dist/core/wire/marshalling.d.ts +122 -0
- package/dist/core/wire/marshalling.js +126 -0
- package/dist/core/wire/wireHub.d.ts +80 -0
- package/dist/core/wire/wireHub.js +116 -0
- package/dist/ctgr/base64url.d.ts +8 -0
- package/dist/ctgr/base64url.js +39 -0
- package/dist/ctgr/chunkAccumulator.d.ts +214 -0
- package/dist/ctgr/chunkAccumulator.js +307 -0
- package/dist/ctgr/chunkStore.d.ts +24 -0
- package/dist/ctgr/chunkStore.js +125 -0
- package/dist/ctgr/codec.d.ts +44 -0
- package/dist/ctgr/codec.js +153 -0
- package/dist/ctgr/compress.d.ts +2 -0
- package/dist/ctgr/compress.js +20 -0
- package/dist/ctgr/crc32.d.ts +2 -0
- package/dist/ctgr/crc32.js +23 -0
- package/dist/ctgr/digest.d.ts +1 -0
- package/dist/ctgr/digest.js +15 -0
- package/dist/ctgr/index.d.ts +9 -0
- package/dist/ctgr/index.js +17 -0
- package/dist/ctgr/types.d.ts +64 -0
- package/dist/ctgr/types.js +21 -0
- package/dist/ctgr/v0compat.d.ts +27 -0
- package/dist/ctgr/v0compat.js +128 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +17 -0
- package/dist/shared/errors.d.ts +101 -0
- package/dist/shared/errors.js +99 -0
- package/dist/testing/bytes.d.ts +32 -0
- package/dist/testing/bytes.js +33 -0
- package/dist/testing/domInspect.d.ts +59 -0
- package/dist/testing/domInspect.js +115 -0
- package/dist/testing/fakeFetch.d.ts +23 -0
- package/dist/testing/fakeFetch.js +21 -0
- package/dist/testing/index.d.ts +3 -0
- package/dist/testing/index.js +11 -0
- package/package.json +373 -0
- package/src/client/wsWire.ts +83 -0
- package/src/config/appConfig.spec.ts +158 -0
- package/src/config/appConfig.ts +372 -0
- package/src/core/activity/index.ts +21 -0
- package/src/core/activity/model.ts +160 -0
- package/src/core/analytics/index.ts +18 -0
- package/src/core/analytics/range.spec.ts +280 -0
- package/src/core/analytics/range.ts +383 -0
- package/src/core/analytics/schedule.spec.ts +311 -0
- package/src/core/analytics/schedule.ts +624 -0
- package/src/core/category/category.spec.ts +144 -0
- package/src/core/category/index.ts +19 -0
- package/src/core/category/memoryStore.ts +104 -0
- package/src/core/category/tree.ts +118 -0
- package/src/core/category/types.ts +59 -0
- package/src/core/color.spec.ts +35 -0
- package/src/core/color.ts +35 -0
- package/src/core/colorScheme/colorScheme.spec.ts +127 -0
- package/src/core/colorScheme/colorSchemeStore.ts +184 -0
- package/src/core/colorScheme/index.ts +23 -0
- package/src/core/domainOwners.ts +32 -0
- package/src/core/download.spec.ts +38 -0
- package/src/core/download.ts +35 -0
- package/src/core/events/ccEventBus.spec.ts +90 -0
- package/src/core/events/ccEventBus.ts +120 -0
- package/src/core/file-tree/fileTreeModel.spec.ts +462 -0
- package/src/core/file-tree/fileTreeModel.ts +511 -0
- package/src/core/folder-tree/actions.spec.ts +103 -0
- package/src/core/folder-tree/actions.ts +153 -0
- package/src/core/folder-tree/index.ts +46 -0
- package/src/core/folder-tree/path.spec.ts +113 -0
- package/src/core/folder-tree/path.ts +148 -0
- package/src/core/folder-tree/tree.spec.ts +157 -0
- package/src/core/folder-tree/tree.ts +244 -0
- package/src/core/layout/index.ts +7 -0
- package/src/core/master-lock/index.ts +50 -0
- package/src/core/master-lock/kdf.spec.ts +95 -0
- package/src/core/master-lock/kdf.ts +145 -0
- package/src/core/master-lock/policy.ts +55 -0
- package/src/core/master-lock/presence.spec.ts +73 -0
- package/src/core/master-lock/presence.ts +71 -0
- package/src/core/master-lock/wire.ts +155 -0
- package/src/core/media/bgRemoval.spec.ts +120 -0
- package/src/core/media/bgRemoval.ts +123 -0
- package/src/core/media/ffmpeg.spec.ts +135 -0
- package/src/core/media/ffmpeg.ts +162 -0
- package/src/core/media/format.spec.ts +41 -0
- package/src/core/media/format.ts +58 -0
- package/src/core/media/index.ts +105 -0
- package/src/core/media/mediaItem.spec.ts +46 -0
- package/src/core/media/mediaItem.ts +40 -0
- package/src/core/media/renderPlan.spec.ts +140 -0
- package/src/core/media/renderPlan.ts +318 -0
- package/src/core/media/segment.spec.ts +159 -0
- package/src/core/media/segment.ts +352 -0
- package/src/core/media/size.spec.ts +19 -0
- package/src/core/media/size.ts +3 -0
- package/src/core/media/trim.spec.ts +75 -0
- package/src/core/media/trim.ts +60 -0
- package/src/core/media/types.ts +147 -0
- package/src/core/media/uploadLimits.ts +18 -0
- package/src/core/media/validate.spec.ts +106 -0
- package/src/core/media/validate.ts +131 -0
- package/src/core/media/videoCuts.spec.ts +327 -0
- package/src/core/media/videoCuts.ts +355 -0
- package/src/core/navigation/breadcrumbCompat.ts +44 -0
- package/src/core/navigation/coverageTest.ts +54 -0
- package/src/core/navigation/index.ts +50 -0
- package/src/core/navigation/migrateNavPreferences.ts +119 -0
- package/src/core/navigation/navigation.spec.ts +388 -0
- package/src/core/navigation/navigationModel.ts +185 -0
- package/src/core/navigation/navigationSelectors.ts +206 -0
- package/src/core/navigation/searchCatalogue.ts +89 -0
- package/src/core/net/reconnectSchedule.spec.ts +49 -0
- package/src/core/net/reconnectSchedule.ts +78 -0
- package/src/core/palette/ShortcutRegistry.ts +69 -0
- package/src/core/palette/fuzzyRank.ts +78 -0
- package/src/core/palette/hotkeys.ts +132 -0
- package/src/core/palette/index.ts +22 -0
- package/src/core/palette/palette.spec.ts +160 -0
- package/src/core/schema/projection.spec.ts +122 -0
- package/src/core/schema/projection.ts +225 -0
- package/src/core/session/claims.ts +181 -0
- package/src/core/sharing/index.ts +33 -0
- package/src/core/sharing/model.spec.ts +104 -0
- package/src/core/sharing/model.ts +296 -0
- package/src/core/site/siteCopy.ts +115 -0
- package/src/core/slots.ts +38 -0
- package/src/core/styles/constructableSheet.ts +86 -0
- package/src/core/tokens/registry.spec.ts +160 -0
- package/src/core/tokens/registry.ts +536 -0
- package/src/core/uiScale/index.ts +32 -0
- package/src/core/uiScale/scale.ts +63 -0
- package/src/core/uiScale/uiScale.spec.ts +77 -0
- package/src/core/uiScale/uiScaleStore.ts +100 -0
- package/src/core/wire/__fixtures__/rowsMarshalWorker.ts +24 -0
- package/src/core/wire/index.ts +56 -0
- package/src/core/wire/ingress.spec.ts +276 -0
- package/src/core/wire/ingress.ts +182 -0
- package/src/core/wire/marshalWorker.spec.ts +196 -0
- package/src/core/wire/marshalWorker.ts +361 -0
- package/src/core/wire/marshalling.spec.ts +139 -0
- package/src/core/wire/marshalling.ts +238 -0
- package/src/core/wire/wireHub.spec.ts +137 -0
- package/src/core/wire/wireHub.ts +213 -0
- package/src/ctgr/base64url.ts +44 -0
- package/src/ctgr/chunkAccumulator.ts +403 -0
- package/src/ctgr/chunkStore.spec.ts +101 -0
- package/src/ctgr/chunkStore.ts +168 -0
- package/src/ctgr/codec.spec.ts +208 -0
- package/src/ctgr/codec.ts +202 -0
- package/src/ctgr/compress.ts +21 -0
- package/src/ctgr/crc32.ts +23 -0
- package/src/ctgr/digest.ts +14 -0
- package/src/ctgr/index.ts +39 -0
- package/src/ctgr/types.ts +80 -0
- package/src/ctgr/v0compat.spec.ts +73 -0
- package/src/ctgr/v0compat.ts +156 -0
- package/src/declaredDepsAreImported.spec.ts +87 -0
- package/src/index.ts +25 -0
- package/src/leafSubpathsImportNothing.spec.ts +287 -0
- package/src/lockfileIsTracked.spec.ts +50 -0
- package/src/namedSubpathsResolve.spec.ts +122 -0
- package/src/noPathDeps.spec.ts +24 -0
- package/src/publishShape.spec.ts +89 -0
- package/src/shared/errors.ts +153 -0
- package/src/skippedTestsAreAnswered.spec.ts +221 -0
- package/src/testTmpRoot.spec.ts +102 -0
- package/src/testing/bytes.ts +38 -0
- package/src/testing/domInspect.spec.ts +79 -0
- package/src/testing/domInspect.ts +127 -0
- package/src/testing/fakeFetch.ts +26 -0
- package/src/testing/index.ts +11 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
descendantIdsOf,
|
|
4
|
+
type FileTreeDrop,
|
|
5
|
+
type FileTreeNode,
|
|
6
|
+
type FileTreeRow,
|
|
7
|
+
filterFileTree,
|
|
8
|
+
findFileTreeNode,
|
|
9
|
+
flattenFileTree,
|
|
10
|
+
matchTypeahead,
|
|
11
|
+
parentIdOf,
|
|
12
|
+
resolveDrop,
|
|
13
|
+
resolveKeyboardMove,
|
|
14
|
+
describeDrop,
|
|
15
|
+
} from './fileTreeModel';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The shape collections actually feeds this: albums at the top level, each one a folder,
|
|
19
|
+
* with derived folders under them. `Dinos` is locked — the server sent it, so it is drawn,
|
|
20
|
+
* but nothing may be dropped into it.
|
|
21
|
+
*/
|
|
22
|
+
const folder = (
|
|
23
|
+
id: string,
|
|
24
|
+
name: string,
|
|
25
|
+
children: FileTreeNode[] = [],
|
|
26
|
+
extra: Partial<FileTreeNode> = {},
|
|
27
|
+
): FileTreeNode => ({ id, name, kind: 'folder', children, ...extra });
|
|
28
|
+
|
|
29
|
+
const TREE: FileTreeNode[] = [
|
|
30
|
+
folder('simpsons', 'Simpsons', [
|
|
31
|
+
folder('simpsons/screencaps', 'screencaps', [
|
|
32
|
+
folder('simpsons/screencaps/s04', 's04'),
|
|
33
|
+
folder('simpsons/screencaps/s05', 's05'),
|
|
34
|
+
]),
|
|
35
|
+
folder('simpsons/posters', 'posters'),
|
|
36
|
+
]),
|
|
37
|
+
folder('dinos', 'Dinos', [], { acceptsDrop: false, access: 'private' }),
|
|
38
|
+
folder('travels', 'Travels', [folder('travels/iceland', 'iceland')]),
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const rowFor = (rows: FileTreeRow[], id: string): FileTreeRow => {
|
|
42
|
+
const row = rows.find((r) => r.node.id === id);
|
|
43
|
+
if (!row) throw new Error(`no row for ${id}`);
|
|
44
|
+
return row;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
describe('flattenFileTree', () => {
|
|
48
|
+
test('draws every row of a fully open tree, with depth and sibling index', () => {
|
|
49
|
+
const rows = flattenFileTree(TREE, []);
|
|
50
|
+
expect(rows.map((r) => r.node.id)).toEqual([
|
|
51
|
+
'simpsons',
|
|
52
|
+
'simpsons/screencaps',
|
|
53
|
+
'simpsons/screencaps/s04',
|
|
54
|
+
'simpsons/screencaps/s05',
|
|
55
|
+
'simpsons/posters',
|
|
56
|
+
'dinos',
|
|
57
|
+
'travels',
|
|
58
|
+
'travels/iceland',
|
|
59
|
+
]);
|
|
60
|
+
expect(rowFor(rows, 'simpsons/screencaps/s04').depth).toBe(2);
|
|
61
|
+
expect(rowFor(rows, 'simpsons/posters').index).toBe(1);
|
|
62
|
+
expect(rowFor(rows, 'travels/iceland').parentId).toBe('travels');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('🔴 `collapsed` is the exception list, so a NEW node is open, not shut', () => {
|
|
66
|
+
// The whole reason the model is inverted. A tree re-derived from the server gains nodes
|
|
67
|
+
// constantly; with an expanded-set the folder you just made would come back closed, at
|
|
68
|
+
// the exact moment its contents are what you are looking at.
|
|
69
|
+
const grown = [...TREE, folder('new', 'Just made', [folder('new/inside', 'inside')])];
|
|
70
|
+
const rows = flattenFileTree(grown, ['simpsons']);
|
|
71
|
+
expect(rows.map((r) => r.node.id)).toContain('new/inside');
|
|
72
|
+
expect(rows.map((r) => r.node.id)).not.toContain('simpsons/posters');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('🔴 an EMPTY folder is still expandable — it gets an arrow like every other folder', () => {
|
|
76
|
+
// Owner, 2026-09-11: "Make them use an arrow icon next to a dir even when the dir is
|
|
77
|
+
// empty." The chevron says FOLDER, not OCCUPIED — VS Code's own rule — and the default
|
|
78
|
+
// used to be `children.length > 0`, which drew a brand-new folder as a leaf until
|
|
79
|
+
// something was put in it. `dinos` has no children.
|
|
80
|
+
const rows = flattenFileTree(TREE, []);
|
|
81
|
+
expect(rowFor(rows, 'dinos').expandable).toBe(true);
|
|
82
|
+
expect(rowFor(rows, 'simpsons').expandable).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('an empty folder OPENS, onto nothing, and can be shut again', () => {
|
|
86
|
+
// The failure this replaces was a trap door: no twisty meant a row that had been
|
|
87
|
+
// collapsed once could never be reopened. Opening onto nothing is the honest answer.
|
|
88
|
+
const shut = flattenFileTree(TREE, ['dinos']);
|
|
89
|
+
expect(rowFor(shut, 'dinos').expanded).toBe(false);
|
|
90
|
+
const open = flattenFileTree(TREE, []);
|
|
91
|
+
expect(rowFor(open, 'dinos').expanded).toBe(true);
|
|
92
|
+
expect(open.filter((r) => r.parentId === 'dinos')).toEqual([]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('`expandable: false` still removes the twisty — the opt-out is kept', () => {
|
|
96
|
+
// For a row that is a folder in name and can never hold anything.
|
|
97
|
+
const rows = flattenFileTree([{ ...folder('inert', 'Inert'), expandable: false }], []);
|
|
98
|
+
expect(rowFor(rows, 'inert').expandable).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('a FILE is never expandable, whatever it says', () => {
|
|
102
|
+
const rows = flattenFileTree([{ id: 'f', name: 'f.png', kind: 'file', expandable: true }], []);
|
|
103
|
+
expect(rowFor(rows, 'f').expandable).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('lookups', () => {
|
|
108
|
+
test('finds a node anywhere, and reports its parent', () => {
|
|
109
|
+
expect(findFileTreeNode(TREE, 'simpsons/screencaps/s05')?.name).toBe('s05');
|
|
110
|
+
expect(parentIdOf(TREE, 'simpsons/screencaps/s05')).toBe('simpsons/screencaps');
|
|
111
|
+
expect(parentIdOf(TREE, 'simpsons')).toBeNull();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('a node the tree no longer holds reports the top level rather than throwing', () => {
|
|
115
|
+
// A refresh between the press and the release can genuinely lose the row.
|
|
116
|
+
expect(parentIdOf(TREE, 'deleted-mid-drag')).toBeNull();
|
|
117
|
+
expect(findFileTreeNode(TREE, 'deleted-mid-drag')).toBeNull();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('descendants include the node itself — the set a drag may not enter', () => {
|
|
121
|
+
expect([...descendantIdsOf(TREE, 'simpsons/screencaps')].sort()).toEqual([
|
|
122
|
+
'simpsons/screencaps',
|
|
123
|
+
'simpsons/screencaps/s04',
|
|
124
|
+
'simpsons/screencaps/s05',
|
|
125
|
+
]);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 🔴 The rule the whole gesture is built around: a drop that cannot happen is never
|
|
131
|
+
* OFFERED. Every case below that returns `target: null` also returns words, because a drop
|
|
132
|
+
* that is silently ignored reads as a broken app rather than a refused one.
|
|
133
|
+
*/
|
|
134
|
+
describe('resolveDrop', () => {
|
|
135
|
+
const rows = flattenFileTree(TREE, []);
|
|
136
|
+
const at = (id: string | null, dragId: string | null, offsetRatio = 0.5, allowReorder = false) =>
|
|
137
|
+
resolveDrop({
|
|
138
|
+
nodes: TREE,
|
|
139
|
+
row: id === null ? null : rowFor(rows, id),
|
|
140
|
+
offsetRatio,
|
|
141
|
+
dragId,
|
|
142
|
+
allowReorder,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('a folder row takes the drop INTO it', () => {
|
|
146
|
+
expect(at('simpsons/posters', 'travels/iceland').target).toEqual({
|
|
147
|
+
kind: 'into',
|
|
148
|
+
parentId: 'simpsons/posters',
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('blank space below the tree means the top level', () => {
|
|
153
|
+
expect(at(null, 'travels/iceland').target).toEqual({ kind: 'root' });
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('🔴 a folder cannot go inside itself, and says so', () => {
|
|
157
|
+
const offer = at('simpsons/screencaps', 'simpsons/screencaps');
|
|
158
|
+
expect(offer.target).toBeNull();
|
|
159
|
+
expect(offer.refused).toBe(true);
|
|
160
|
+
expect(offer.reason).toBe('A folder cannot go inside itself');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('🔴 nor inside one of its own folders', () => {
|
|
164
|
+
const offer = at('simpsons/screencaps/s04', 'simpsons/screencaps');
|
|
165
|
+
expect(offer.target).toBeNull();
|
|
166
|
+
expect(offer.refused).toBe(true);
|
|
167
|
+
expect(offer.reason).toBe('A folder cannot go inside one of its own folders');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("🔴 a folder the consumer vetoed refuses with the consumer's words", () => {
|
|
171
|
+
const offer = resolveDrop({
|
|
172
|
+
nodes: TREE,
|
|
173
|
+
row: rowFor(rows, 'dinos'),
|
|
174
|
+
offsetRatio: 0.5,
|
|
175
|
+
dragId: 'travels/iceland',
|
|
176
|
+
lockedReason: 'Unlock Dinos to file things into it',
|
|
177
|
+
});
|
|
178
|
+
expect(offer.target).toBeNull();
|
|
179
|
+
expect(offer.reason).toBe('Unlock Dinos to file things into it');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('an EXTERNAL payload (no dragId) is legal anywhere a folder accepts it', () => {
|
|
183
|
+
// Files from the desktop, or tiles from the gallery: nothing to cycle, so the only
|
|
184
|
+
// refusal left is the consumer's veto.
|
|
185
|
+
expect(at('simpsons/screencaps', null).target).toEqual({
|
|
186
|
+
kind: 'into',
|
|
187
|
+
parentId: 'simpsons/screencaps',
|
|
188
|
+
});
|
|
189
|
+
expect(at('dinos', null).refused).toBe(true);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('a FILE row means the folder that row is in — never a wall of dead rows', () => {
|
|
193
|
+
const withFile: FileTreeNode[] = [
|
|
194
|
+
folder('a', 'A', [{ id: 'a/one.jpg', name: 'one.jpg', kind: 'file' }]),
|
|
195
|
+
];
|
|
196
|
+
const fileRows = flattenFileTree(withFile, []);
|
|
197
|
+
const offer = resolveDrop({
|
|
198
|
+
nodes: withFile,
|
|
199
|
+
row: fileRows[1] as FileTreeRow,
|
|
200
|
+
offsetRatio: 0.5,
|
|
201
|
+
dragId: null,
|
|
202
|
+
});
|
|
203
|
+
expect(offer.target).toEqual({ kind: 'into', parentId: 'a' });
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('🔴 without allowReorder there is no between-rows target at all', () => {
|
|
207
|
+
// collections derives its folders from the paths its files carry and sorts them by
|
|
208
|
+
// name, so there is no order to change. Offering a marker would draw a promise the app
|
|
209
|
+
// cannot keep.
|
|
210
|
+
for (const ratio of [0, 0.05, 0.5, 0.95, 1]) {
|
|
211
|
+
expect(at('simpsons/posters', 'travels/iceland', ratio).target).toEqual({
|
|
212
|
+
kind: 'into',
|
|
213
|
+
parentId: 'simpsons/posters',
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test('with allowReorder, the edges of a row are between and the middle is into', () => {
|
|
219
|
+
expect(at('simpsons/posters', 'travels/iceland', 0.1, true).target).toEqual({
|
|
220
|
+
kind: 'before',
|
|
221
|
+
siblingId: 'simpsons/posters',
|
|
222
|
+
parentId: 'simpsons',
|
|
223
|
+
});
|
|
224
|
+
expect(at('simpsons/posters', 'travels/iceland', 0.9, true).target).toEqual({
|
|
225
|
+
kind: 'after',
|
|
226
|
+
siblingId: 'simpsons/posters',
|
|
227
|
+
parentId: 'simpsons',
|
|
228
|
+
});
|
|
229
|
+
expect(at('simpsons/posters', 'travels/iceland', 0.5, true).target).toEqual({
|
|
230
|
+
kind: 'into',
|
|
231
|
+
parentId: 'simpsons/posters',
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* 🔴 The consumer's veto, which is the seam collections needs.
|
|
238
|
+
*
|
|
239
|
+
* `acceptsDrop` is a property of the TARGET alone, and collections' real rule is about the
|
|
240
|
+
* pair: its server can re-path a folder inside one album and has no route for moving one to
|
|
241
|
+
* another album, while a FILE moves between albums freely. A target-only flag cannot say
|
|
242
|
+
* that, and saying it after the drop is exactly the "marker, drop, error toast" this tree
|
|
243
|
+
* exists to avoid.
|
|
244
|
+
*/
|
|
245
|
+
describe('resolveDrop with a consumer veto', () => {
|
|
246
|
+
const rows = flattenFileTree(TREE, []);
|
|
247
|
+
const albumOf = (id: string) => id.split('/')[0];
|
|
248
|
+
const sameAlbum = (dragId: string | null, target: FileTreeDrop): string | null =>
|
|
249
|
+
dragId && target.kind === 'into' && albumOf(dragId) !== albumOf(target.parentId)
|
|
250
|
+
? 'A folder cannot move to another collection'
|
|
251
|
+
: null;
|
|
252
|
+
|
|
253
|
+
test('refuses across albums, in words, before the marker is drawn', () => {
|
|
254
|
+
const offer = resolveDrop({
|
|
255
|
+
nodes: TREE,
|
|
256
|
+
row: rowFor(rows, 'travels'),
|
|
257
|
+
offsetRatio: 0.5,
|
|
258
|
+
dragId: 'simpsons/posters',
|
|
259
|
+
canDrop: sameAlbum,
|
|
260
|
+
});
|
|
261
|
+
expect(offer.target).toBeNull();
|
|
262
|
+
expect(offer.refused).toBe(true);
|
|
263
|
+
expect(offer.reason).toBe('A folder cannot move to another collection');
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('and allows the same move inside one album', () => {
|
|
267
|
+
const offer = resolveDrop({
|
|
268
|
+
nodes: TREE,
|
|
269
|
+
row: rowFor(rows, 'simpsons/screencaps'),
|
|
270
|
+
offsetRatio: 0.5,
|
|
271
|
+
dragId: 'simpsons/posters',
|
|
272
|
+
canDrop: sameAlbum,
|
|
273
|
+
});
|
|
274
|
+
expect(offer.target).toEqual({ kind: 'into', parentId: 'simpsons/screencaps' });
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test('an external payload (no dragId) is never vetoed by a rule about the dragged row', () => {
|
|
278
|
+
const offer = resolveDrop({
|
|
279
|
+
nodes: TREE,
|
|
280
|
+
row: rowFor(rows, 'travels'),
|
|
281
|
+
offsetRatio: 0.5,
|
|
282
|
+
dragId: null,
|
|
283
|
+
canDrop: sameAlbum,
|
|
284
|
+
});
|
|
285
|
+
expect(offer.target).toEqual({ kind: 'into', parentId: 'travels' });
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
describe('matchTypeahead', () => {
|
|
290
|
+
const rows = flattenFileTree(TREE, []);
|
|
291
|
+
test('jumps to the next row whose name starts with what was typed', () => {
|
|
292
|
+
expect(rows[matchTypeahead(rows, 'tra', 0)]?.node.id).toBe('travels');
|
|
293
|
+
expect(rows[matchTypeahead(rows, 's0', 1)]?.node.id).toBe('simpsons/screencaps/s04');
|
|
294
|
+
});
|
|
295
|
+
test('wraps, so it does not die at the bottom of the list', () => {
|
|
296
|
+
expect(rows[matchTypeahead(rows, 'simp', rows.length - 1)]?.node.id).toBe('simpsons');
|
|
297
|
+
});
|
|
298
|
+
test('an empty query and a miss both mean "stay where you are"', () => {
|
|
299
|
+
expect(matchTypeahead(rows, '', 0)).toBe(-1);
|
|
300
|
+
expect(matchTypeahead(rows, 'zzz', 0)).toBe(-1);
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
describe('filterFileTree', () => {
|
|
305
|
+
test('keeps the folders on the way to a match', () => {
|
|
306
|
+
const found = filterFileTree(TREE, 's05');
|
|
307
|
+
expect(found.map((n) => n.id)).toEqual(['simpsons']);
|
|
308
|
+
expect(flattenFileTree(found, []).map((r) => r.node.id)).toEqual([
|
|
309
|
+
'simpsons',
|
|
310
|
+
'simpsons/screencaps',
|
|
311
|
+
'simpsons/screencaps/s05',
|
|
312
|
+
]);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test('🔴 a folder that MATCHES keeps everything inside it', () => {
|
|
316
|
+
// Searching for a folder means you want to see what is in it — not a folder that looks
|
|
317
|
+
// empty because its children happen to spell something else.
|
|
318
|
+
const found = filterFileTree(TREE, 'screencaps');
|
|
319
|
+
expect(flattenFileTree(found, []).map((r) => r.node.id)).toEqual([
|
|
320
|
+
'simpsons',
|
|
321
|
+
'simpsons/screencaps',
|
|
322
|
+
'simpsons/screencaps/s04',
|
|
323
|
+
'simpsons/screencaps/s05',
|
|
324
|
+
]);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test('an empty query is the whole tree, by identity', () => {
|
|
328
|
+
expect(filterFileTree(TREE, ' ')).toBe(TREE);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* ⌥→ / ⌥← — the keyboard's half of the drag, and the only half touch has.
|
|
334
|
+
*
|
|
335
|
+
* 🔴 The assertions that matter are the REFUSALS. A keyboard route that can reach a
|
|
336
|
+
* destination the pointer refuses is a second, weaker opinion about what is legal, and it is
|
|
337
|
+
* the one nobody looks at because the pointer is what gets tested by hand.
|
|
338
|
+
*/
|
|
339
|
+
describe('resolveKeyboardMove', () => {
|
|
340
|
+
const rows = flattenFileTree(TREE, []);
|
|
341
|
+
|
|
342
|
+
test('→ demotes into the nearest FOLDER above it among its own siblings', () => {
|
|
343
|
+
// `posters` sits under `simpsons` after `screencaps`; the only unambiguous "in" is the
|
|
344
|
+
// folder immediately above it at the same level.
|
|
345
|
+
expect(
|
|
346
|
+
resolveKeyboardMove({
|
|
347
|
+
nodes: TREE,
|
|
348
|
+
rows,
|
|
349
|
+
row: rowFor(rows, 'simpsons/posters'),
|
|
350
|
+
direction: 'in',
|
|
351
|
+
}).target,
|
|
352
|
+
).toEqual({ kind: 'into', parentId: 'simpsons/screencaps' });
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test('→ with nothing above it says so rather than doing nothing', () => {
|
|
356
|
+
const offer = resolveKeyboardMove({
|
|
357
|
+
nodes: TREE,
|
|
358
|
+
rows,
|
|
359
|
+
row: rowFor(rows, 'simpsons'),
|
|
360
|
+
direction: 'in',
|
|
361
|
+
});
|
|
362
|
+
expect(offer.target).toBeNull();
|
|
363
|
+
expect(offer.reason).toBe('There is no folder above it to move into');
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test('🔴 → refuses a LOCKED folder with the consumer‘s words, exactly as a drop does', () => {
|
|
367
|
+
// `dinos` is `acceptsDrop: false`. `travels` is the row after it, so `dinos` is the
|
|
368
|
+
// nearest folder above — reachable by keyboard and refused by pointer would be the bug.
|
|
369
|
+
const offer = resolveKeyboardMove({
|
|
370
|
+
nodes: TREE,
|
|
371
|
+
rows,
|
|
372
|
+
row: rowFor(rows, 'travels'),
|
|
373
|
+
direction: 'in',
|
|
374
|
+
lockedReason: 'That album is locked',
|
|
375
|
+
});
|
|
376
|
+
expect(offer.target).toBeNull();
|
|
377
|
+
expect(offer.reason).toBe('That album is locked');
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test('← promotes to the folder its own folder is in', () => {
|
|
381
|
+
expect(
|
|
382
|
+
resolveKeyboardMove({
|
|
383
|
+
nodes: TREE,
|
|
384
|
+
rows,
|
|
385
|
+
row: rowFor(rows, 'simpsons/screencaps/s04'),
|
|
386
|
+
direction: 'out',
|
|
387
|
+
}).target,
|
|
388
|
+
).toEqual({ kind: 'into', parentId: 'simpsons' });
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
test('← at depth 1 is the top level, and at depth 0 is a refusal with words', () => {
|
|
392
|
+
expect(
|
|
393
|
+
resolveKeyboardMove({
|
|
394
|
+
nodes: TREE,
|
|
395
|
+
rows,
|
|
396
|
+
row: rowFor(rows, 'simpsons/posters'),
|
|
397
|
+
direction: 'out',
|
|
398
|
+
}).target,
|
|
399
|
+
).toEqual({ kind: 'root' });
|
|
400
|
+
const offer = resolveKeyboardMove({
|
|
401
|
+
nodes: TREE,
|
|
402
|
+
rows,
|
|
403
|
+
row: rowFor(rows, 'travels'),
|
|
404
|
+
direction: 'out',
|
|
405
|
+
});
|
|
406
|
+
expect(offer.target).toBeNull();
|
|
407
|
+
expect(offer.reason).toBe('It is already at the top level');
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test('🔴 the consumer‘s `canDrop` gets the last word in both directions', () => {
|
|
411
|
+
const canDrop = () => 'No route for that on the server';
|
|
412
|
+
for (const direction of ['in', 'out'] as const) {
|
|
413
|
+
const offer = resolveKeyboardMove({
|
|
414
|
+
nodes: TREE,
|
|
415
|
+
rows,
|
|
416
|
+
row: rowFor(rows, 'simpsons/posters'),
|
|
417
|
+
direction,
|
|
418
|
+
canDrop,
|
|
419
|
+
});
|
|
420
|
+
expect(offer.target).toBeNull();
|
|
421
|
+
expect(offer.reason).toBe('No route for that on the server');
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test('a shut folder does not hide its own SIBLINGS, so the level is still whole', () => {
|
|
426
|
+
// The visible rows are what the level is read off. `screencaps` being shut removes its
|
|
427
|
+
// children, never its peers — which is what makes reading siblings off `rows` correct.
|
|
428
|
+
const shut = flattenFileTree(TREE, ['simpsons/screencaps']);
|
|
429
|
+
expect(
|
|
430
|
+
resolveKeyboardMove({
|
|
431
|
+
nodes: TREE,
|
|
432
|
+
rows: shut,
|
|
433
|
+
row: rowFor(shut, 'simpsons/posters'),
|
|
434
|
+
direction: 'in',
|
|
435
|
+
}).target,
|
|
436
|
+
).toEqual({ kind: 'into', parentId: 'simpsons/screencaps' });
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
describe('describeDrop', () => {
|
|
441
|
+
test('names the destination, because "where to" is the question a drag asks', () => {
|
|
442
|
+
expect(describeDrop(TREE, { kind: 'into', parentId: 'simpsons/screencaps' })).toBe(
|
|
443
|
+
'Into screencaps',
|
|
444
|
+
);
|
|
445
|
+
expect(describeDrop(TREE, { kind: 'root' })).toBe('To the top level');
|
|
446
|
+
expect(describeDrop(TREE, { kind: 'before', siblingId: 'dinos', parentId: null })).toBe(
|
|
447
|
+
'Above Dinos',
|
|
448
|
+
);
|
|
449
|
+
expect(describeDrop(TREE, { kind: 'after', siblingId: 'dinos', parentId: null })).toBe(
|
|
450
|
+
'Below Dinos',
|
|
451
|
+
);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
test('🔴 no target is SILENT — a refusal has its own words and must not be doubled', () => {
|
|
455
|
+
expect(describeDrop(TREE, null)).toBe('');
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
test('a target the tree no longer holds still says something true', () => {
|
|
459
|
+
// The tree can be re-derived from the server between the press and the release.
|
|
460
|
+
expect(describeDrop(TREE, { kind: 'into', parentId: 'gone' })).toBe('Into this folder');
|
|
461
|
+
});
|
|
462
|
+
});
|