cypress-ag-grid
Version:
Cypress plugin to interact with ag grid
638 lines (591 loc) • 22.1 kB
JavaScript
/// <reference types="cypress" />
import { filterOperator } from "./filterOperator.enum";
import { filterTab } from "./menuTab.enum";
import { sort } from "./sort.enum";
function isRowNotDestroyed(rowElement) {
const rect = rowElement.getBoundingClientRect();
const viewPortRect = rowElement.parentElement.getBoundingClientRect();
return (
rect.top >= viewPortRect.top &&
rect.left >= viewPortRect.left &&
rect.bottom <= viewPortRect.bottom &&
rect.right <= viewPortRect.right
);
}
export const agGridWaitForAnimation = async (agGridElement) => {
if (agGridElement.get().length < 1) {
throw new Error(`Couldn't find the element ${agGridElement}`);
}
const animations = agGridElement.get()[0].getAnimations({ subtree: true });
await Promise.all(
animations.map(async (animation) => {
try {
await animation.finished;
} catch (error) {
if (error.name === "AbortError") return;
console.error("error", error, error.name);
throw error;
}
})
);
return agGridElement;
};
/**
* Uses the attribute value's index and sorts the data accordingly.
* For our purposes, we are getting the attribute with the items' indices and sorting accordingly.
*
* @param {*} index
* @returns
*/
function sortElementsByAttributeValue(attribute) {
return (a, b) => {
const contentA = parseInt(a.attributes[attribute].nodeValue, 10).valueOf();
const contentB = parseInt(b.attributes[attribute].nodeValue, 10).valueOf();
return contentA < contentB ? -1 : contentA > contentB ? 1 : 0;
};
}
/**
* Retrieves the values from the *displayed* page in ag grid and assigns each value to its respective column name.
* @param agGridElement The get() selector for which ag grid table you wish to retrieve.
* @param options Provide an array of columns you wish to exclude from the table retrieval.
*/
export const getAgGridData = async (agGridElement, options = {}) => {
await agGridWaitForAnimation(agGridElement);
return _getAgGrid(agGridElement, options, false);
};
/**
* Retrieves the values from the *displayed* page in ag grid and assigns each value to its respective column name.
* @param agGridElement The get() selector for which ag grid table you wish to retrieve.
* @param options Provide an array of columns you wish to exclude from the table retrieval.
*/
export const getAgGridElements = async (agGridElement, options = {}) => {
await agGridWaitForAnimation(agGridElement);
return _getAgGrid(agGridElement, options, true);
};
function _getAgGrid(agGridElement, options = {}, returnElements) {
const agGridColumnSelectors =
".ag-pinned-left-cols-container^.ag-center-cols-clipper^.ag-center-cols-viewport^.ag-pinned-right-cols-container";
if (agGridElement.get().length > 1)
throw new Error(
`Selector "${agGridElement.selector}" returned more than 1 element.`
);
const tableElement = agGridElement.get()[0].querySelectorAll(".ag-root")[0];
const agGridSelectors = agGridColumnSelectors.split("^");
const headers = [
...tableElement.querySelectorAll(".ag-header-row-column [aria-colindex]"),
]
.sort(sortElementsByAttributeValue("aria-colindex"))
.map((headerElement) => {
// Check if the elements returned are already .ag-header-cell-text elements
// If not, query for that element and return the text content
let headerCells = [
...headerElement.querySelectorAll(".ag-header-cell-text"),
];
if (headerCells.length === 0) {
return [headerElement].map((e) => e.textContent.trim());
} else {
return [...headerElement.querySelectorAll(".ag-header-cell-text")].map(
(e) => e.textContent.trim()
);
}
})
.flat();
let allRows = [];
let rows = [];
agGridSelectors.forEach((selector) => {
const _rows = [
...tableElement.querySelectorAll(
`${selector}:not(.ag-hidden) .ag-row:not(.ag-opacity-zero)`
),
]
// When animation is enabled, ag-grid destroys rows in 2 phases,
// first it runs an animation to place rows to be destroyed just outside
// the viewport.
// In the second phase those rows are removed from the DOM.
// Because we get here AFTER all animations are finished, it is possible,
// those rows are still in the DOM, but are not visible.
// therefore those rows should be filtered out.
.filter(isRowNotDestroyed)
// Sort rows by their row-index attribute value
.sort(sortElementsByAttributeValue("row-index"))
.map((row) => {
// Sort row cells by their aria-colindex attribute value
// First check if elements returned already contain the aria-colindex
// If not, just query for the .ag-cell
let rowCells = [...row.querySelectorAll(".ag-cell[aria-colindex]")];
if (rowCells.length === 0) {
rowCells = [...row.querySelectorAll(".ag-cell")];
}
const rowIndex = parseInt(
row.attributes["row-index"].nodeValue,
10
).valueOf();
if (allRows[rowIndex]) {
allRows[rowIndex] = [...allRows[rowIndex], ...rowCells];
} else {
allRows[rowIndex] = rowCells;
}
});
});
// Remove any empty arrays before merging
allRows = allRows.filter(function (ele) {
return ele.length;
});
// Remove duplicate entries from allRows
// In some instances we see cell duplication for non-unique rows
allRows = allRows.map((row) => {
return row.filter((cell, index) => {
return row.indexOf(cell) === index;
});
});
if (!allRows.length) rows = [];
else {
rows = allRows
.filter((rowCells) => rowCells.length)
.map((rowCells) =>
rowCells
.sort(sortElementsByAttributeValue("aria-colindex"))
.map((e) => {
if (returnElements) {
return e;
} else {
return e.textContent.trim();
}
})
);
}
// if options.rawValues = true, return headers & rows values as arrays instead of mapping as objects
if (options.valuesArray) {
return { headers, rows };
}
// return structured object from headers and rows variables
return rows.map((row) =>
row.reduce((acc, curr, idx) => {
if (
//@ts-ignore
(options.onlyColumns && !options.onlyColumns.includes(headers[idx])) ||
headers[idx] === undefined
) {
// dont include columns that are not present in onlyColumns, or if the header is undefined
return { ...acc };
}
return { ...acc, [headers[idx]]: curr };
}, {})
);
}
/**
* Retrieve the ag grid column header element based on its column name value
* @param columnName The name of the column's header to retrieve.
*/
function getColumnHeaderElement(agGridElement, columnName) {
return cy
.get(agGridElement)
.find(".ag-header-cell-text")
.contains(new RegExp("^" + columnName + "$", "g"));
}
/**
* * Performs sorting operation on the specified column
* @param {*} agGridElement The get() selector for which ag grid table you wish to retrieve.
* @param columnName The name of the column you wish to sort
* @param sortDirection sort enum value
* @returns
*/
export function sortColumnBy(agGridElement, columnName, sortDirection) {
if (sortDirection.toLowerCase() === "ascending") {
sortDirection = "asc";
} else if (sortDirection.toLowerCase() === "descending") {
sortDirection = "desc";
}
if (sortDirection === sort.ascending || sortDirection === sort.descending) {
return getColumnHeaderElement(agGridElement, columnName)
.parents(".ag-header-cell .ag-cell-label-container")
.invoke("attr", "class")
.then((value) => {
cy.log(`sort: ${sortDirection}`);
if (!value.includes(`ag-header-cell-sorted-${sortDirection}`)) {
getColumnHeaderElement(agGridElement, columnName).click();
sortColumnBy(agGridElement, columnName, sortDirection);
}
});
} else {
throw new Error("sortDirection must be either 'asc' or 'desc'.");
}
}
function getMenuTabElement(agGridElement, tab) {
return cy
.get(agGridElement)
.find(".ag-tab")
.find(`.ag-icon-${tab}`)
.filter(":visible");
}
/**
* Will select the specified filter tab if it is not already selected
* @param tab
*/
function selectMenuTab(agGridElement, tab) {
cy.get(agGridElement).then((agGr) => {
if (agGr.find('.ag-menu-list').length > 0) {
cy.log('Menu uses a list, not tabs');
} else {
getMenuTabElement(agGridElement, tab).then(($ele) => {
cy.wrap($ele)
.parent("span")
.invoke("attr", "class")
.then(($attr) => {
if (!$attr.includes("selected")) {
cy.wrap($ele).click();
}
});
});
}
})
}
/**
* Returns the filter button element for a specified column
* @param columnName
*/
function getFilterColumnButtonElement(
agGridElement,
columnName,
isFloatingFilter = false
) {
let columnIndex;
if (isFloatingFilter)
return getColumnHeaderElement(agGridElement, columnName)
.parents(".ag-header-cell")
.then(($ele) => {
cy.wrap($ele)
.invoke("attr", "aria-colindex")
.then((colIndex) => {
columnIndex = colIndex;
})
.then(() => {
cy.wrap($ele)
.parents(".ag-header-row-column")
.siblings(".ag-header-row-column-filter")
.find(`.ag-header-cell[aria-colindex=${columnIndex}]`)
.find(".ag-floating-filter-button");
});
});
else
return getColumnHeaderElement(agGridElement, columnName)
.parent()
.siblings(".ag-header-cell-filter-button");
}
/**
*
* @param filterValue value to input into the filter textbox
* @param operator (optional) use if using a search operator (i.e. Less Than, Equals, etc...use filterOperator.enum values)
* @param noMenuTabs (optional) boolean indicating if the menu has tabs.
*/
function filterBySearchTerm(agGridElement, options) {
const filterValue = options.searchCriteria.filterValue;
const operator = options.searchCriteria.operator;
const searchInputIndex = options.searchCriteria.searchInputIndex || 0;
const isMultiFilter = options.searchCriteria.isMultiFilter;
const noMenuTabs = options.noMenuTabs;
// Navigate to the filter tab
// if (!noMenuTabs) {
// selectMenuTab(agGridElement, filterTab.filter);
// }
if (operator) {
const elem = cy
.get(agGridElement)
.find(".ag-filter")
.find(".ag-picker-field-wrapper")
.filter(":visible")
.eq(searchInputIndex);
cy.get(agGridElement).agGridWaitForAnimation();
elem.click();
cy.get(agGridElement)
.find(".ag-popup .ag-list")
.find("span")
.contains(operator)
.click();
}
// Input filter term and allow grid a moment to render the results
if (
operator !== filterOperator.blank &&
operator !== filterOperator.notBlank
) {
cy.get(agGridElement)
.find(".ag-popup-child")
.find("input")
.filter(":visible")
.as("filterInput");
}
// If it's a multi filter, de-select the 'select-all' checkbox
if (isMultiFilter) {
const selectAllText = options.selectAllLocaleText || "(Select All)";
toggleColumnCheckboxFilter(agGridElement, selectAllText, false, true);
}
// Get the saved filter input and enter the search term
if (
operator !== filterOperator.blank &&
operator !== filterOperator.notBlank
) {
cy.get("@filterInput").then(($ele) => {
cy.wrap($ele).eq(searchInputIndex).clear().type(filterValue + '{enter}');
});
}
// Finally, if a multi-filter, select the filter value's checkbox
if (isMultiFilter) {
toggleColumnCheckboxFilter(agGridElement, filterValue, true, true);
}
}
function applyColumnFilter(agGridElement, hasApplyButton, noMenuTabs) {
if (hasApplyButton) {
cy.get(agGridElement)
.find(".ag-filter-apply-panel-button")
.contains("Apply")
.click();
}
if (!noMenuTabs) {
cy.get(agGridElement).then((agGr) => {
if (agGr.find('.ag-tab').length === 0) {
cy.log('Menu uses a list, not tabs');
cy.get(agGridElement).agGridWaitForAnimation();
} else {
getMenuTabElement(agGridElement, filterTab.filter).click();
}
})
}
}
/**
* Either toggle
* @param filterValue
* @param doSelect
* @param hasTabs
*/
function toggleColumnCheckboxFilter(
agGridElement,
filterValue,
doSelect,
noMenuTabs = false
) {
// if (!noMenuTabs) {
// selectMenuTab(agGridElement, filterTab.filter);
// }
cy.get(agGridElement)
.find(".ag-input-field-label")
.contains(filterValue)
.siblings("div")
.find("input")
.then(($ele) => {
if (doSelect) cy.wrap($ele).check();
else cy.wrap($ele).uncheck();
});
}
function populateSearchCriteria(
searchCriteria,
hasApplyButton = false,
noMenuTabs = false,
selectAllLocaleText = "(Select All)"
) {
const options = {};
options.searchCriteria = { ...searchCriteria };
options.selectAllLocaleText = selectAllLocaleText;
options.hasApplyButton = hasApplyButton;
options.noMenuTabs = noMenuTabs;
return options;
}
/**
* Will add or remove a column from ag grid.
* @param columnName The column name to add/remove
* @param pin 'left', 'right' or null
*/
export function pinColumn(agGridElement, columnName, pin) {
getColumnHeaderElement(agGridElement, columnName)
.parent()
.siblings(".ag-header-cell-menu-button")
.click();
selectMenuTab(agGridElement, filterTab.general);
cy.get(agGridElement).find(".ag-menu-option").contains("Pin Column").click();
var selectedOption;
switch (pin) {
case "left":
selectedOption = "Pin Left";
break;
case "right":
selectedOption = "Pin Right";
break;
default:
selectedOption = "No Pin";
break;
}
cy.get(agGridElement)
.find(".ag-menu-option")
.contains(selectedOption)
.click();
}
/**
* * Performs a filter operation on the specified column via the context menu using plain text search
* @param agGridElement The get() selector for which ag grid table you wish to retrieve.
* @param {{searchCriteria:[{columnName:string,filterValue:string,operator?:string}], hasApplyButton?:boolean}} options JSON with search properties
* @param options.searchCriteria JSON with search properties
* @param options.searchCriteria.columnName [REQUIRED] name of the column to filter
* @param options.searchCriteria.filterValue [REQUIRED] value to input into the filter textbox
* @param options.searchCriteria.operator [Optional] Use if using a search operator (i.e. Less Than, Equals, etc...use filterOperator.enum values).
* @param options.hasApplyButton [Optional] True if "Apply" button is used, false if filters by text input automatically.
* @param options.noMenuTabs [Optional] True if you use for example the community edition of ag-grid, which has no menu tabs
*/
export function filterBySearchTextColumnMenu(agGridElement, options) {
// Check if there are multiple search criteria provided by attempting to access the columnName
if (!options.searchCriteria.columnName) {
options.searchCriteria.forEach((_searchCriteria) => {
const _options = populateSearchCriteria(
_searchCriteria,
options.hasApplyButton,
options.noMenuTabs,
options.isMultiFilter
);
_filterBySearchTextColumnMenu(agGridElement, _options);
});
} else {
_filterBySearchTextColumnMenu(agGridElement, options);
}
}
function _filterBySearchTextColumnMenu(agGridElement, options) {
// Get the header's menu element
getFilterColumnButtonElement(
agGridElement,
options.searchCriteria.columnName
).click();
filterBySearchTerm(agGridElement, options);
applyColumnFilter(agGridElement, options.hasApplyButton, options.noMenuTabs);
}
/**
* * Performs a filter operation on the specified column via the column's floating filter field using plain text search
* @param agGridElement The get() selector for which ag grid table you wish to retrieve.
* @param {{searchCriteria:[{columnName:string,filterValue:string,operator?:string}], hasApplyButton?:boolean, noMenuTab?:boolean, selectAllLocaleText:string}} options JSON with search properties
* @param options.searchCriteria JSON with search properties and options
* @param options.searchCriteria.columnName name of the column to filter
* @param options.searchCriteria.filterValue value to input into the filter textbox
* @param options.searchCriteria.searchInputIndex [Optional] Uses 0 by default. Index of which filter box to use in event of having multiple search conditionals
* @param options.searchCriteria.operator [Optional] Use if using a search operator (i.e. Less Than, Equals, etc...use filterOperator.enum values).
* @param options.hasApplyButton [Optional] True if "Apply" button is used, false if filters by text input automatically.
* @param options.noMenuTabs [Optional] True if you use, for example, the community edition of ag-grid, which has no menu tabs
* @param options.selectAllLocaleText [Optional] Pass in the locale text value of "(Select All)" for when you are filtering by checkbox - this wil first deselect the "(Select All)" option before selecting your filter value
*/
export function filterBySearchTextColumnFloatingFilter(agGridElement, options) {
// Check if there are multiple search criteria provided by attempting to access the columnName
if (!options.searchCriteria.columnName) {
options.searchCriteria.forEach((_searchCriteria) => {
const _options = populateSearchCriteria(
_searchCriteria,
options.hasApplyButton,
options.noMenuTabs
);
_filterBySearchTextColumnFloatingFilter(agGridElement, _options);
});
} else {
_filterBySearchTextColumnFloatingFilter(agGridElement, options);
}
}
function _filterBySearchTextColumnFloatingFilter(agGridElement, options) {
cy.get(agGridElement).then((agGridElement) => {
getFilterColumnButtonElement(
agGridElement,
options.searchCriteria.columnName,
true
).click();
filterBySearchTerm(agGridElement, options);
applyColumnFilter(
agGridElement,
options.hasApplyButton,
options.noMenuTabs
);
});
}
/**
* * Performs a filter operation on the specified column and selects only the provided filterValue
* @param agGridElement The get() selector for which ag grid table you wish to retrieve.
* @param {{searchCriteria:[{columnName:string,filterValue:string], hasApplyButton?:boolean}} options JSON with search values and options
* @param options.searchCriteria [REQUIRED] JSON with search properties
* @param options.searchCriteria.columnName [REQUIRED] name of the column to filter
* @param options.searchCriteria.filterValue [REQUIRED] value to input into the filter textbox
* @param options.hasApplyButton [Optional] True if "Apply" button is used, false if filters by text input automatically.
* @param options.noMenuTabs [Optional] True if you use for example the community edition of ag-grid, which has no menu tabs
*/
export function filterByCheckboxColumnMenu(agGridElement, options) {
// Check if there are multiple search criteria provided by attempting to access the columnName
if (!options.searchCriteria.columnName) {
options.searchCriteria.forEach((_searchCriteria) => {
const _options = populateSearchCriteria(
_searchCriteria,
options.hasApplyButton,
options.noMenuTabs,
options.selectAllLocaleText
);
_filterByCheckboxColumnMenu(agGridElement, _options);
});
} else {
_filterByCheckboxColumnMenu(agGridElement, options);
}
}
function _filterByCheckboxColumnMenu(agGridElement, options) {
cy.get(agGridElement).then((agGridElement) => {
getFilterColumnButtonElement(
agGridElement,
options.searchCriteria.columnName
).click();
const selectAllText = options.selectAllLocaleText || "(Select All)";
toggleColumnCheckboxFilter(
agGridElement,
selectAllText,
false,
options.noMenuTabs
);
toggleColumnCheckboxFilter(
agGridElement,
options.searchCriteria.filterValue,
true,
options.noMenuTabs
);
applyColumnFilter(
agGridElement,
options.hasApplyButton,
options.noMenuTabs
);
});
}
/**
* Will perform a filter for all search criteria provided, then selects all found entries in the grid
* @param searchCriteria a "\^" delimited string of all columns and searchCriteria to search for in the grid (i.e. "Name=John Smith^Rate Plan=Standard"
*/
export function filterGridEntriesBySearchText(
agGridElement,
searchCriteria,
isFloatingFilter = false
) {
if (isFloatingFilter) {
filterBySearchTextColumnFloatingFilter(agGridElement, searchCriteria);
} else {
filterBySearchTextColumnMenu(agGridElement, searchCriteria);
}
}
/**
* Will add or remove a column from ag grid.
* @param columnName The column name to add/remove
* @param doRemove true will remove the column. false will add the column.
*/
export function toggleColumnFromSideBar(agGridElement, columnName, doRemove) {
cy.get(agGridElement)
.find(".ag-column-select-header-filter-wrapper")
.find("input")
.then(($columnFilterInputField) => {
if (!$columnFilterInputField.is(":visible")) {
cy.get(".ag-side-buttons").find("span").contains("Columns").click();
}
cy.get(agGridElement).agGridWaitForAnimation();
cy.wrap($columnFilterInputField).clear().type(columnName);
cy.get(".ag-column-select-column-label")
.contains(columnName)
.parent()
.find("input")
.then(($columnCheckbox) => {
if (doRemove) cy.wrap($columnCheckbox).uncheck();
else cy.wrap($columnCheckbox).check();
});
});
}