import { UniDriver, waitFor } from '@wix/wix-ui-test-utils/unidriver'; import { AddItemUniDriver, CardGalleryItemUniDriver, IconButtonUniDriver, LiveRegionUniDriver, MessageModalLayoutUniDriver, PopoverMenuUniDriver, TextButtonUniDriver, } from '@wix/design-system/dist/testkit/unidriver'; import { CollectionToolbarUniDriver } from '../CollectionToolbar/CollectionToolbar.uni.driver'; import { DragAndDropUniDriver } from '../DragAndDrop/DragAndDrop.uni.driver'; import { CollectionEmptyStateUniDriver } from '../EmptyState/CollectionEmptyState.uni.driver'; import { ToolbarCollectionErrorStateUniDriver } from '../ErrorState/ToolbarCollectionErrorState.uni.driver'; import { SledPage } from '../../sledPage'; import { ExportButtonUniDriver } from '../ExportButton/ExportButton.uni.driver'; import { ExportModalUniDriver } from '../ExportButton/ExportModal.uni.driver'; import { SkeletonGridUniDriver } from './SkeletonGrid.uni.driver'; import { baseUniDriverFactory, isSled2, isSled3 } from '../../unidriver'; import { SummaryBarUniDriver } from '../SummaryBar/SummaryBar.uni.driver'; import { CollectionPremiumEmptyStateUniDriver } from '../EmptyState/CollectionPremiumEmptyState.uni.driver'; import { CollectionNoResultsStateUniDriver } from '../EmptyState/CollectionNoResultsState.uni.driver'; import { CollectionSectionHeaderUniDriver } from '../CollectionSectionHeader/CollectionSectionHeader.uni.driver'; export function GridUniDriver(base: UniDriver, body: UniDriver) { const initialLoaderBase = () => base.$(`[data-hook="grid-initial-loader"]`); const initialLoader = () => SkeletonGridUniDriver(initialLoaderBase(), body); const infiniteScrollLoaderBase = () => base.$(`[data-hook="infinite-scroll-loader"]`); const infiniteScrollLoader = () => SkeletonGridUniDriver(infiniteScrollLoaderBase(), body); const slidingModalBase = () => body.$('[data-hook="cairo-side-panel-modal"]'); const getEmptyStateByDataHook = (dataHook = 'collection-empty-state') => CollectionEmptyStateUniDriver(base.$(`[data-hook="${dataHook}"]`), body); const getPremiumEmptyStateByDataHook = (dataHook: string) => CollectionPremiumEmptyStateUniDriver( base.$(`[data-hook="${dataHook}"]`), body, ); const getNoResultsStateByDataHook = (dataHook = 'collection-empty-state') => CollectionNoResultsStateUniDriver( base.$(`[data-hook="${dataHook}"]`), body, ); const viewsToolbarItem = () => base.$('[data-hook="collection-table-toolbar-views-item"]'); const getCardGalleryItemByIndexBase = (index: number) => { return base.$( `[data-hook="virtual-grid-item"][data-index="index-${index}"]`, ); }; const getCardGalleryItemFooterImageByIndexBase = (index: number) => { return base.$( `[data-hook="virtual-grid-item"][data-index="index-${index}"] [data-hook="footer-image"]`, ); }; const getCardDragElementByIndexBase = (index: number) => { return getCardGalleryItemByIndexBase(index).$( '[data-hook="grid-card-item-events"]', ); }; const getCardGalleryItemByIndex = (index: number) => { const itemDriver = CardGalleryItemUniDriver( getCardGalleryItemByIndexBase(index), body, ); const getPrimaryAction = () => getCardGalleryItemByIndexBase(index).$('[data-hook="primary-action"]'); return { ...itemDriver, hoverPrimaryAction: () => getPrimaryAction().hover(), isPrimaryActionExists: () => getPrimaryAction().exists(), }; }; const getAddItem = () => AddItemUniDriver(base.$(`[data-hook="grid-add-item-container"]`), body); const getCardGalleryItemPopoverMenuByIndex = async (index: number) => { // Popover isn't active until we specifically call it await (await getCardGalleryItemByIndex(index)).getSettingsMenu(); const popoverBase = base.$$(`[data-hook="popover"]`).get(index); const popoverMenu = PopoverMenuUniDriver(popoverBase, body); const trigger = popoverBase .$$('[data-hook="popover-icon-button-trigger"]') .get(0); const iconButton = IconButtonUniDriver(trigger, base); return { ...popoverMenu, clickTriggerElement: () => iconButton.click(), }; }; const getActionModal = () => MessageModalLayoutUniDriver( body.$('[data-hook="collection-action-modal"]'), body, ); const getToolbar = () => CollectionToolbarUniDriver( base.$('[data-hook="collection-toolbar"]'), body, ); const scrollDown = async ( scrollableElementDataHook = 'page-scrollable-content', ) => { const scrollableDriver = body.$( `[data-hook="${scrollableElementDataHook}"]`, ); const native = await scrollableDriver.getNative(); // puppeteer if (isSled2(native)) { const { page } = native as { page: SledPage['page'] }; await page.evaluate((_dataHook: string) => { const scrollableElement = document.querySelector( `[data-hook="${_dataHook}"]`, )!; scrollableElement.scrollBy({ behavior: 'smooth', top: scrollableElement.scrollHeight, }); }, scrollableElementDataHook); await page.waitForTimeout(1000); // delay scroll resolve by 1 sec } // playwright else if (isSled3(native)) { const locator = (await scrollableDriver.unwrap()) as { evaluate: (fn: (el: HTMLElement) => void) => Promise; page: () => { waitForTimeout: (ms: number) => Promise }; }; await locator.evaluate((scrollableElement) => { scrollableElement.scrollBy({ behavior: 'smooth', top: scrollableElement.scrollHeight, }); }); await locator.page().waitForTimeout(1000); } // jsdom else { if (await scrollableDriver.exists()) { await native.dispatchEvent(new Event('scroll')); } } }; const getDragAndDrop = () => DragAndDropUniDriver(base); const getErrorStateByDataHook = ( dataHook = 'placeholder-states-error-state', ) => ToolbarCollectionErrorStateUniDriver( base.$(`[data-hook="${dataHook}"]`), body, ); const refreshLoaderBase = () => base.$(`[data-hook="grid-refresh-loader"]`); const refreshLoader = () => SkeletonGridUniDriver(refreshLoaderBase(), body); const getNavigationModal = () => MessageModalLayoutUniDriver(body.$('[data-hook="navigation-modal"]'), body); const getExportModal = () => ExportModalUniDriver(base, body); const getExportButton = () => ExportButtonUniDriver(base.$('[data-hook="export-button"]'), body); const getRenderedItemIndexAt = async (i: number) => { const card = base .$$('[data-hook="virtual-grid-item"]') .filter((e) => e.$('[data-hook="grid-card-item"]').exists()) .get(i); const dataIndexAttr = await card.attr('data-index'); const index = Number(dataIndexAttr?.replace(/^index-/, '')); return Number.isInteger(index) ? index : -1; }; const getLastRenderedItemIndex = async () => { const cards = base .$$('[data-hook="virtual-grid-item"]') .filter((e) => e.$('[data-hook="grid-card-item"]').exists()); const lastCard = cards.get((await cards.count()) - 1); const dataIndexAttr = await lastCard.attr('data-index'); const index = Number(dataIndexAttr?.replace(/^index-/, '')); return Number.isInteger(index) ? index : null; }; const scrollDownToItem = ( index: number, { timeout = 15000, delay = 1000, scrollableElementDataHook = 'page-scrollable-content', scrollIntoView, }: { timeout?: number; delay?: number; scrollableElementDataHook?: string; scrollIntoView?: boolean; } = {}, ) => { return waitFor( async () => { if (await getCardGalleryItemByIndexBase(index).exists()) { if (scrollIntoView) { await getCardGalleryItemByIndexBase(index).scrollIntoView?.(); } return true; } await scrollDown(scrollableElementDataHook); return getCardGalleryItemByIndexBase(index).exists(); }, timeout, delay, ); }; const scrollDownToAddItem = ({ timeout = 5000, delay = 1000, scrollableElementDataHook = 'page-scrollable-content', }: { timeout?: number; delay?: number; scrollableElementDataHook?: string; } = {}) => { return waitFor( async () => { if (await getAddItem().exists()) { return true; } await scrollDown(scrollableElementDataHook); return getAddItem().exists(); }, timeout, delay, ); }; const _getInnerElement = () => base.$('[data-hook="virtual-inner-element"]'); const getLiveRegion = () => LiveRegionUniDriver(base.$('[data-hook="collection-live-region"]'), body); const getSummaryBar = () => SummaryBarUniDriver(base, body); return { ...baseUniDriverFactory(base), /** * Wait until root element to exist * @param timeout - time to wait until bailing */ wait: (timeout?: number) => base.wait(timeout), /** Wait for the initial fetch loader to be shown */ waitInitialLoader: (timeout?: number) => initialLoaderBase().wait(timeout), /** Wait for the initial fetch loader to be removed */ waitInitialLoaderRemoved: (timeout?: number) => waitFor(async () => !(await initialLoaderBase().exists()), timeout), /** Indicates whether initial loader is in loading status */ isInitialLoaderLoading: () => initialLoader().exists(), /** Indicates whether initial loader exists */ initialLoaderExists: () => initialLoader().exists(), /** Wait for the infinite scroll loader to be shown */ waitInfiniteScrollLoader: (timeout?: number) => infiniteScrollLoaderBase().wait(timeout), infiniteScrollSkeletonCount: () => infiniteScrollLoader().skeletonItemsCount(), /** Indicates whether infinite scroll loader exists */ infiniteScrollLoaderExists: () => infiniteScrollLoader().exists(), /** Indicates whether table refresh loader exists */ refreshLoaderExists: () => refreshLoader().exists(), refreshLoaderSkeletonCount: () => refreshLoader().skeletonItemsCount(), getErrorStateByDataHook, /** Get initial error state before table renders */ getCardErrorStateByDataHook: ( dataHook = 'placeholder-states-error-state', ) => getErrorStateByDataHook(dataHook), /** Get collection empty state within the table that by a specific data hook */ getEmptyStateByDataHook, /** Get premium empty state within the table that by a specific data hook */ getPremiumEmptyStateByDataHook, /** Get collection no results state within the table that by a specific data hook */ getNoResultsStateByDataHook, /** Get the number of actually rendered grid cards */ getRenderedItemsCount: () => base.$$('[data-hook="grid-card-item"]').count(), gridContainerExists: () => _getInnerElement().exists(), getVirtualizedRowsCount: async () => { const str = await _getInnerElement().attr('data-count'); return !isNaN(Number(str)) ? Number(str) : 0; }, initialLoaderSkeletonCount: () => infiniteScrollLoader().skeletonItemsCount(), /** Get the number rendered grid cards available on the client */ getLastRenderedItemIndex, getLastRenderedItem: async () => { const i = await getLastRenderedItemIndex(); return i != null ? getCardGalleryItemByIndex(i) : null; }, /** Get the CardGalleryItem by index */ getCardGalleryItemByIndex, /** Wait CardGalleryItem by index */ waitCardGalleryItemByIndex: async (index: number, timeout?: number) => (await getCardGalleryItemByIndexBase(index)).wait(timeout), /** Get the CardGalleryItem's PopoverMenu by index */ getCardGalleryItemPopoverMenuByIndex, getCardDefaultEmptyState: getEmptyStateByDataHook, getActionModal, /** Get the `` driver for the `renderAddItem` */ getAddItem, getToolbar, viewsToolbarItemLabelExists: () => viewsToolbarItem().$('span').exists(), getViewsToolbarItemLabelText: () => viewsToolbarItem().$('span').text(), /** Get tabs filter driver */ getToolbarTabs: () => getToolbar().getTabs(), /** Enter text into the search input */ enterSearchText: (text: string) => getToolbar().enterSearchText(text), clearSearchText: () => getToolbar().clearSearchText(), getEmptyStateClearFiltersButton: (emptyStateDataHook: string) => TextButtonUniDriver( base .$(`[data-hook="${emptyStateDataHook}"]`) .$('[data-hook="empty-state-clear-filters-button"]'), body, ), /** * Triggers "scroll" event on the scroll element. * @param scrollableElementDataHook - The data hook of the scrollable element (Optional, defaults to "page-scrollable-content"). */ scrollDown, /** * Triggers "scroll" event on the scroll element, until item exists * @param index * @param timeout - how much to to wait for item exists * @param delay - how much to to wait between each check * @param scrollableElementDataHook */ scrollDownToItem, scrollDownToAddItem, scrollToTop: async ( scrollableElementDataHook = 'page-scrollable-content', ) => { const pageScrollContent = body.$( `[data-hook="${scrollableElementDataHook}"]`, ); if (await pageScrollContent.exists()) { await ( await pageScrollContent.getNative() ).dispatchEvent( Object.assign(new Event('scroll'), { _testScroll: 'top' }), ); } }, /** @private */ _getInnerElement, getDragHandle: (index: number) => { return getCardGalleryItemByIndexBase(index).$( '[data-hook="drag-handle-button"]', ); }, _getDragAndDropDriver: getDragAndDrop, escapeDrag: async (sourceIndex: number) => getDragAndDrop().escapeDrag( (await getCardGalleryItemByIndexBase(sourceIndex)).$( `[data-hook="drag-handle-button"]`, ), ), dragStart: async ( sourceIndex: number, options: { page?: SledPage['page']; x?: number } = {}, ) => getDragAndDrop().dragStart( await getCardDragElementByIndexBase(sourceIndex), options, ), dragStartFromHandleAndKeyboard: async (sourceIndex: number) => getDragAndDrop().startKeyboardDrag( (await getCardGalleryItemByIndexBase(sourceIndex)).$( `[data-hook="drag-handle-button"]`, ), ), dragStartFromHandle: async (sourceIndex: number) => getDragAndDrop().dragStart( (await getCardGalleryItemByIndexBase(sourceIndex)).$( `[data-hook="drag-handle-button"]`, ), ), dragMove: () => getDragAndDrop().dragMove(), dragEnd: async (targetIndex: number) => getDragAndDrop().dragEnd( await getCardDragElementByIndexBase(targetIndex), ), /** * Drags an element over another element. * @param {number} sourceIndex * @param {number} targetIndex * @param [options] * @param {number} [options.steps] number of movement steps (only for puppeteer) */ dragOver: async ( sourceIndex: number, targetIndex: number, options: { steps?: number } = {}, ) => { return getDragAndDrop().dragOver( await getCardDragElementByIndexBase(sourceIndex), await getCardDragElementByIndexBase(targetIndex), options, ); }, getNavigationModal, getLiveRegionText: async () => (await getLiveRegion()).getMessage(), getLiveRegionRole: async () => (await getLiveRegion()).getRole(), getDragAndDropLiveRegionText: () => body.$('[data-hook="grid-dnd-live-region"]').text(), _getCardDragElementByIndexBase: getCardDragElementByIndexBase, _getCardGalleryItemByIndexBase: getCardGalleryItemByIndexBase, waitSidePanelFullyOpened: () => { return waitFor( async () => (await slidingModalBase().attr('data-transition-status')) === 'entered', ); }, /** Get an export button driver **/ getExportButton, /** Get an export modal driver **/ getExportModal, /** Is 'ExportTo' modal open */ isExportModalOpen: () => getExportButton().getModal().isOpen(), getCardGalleryItemFooterImageByIndexBase, getRenderedItemIndexAt, getDragOverlayCardGalleryItem: () => CardGalleryItemUniDriver( body.$('[data-hook="draggable-card-overlay"]'), body, ), internalScrollDownToItem: ( index: number, options: { timeout?: number; delay?: number; } = {}, ) => scrollDownToItem(index, { ...options, scrollableElementDataHook: 'toolbar-collection-content-auto-sizer', }), /** * Gets collection summary bar */ getSummaryBar, getSectionHeaderAt: (index: number) => CollectionSectionHeaderUniDriver( base.$$('[data-hook="grid-section-header"]').get(index), body, ), getSectionHeadersCount: () => base.$$('[data-hook="grid-section-header"]').count(), getSectionGridAt: (index: number) => GridUniDriver( base.$$('[data-hook="grid-section-and-repeater"]').get(index), body, ), }; } export default GridUniDriver;