// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'chrome://compare/horizontal_carousel.js';
import 'chrome://compare/table.js';
import type {TableColumn} from 'chrome://compare/app.js';
import {getTrustedHTML} from 'chrome://resources/js/static_types.js';
import {assertEquals, assertFalse, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {eventToPromise, isVisible, whenCheck} from 'chrome://webui-test/test_util.js';
import {$$} from './test_support.js';
suite('HorizontalCarouselTest', () => {
async function setupColumns({numColumns}: {numColumns: number}) {
const tableElement =
document.body.querySelector('product-specifications-table');
if (!tableElement) {
return;
}
const columns: TableColumn[] = [];
for (let i = 0; i < numColumns; i++) {
columns.push({
selectedItem: {
title: `${i}`,
url: `https://${i}`,
imageUrl: `https://${i}`,
},
productDetails: [],
});
}
const carouselElement = document.body.querySelector('horizontal-carousel')!;
const eventPromise =
eventToPromise('intersection-observed', carouselElement);
tableElement.columns = columns;
const event = await eventPromise;
assertTrue(!!event);
}
function isVisibleWithinContainer(
child: HTMLElement, container: HTMLElement): boolean {
const childRect = child.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
return (
childRect.right >= containerRect.left &&
childRect.left <= containerRect.right &&
childRect.bottom >= containerRect.top &&
childRect.top <= containerRect.bottom);
}
[1, 5, 6, 10].forEach(numColumns => {
test(
`buttons and selector render correctly for ${numColumns} columns`,
async () => {
// Arrange.
document.body.innerHTML = getTrustedHTML`
<horizontal-carousel>
<product-specifications-table slot="table">
</product-specifications-table>
<div slot="selector" id="selector"></div>
</horizontal-carousel>`;
const carouselElement =
document.body.querySelector('horizontal-carousel')!;
const carouselContainer = carouselElement.$.carouselContainer;
// Remove restrictions on `carouselContainer`'s width,
// by setting `carouselElement`'s width to the maximum width
// `carouselContainer` can have.
const maxWidth =
carouselContainer.computedStyleMap().get('max-width');
carouselElement.style.width =
maxWidth !== undefined ? maxWidth.toString() : 'initial';
// Act.
await setupColumns({numColumns: numColumns});
// Assert.
const selector =
document.body.querySelector<HTMLElement>('#selector')!;
assertTrue(isVisibleWithinContainer(selector, carouselContainer));
assertFalse(isVisible(carouselElement.$.backButton));
if (numColumns < 6) {
assertFalse(isVisible(carouselElement.$.forwardButton));
} else {
assertTrue(isVisible(carouselElement.$.forwardButton));
}
});
});
test('clicking forward button surfaces back button', async () => {
// Arrange.
document.body.innerHTML = getTrustedHTML`
<horizontal-carousel>
<product-specifications-table slot="table">
</product-specifications-table>
<div slot="selector"></div>
</horizontal-carousel>`;
await setupColumns({numColumns: 6});
const carouselElement = document.body.querySelector('horizontal-carousel')!;
assertTrue(isVisible(carouselElement.$.forwardButton));
assertFalse(isVisible(carouselElement.$.backButton));
// Act.
const eventPromise =
eventToPromise('intersection-observed', carouselElement);
carouselElement.$.forwardButton.click();
const event = await eventPromise;
// Assert.
assertTrue(!!event);
assertTrue(isVisible(carouselElement.$.backButton));
});
test('clicking back button resurfaces forward button', async () => {
// Arrange.
document.body.innerHTML = getTrustedHTML`
<horizontal-carousel>
<product-specifications-table slot="table">
</product-specifications-table>
<div slot="selector"></div>
</horizontal-carousel>`;
await setupColumns({numColumns: 6});
const carouselElement = document.body.querySelector('horizontal-carousel')!;
assertTrue(isVisible(carouselElement.$.forwardButton));
// Scroll to the end of the carousel, to ensure the back button has
// something to scroll back.
carouselElement.$.carouselContainer.scrollTo(
{left: carouselElement.scrollWidth});
// Wait until the forward button is hidden, not when 'intersection-observed'
// fires, as manual scrolling may trigger it more than once.
await whenCheck(
carouselElement.$.forwardButton,
() => !isVisible(carouselElement.$.forwardButton));
assertTrue(isVisible(carouselElement.$.backButton));
// Act.
const eventPromise =
eventToPromise('intersection-observed', carouselElement);
carouselElement.$.backButton.click();
const event = await eventPromise;
// Assert.
assertTrue(!!event);
assertTrue(isVisible(carouselElement.$.forwardButton));
});
test('focusing on carousel item scrolls item into view', async () => {
// Arrange.
document.body.innerHTML = getTrustedHTML`
<horizontal-carousel>
<product-specifications-table slot="table">
</product-specifications-table>
<div slot="selector"></div>
</horizontal-carousel>`;
await setupColumns({numColumns: 6});
const carouselElement = document.body.querySelector('horizontal-carousel')!;
assertTrue(isVisible(carouselElement.$.forwardButton));
const tableElement =
document.body.querySelector('product-specifications-table')!;
// Columns themselves aren't focusable, but the nested
// `#currentProductContainer` in their `product-selector`s are.
const productSelectors =
tableElement.shadowRoot!.querySelectorAll<HTMLElement>(
'.col product-selector');
assertEquals(6, productSelectors.length);
let numScrollEnd = 0;
const carouselContainer = carouselElement.$.carouselContainer;
carouselContainer.addEventListener('scrollend', () => {
numScrollEnd++;
});
// Act - focus on the last column's focusable child.
const focusableChild6 =
$$<HTMLElement>(productSelectors[5]!, '#currentProductContainer');
assertTrue(!!focusableChild6);
let scrollEnd = eventToPromise('scrollend', carouselContainer);
focusableChild6.focus();
await scrollEnd;
// Assert.
// Forward button hides, and the back button shows, because we've scrolled
// to the end of the container.
assertEquals(1, numScrollEnd);
assertTrue(isVisibleWithinContainer(focusableChild6, carouselContainer));
await whenCheck(
carouselElement.$.forwardButton,
() => !isVisible(carouselElement.$.forwardButton));
assertTrue(isVisible(carouselElement.$.backButton));
// Act - focus on the first column's focusable child.
const focusableChild1 =
$$<HTMLElement>(productSelectors[0]!, '#currentProductContainer');
assertTrue(!!focusableChild1);
scrollEnd = eventToPromise('scrollend', carouselContainer);
focusableChild1.focus();
await scrollEnd;
// Assert.
// The back button hides, and the forward button shows, since we scrolled
// back to the start of the container.
assertEquals(2, numScrollEnd);
assertTrue(isVisibleWithinContainer(focusableChild1, carouselContainer));
await whenCheck(
carouselElement.$.backButton,
() => !isVisible(carouselElement.$.backButton));
assertTrue(isVisible(carouselElement.$.forwardButton));
});
});