blob: b9e5af36f3bc8e935b173997c9c8fea6c0a3afed [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {
dispatchClickEvent,
getEventPromise,
renderElementIntoDOM,
} from '../../../testing/DOMHelpers.js';
import {setupLocaleHooks} from '../../../testing/LocaleHelpers.js';
import * as Lit from '../../lit/lit.js';
import * as RenderCoordinator from '../render_coordinator/render_coordinator.js';
import type {ItemEditEvent, ItemRemoveEvent} from './List.js';
import * as List from './lists.js'; // eslint-disable-line @devtools/es-modules-import
const {html, render} = Lit;
async function renderListComponent(
items: Lit.TemplateResult, editable?: boolean, deletable?: boolean): Promise<List.List.List> {
const component = new List.List.List();
if (editable) {
component.editable = true;
}
if (deletable) {
component.deletable = true;
}
render(items, component);
renderElementIntoDOM(component);
await RenderCoordinator.done();
return component;
}
describe('List', () => {
setupLocaleHooks();
it('renders items from light DOM and assigns slot attributes', async () => {
const component = await renderListComponent(html`
<p>Item 1</p>
<div>Item 2</div>
`);
assert.isNotNull(component.shadowRoot);
const listItems = component.shadowRoot.querySelectorAll('li[role="listitem"]');
assert.lengthOf(listItems, 2, 'Should render two list items');
const item1Child = component.querySelector('p');
const item2Child = component.querySelector('div');
assert.strictEqual(item1Child?.getAttribute('slot'), 'slot-0', 'First child must have slot="slot-0"');
assert.strictEqual(item2Child?.getAttribute('slot'), 'slot-1', 'Second child must have slot="slot-1"');
assert.strictEqual(listItems[0].querySelector('slot')?.assignedNodes()[0].textContent, 'Item 1');
assert.strictEqual(listItems[1].querySelector('slot')?.assignedNodes()[0].textContent, 'Item 2');
});
it('shows no control buttons per default', async () => {
const component = await renderListComponent(html`
<p>Item 1</p>
`);
assert.isNotNull(component.shadowRoot);
const buttons = component.shadowRoot.querySelectorAll('devtools-button');
assert.lengthOf(buttons, 0, 'No buttons should be rendered by default');
});
it('renders li items with tabindex=0 per default for keyboard navigation', async () => {
const component = await renderListComponent(html`<p>Item 1</p>`);
assert.isNotNull(component.shadowRoot);
const listItem = component.shadowRoot.querySelector('li[role="listitem"]');
assert.isNotNull(listItem, 'List item wrapper should exist');
assert.strictEqual(listItem.getAttribute('tabindex'), '0', 'List item should be focusable by default');
});
it('shows only the edit button when editable is set', async () => {
const component = await renderListComponent(
html`
<p>Item 1</p>
`,
true);
assert.isNotNull(component.shadowRoot);
const buttons = component.shadowRoot.querySelectorAll('devtools-button');
assert.lengthOf(buttons, 1, 'Exactly one button should be rendered');
const editButton = component.shadowRoot.querySelector('devtools-button[title="Edit"]');
assert.isNotNull(editButton, 'Edit button should be present');
});
it('shows only the remove button when deletable is set', async () => {
const component = await renderListComponent(
html`
<p>Item 1</p>
`,
undefined, true);
assert.isNotNull(component.shadowRoot);
const buttons = component.shadowRoot.querySelectorAll('devtools-button');
assert.lengthOf(buttons, 1, 'Exactly one button should be rendered');
const removeButton = component.shadowRoot.querySelector('devtools-button[title="Remove"]');
assert.isNotNull(removeButton, 'Remove button should be present');
});
it('omits tabindex when list item focus is disabled via attribute', async () => {
const component = await renderListComponent(html`<p>Item 1</p>`);
assert.isNotNull(component.shadowRoot);
let listItem = component.shadowRoot.querySelector('li[role="listitem"]');
assert.strictEqual(listItem?.getAttribute('tabindex'), '0', 'Initial state must be focusable');
component.setAttribute('disable-li-focus', 'true');
await RenderCoordinator.done();
listItem = component.shadowRoot.querySelector('li[role="listitem"]');
assert.strictEqual(listItem?.getAttribute('tabindex'), '-1', 'Tabindex must be -1 when attribute is set');
component.removeAttribute('disable-li-focus');
await RenderCoordinator.done();
listItem = component.shadowRoot.querySelector('li[role="listitem"]');
assert.strictEqual(listItem?.getAttribute('tabindex'), '0', 'Tabindex must return when attribute is removed');
});
it('shows both edit and remove buttons when both flags are set', async () => {
const component = await renderListComponent(html`<p>Item 1</p>`, true, true);
assert.isNotNull(component.shadowRoot);
const buttons = component.shadowRoot.querySelectorAll('devtools-button');
assert.lengthOf(buttons, 2, 'Exactly two buttons should be rendered');
assert.isNotNull(component.shadowRoot.querySelector('devtools-button[title="Edit"]'));
assert.isNotNull(component.shadowRoot.querySelector('devtools-button[title="Remove"]'));
});
it('dispatches an "edit" event when the edit button is clicked', async () => {
const component = await renderListComponent(
html`
<p>Item 1</p>
<p>Item 2</p>
`,
true, false);
assert.isNotNull(component.shadowRoot);
const listItems = component.shadowRoot.querySelectorAll('li');
const secondItem = listItems[1];
const editButton = secondItem.querySelector<HTMLElement>('devtools-button[title="Edit"]');
assert.instanceOf(editButton, HTMLElement);
const eventPromise = getEventPromise<ItemEditEvent>(component, 'edit');
dispatchClickEvent(editButton);
const event = await eventPromise;
assert.deepEqual(event.detail, {index: 1});
});
it('dispatches a "delete" event when the remove button is clicked', async () => {
const component = await renderListComponent(
html`
<p>Item 1</p>
<p>Item 2</p>
`,
false, true);
assert.isNotNull(component.shadowRoot);
const listItems = component.shadowRoot.querySelectorAll('li');
const firstItem = listItems[0];
const removeButton = firstItem.querySelector<HTMLElement>('devtools-button[title="Remove"]');
assert.instanceOf(removeButton, HTMLElement);
const eventPromise = getEventPromise<ItemRemoveEvent>(component, 'delete');
dispatchClickEvent(removeButton);
const event = await eventPromise;
assert.deepEqual(event.detail, {index: 0});
});
it('adds an item when a new child is appended', async () => {
const component = await renderListComponent(html`
<p>Item 1</p>
`);
assert.isNotNull(component.shadowRoot);
let listItems = component.shadowRoot.querySelectorAll('li');
assert.lengthOf(listItems, 1);
const newItem = document.createElement('p');
newItem.textContent = 'Item 2';
component.appendChild(newItem);
await RenderCoordinator.done();
listItems = component.shadowRoot.querySelectorAll('li');
assert.lengthOf(listItems, 2, 'Should have two items after adding one');
assert.strictEqual(listItems[1].querySelector('slot')?.assignedNodes()[0].textContent, 'Item 2');
});
it('removes an item when a child is removed', async () => {
const component = await renderListComponent(html`
<p>Item 1</p>
<p>Item 2</p>
`);
assert.isNotNull(component.shadowRoot);
let listItems = component.shadowRoot.querySelectorAll('li');
assert.lengthOf(listItems, 2);
const itemToRemove = component.querySelector('p:last-child');
assert.instanceOf(itemToRemove, HTMLParagraphElement);
itemToRemove.remove();
await RenderCoordinator.done();
listItems = component.shadowRoot.querySelectorAll('li');
assert.lengthOf(listItems, 1, 'Should have one item after removing one');
assert.strictEqual(listItems[0].querySelector('slot')?.assignedNodes()[0].textContent, 'Item 1');
});
});