Skip to content

Commit 103a382

Browse files
OrKoNdevtools-frontend-scoped@luci-project-accounts.iam.gserviceaccount.com
authored andcommitted
[elements] Introduce declarative view for DOMTreeWidget
Adds an alternative DECLARATIVE_VIEW in DOMTreeWidget that uses <devtools-tree> and ElementsTreeWidget without relying on legacy ElementsTreeOutline / ElementsTreeElement. This view is disabled by default and comes with a TODO list to make it on-par with the current imperative view. The goal of doing it this way is to reduce the volume of changes and minimize the need for keeping existing code backward compatible. Bug: 407751692 Change-Id: I6b2f54b3ccad108d24621fde692773aa993d6df3 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/8276597 Reviewed-by: Simon Zünd <szuend@chromium.org> Commit-Queue: Alex Rudenko <alexrudenko@chromium.org>
1 parent abb29d7 commit 103a382

4 files changed

Lines changed: 865 additions & 7 deletions

File tree

front_end/panels/elements/DOMTreeWidget.test.ts

Lines changed: 246 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ describeWithEnvironment('DOMTreeWidget', () => {
302302
rootTreeElement.expand();
303303
domTree.performUpdate();
304304

305-
await new Promise(resolve => setTimeout(resolve, 0));
305+
await UI.Widget.Widget.allUpdatesComplete;
306306

307307
await assertScreenshot('elements/elements_tree_outline_default.png');
308308
} finally {
@@ -311,4 +311,249 @@ describeWithEnvironment('DOMTreeWidget', () => {
311311
});
312312
});
313313

314+
describe('DECLARATIVE_VIEW', () => {
315+
it('renders DOM tree declaratively using <devtools-tree> and ElementsTreeWidget', async () => {
316+
const {domTree, domModel} = setupDOMTreeWidget(target, Elements.ElementsTreeOutline.DECLARATIVE_VIEW);
317+
try {
318+
const rootNode = createTestDOMTree(domModel, {
319+
nodeId: 1,
320+
nodeName: 'DIV',
321+
children: [{nodeId: 2, nodeName: '#text', nodeValue: 'Hello world'}],
322+
});
323+
domTree.rootDOMNode = rootNode;
324+
domTree.performUpdate();
325+
326+
// Ensure ElementsTreeOutline is not created in declarative mode.
327+
assert.isUndefined(domTree.getTreeOutlineForTesting());
328+
329+
// Wait for devtools-tree to render its template.
330+
await UI.Widget.Widget.allUpdatesComplete;
331+
332+
const tree = domTree.contentElement.querySelector<UI.TreeOutline.TreeViewElement>('devtools-tree');
333+
assert.exists(tree);
334+
335+
const internalTree = tree.getInternalTreeOutlineForTest();
336+
const rootTreeElements = internalTree.rootElement().children();
337+
assert.lengthOf(rootTreeElements, 1);
338+
339+
// Verify ElementsTreeWidget is rendered for the root node.
340+
const widgetElement = rootTreeElements[0].listItemElement.querySelector('devtools-widget');
341+
assert.exists(widgetElement);
342+
const widget = UI.Widget.Widget.get(widgetElement);
343+
assert.instanceOf(widget, Elements.ElementsTreeElement.ElementsTreeWidget);
344+
assert.strictEqual((widget as Elements.ElementsTreeElement.ElementsTreeWidget).node, rootNode);
345+
} finally {
346+
domTree.detach();
347+
}
348+
});
349+
350+
it('handles selection and expansion in declarative view', async () => {
351+
const {domTree, domModel} = setupDOMTreeWidget(target, Elements.ElementsTreeOutline.DECLARATIVE_VIEW);
352+
try {
353+
const rootNode = createTestDOMTree(domModel, {
354+
nodeId: 1,
355+
nodeName: 'DIV',
356+
children: [{nodeId: 2, nodeName: 'SPAN'}],
357+
});
358+
domTree.rootDOMNode = rootNode;
359+
360+
const childNode = rootNode.children()![0];
361+
domTree.selectDOMNode(childNode);
362+
assert.strictEqual(domTree.selectedDOMNode(), childNode);
363+
assert.isTrue(domTree.isNodeExpanded(rootNode));
364+
365+
domTree.setNodeExpanded(rootNode, false);
366+
assert.isFalse(domTree.isNodeExpanded(rootNode));
367+
} finally {
368+
domTree.detach();
369+
}
370+
});
371+
372+
it('supports omitRootDOMNode in declarative view', async () => {
373+
const {domTree, domModel} = setupDOMTreeWidget(target, Elements.ElementsTreeOutline.DECLARATIVE_VIEW);
374+
try {
375+
domTree.omitRootDOMNode = true;
376+
const rootNode = createTestDOMTree(domModel, {
377+
nodeId: 1,
378+
nodeName: 'BODY',
379+
children: [{nodeId: 2, nodeName: 'H1'}],
380+
});
381+
domTree.rootDOMNode = rootNode;
382+
domTree.performUpdate();
383+
384+
// Wait for devtools-tree to render its template.
385+
await UI.Widget.Widget.allUpdatesComplete;
386+
387+
const tree = domTree.contentElement.querySelector<UI.TreeOutline.TreeViewElement>('devtools-tree');
388+
assert.exists(tree);
389+
390+
const internalTree = tree.getInternalTreeOutlineForTest();
391+
const children = internalTree.rootElement().children();
392+
assert.lengthOf(children, 1);
393+
394+
const widgetElement = children[0].listItemElement.querySelector('devtools-widget');
395+
assert.exists(widgetElement);
396+
const widget = UI.Widget.Widget.get(widgetElement) as Elements.ElementsTreeElement.ElementsTreeWidget;
397+
assert.exists(widget);
398+
assert.strictEqual(widget.node, rootNode.children()![0]);
399+
} finally {
400+
domTree.detach();
401+
}
402+
});
403+
404+
it('fetches children asynchronously when children are not loaded initially', async () => {
405+
const {domTree, domModel} = setupDOMTreeWidget(target, Elements.ElementsTreeOutline.DECLARATIVE_VIEW);
406+
try {
407+
domTree.omitRootDOMNode = true;
408+
const rootNode = SDK.DOMModel.DOMNode.create(domModel, null, false, {
409+
nodeId: 1 as Protocol.DOM.NodeId,
410+
backendNodeId: 1 as Protocol.DOM.BackendNodeId,
411+
nodeType: Node.DOCUMENT_NODE,
412+
nodeName: '#document',
413+
localName: '',
414+
nodeValue: '',
415+
childNodeCount: 1,
416+
});
417+
assert.isNotNull(rootNode);
418+
419+
let getChildNodesCallback: ((children: SDK.DOMModel.DOMNode[]|null) => void)|undefined;
420+
sinon.stub(rootNode, 'getChildNodes').callsFake(callback => {
421+
getChildNodesCallback = callback;
422+
const htmlNode = createTestDOMTree(domModel, {nodeId: 2, nodeName: 'HTML'});
423+
rootNode.childrenInternal = [htmlNode];
424+
});
425+
426+
domTree.rootDOMNode = rootNode;
427+
domTree.performUpdate();
428+
429+
assert.isDefined(getChildNodesCallback);
430+
getChildNodesCallback?.(rootNode.childrenInternal);
431+
domTree.performUpdate();
432+
433+
await UI.Widget.Widget.allUpdatesComplete;
434+
435+
const tree = domTree.contentElement.querySelector<UI.TreeOutline.TreeViewElement>('devtools-tree');
436+
assert.exists(tree);
437+
438+
const internalTree = tree.getInternalTreeOutlineForTest();
439+
const children = internalTree.rootElement().children();
440+
assert.lengthOf(children, 1);
441+
} finally {
442+
domTree.detach();
443+
}
444+
});
445+
446+
it('renders screenshot of declarative view', async () => {
447+
const {domTree, domModel} =
448+
setupDOMTreeWidget(target, Elements.ElementsTreeOutline.DECLARATIVE_VIEW, {includeCommonStyles: true});
449+
try {
450+
const rootNode = createTestDOMTree(domModel, {
451+
nodeId: 1,
452+
nodeName: 'DIV',
453+
attributes: ['id', 'container', 'class', 'main-view'],
454+
children: [
455+
{nodeId: 2, nodeName: 'H1', children: [{nodeId: 3, nodeName: '#text', nodeValue: 'Title'}]},
456+
{nodeId: 4, nodeName: 'SPAN', children: [{nodeId: 5, nodeName: '#text', nodeValue: 'Description'}]},
457+
],
458+
});
459+
domTree.rootDOMNode = rootNode;
460+
domTree.setNodeExpanded(rootNode, true);
461+
domTree.performUpdate();
462+
463+
await UI.Widget.Widget.allUpdatesComplete;
464+
465+
await assertScreenshot('elements/elements_tree_outline_declarative.png');
466+
} finally {
467+
domTree.detach();
468+
}
469+
});
470+
471+
it('supports maxTreeDepth in declarative view', async () => {
472+
const {domTree, domModel} = setupDOMTreeWidget(target, Elements.ElementsTreeOutline.DECLARATIVE_VIEW);
473+
try {
474+
const rootNode = createTestDOMTree(domModel, {
475+
nodeId: 1,
476+
nodeName: 'DIV',
477+
children: [{
478+
nodeId: 2,
479+
nodeName: 'P',
480+
children: [{nodeId: 3, nodeName: 'SPAN'}],
481+
}],
482+
});
483+
domTree.rootDOMNode = rootNode;
484+
domTree.maxTreeDepth = 1;
485+
domTree.setNodeExpanded(rootNode, true);
486+
domTree.performUpdate();
487+
488+
await UI.Widget.Widget.allUpdatesComplete;
489+
490+
const tree = domTree.contentElement.querySelector<UI.TreeOutline.TreeViewElement>('devtools-tree');
491+
assert.exists(tree);
492+
const internalTree = tree.getInternalTreeOutlineForTest();
493+
const rootTreeElements = internalTree.rootElement().children();
494+
assert.lengthOf(rootTreeElements, 1);
495+
} finally {
496+
domTree.detach();
497+
}
498+
});
499+
500+
it('filters comment nodes based on showComments setting in declarative view', async () => {
501+
const {domTree, domModel} = setupDOMTreeWidget(target, Elements.ElementsTreeOutline.DECLARATIVE_VIEW);
502+
try {
503+
const rootNode = createTestDOMTree(domModel, {
504+
nodeId: 1,
505+
nodeName: 'DIV',
506+
children: [
507+
{nodeId: 2, nodeName: '#comment', nodeType: Node.COMMENT_NODE, nodeValue: 'comment text'},
508+
{nodeId: 3, nodeName: 'SPAN'},
509+
],
510+
});
511+
domTree.rootDOMNode = rootNode;
512+
domTree.showComments = false;
513+
domTree.setNodeExpanded(rootNode, true);
514+
domTree.performUpdate();
515+
516+
await UI.Widget.Widget.allUpdatesComplete;
517+
518+
const tree = domTree.contentElement.querySelector<UI.TreeOutline.TreeViewElement>('devtools-tree');
519+
assert.exists(tree);
520+
const internalTree = tree.getInternalTreeOutlineForTest();
521+
const rootTreeElements = internalTree.rootElement().children();
522+
assert.isNotEmpty(rootTreeElements);
523+
// With comments filtered out, child elements are [SPAN, </DIV>].
524+
const childTreeElements = rootTreeElements[0].children();
525+
assert.lengthOf(childTreeElements, 2);
526+
const spanWidgetElement = childTreeElements[0].listItemElement.querySelector('devtools-widget');
527+
assert.exists(spanWidgetElement);
528+
const widget = UI.Widget.Widget.get(spanWidgetElement) as Elements.ElementsTreeElement.ElementsTreeWidget;
529+
assert.strictEqual(widget.node?.nodeName(), 'SPAN');
530+
} finally {
531+
domTree.detach();
532+
}
533+
});
534+
535+
it('sets correct jslog attributes on treeitems in declarative view', async () => {
536+
const {domTree, domModel} = setupDOMTreeWidget(target, Elements.ElementsTreeOutline.DECLARATIVE_VIEW);
537+
try {
538+
const rootNode = createTestDOMTree(domModel, {nodeId: 1, nodeName: 'DIV'});
539+
domTree.rootDOMNode = rootNode;
540+
domTree.performUpdate();
541+
542+
await UI.Widget.Widget.allUpdatesComplete;
543+
544+
const tree = domTree.contentElement.querySelector<UI.TreeOutline.TreeViewElement>('devtools-tree');
545+
assert.exists(tree);
546+
const internalTree = tree.getInternalTreeOutlineForTest();
547+
const rootTreeElements = internalTree.rootElement().children();
548+
assert.lengthOf(rootTreeElements, 1);
549+
550+
const jslog = rootTreeElements[0].listItemElement.getAttribute('jslog');
551+
assert.isNotNull(jslog);
552+
assert.include(jslog, 'TreeItem');
553+
assert.include(jslog, 'elementsTreeOutline');
554+
} finally {
555+
domTree.detach();
556+
}
557+
});
558+
});
314559
});

0 commit comments

Comments
 (0)