UNPKG

chrome-devtools-frontend

Version:
436 lines (377 loc) 15.9 kB
// Copyright 2020 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import type * as SDK from '../../../core/sdk/sdk.js'; import { assertElements, dispatchClickEvent, doubleRaf, renderElementIntoDOM, waitForScrollLeft, } from '../../../testing/DOMHelpers.js'; import { deinitializeGlobalVars, initializeGlobalVars, } from '../../../testing/EnvironmentHelpers.js'; import {withNoMutations} from '../../../testing/MutationHelpers.js'; import * as RenderCoordinator from '../../../ui/components/render_coordinator/render_coordinator.js'; import * as ElementsComponents from './components.js'; interface MakeCrumbOptions extends Partial<ElementsComponents.Helper.DOMNode> { attributes?: Record<string, string>; } /* * This very clearly is not a real legacy SDK DOMNode, but for the purposes of * the test we just need something that presents as one, and doesn't need to * implement anything */ const FAKE_LEGACY_SDK_DOM_NODE = {} as unknown as SDK.DOMModel.DOMNode; const makeCrumb = (overrides: MakeCrumbOptions = {}) => { const attributes = overrides.attributes || {}; const newCrumb: ElementsComponents.Helper.DOMNode = { parentNode: null, nodeType: Node.ELEMENT_NODE, id: 1, pseudoType: '', shadowRootType: '', nodeName: 'body', nodeNameNicelyCased: 'body', legacyDomNode: FAKE_LEGACY_SDK_DOM_NODE, highlightNode: () => {}, clearHighlight: () => {}, getAttribute: x => attributes[x] || '', ...overrides, }; return newCrumb; }; describe('ElementsBreadcrumbs', () => { before(async () => { await initializeGlobalVars(); }); after(async () => { await deinitializeGlobalVars(); }); describe('#determineElementTitle', () => { it('returns (text) for text nodes', () => { const node = makeCrumb({nodeType: Node.TEXT_NODE}); const title = ElementsComponents.ElementsBreadcrumbsUtils.determineElementTitle(node); assert.deepEqual(title, { main: '(text)', extras: {}, }); }); it('returns <!--> for comments', () => { const node = makeCrumb({nodeType: Node.COMMENT_NODE}); const title = ElementsComponents.ElementsBreadcrumbsUtils.determineElementTitle(node); assert.deepEqual(title, {main: '<!-->', extras: {}}); }); it('returns <!doctype> for doctypes', () => { const node = makeCrumb({nodeType: Node.DOCUMENT_TYPE_NODE}); const title = ElementsComponents.ElementsBreadcrumbsUtils.determineElementTitle(node); assert.deepEqual(title, {main: '<!doctype>', extras: {}}); }); describe('for DOCUMENT_FRAGMENT_NODE types', () => { it('shows the shadowRoot if the document is a shadowRootType', () => { const node = makeCrumb({ nodeType: Node.DOCUMENT_FRAGMENT_NODE, shadowRootType: 'shadowRoot', nodeNameNicelyCased: 'test-elem', }); const title = ElementsComponents.ElementsBreadcrumbsUtils.determineElementTitle(node); assert.deepEqual(title, {main: '#shadow-root', extras: {}}); }); it('shows the nice name if there is not a shadow root', () => { const node = makeCrumb({ nodeType: Node.DOCUMENT_FRAGMENT_NODE, shadowRootType: undefined, nodeNameNicelyCased: 'test-elem', }); const title = ElementsComponents.ElementsBreadcrumbsUtils.determineElementTitle(node); assert.deepEqual(title, {main: 'test-elem', extras: {}}); }); }); describe('for element nodes', () => { it('takes the nicely cased node name by default', () => { const node = makeCrumb({nodeType: Node.ELEMENT_NODE, nodeNameNicelyCased: 'div'}); const title = ElementsComponents.ElementsBreadcrumbsUtils.determineElementTitle(node); assert.deepEqual(title, {main: 'div', extras: {}}); }); it('uses the pseudoType if that is passed', () => { const node = makeCrumb({nodeType: Node.ELEMENT_NODE, pseudoType: 'test'}); const title = ElementsComponents.ElementsBreadcrumbsUtils.determineElementTitle(node); assert.deepEqual(title, {main: '::test', extras: {}}); }); it('adds the ID as an extra if present', () => { const node = makeCrumb({nodeType: Node.ELEMENT_NODE, nodeNameNicelyCased: 'div', attributes: {id: 'test'}}); const title = ElementsComponents.ElementsBreadcrumbsUtils.determineElementTitle(node); assert.deepEqual(title, { main: 'div', extras: { id: 'test', }, }); }); it('adds classes as extras if present', () => { const node = makeCrumb({ nodeType: Node.ELEMENT_NODE, nodeNameNicelyCased: 'div', attributes: {class: 'class1 class2'}, }); const title = ElementsComponents.ElementsBreadcrumbsUtils.determineElementTitle(node); assert.deepEqual(title, { main: 'div', extras: { classes: ['class1', 'class2'], }, }); }); }); it('falls back to the nicely cased name if the node is any other type', () => { const node = makeCrumb({ nodeType: Node.CDATA_SECTION_NODE, nodeNameNicelyCased: 'not-special-cased-node-type', }); const title = ElementsComponents.ElementsBreadcrumbsUtils.determineElementTitle(node); assert.deepEqual(title, { main: 'not-special-cased-node-type', extras: {}, }); }); }); describe('crumbsToRender', () => { it('returns an empty array when there is no selected node', () => { const result = ElementsComponents.ElementsBreadcrumbsUtils.crumbsToRender([], null); assert.deepEqual(result, []); }); it('excludes the document node', () => { const documentCrumb = makeCrumb({ nodeType: Node.DOCUMENT_NODE, id: 1, nodeName: 'document', nodeNameNicelyCased: 'document', }); const bodyCrumb = makeCrumb({ nodeType: Node.ELEMENT_NODE, id: 2, nodeName: 'body', nodeNameNicelyCased: 'body', }); const result = ElementsComponents.ElementsBreadcrumbsUtils.crumbsToRender([documentCrumb, bodyCrumb], bodyCrumb); assert.deepEqual(result, [ { title: { main: 'body', extras: {}, }, selected: true, node: bodyCrumb, originalNode: bodyCrumb.legacyDomNode, }, ]); }); }); describe('rendering breadcrumbs', () => { async function renderBreadcrumbs(data: ElementsComponents.ElementsBreadcrumbs.ElementsBreadcrumbsData): Promise<{ component: ElementsComponents.ElementsBreadcrumbs.ElementsBreadcrumbs, shadowRoot: ShadowRoot, }> { const component = new ElementsComponents.ElementsBreadcrumbs.ElementsBreadcrumbs(); renderElementIntoDOM(component); component.data = data; await RenderCoordinator.done(); assert.isNotNull(component.shadowRoot); return { component, shadowRoot: component.shadowRoot, }; } const bodyCrumb = makeCrumb({ nodeType: Node.ELEMENT_NODE, id: 2, nodeName: 'body', nodeNameNicelyCased: 'body', }); const divCrumb = makeCrumb({ nodeType: Node.ELEMENT_NODE, id: 3, nodeName: 'div', nodeNameNicelyCased: 'div', attributes: { id: 'test-id', }, }); it('renders all the breadcrumbs provided', async () => { const {shadowRoot} = await renderBreadcrumbs({ crumbs: [divCrumb, bodyCrumb], selectedNode: bodyCrumb, }); const crumbs = Array.from(shadowRoot.querySelectorAll('[data-crumb]')); assert.lengthOf(crumbs, 2); }); it('highlights the active breadcrumb', async () => { const {shadowRoot} = await renderBreadcrumbs({ crumbs: [divCrumb, bodyCrumb], selectedNode: bodyCrumb, }); const activeCrumbs = shadowRoot.querySelectorAll('.crumb.selected'); assert.lengthOf(activeCrumbs, 1); }); it('updates the text if a crumb\'s title changes', async () => { const {component, shadowRoot} = await renderBreadcrumbs({ crumbs: [divCrumb, bodyCrumb], selectedNode: bodyCrumb, }); await withNoMutations(shadowRoot, async shadowRoot => { const newDiv: ElementsComponents.Helper.DOMNode = {...divCrumb, nodeName: 'span', nodeNameNicelyCased: 'span'}; component.data = { crumbs: [newDiv, bodyCrumb], selectedNode: bodyCrumb, }; await RenderCoordinator.done(); const renderedTextForUpdatedCrumb = shadowRoot.querySelector('.crumb:last-child devtools-node-text'); assert.instanceOf(renderedTextForUpdatedCrumb, HTMLElement); assert.strictEqual(renderedTextForUpdatedCrumb.dataset.nodeTitle, 'span'); }); }); describe('when the breadcrumbs overflow', () => { const bodyCrumb = makeCrumb({ nodeType: Node.ELEMENT_NODE, id: 2, nodeName: 'body', nodeNameNicelyCased: 'body', attributes: { class: 'test-class-1 test-class-2 test-class-3', }, }); const divCrumb = makeCrumb({ nodeType: Node.ELEMENT_NODE, id: 3, nodeName: 'div', nodeNameNicelyCased: 'div', attributes: { id: 'test-id-with-a-really-long-name', }, }); it('shows the scrolling icons if the crumbs do not fit in their container', async () => { const thinWrapper = document.createElement('div'); thinWrapper.style.width = '400px'; const component = new ElementsComponents.ElementsBreadcrumbs.ElementsBreadcrumbs(); thinWrapper.appendChild(component); renderElementIntoDOM(thinWrapper); component.data = { crumbs: [divCrumb, bodyCrumb], selectedNode: bodyCrumb, }; await RenderCoordinator.done(); assert.isNotNull(component.shadowRoot); const scrollButtons = component.shadowRoot.querySelectorAll('button.overflow'); assertElements(scrollButtons, HTMLButtonElement); assert.lengthOf(scrollButtons, 2, 'there are two scroll buttons'); const leftButton = scrollButtons[0]; const rightButton = scrollButtons[1]; assert.isTrue(leftButton.disabled); assert.isFalse(rightButton.disabled); }); it('disables the right button once the user has scrolled to the end', async () => { const thinWrapper = document.createElement('div'); thinWrapper.style.width = '400px'; const component = new ElementsComponents.ElementsBreadcrumbs.ElementsBreadcrumbs(); thinWrapper.appendChild(component); renderElementIntoDOM(thinWrapper); component.data = { crumbs: [divCrumb, bodyCrumb], selectedNode: bodyCrumb, }; await RenderCoordinator.done(); assert.isNotNull(component.shadowRoot); const rightButton = component.shadowRoot.querySelector('button.overflow.right'); assert.instanceOf(rightButton, HTMLButtonElement); assert.isFalse(rightButton.disabled); await withNoMutations(component.shadowRoot, async shadowRoot => { dispatchClickEvent(rightButton); const scrollWrapper = shadowRoot.querySelector('.crumbs-window'); assert.instanceOf(scrollWrapper, HTMLDivElement); await waitForScrollLeft(scrollWrapper, 100); await RenderCoordinator.done(); assert.isTrue(rightButton.disabled); }); }); it('hides the overflow buttons should the user resize the window to be large enough', async () => { const thinWrapper = document.createElement('div'); thinWrapper.style.width = '400px'; const component = new ElementsComponents.ElementsBreadcrumbs.ElementsBreadcrumbs(); thinWrapper.appendChild(component); renderElementIntoDOM(thinWrapper); component.data = { crumbs: [divCrumb, bodyCrumb], selectedNode: bodyCrumb, }; await RenderCoordinator.done(); assert.isNotNull(component.shadowRoot); const leftButton = component.shadowRoot.querySelector('button.overflow.left'); assert.instanceOf(leftButton, HTMLButtonElement); const rightButton = component.shadowRoot.querySelector('button.overflow.right'); assert.instanceOf(rightButton, HTMLButtonElement); assert.isFalse(leftButton.classList.contains('hidden')); assert.isFalse(rightButton.classList.contains('hidden')); thinWrapper.style.width = '800px'; // Changing the width should trigger the resize observer, so we need to wait for that to happen. await doubleRaf(); await RenderCoordinator.done(); assert.isTrue(leftButton.classList.contains('hidden')); assert.isTrue(rightButton.classList.contains('hidden')); }); it('hides the overflow should the list of nodes change so the crumbs no longer overflow', async () => { const thinWrapper = document.createElement('div'); thinWrapper.style.width = '400px'; const component = new ElementsComponents.ElementsBreadcrumbs.ElementsBreadcrumbs(); thinWrapper.appendChild(component); renderElementIntoDOM(thinWrapper); component.data = { crumbs: [divCrumb, bodyCrumb], selectedNode: bodyCrumb, }; await RenderCoordinator.done(); assert.isNotNull(component.shadowRoot); const leftButton = component.shadowRoot.querySelector('button.overflow.left'); assert.instanceOf(leftButton, HTMLButtonElement); const rightButton = component.shadowRoot.querySelector('button.overflow.right'); assert.instanceOf(rightButton, HTMLButtonElement); // Ensure the buttons are visible now assert.isFalse(leftButton.classList.contains('hidden')); assert.isFalse(rightButton.classList.contains('hidden')); component.data = { crumbs: [bodyCrumb], selectedNode: bodyCrumb, }; await RenderCoordinator.done(); // The buttons are hidden now the list is no longer overflowing assert.isTrue(leftButton.classList.contains('hidden')); assert.isTrue(rightButton.classList.contains('hidden')); }); it('shows the overflow buttons should the user resize the window down to be small', async () => { const thinWrapper = document.createElement('div'); thinWrapper.style.width = '800px'; const component = new ElementsComponents.ElementsBreadcrumbs.ElementsBreadcrumbs(); thinWrapper.appendChild(component); renderElementIntoDOM(thinWrapper); component.data = { crumbs: [divCrumb, bodyCrumb], selectedNode: bodyCrumb, }; await RenderCoordinator.done(); assert.isNotNull(component.shadowRoot); const leftButton = component.shadowRoot.querySelector('button.overflow.left'); assert.instanceOf(leftButton, HTMLButtonElement); const rightButton = component.shadowRoot.querySelector('button.overflow.right'); assert.instanceOf(rightButton, HTMLButtonElement); assert.isTrue(leftButton.classList.contains('hidden')); assert.isTrue(rightButton.classList.contains('hidden')); thinWrapper.style.width = '400px'; // Give the resize observer time to fire. await doubleRaf(); await RenderCoordinator.done(); assert.isFalse(leftButton.classList.contains('hidden')); assert.isFalse(rightButton.classList.contains('hidden')); }); }); }); });