@lukekaalim/act-insight 0.0.2-alpha.6 → 1.1.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/CHANGELOG.md +38 -0
- package/InsightApp.ts +364 -169
- package/ScheduleControls.ts +61 -0
- package/TreeViewer.module.css +10 -1
- package/TreeViewer.ts +34 -21
- package/Virtual.ts +64 -0
- package/index.ts +4 -1
- package/lookup.ts +185 -0
- package/package.json +4 -4
- package/utils.ts +59 -47
- package/vite.config.ts +4 -4
- package/CommitViewer.module.css +0 -5
- package/CommitViewer.ts +0 -104
- package/ThreadControls.ts +0 -22
- package/ThreadViewer.module.css +0 -5
- package/ThreadViewer.ts +0 -185
- package/debug.ts +0 -80
- package/devtool_page.ts +0 -0
- package/devtool_panel.ts +0 -0
- package/extension_main.ts +0 -0
- package/insight.test.ts +0 -29
package/CommitViewer.ts
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import { Component, Deps, h, Ref, refSymbol, useState } from "@lukekaalim/act";
|
|
2
|
-
import { Commit, CommitID, CommitTree, Reconciler } from "@lukekaalim/act-recon";
|
|
3
|
-
import { hs } from "@lukekaalim/act-web"
|
|
4
|
-
import { getElementName } from "./utils";
|
|
5
|
-
import { CommitAttributeTag } from "./AttributeTag";
|
|
6
|
-
import classes from './CommitViewer.module.css';
|
|
7
|
-
import { CommitReport, CommitStateReport, ValueReport } from "@lukekaalim/act-debug";
|
|
8
|
-
|
|
9
|
-
export type CommitViewerProps = {
|
|
10
|
-
commit: CommitReport,
|
|
11
|
-
state: CommitStateReport,
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
export const CommitViewer: Component<CommitViewerProps> = ({ commit, state }) => {
|
|
15
|
-
|
|
16
|
-
const isRef = (value: unknown): value is Ref<unknown> => (
|
|
17
|
-
typeof value === 'object' && !!value && (refSymbol in value)
|
|
18
|
-
);
|
|
19
|
-
|
|
20
|
-
//const refs = state && [...state.values]
|
|
21
|
-
// .filter(([id, value]) => isRef(value))
|
|
22
|
-
|
|
23
|
-
const [, setRender] = useState(0);
|
|
24
|
-
const rerender = () => {
|
|
25
|
-
setRender(r => r + 1);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return hs('div', { className: classes.commitViewer }, [
|
|
29
|
-
hs('h3', {}, commit.element.type),
|
|
30
|
-
hs('div', {}, [
|
|
31
|
-
h(CommitAttributeTag, { name: 'Version', value: commit.version.toString() }),
|
|
32
|
-
h(CommitAttributeTag, { name: 'ID', value: commit.id.toString() }),
|
|
33
|
-
//!!state.props.key && [
|
|
34
|
-
// h(CommitAttributeTag, { name: 'Key', value: getValueName(commit.element.props.key) }),
|
|
35
|
-
//]
|
|
36
|
-
]),
|
|
37
|
-
state.props.length > 0 && [
|
|
38
|
-
hs('h4', {}, 'Props'),
|
|
39
|
-
hs('ul', {}, state.props.map(prop =>
|
|
40
|
-
hs('li', {}, [
|
|
41
|
-
h(CommitAttributeTag, { name: 'Key', value: prop.name }),
|
|
42
|
-
h(CommitAttributeTag, { name: 'Value', value: getTextForValue(prop.value) }),
|
|
43
|
-
]))),
|
|
44
|
-
],
|
|
45
|
-
state.values.length && [
|
|
46
|
-
hs('h4', {}, 'useState'),
|
|
47
|
-
hs('ul', {}, state.values.map(value =>
|
|
48
|
-
hs('li', {}, [
|
|
49
|
-
h(CommitAttributeTag, { name: 'Key', value: value.id.toString() }),
|
|
50
|
-
h(CommitAttributeTag, { name: 'Value', value: getTextForValue(value.value) }),
|
|
51
|
-
]))),
|
|
52
|
-
],
|
|
53
|
-
])
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export type ValueViewerProps = {
|
|
57
|
-
value: ValueReport,
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export const getTextForValue = (value: ValueReport) => {
|
|
61
|
-
switch (value.type) {
|
|
62
|
-
case 'primitive':
|
|
63
|
-
switch (typeof value.value) {
|
|
64
|
-
case 'object':
|
|
65
|
-
return `null`;
|
|
66
|
-
case 'string':
|
|
67
|
-
case 'boolean':
|
|
68
|
-
case 'number':
|
|
69
|
-
return value.value.toString();
|
|
70
|
-
}
|
|
71
|
-
case 'complex':
|
|
72
|
-
return value.name;
|
|
73
|
-
case 'undefined':
|
|
74
|
-
return `undefined`;
|
|
75
|
-
default:
|
|
76
|
-
return `${value.type}`;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export const getValueName = (value: unknown) => {
|
|
81
|
-
switch (typeof value) {
|
|
82
|
-
case 'object':
|
|
83
|
-
if (!value)
|
|
84
|
-
return 'null';
|
|
85
|
-
if (Array.isArray(value))
|
|
86
|
-
return `Array[${value.length}]`;
|
|
87
|
-
if (value.constructor === ({}).constructor)
|
|
88
|
-
return JSON.stringify(value, null, 2);
|
|
89
|
-
|
|
90
|
-
return value.constructor.name;
|
|
91
|
-
case undefined:
|
|
92
|
-
return 'undefined';
|
|
93
|
-
case 'string':
|
|
94
|
-
case 'number':
|
|
95
|
-
case 'boolean':
|
|
96
|
-
case 'symbol':
|
|
97
|
-
return (value as string | number | boolean | symbol).toString();
|
|
98
|
-
case 'function':
|
|
99
|
-
return `${(value as Function).name || 'Function'}()`;
|
|
100
|
-
default:
|
|
101
|
-
console.log(value);
|
|
102
|
-
return typeof value
|
|
103
|
-
}
|
|
104
|
-
}
|
package/ThreadControls.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { h } from "@lukekaalim/act";
|
|
2
|
-
|
|
3
|
-
export type ThreadControlsProps = {
|
|
4
|
-
onPlay: () => void,
|
|
5
|
-
onPause: () => void,
|
|
6
|
-
onStep: () => void,
|
|
7
|
-
onPlay: () => void,
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
export const ThreadControls = () => {
|
|
11
|
-
return [
|
|
12
|
-
h('button', {}, 'Play'),
|
|
13
|
-
h('button', {}, 'Pause'),
|
|
14
|
-
h('button', {}, 'Step'),
|
|
15
|
-
h('button', {}, 'Play To End'),
|
|
16
|
-
h(PendingWorkNotifier),
|
|
17
|
-
]
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const PendingWorkNotifier = () => {
|
|
21
|
-
return 'pending';
|
|
22
|
-
}
|
package/ThreadViewer.module.css
DELETED
package/ThreadViewer.ts
DELETED
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
import { Component, h, useMemo } from "@lukekaalim/act"
|
|
2
|
-
import { Commit, CommitID, CommitPath, CommitRef, CommitTree, DeltaSet, Update, WorkReason, WorkThread } from "@lukekaalim/act-recon"
|
|
3
|
-
import { hs } from "@lukekaalim/act-web"
|
|
4
|
-
import { findCommonAncestor, getElementName } from "./utils"
|
|
5
|
-
import { CommitAttributeTag } from "./AttributeTag"
|
|
6
|
-
import { CommitPreview, TreeViewer } from "./TreeViewer"
|
|
7
|
-
|
|
8
|
-
import classes from './ThreadViewer.module.css';
|
|
9
|
-
|
|
10
|
-
export type ThreadViewerProps = {
|
|
11
|
-
thread: WorkThread,
|
|
12
|
-
tree: CommitTree,
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export const ThreadViewer: Component<ThreadViewerProps> = ({ thread, tree }) => {
|
|
16
|
-
const appliedTree = useMemo(() => {
|
|
17
|
-
const tempTree = CommitTree.clone(tree);
|
|
18
|
-
DeltaSet.apply(thread.deltas, tempTree)
|
|
19
|
-
return tempTree;
|
|
20
|
-
}, [tree, thread]);
|
|
21
|
-
|
|
22
|
-
const fiberCommits = new Set([...thread.pendingUpdates].map(u => u.ref.id));
|
|
23
|
-
|
|
24
|
-
const nextUpdate = thread.pendingUpdates[thread.pendingUpdates.length - 1];
|
|
25
|
-
|
|
26
|
-
const getCommitColor = (commit: Commit) => {
|
|
27
|
-
const isUpdating = fiberCommits.has(commit.id);
|
|
28
|
-
const mustVisit = thread.mustVisit.has(commit.id);
|
|
29
|
-
const mustRender = thread.mustRender.has(commit.id);
|
|
30
|
-
const visited = thread.visited.has(commit.id);
|
|
31
|
-
|
|
32
|
-
if (visited)
|
|
33
|
-
return '#7efb8c';
|
|
34
|
-
if (isUpdating)
|
|
35
|
-
return 'blue'
|
|
36
|
-
if (mustRender)
|
|
37
|
-
return 'red';
|
|
38
|
-
if (mustVisit)
|
|
39
|
-
return 'orange';
|
|
40
|
-
|
|
41
|
-
return 'white';
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const renderCommit = (depth: number) => (commit: Commit) => {
|
|
46
|
-
const color = getCommitColor(commit);
|
|
47
|
-
console.log(color);
|
|
48
|
-
const commitNode = h(CommitPreview, {
|
|
49
|
-
commit,
|
|
50
|
-
tree: appliedTree,
|
|
51
|
-
depth,
|
|
52
|
-
renderCommit: renderCommit(depth + 1),
|
|
53
|
-
onSelectCommit: _ => {},
|
|
54
|
-
selectedCommits: new Set<CommitID>(),
|
|
55
|
-
color,
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
return commitNode
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return [
|
|
62
|
-
h('h3', {}, 'Next Update'),
|
|
63
|
-
nextUpdate ? h(UpdateViewer, { update: nextUpdate, tree, thread }) : 'Send to Renderer',
|
|
64
|
-
thread.reasons.map(reason => {
|
|
65
|
-
const commit = appliedTree.commits.get(reason.ref.id);
|
|
66
|
-
if (!commit)
|
|
67
|
-
return null;
|
|
68
|
-
return h(TreeViewer, { tree: appliedTree, roots: [commit], selectedCommits: new Set<CommitID>(), renderCommit: renderCommit(0) });
|
|
69
|
-
}),
|
|
70
|
-
hs('h3', {}, 'Fibers'),
|
|
71
|
-
hs('ul', {}, [...thread.pendingUpdates].map((update) => hs('li', {}, h(UpdateViewer, { update, tree })))),
|
|
72
|
-
hs('h3', {}, 'Visited'),
|
|
73
|
-
hs('ul', {}, [...thread.visited].map(([id, ref]) => hs('li', {}, id))),
|
|
74
|
-
hs('h3', {}, 'Must Visit'),
|
|
75
|
-
hs('ul', {}, [...thread.mustVisit].map((id) => hs('li', {}, id))),
|
|
76
|
-
hs('h3', {}, 'Must Render'),
|
|
77
|
-
hs('ul', {}, [...thread.mustRender].map(([id, ref]) => hs('li', {}, id))),
|
|
78
|
-
hs('h3', {}, 'Created'),
|
|
79
|
-
hs('ul', {}, thread.deltas.created.map(delta => hs('li', {}, [delta.next.id,' ', getElementName(delta.next.element)]))),
|
|
80
|
-
hs('h3', {}, 'Removed'),
|
|
81
|
-
hs('ul', {}, thread.deltas.removed.map(delta => hs('li', {}, [delta.ref.id,' ', getElementName(delta.prev.element)]))),
|
|
82
|
-
hs('h3', {}, 'Updated'),
|
|
83
|
-
hs('ul', {}, thread.deltas.updated.map(delta => hs('li', {}, [delta.next.id,' ', getElementName(delta.next.element)]))),
|
|
84
|
-
hs('h3', {}, 'Reasons'),
|
|
85
|
-
h(ReasonsListViewer, { reasons: thread.reasons, tree }),
|
|
86
|
-
]
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
type DeltaViewerProps = {
|
|
90
|
-
deltas: DeltaSet,
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const DeltaViewer = () => {
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
type UpdateViewerProps = {
|
|
98
|
-
tree: CommitTree,
|
|
99
|
-
thread: WorkThread,
|
|
100
|
-
update: Update,
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
const UpdateViewer: Component<UpdateViewerProps> = ({ update, tree, thread }) => {
|
|
104
|
-
const commit = tree.commits.get(update.ref.id);
|
|
105
|
-
|
|
106
|
-
const isEqual = update.next && update.prev && update.next.id === update.prev.element.id;
|
|
107
|
-
|
|
108
|
-
return hs('div', { className: classes.updateViewer }, [
|
|
109
|
-
!isEqual && update.next && typeof update.next.type === 'function' && [
|
|
110
|
-
h('span', {}, `Calling render function for component: `),
|
|
111
|
-
h('pre', { style: { display: 'inline' } }, `"${update.next.type.name}"`),
|
|
112
|
-
],
|
|
113
|
-
isEqual && [
|
|
114
|
-
thread.mustVisit.has(update.ref.id) ? [
|
|
115
|
-
thread.mustRender.has(update.ref.id) ?
|
|
116
|
-
h('span', {}, `Marked for rendering`) :
|
|
117
|
-
h('span', {}, `Visiting but not rendering`),
|
|
118
|
-
] : [
|
|
119
|
-
h('span', {}, `Skipping render and visit - fiber ends here`)
|
|
120
|
-
]
|
|
121
|
-
],
|
|
122
|
-
h(CommitAttributeTag, { name: 'Type', value: getUpdateType(update) }),
|
|
123
|
-
h(CommitAttributeTag, { name: 'ID', value: update.ref.id.toString() }),
|
|
124
|
-
update.prev && h(CommitAttributeTag, { name: 'Prev ID', value: (update.prev && update.prev.id.toString()) || 'null' }),
|
|
125
|
-
update.next && h(CommitAttributeTag, { name: 'Next Element', value: (update.next && getElementName(update.next)) || 'null' }),
|
|
126
|
-
])
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const getUpdateType = (update: Update) => {
|
|
130
|
-
if (update.prev && !update.next)
|
|
131
|
-
return 'Remove';
|
|
132
|
-
if (update.prev && update.next)
|
|
133
|
-
if (update.moved)
|
|
134
|
-
return "Move & Update"
|
|
135
|
-
else
|
|
136
|
-
return "Update"
|
|
137
|
-
if (!update.prev && update.next)
|
|
138
|
-
return "Create";
|
|
139
|
-
return "???"
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
type ReasonsListViewerProps = {
|
|
143
|
-
reasons: WorkReason[],
|
|
144
|
-
tree: CommitTree,
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
const ReasonsListViewer: Component<ReasonsListViewerProps> = ({ reasons, tree }) => {
|
|
148
|
-
const firstReason = reasons[0];
|
|
149
|
-
if (!firstReason)
|
|
150
|
-
return null;
|
|
151
|
-
|
|
152
|
-
return [
|
|
153
|
-
h('p', {}, `Started rendering because:`),
|
|
154
|
-
h('p', {}, h(ReasonViewer, { reason: firstReason, tree })),
|
|
155
|
-
reasons.length > 1 && [
|
|
156
|
-
h('p', {}, `The following updates were also batched with this render:`),
|
|
157
|
-
h('ol', {}, reasons.slice(1).map(reason => {
|
|
158
|
-
return h('li', {}, h(ReasonViewer, { reason, tree }));
|
|
159
|
-
}))
|
|
160
|
-
]
|
|
161
|
-
]
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
type ReasonsViewerProps = {
|
|
165
|
-
reason: WorkReason,
|
|
166
|
-
tree: CommitTree,
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
const ReasonViewer: Component<ReasonsViewerProps> = ({ reason, tree }) => {
|
|
170
|
-
switch (reason.type) {
|
|
171
|
-
case 'mount':
|
|
172
|
-
return [
|
|
173
|
-
`Mounting a new tree using:`,
|
|
174
|
-
h('pre', { style: { display: 'inline' } }, getElementName(reason.element)),
|
|
175
|
-
];
|
|
176
|
-
case 'target':
|
|
177
|
-
const targetCommit = tree.commits.get(reason.ref.id) as Commit;
|
|
178
|
-
return [
|
|
179
|
-
h(CommitAttributeTag, { name: 'CommitID', value: targetCommit.id.toString() }),
|
|
180
|
-
` `,
|
|
181
|
-
h('pre', { style: { display: 'inline' } }, getElementName(targetCommit.element)),
|
|
182
|
-
` requested a re-render`
|
|
183
|
-
];
|
|
184
|
-
}
|
|
185
|
-
}
|
package/debug.ts
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { Commit, CommitTree, createReconciler, Scheduler, WorkID, WorkThread } from "@lukekaalim/act-recon";
|
|
2
|
-
import { createId, h, Node } from "@lukekaalim/act";
|
|
3
|
-
import { createRenderFunction, RenderFunction, RenderSpace } from "@lukekaalim/act-backstage";
|
|
4
|
-
import { createDOMScheduler, createWebSpace, HTML, render } from "@lukekaalim/act-web";
|
|
5
|
-
import { InsightApp } from "./InsightApp";
|
|
6
|
-
|
|
7
|
-
export type DebugScheduler = {
|
|
8
|
-
isWorkPending(): boolean,
|
|
9
|
-
work(): void,
|
|
10
|
-
|
|
11
|
-
inner: Scheduler,
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export const createDebugScheduler = (): DebugScheduler => {
|
|
15
|
-
const pending = new Map<WorkID, () => void>();
|
|
16
|
-
|
|
17
|
-
const inner: Scheduler = {
|
|
18
|
-
requestWork(callback) {
|
|
19
|
-
const id = createId<'WorkID'>();
|
|
20
|
-
pending.set(id, callback);
|
|
21
|
-
return id;
|
|
22
|
-
},
|
|
23
|
-
cancelWork(workId) {
|
|
24
|
-
pending.delete(workId)
|
|
25
|
-
},
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return {
|
|
29
|
-
isWorkPending() {
|
|
30
|
-
return pending.size > 0;
|
|
31
|
-
},
|
|
32
|
-
work() {
|
|
33
|
-
const pendingWork = [...pending];
|
|
34
|
-
pending.clear();
|
|
35
|
-
for (const [,callback] of pendingWork)
|
|
36
|
-
callback();
|
|
37
|
-
},
|
|
38
|
-
inner,
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export const renderDebug = (node: Node, createSpace: (tree: CommitTree) => RenderSpace) => {
|
|
43
|
-
const debugScheduler = createDebugScheduler();
|
|
44
|
-
|
|
45
|
-
const reconciler = createReconciler(debugScheduler.inner);
|
|
46
|
-
|
|
47
|
-
const threadCompleteSub = reconciler.on('on-thread-complete', (thread: WorkThread) => {
|
|
48
|
-
space.create(thread.deltas).configure();
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
const space = createSpace(reconciler.tree);
|
|
52
|
-
|
|
53
|
-
const onReady = () => {
|
|
54
|
-
reconciler.mount(node);
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
const debugWindow = window.open('', 'debug', 'popup');
|
|
58
|
-
if (debugWindow) {
|
|
59
|
-
const node = h(InsightApp, { reconciler, onReady, scheduler: debugScheduler });
|
|
60
|
-
const root = debugWindow.document.body;
|
|
61
|
-
for (const child of [...debugWindow.document.body.children, ...debugWindow.document.head.children])
|
|
62
|
-
child.remove()
|
|
63
|
-
|
|
64
|
-
for (const child of document.head.children) {
|
|
65
|
-
if (child instanceof HTMLStyleElement) {
|
|
66
|
-
debugWindow.document.head.append(child.cloneNode(true));
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
const renderWeb = createRenderFunction<HTMLElement>(
|
|
70
|
-
createDOMScheduler(),
|
|
71
|
-
(tree, root) => createWebSpace(tree, root, debugWindow)
|
|
72
|
-
)(h(HTML, {}, node), root);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return {
|
|
76
|
-
stop() {
|
|
77
|
-
threadCompleteSub.cancel();
|
|
78
|
-
},
|
|
79
|
-
}
|
|
80
|
-
};
|
package/devtool_page.ts
DELETED
|
File without changes
|
package/devtool_panel.ts
DELETED
|
File without changes
|
package/extension_main.ts
DELETED
|
File without changes
|
package/insight.test.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import '@types/node';
|
|
2
|
-
|
|
3
|
-
import { OpaqueID } from '@lukekaalim/act'
|
|
4
|
-
import { CommitRef } from '@lukekaalim/act-recon'
|
|
5
|
-
import { describe, it } from 'node:test'
|
|
6
|
-
import { deepEqual } from 'node:assert/strict'
|
|
7
|
-
import { findCommonAncestor } from './utils'
|
|
8
|
-
|
|
9
|
-
describe('@lukekaalim/act-insight', () => {
|
|
10
|
-
describe('findCommonAncestor', () => {
|
|
11
|
-
it ('should find a common ancestor', () => {
|
|
12
|
-
const a: OpaqueID<'CommitID'> = 'A' as any;
|
|
13
|
-
const b: OpaqueID<'CommitID'> = 'B' as any;
|
|
14
|
-
const c: OpaqueID<'CommitID'> = 'C' as any;
|
|
15
|
-
const d: OpaqueID<'CommitID'> = 'D' as any;
|
|
16
|
-
const e: OpaqueID<'CommitID'> = 'E' as any;
|
|
17
|
-
const f: OpaqueID<'CommitID'> = 'F' as any;
|
|
18
|
-
|
|
19
|
-
const refZ = CommitRef.from([a]);
|
|
20
|
-
const refA = CommitRef.from([a, b]);
|
|
21
|
-
const refB = CommitRef.from([a, c]);
|
|
22
|
-
const refC = CommitRef.from([a, c, d, e]);
|
|
23
|
-
const refD = CommitRef.from([a, c, f]);
|
|
24
|
-
|
|
25
|
-
deepEqual(findCommonAncestor([refA, refB]), refZ)
|
|
26
|
-
deepEqual(findCommonAncestor([refB, refC, refD]), refB)
|
|
27
|
-
})
|
|
28
|
-
})
|
|
29
|
-
})
|