diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 0000000..ecac341 --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,5 @@ +# Changesets + +This directory stores release metadata for the monorepo. + +Run `npm run changeset` after changing one or more publishable packages, then commit the generated markdown file. diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 0000000..2eada15 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.0.4/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [], + "linked": [], + "access": "public", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": [] +} diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 2a7f607..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,14 +0,0 @@ -# Use the latest 2.1 version of CircleCI pipeline process engine. -# See: https://circleci.com/docs/2.0/configuration-reference -version: 2.1 - -orbs: - # "cypress-io/cypress@1" installs the latest published - # version "1.x.y" of the orb. We recommend you then use - # the strict explicit version "cypress-io/cypress@1.x.y" - # to lock the version and prevent unexpected CI changes - cypress: cypress-io/cypress@3 -workflows: - run_cypress_tests: - jobs: - - cypress/run # "run" job comes from "cypress" orb \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2d49c9a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,74 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + +jobs: + core-tests: + name: Core Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Run core tests + run: npm run test:core + + cypress-tests: + name: Cypress Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Run Cypress suite + uses: cypress-io/github-action@v6 + with: + install: false + command: npm run test:cypress:all + + playwright-tests: + name: Playwright Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Install Playwright browsers + run: npx playwright install --with-deps chromium + + - name: Run Playwright suite + run: npm run test:playwright diff --git a/.gitignore b/.gitignore index a522e6f..9225d50 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ -node_modules -.idea -cypress/screenshots -cypress/videos -.DS_Store \ No newline at end of file +node_modules +.idea +cypress/screenshots +cypress/videos +.DS_Store +packages/playwright-ag-grid/test-results +packages/playwright-ag-grid/playwright-report diff --git a/README.md b/README.md index 3ff0512..9ee49b0 100644 --- a/README.md +++ b/README.md @@ -1,414 +1,37 @@ -# cypress-ag-grid -Cypress plugin for interacting with and validating against ag grid. +# ag-grid-testing -## Table of Contents - * [Installation](#installation) - * [Usage](#usage) - + [Grid Interaction](#) - - [Getting Data From the Grid](#getting-data-from-the-grid) - - [Getting Select Row Data](#getting-select-row-data) - - [Getting Elements From the Grid](#getting-elements-from-the-grid) - - [Sorting Columns](#sorting-columns) - - [Pinning Columns](#pinning-columns) - + [Grid Filtering](#) - - [Filter Options](#filter-options) - - [Filter by Text - Column Menu](#filter-by-text---column-menu) - - [Filterby Text - Floating Filter](#filterby-text---floating-filter) - - [Filter by Checkbox - Column Menu](#filter-by-checkbox---column-menu) - - [Filtering - Localization and Internationalization](#filtering---localization-and-internationalization) - - [Add or Remove Columns](#add-or-remove-columns) - + [Grid Validation](#) - - [Validate Paginated Table](#validate-paginated-table) - - [Validate Table in the Exact Order](#validate-table-in-the-exact-order) - - [Validate Subset of Table Data](#validate-subset-of-table-data) - - [Validate Empty Grid](#validate-empty-grid) - * [Limitations](#limitations) - * [Credit](#credit) -
-
+Monorepo for Node-based AG Grid test helpers. -## Installation +Current packages: -```bash -npm install cypress-ag-grid --save-dev -``` -Then include the following in your `support/index.js` file (Cypress v9 and below) or `support/e2e.(js|ts)` file (Cypress 10 and above): - -```javascript -import "cypress-ag-grid"; -``` -## Usage -Consider the ag grid example below: -![alt text](./ag-grid-example.png "AG Grid") - -With the following DOM structure: -![alt text](./ag-grid-example-dom.png "AG Grid Dom") -
-
-### Getting Data From the Grid: -To get the Ag Grid data, you must chain `.getAgGridData()` after the `cy.get()` command for the topmost level of the grid, including controls and headers (see selected DOM element in above image). - -Correct Usage: -```javascript -cy.get("#myGrid").getAgGridData() -``` - -Incorrect Usage: -```javascript -cy.getAgGridData(); -``` - -The correct command will return the following: -```json -[ - { "Year": "2020", "Make": "Toyota", "Model": "Celica" }, - { "Year": "2020", "Make": "Ford", "Model": "Mondeo" }, - { "Year": "2020", "Make": "Porsche", "Model": "Boxter" }, - { "Year": "2020", "Make": "BMW", "Model": "3-series" }, - { "Year": "2020", "Make": "Mercedes", "Model": "GLC300" }, -] -``` -
-
- -### Getting Select Row Data -To only get certain rows of data, pass the header values into the `getAgGridData()` command, like so: - -```javascript -cy.get("#myGrid").getAgGridData({ onlyColumns: ["Year", "Make"] }) -``` - -The above command will return the follwoing: -```json -[ - { "Year": "2020", "Make": "Toyota"}, - { "Year": "2020", "Make": "Ford"}, - { "Year": "2020", "Make": "Porsche"}, - { "Year": "2020", "Make": "BMW"}, - { "Year": "2020", "Make": "Mercedes"}, -] -``` -
-
- -### Getting Elements From the Grid -To get the Ag Grid data as elements (if you want to interact with the cells themselves), you must chain `.getAgGridElements()` after the `cy.get()` command for the topmost level of the grid, including controls and headers (see selected DOM element in above image). -```javascript - cy.get(agGridSelector) - .getAgGridElements() - .then((tableElements) => { - const porscheRow = tableElements.find( - (row) => row.Price.innerText === "72000" - ); - const priceCell = porscheRow.Price; - cy.wrap(priceCell).dblclick().type("66000{enter}"); - }); - -``` - -The above example will grab the table as elements, finds the row whose `Price` equals `72000`. It then gets the `Price` cell for that row, double clicks on it to enable an editable input, and changes the value of the cell. -
-
- -### Sorting Columns -This command will sort the specified column by the sort direction specified. - -Defintion: -`.agGridSortColumn(columnName:String, sortDirection:String)` - -Example: - -```javascript -cy.get("#myGrid").agGridSortColumn("Model", "descending"); -``` -
-
- -### Pinning Columns -This command will pin the specified column. -Definition -`.agGridPinColumn(columnName: string, pin: ['left', 'right', null])` - -Example: -```javascript -cy.get("#myGrid").agGridPinColumn("Model", "left") // Pins the "Model" column to the left -cy.get("#myGrid").agGridPinColumn("Model", "right") // Pins the "Model" column to the right -cy.get("#mGrid").agGridPinColumn("Model") // Removes the pin - -``` -
-
- -### Filter Options - -The below filtering commands takes an `options` parameter comprised of the following properties: - -```javascript -options: { - searchCriteria: [{ - columnName: string; - filterValue: string; - operator?: string; - isMultiFilter?: boolean; - }]; - hasApplyButton?: boolean; - noMenuTabs?: boolean; - selectAllLocaleText: string; -} - -/** -- options.searchCriteria JSON with search properties and options -- options.searchCriteria.columnName name of the column to filter -- options.searchCriteria.filterValue value to input into the filter textbox -- options.searchCriteria.searchInputIndex [Optional] Uses 0 by default. Index of which filter box to use in event of having multiple search conditionals -- options.searchCriteria.operator [Optional] Use if using a search operator (i.e. Less Than, Equals, etc...use filterOperator.enum values). -- options.searchCriteria.isMultiFilter [Optional] Used when floating filter menu has checkbox options vs freeform text input. -- options.hasApplyButton [Optional] True if "Apply" button is used, false if filters by text input automatically. -- options.noMenuTabs [Optional] True if you use, for example, the community edition of ag-grid, which has no menu tabs -- options.selectAllLocaleText [Optional] Pass in the locale text value of "Select All" for when you are filtering by checkbox - this will first deselect the "Select All" option before selecting your filter value -*/ -``` - -### Filter by Text - Column Menu -This command will filter a column by a text value from its menu. In the options, you must specify a `searchCriteria` objects containing one or more objects with `columnName`, `filterValue`, and optionally `operator` (i.e. Contains, Not contains, Equals, etc.). - -![alt text](./ag-grid-example-filter-text-menu.png "AG Grid Dom - Filter by Text Menu") - -Definition: `.agGridColumnFilterTextMenu(options: {})` +- `@kpmck/ag-grid-core`: framework-agnostic DOM and utility logic +- `cypress-ag-grid`: Cypress plugin package, preserving the existing npm package name +- `playwright-ag-grid`: Playwright adapter package -Example: -```javascript -cy.get("#myGrid").agGridColumnFilterTextMenu({ - searchCriteria:[{ - columnName: "Model", - filterValue: "GLC300", - operator:"Equals" - }, - { - columnName: "Make", - filterValue: "Mercedes", - operator:"Equals" - } - ], - hasApplyButton: false -}) -```` -The above command will filter the Model column for the value 'GLC300' and set the filter operator to 'Equals'. It will then apply a secondary filter on the Make column for 'Mercedes'. -
-
-### Filterby Text - Floating Filter -This command will filter a column by a text value from its floating filter (if applicable). This command will filter a column by a text value from its floating menu. In the options, you must specify a `searchCriteria` object with `columnName`, `filterValue`, and optionally `operator` (i.e. Contains, Not contains, Equals, etc.) and `searchInputIndex` in the event you wish to apply multiple text conditions (see below for multi-condition example). +## Workspace Scripts -![alt text](./ag-grid-example-filter-text-floating.png "AG Grid Dom - Filter by Text Floating") +From the repository root: -Definition: .agGridColumnFilterTextFloating(options: {}) - -Example: -``` - cy.get(agGridSelector).agGridColumnFilterTextFloating({ - searchCriteria: { - columnName: "Make", - filterValue: "Ford", - }, - hasApplyButton: true, - }); -``` - -The above example will search for the Make `Ford` from the floating text menu filter. - -If you have the option for multiple conditions on the floating filter, you can do two searches, specifying the `searchInputIndex` parameter in the `searchCriteria` object. The below example will ssarch for any `Make` that contains `B` AND `MW`: - -Example: -``` - cy.get(agGridSelector).agGridColumnFilterTextFloating({ - searchCriteria: { - columnName: "Make", - filterValue: "B", - searchInputIndex: 0, - }, - hasApplyButton: true, - }); - cy.get(agGridSelector).agGridColumnFilterTextFloating({ - searchCriteria: { - columnName: "Make", - filterValue: "MW", - searchInputIndex: 1, - }, - hasApplyButton: true, - }); -``` -![alt text](./ag-grid-example-filter-text-floating-multi-condition.png "AG Grid Dom - Filter by Text Floating") - -For `Between`, pass the lower and upper bounds as two entries for the same column. The command will target the first and second visible inputs for that single `Between` condition: - -```javascript - cy.get(agGridSelector).agGridColumnFilterTextFloating({ - searchCriteria: [ - { - columnName: "Year", - filterValue: "1990", - operator: filterOperator.inRange, - }, - { - columnName: "Year", - filterValue: "2011", - operator: filterOperator.inRange, - }, - ], - hasApplyButton: true, - }); -``` - -
-
- -### Filter by Checkbox - Column Menu -This command will filter a column by a checkbox text value from its menu. -![alt text](./ag-grid-example-filter-checkbox-menu.png "AG Grid Dom - Filter by Checkbox Menu") - -Definition: -```javascript -.agGridColumnFilterCheckboxMenu(options={}) -``` - -Example: -```javascript - cy.get("#myGrid").agGridColumnFilterCheckboxMenu({ - searchCriteria: { - columnName: "Model", - filterValue: "2002", - }, - hasApplyButton: true, - }); - -``` -
- -### Filtering - Localization and Internationalization -When we filter by checkbox, we first deselect the Select All checkbox to ensure we ONLY select the specified checkbox. Since AG grid allows for localization, we need a way to be able to pass in the localeText for Select All. This is the only area of this plugin that has a hard-coded value, so no other localization accommodations are needed. - -``` - cy.get("#myGrid").agGridColumnFilterCheckboxMenu({ - searchCriteria: { - columnName: "Model", - filterValue: "2002", - }, - selectAllLocaleText: "Tout Sélectionner" - hasApplyButton: true, - }); -``` -
- -### Add or Remove Columns -This command will toggle the specified column from the grid's sidebar. - -Definition:`.agGridToggleColumnsSideBar(columnName:String, doRemove:boolean)` - -Example: -```javascript -// This will remove the column "Year" from the grid -cy.get("#myGrid").agGridToggleColumnsSideBar("Year", true); -``` -
-
- -### Validate Paginated Table -This command will validate the paginated grid's data. The supplied expectedPaginatedTableData must be paginated as it's shown in the grid. - -Definition: `agGridValidatePaginatedTable(expectedPaginatedTableData, onlyColumns = {})` - -Example: -```javascript - const expectedPaginatedTableData = [ - [ - { "Year": "2020", "Make": "Toyota", "Model": "Celica" }, - { "Year": "2020", "Make": "Ford", "Model": "Mondeo" }, - { "Year": "2020", "Make": "Porsche", "Model": "Boxter" }, - { "Year": "2020", "Make": "BMW", "Model": "3-series" }, - { "Year": "2020", "Make": "Mercedes", "Model": "GLC300" }, - ], - [ - { "Year": "2020", "Make": "Honda", "Model": "Civic" }, - { "Year": "2020", "Make": "Honda", "Model": "Accord" }, - { "Year": "2020", "Make": "Ford", "Model": "Taurus" }, - { "Year": "2020", "Make": "Hyundai", "Model": "Elantra" }, - { "Year": "2020", "Make": "Toyota", "Model": "Celica" }, - ], - ...other table data - ]; - cy.get("#myGrid").agGridValidatePaginatedTable( - expectedPaginatedTableData, onlyColumns ={"Year", "Make", "Model"} - ); - }); -``` -
-
- -### Validate Table in the Exact Order -This command will verify the table data is displayed exactly in the same order as the supplied expected table data. This will ONLY validate the first page of a paginated table. - -Definition: `.agGridValidateRowsExactOrder(actualTableData, expectedTableData)` - -Example: -```javascript -cy.get("#myGrid") -.getAgGridData() -.then((actualTableData) => { - cy.agGridValidateRowsExactOrder(actualTableData, expectedTableData); -}); +```bash +npm run test +npm run test:v33 +npm run test:v34 +npm run test:v35 +npm run test:watch ``` -
-
-### Validate Subset of Table Data -This command will validate a subset of the table data. Ideal for verifying one or more records, or verify records without specified columns. +These forward to the `cypress-ag-grid` workspace package. -Definition:: `agGridValidateRowsSubset(actualTableData, expectedTableData)` +## Package Layout -Example: -```javascript - const expectedTableData = [ - { "Year": "2020", "Make": "Toyota", "Model": "Celica" }, - { "Year": "2020", "Make": "Ford", "Model": "Mondeo" }, - { "Year": "2020", "Make": "Porsche", "Model": "Boxter" }, - { "Year": "2020", "Make": "BMW", "Model": "3-series" }, - { "Year": "2020", "Make": "Mercedes", "Model": "GLC300" }, - ]; - cy.get(agGridSelector) - .getAgGridData({ onlyColumns: ["Year", "Make", "Model"] }) - .then((actualTableData) => { - cy.agGridValidateRowsSubset(actualTableData, expectedTableData); - }); - }); +```text +packages/ + ag-grid-core/ + cypress-ag-grid/ + playwright-ag-grid/ ``` -
-
- -### Validate Empty Grid -This will verify the table data is empty. -Definition:`agGridValidateEmptyTable(actualTableData, expectedTableData)` - -Example: -```javascript - cy.get(agGridSelector) - .getAgGridData() - .then((actualTableData) => { - cy.agGridValidateEmptyTable(actualTableData); - }); -``` +## Repository Naming -## Limitations -* ~~Unable to validate deeply nested row groups~~ As of v2.x, using `.getAgGridElements()` you should be able to accomplish this. -* ~~Unable to validate deeply nested column groups~~ As of v2.x, using `.getAgGridElements()` you should be able to accomplish this. -* Unable to validate the entirety of an unlimited scrolling grid. -* Unable to validate data that is out of view. The DOM will register the ag grid data as it's scrolled into view. - * To combat this, in your code where the ag grid is called, check if the Cypress window is controlling the app and set the ag grid object to `.sizeColumnsToFit()`. You can see an example of this in the `app/grid.js` file of this repository. Read more [here](https://www.ag-grid.com/javascript-grid/column-sizing/#size-columns-to-fit) - * Example: - ```javascript - if(window.Cypress){ - this.api.sizeColumnsToFit(); - } - ``` -## Credit -A portion of the logic to retrieve table data was expanded upon from the project [Cypress-Get-Table](https://github.com/roggerfe/cypress-get-table) by [Rogger Fernandez](https://github.com/roggerfe). +The local monorepo root now uses the neutral workspace name `ag-grid-testing`. +If you rename the GitHub repository to match, update the repository URLs in the package manifests at the same time. diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..ddeee49 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,49 @@ +# Releasing + +This repository uses npm workspaces plus Changesets for versioning and publishing. + +## What Publishes + +- `cypress-ag-grid` +- `playwright-ag-grid` +- `@kpmck/ag-grid-core` + +Each package publishes to npm using the `name` field in its own `package.json`. + +## Creating A Release Entry + +After changing one or more publishable packages: + +```bash +npm run changeset +``` + +Choose the packages that changed and whether each change is a patch, minor, or major release. Commit the generated markdown file in `.changeset/`. + +## Local Commands + +```bash +npm run version-packages +npm run release +``` + +- `version-packages` applies pending changesets, updates package versions, and updates internal dependency ranges. +- `release` publishes the changed packages to npm. + +## GitHub Actions + +- `.github/workflows/ci.yml` runs the core, Cypress, and Playwright test suites on pull requests and pushes to `main`. + +Automated publishing is currently disabled. Releases are intended to be versioned and published manually from a local machine for now. + +## Manual Release Flow + +1. Run `npm run changeset` and commit the generated changeset file. +2. When you are ready to cut a release, run: + +```bash +npm run version-packages +npm run release +``` + +3. Commit the version updates and tags produced by the release process as needed. diff --git a/package-lock.json b/package-lock.json index 9addce3..ad88ea1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,442 @@ { - "name": "cypress-ag-grid", + "name": "ag-grid-testing", "version": "3.3.5", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "cypress-ag-grid", - "version": "3.3.5", - "license": "MIT", + "name": "ag-grid-testing", + "workspaces": [ + "packages/*" + ], "devDependencies": { - "cypress": "^15.12.0" + "@changesets/cli": "^2.30.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@changesets/apply-release-plan": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-7.1.0.tgz", + "integrity": "sha512-yq8ML3YS7koKQ/9bk1PqO0HMzApIFNwjlwCnwFEXMzNe8NpzeeYYKCmnhWJGkN8g7E51MnWaSbqRcTcdIxUgnQ==", + "dev": true, + "dependencies": { + "@changesets/config": "^3.1.3", + "@changesets/get-version-range-type": "^0.4.0", + "@changesets/git": "^3.0.4", + "@changesets/should-skip-package": "^0.1.2", + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3", + "detect-indent": "^6.0.0", + "fs-extra": "^7.0.1", + "lodash.startcase": "^4.4.0", + "outdent": "^0.5.0", + "prettier": "^2.7.1", + "resolve-from": "^5.0.0", + "semver": "^7.5.3" + } + }, + "node_modules/@changesets/apply-release-plan/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@changesets/apply-release-plan/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@changesets/apply-release-plan/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/@changesets/assemble-release-plan": { + "version": "6.0.9", + "resolved": "https://registry.npmjs.org/@changesets/assemble-release-plan/-/assemble-release-plan-6.0.9.tgz", + "integrity": "sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ==", + "dev": true, + "dependencies": { + "@changesets/errors": "^0.2.0", + "@changesets/get-dependents-graph": "^2.1.3", + "@changesets/should-skip-package": "^0.1.2", + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3", + "semver": "^7.5.3" + } + }, + "node_modules/@changesets/changelog-git": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@changesets/changelog-git/-/changelog-git-0.2.1.tgz", + "integrity": "sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==", + "dev": true, + "dependencies": { + "@changesets/types": "^6.1.0" + } + }, + "node_modules/@changesets/cli": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/@changesets/cli/-/cli-2.30.0.tgz", + "integrity": "sha512-5D3Nk2JPqMI1wK25pEymeWRSlSMdo5QOGlyfrKg0AOufrUcjEE3RQgaCpHoBiM31CSNrtSgdJ0U6zL1rLDDfBA==", + "dev": true, + "dependencies": { + "@changesets/apply-release-plan": "^7.1.0", + "@changesets/assemble-release-plan": "^6.0.9", + "@changesets/changelog-git": "^0.2.1", + "@changesets/config": "^3.1.3", + "@changesets/errors": "^0.2.0", + "@changesets/get-dependents-graph": "^2.1.3", + "@changesets/get-release-plan": "^4.0.15", + "@changesets/git": "^3.0.4", + "@changesets/logger": "^0.1.1", + "@changesets/pre": "^2.0.2", + "@changesets/read": "^0.6.7", + "@changesets/should-skip-package": "^0.1.2", + "@changesets/types": "^6.1.0", + "@changesets/write": "^0.4.0", + "@inquirer/external-editor": "^1.0.2", + "@manypkg/get-packages": "^1.1.3", + "ansi-colors": "^4.1.3", + "enquirer": "^2.4.1", + "fs-extra": "^7.0.1", + "mri": "^1.2.0", + "package-manager-detector": "^0.2.0", + "picocolors": "^1.1.0", + "resolve-from": "^5.0.0", + "semver": "^7.5.3", + "spawndamnit": "^3.0.1", + "term-size": "^2.1.0" + }, + "bin": { + "changeset": "bin.js" + } + }, + "node_modules/@changesets/cli/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@changesets/cli/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@changesets/cli/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/@changesets/config": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@changesets/config/-/config-3.1.3.tgz", + "integrity": "sha512-vnXjcey8YgBn2L1OPWd3ORs0bGC4LoYcK/ubpgvzNVr53JXV5GiTVj7fWdMRsoKUH7hhhMAQnsJUqLr21EncNw==", + "dev": true, + "dependencies": { + "@changesets/errors": "^0.2.0", + "@changesets/get-dependents-graph": "^2.1.3", + "@changesets/logger": "^0.1.1", + "@changesets/should-skip-package": "^0.1.2", + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3", + "fs-extra": "^7.0.1", + "micromatch": "^4.0.8" + } + }, + "node_modules/@changesets/config/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@changesets/config/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@changesets/config/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/@changesets/errors": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@changesets/errors/-/errors-0.2.0.tgz", + "integrity": "sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==", + "dev": true, + "dependencies": { + "extendable-error": "^0.1.5" + } + }, + "node_modules/@changesets/get-dependents-graph": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@changesets/get-dependents-graph/-/get-dependents-graph-2.1.3.tgz", + "integrity": "sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==", + "dev": true, + "dependencies": { + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3", + "picocolors": "^1.1.0", + "semver": "^7.5.3" + } + }, + "node_modules/@changesets/get-release-plan": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@changesets/get-release-plan/-/get-release-plan-4.0.15.tgz", + "integrity": "sha512-Q04ZaRPuEVZtA+auOYgFaVQQSA98dXiVe/yFaZfY7hoSmQICHGvP0TF4u3EDNHWmmCS4ekA/XSpKlSM2PyTS2g==", + "dev": true, + "dependencies": { + "@changesets/assemble-release-plan": "^6.0.9", + "@changesets/config": "^3.1.3", + "@changesets/pre": "^2.0.2", + "@changesets/read": "^0.6.7", + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3" + } + }, + "node_modules/@changesets/get-version-range-type": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@changesets/get-version-range-type/-/get-version-range-type-0.4.0.tgz", + "integrity": "sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==", + "dev": true + }, + "node_modules/@changesets/git": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@changesets/git/-/git-3.0.4.tgz", + "integrity": "sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw==", + "dev": true, + "dependencies": { + "@changesets/errors": "^0.2.0", + "@manypkg/get-packages": "^1.1.3", + "is-subdir": "^1.1.1", + "micromatch": "^4.0.8", + "spawndamnit": "^3.0.1" + } + }, + "node_modules/@changesets/logger": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@changesets/logger/-/logger-0.1.1.tgz", + "integrity": "sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==", + "dev": true, + "dependencies": { + "picocolors": "^1.1.0" + } + }, + "node_modules/@changesets/parse": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@changesets/parse/-/parse-0.4.3.tgz", + "integrity": "sha512-ZDmNc53+dXdWEv7fqIUSgRQOLYoUom5Z40gmLgmATmYR9NbL6FJJHwakcCpzaeCy+1D0m0n7mT4jj2B/MQPl7A==", + "dev": true, + "dependencies": { + "@changesets/types": "^6.1.0", + "js-yaml": "^4.1.1" + } + }, + "node_modules/@changesets/pre": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@changesets/pre/-/pre-2.0.2.tgz", + "integrity": "sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==", + "dev": true, + "dependencies": { + "@changesets/errors": "^0.2.0", + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3", + "fs-extra": "^7.0.1" + } + }, + "node_modules/@changesets/pre/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@changesets/pre/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@changesets/pre/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/@changesets/read": { + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/@changesets/read/-/read-0.6.7.tgz", + "integrity": "sha512-D1G4AUYGrBEk8vj8MGwf75k9GpN6XL3wg8i42P2jZZwFLXnlr2Pn7r9yuQNbaMCarP7ZQWNJbV6XLeysAIMhTA==", + "dev": true, + "dependencies": { + "@changesets/git": "^3.0.4", + "@changesets/logger": "^0.1.1", + "@changesets/parse": "^0.4.3", + "@changesets/types": "^6.1.0", + "fs-extra": "^7.0.1", + "p-filter": "^2.1.0", + "picocolors": "^1.1.0" + } + }, + "node_modules/@changesets/read/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@changesets/read/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@changesets/read/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/@changesets/should-skip-package": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@changesets/should-skip-package/-/should-skip-package-0.1.2.tgz", + "integrity": "sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==", + "dev": true, + "dependencies": { + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3" + } + }, + "node_modules/@changesets/types": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@changesets/types/-/types-6.1.0.tgz", + "integrity": "sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==", + "dev": true + }, + "node_modules/@changesets/write": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@changesets/write/-/write-0.4.0.tgz", + "integrity": "sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==", + "dev": true, + "dependencies": { + "@changesets/types": "^6.1.0", + "fs-extra": "^7.0.1", + "human-id": "^4.1.1", + "prettier": "^2.7.1" + } + }, + "node_modules/@changesets/write/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@changesets/write/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@changesets/write/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" } }, "node_modules/@cypress/request": { @@ -60,6 +487,183 @@ "ms": "^2.1.1" } }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@kpmck/ag-grid-core": { + "resolved": "packages/ag-grid-core", + "link": true + }, + "node_modules/@manypkg/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@manypkg/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.5.5", + "@types/node": "^12.7.1", + "find-up": "^4.1.0", + "fs-extra": "^8.1.0" + } + }, + "node_modules/@manypkg/find-root/node_modules/@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", + "dev": true + }, + "node_modules/@manypkg/find-root/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@manypkg/find-root/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@manypkg/find-root/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/@manypkg/get-packages": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@manypkg/get-packages/-/get-packages-1.1.3.tgz", + "integrity": "sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.5.5", + "@changesets/types": "^4.0.1", + "@manypkg/find-root": "^1.1.0", + "fs-extra": "^8.1.0", + "globby": "^11.0.0", + "read-yaml-file": "^1.1.0" + } + }, + "node_modules/@manypkg/get-packages/node_modules/@changesets/types": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@changesets/types/-/types-4.1.0.tgz", + "integrity": "sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==", + "dev": true + }, + "node_modules/@manypkg/get-packages/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@manypkg/get-packages/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@manypkg/get-packages/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@playwright/test": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", + "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", + "dev": true, + "dependencies": { + "playwright": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@types/node": { "version": "18.18.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.3.tgz", @@ -176,6 +780,21 @@ } ] }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -262,6 +881,18 @@ "tweetnacl": "^0.14.3" } }, + "node_modules/better-path-resolve": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/better-path-resolve/-/better-path-resolve-1.0.0.tgz", + "integrity": "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==", + "dev": true, + "dependencies": { + "is-windows": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/blob-util": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz", @@ -274,6 +905,18 @@ "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", "dev": true }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -379,6 +1022,12 @@ "node": ">=8" } }, + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "dev": true + }, "node_modules/ci-info": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", @@ -588,6 +1237,10 @@ "node": "^20.1.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/cypress-ag-grid": { + "resolved": "packages/cypress-ag-grid", + "link": true + }, "node_modules/cypress/node_modules/tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", @@ -644,6 +1297,27 @@ "node": ">=0.4.0" } }, + "node_modules/detect-indent": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", + "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -684,12 +1358,13 @@ } }, "node_modules/enquirer": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", - "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", "dev": true, "dependencies": { - "ansi-colors": "^4.1.1" + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" }, "engines": { "node": ">=8.6" @@ -749,6 +1424,19 @@ "node": ">=0.8.0" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/eventemitter2": { "version": "6.4.7", "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz", @@ -796,6 +1484,12 @@ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "dev": true }, + "node_modules/extendable-error": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/extendable-error/-/extendable-error-0.1.7.tgz", + "integrity": "sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==", + "dev": true + }, "node_modules/extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -825,6 +1519,31 @@ "node >=0.6.0" ] }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", @@ -849,6 +1568,31 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -889,6 +1633,20 @@ "node": ">=10" } }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -959,6 +1717,18 @@ "assert-plus": "^1.0.0" } }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/global-dirs": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.0.tgz", @@ -974,6 +1744,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -1079,6 +1869,15 @@ "node": ">=0.10" } }, + "node_modules/human-id": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/human-id/-/human-id-4.1.3.tgz", + "integrity": "sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==", + "dev": true, + "bin": { + "human-id": "dist/cli.js" + } + }, "node_modules/human-signals": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", @@ -1088,6 +1887,22 @@ "node": ">=8.12.0" } }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -1108,6 +1923,15 @@ } ] }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", @@ -1126,6 +1950,15 @@ "node": ">=10" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -1135,6 +1968,18 @@ "node": ">=8" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-installed-globally": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", @@ -1151,6 +1996,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-path-inside": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", @@ -1172,6 +2026,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-subdir": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-subdir/-/is-subdir-1.2.0.tgz", + "integrity": "sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==", + "dev": true, + "dependencies": { + "better-path-resolve": "1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", @@ -1190,6 +2056,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -1202,6 +2077,18 @@ "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", "dev": true }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", @@ -1274,6 +2161,18 @@ } } }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/lodash": { "version": "4.17.23", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", @@ -1286,6 +2185,12 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "dev": true }, + "node_modules/lodash.startcase": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", + "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", + "dev": true + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -1366,6 +2271,28 @@ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -1405,6 +2332,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -1465,6 +2401,60 @@ "integrity": "sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==", "dev": true }, + "node_modules/outdent": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/outdent/-/outdent-0.5.0.tgz", + "integrity": "sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==", + "dev": true + }, + "node_modules/p-filter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-filter/-/p-filter-2.1.0.tgz", + "integrity": "sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==", + "dev": true, + "dependencies": { + "p-map": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-filter/node_modules/p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/p-map": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", @@ -1480,6 +2470,33 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/package-manager-detector": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.11.tgz", + "integrity": "sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==", + "dev": true, + "dependencies": { + "quansync": "^0.2.7" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -1489,6 +2506,15 @@ "node": ">=8" } }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -1501,6 +2527,24 @@ "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", "dev": true }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", @@ -1510,6 +2554,55 @@ "node": ">=0.10.0" } }, + "node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "dev": true, + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-ag-grid": { + "resolved": "packages/playwright-ag-grid", + "link": true + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-bytes": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", @@ -1562,6 +2655,88 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ] + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/read-yaml-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-yaml-file/-/read-yaml-file-1.1.0.tgz", + "integrity": "sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.5", + "js-yaml": "^3.6.1", + "pify": "^4.0.1", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/read-yaml-file/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/read-yaml-file/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/read-yaml-file/node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/request-progress": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", @@ -1571,6 +2746,15 @@ "throttleit": "^1.0.0" } }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -1584,12 +2768,45 @@ "node": ">=8" } }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rfdc": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", "dev": true }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/rxjs": { "version": "7.5.5", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.5.tgz", @@ -1625,6 +2842,18 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -1724,6 +2953,15 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/slice-ansi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", @@ -1738,6 +2976,34 @@ "node": ">=8" } }, + "node_modules/spawndamnit": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spawndamnit/-/spawndamnit-3.0.1.tgz", + "integrity": "sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.5", + "signal-exit": "^4.0.1" + } + }, + "node_modules/spawndamnit/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, "node_modules/sshpk": { "version": "1.18.0", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", @@ -1789,6 +3055,15 @@ "node": ">=8" } }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -1839,6 +3114,18 @@ "url": "https://www.buymeacoffee.com/systeminfo" } }, + "node_modules/term-size": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz", + "integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/throttleit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", @@ -1878,6 +3165,18 @@ "node": ">=14.14" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/tough-cookie": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", @@ -2023,6 +3322,31 @@ "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } + }, + "packages/ag-grid-core": { + "name": "@kpmck/ag-grid-core", + "version": "0.1.0", + "license": "MIT" + }, + "packages/cypress-ag-grid": { + "version": "3.3.5", + "license": "MIT", + "dependencies": { + "@kpmck/ag-grid-core": "0.1.0" + }, + "devDependencies": { + "cypress": "^15.12.0" + } + }, + "packages/playwright-ag-grid": { + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@kpmck/ag-grid-core": "0.1.0" + }, + "devDependencies": { + "@playwright/test": "^1.52.0" + } } } } diff --git a/package.json b/package.json index 5a4a211..0728a9c 100644 --- a/package.json +++ b/package.json @@ -1,32 +1,30 @@ { - "name": "cypress-ag-grid", - "version": "3.3.5", - "description": "Cypress plugin to interact with ag grid", - "main": "src/index.js", - "repository": { - "type": "git", - "url": "git+https://github.com/kpmck/cypress-ag-grid.git" - }, - "keywords": [ - "aggrid", - "ag-grid", - "cypress", - "cypress-io", - "e2e testing", - "cypress table", - "cypress ag grid", - "cypress aggrid" + "name": "ag-grid-testing", + "private": true, + "workspaces": [ + "packages/*" ], "scripts": { - "test": "npx cypress run --headless --spec \"cypress/e2e/*.cy.js\"", - "test:v33": "npx cypress run --headless --spec \"cypress/e2e/*.v33.cy.js\"", - "test:v34": "npx cypress run --headless --spec \"cypress/e2e/*.v34.cy.js\"", - "test:v35": "npx cypress run --headless --spec \"cypress/e2e/*.v35.cy.js\"", - "test:watch": "npx cypress open" + "changeset": "changeset", + "version-packages": "changeset version", + "release": "changeset publish", + "test:core": "npm run test --workspace @kpmck/ag-grid-core", + "test:cypress": "npm run test:all --workspace cypress-ag-grid", + "test:cypress:all": "npm run test:all --workspace cypress-ag-grid", + "test:cypress:v33": "npm run test:v33 --workspace cypress-ag-grid", + "test:cypress:v34": "npm run test:v34 --workspace cypress-ag-grid", + "test:cypress:v35": "npm run test:v35 --workspace cypress-ag-grid", + "test:cypress:watch": "npm run test:watch --workspace cypress-ag-grid", + "test:playwright": "npm run test:all --workspace playwright-ag-grid", + "test:playwright:all": "npm run test:all --workspace playwright-ag-grid", + "test:playwright:v33": "npm run test:v33 --workspace playwright-ag-grid", + "test:playwright:v34": "npm run test:v34 --workspace playwright-ag-grid", + "test:playwright:v35": "npm run test:v35 --workspace playwright-ag-grid", + "test:playwright:watch": "npm run test:watch --workspace playwright-ag-grid", + "test:all": "npm run test:core && npm run test:cypress:all && npm run test:playwright:all", + "test": "npm run test:all" }, - "author": "Kerry McKeever ", - "license": "MIT", "devDependencies": { - "cypress": "^15.12.0" + "@changesets/cli": "^2.30.0" } } diff --git a/packages/ag-grid-core/README.md b/packages/ag-grid-core/README.md new file mode 100644 index 0000000..c690371 --- /dev/null +++ b/packages/ag-grid-core/README.md @@ -0,0 +1,11 @@ +# @kpmck/ag-grid-core + +Framework-agnostic AG Grid helpers shared by the adapter packages in this monorepo. + +Planned responsibilities: + +- DOM traversal and extraction +- AG Grid animation waiting helpers +- Shared types and selectors + +Framework-specific command registration and assertions stay in adapter packages such as `cypress-ag-grid` and `playwright-ag-grid`. diff --git a/packages/ag-grid-core/package.json b/packages/ag-grid-core/package.json new file mode 100644 index 0000000..4e1f967 --- /dev/null +++ b/packages/ag-grid-core/package.json @@ -0,0 +1,31 @@ +{ + "name": "@kpmck/ag-grid-core", + "version": "0.1.0", + "description": "Framework-agnostic AG Grid DOM helpers for Node-based test tooling", + "type": "module", + "main": "src/index.js", + "types": "src/index.d.ts", + "exports": { + ".": "./src/index.js" + }, + "publishConfig": { + "access": "public" + }, + "scripts": { + "test": "node --test" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/kpmck/cypress-ag-grid.git" + }, + "keywords": [ + "ag-grid", + "aggrid", + "testing", + "dom", + "playwright", + "cypress" + ], + "author": "Kerry McKeever ", + "license": "MIT" +} diff --git a/packages/ag-grid-core/src/index.d.ts b/packages/ag-grid-core/src/index.d.ts new file mode 100644 index 0000000..f70ac15 --- /dev/null +++ b/packages/ag-grid-core/src/index.d.ts @@ -0,0 +1,44 @@ +export interface AgGridExtractionOptions { + onlyColumns?: string[]; + valuesArray?: boolean; + returnElements?: boolean; + timeoutMs?: number; +} + +export declare const filterOperator: Record; +export declare const sort: Record; +export declare const filterTab: Record; + +export declare function isRowNotDestroyed(rowElement: Element): boolean; + +export declare function waitForAgGridAnimation( + agGridRootElement: Element, + options?: AgGridExtractionOptions +): Promise; + +export declare function getAgGridHeaders(tableElement: Element): string[]; + +export declare function extractAgGrid( + agGridRootElement: Element, + options?: AgGridExtractionOptions +): Array> | { headers: string[]; rows: Array> }; + +export declare function extractAgGridData( + agGridRootElement: Element, + options?: AgGridExtractionOptions +): Promise> | { headers: string[]; rows: string[][] }>; + +export declare function extractAgGridElements( + agGridRootElement: Element, + options?: AgGridExtractionOptions +): Promise> | { headers: string[]; rows: Element[][] }>; + +export declare function browserExtractAgGrid( + agGridRootElement: Element, + options?: AgGridExtractionOptions +): Array> | { headers: string[]; rows: string[][] }; + +export declare function browserWaitForAgGridAnimation( + agGridRootElement: Element, + options?: AgGridExtractionOptions +): Promise; diff --git a/packages/ag-grid-core/src/index.js b/packages/ag-grid-core/src/index.js new file mode 100644 index 0000000..cd91ffa --- /dev/null +++ b/packages/ag-grid-core/src/index.js @@ -0,0 +1,452 @@ +const AG_GRID_COLUMN_SELECTORS = [ + ".ag-pinned-left-cols-container", + ".ag-center-cols-clipper", + ".ag-center-cols-viewport", + ".ag-pinned-right-cols-container", +]; + +const DEFAULT_ANIMATION_TIMEOUT_MS = 5000; + +export const filterOperator = { + contains: "Contains", + notContains: "Does not contain", + equals: "Equals", + notEquals: "Does not equal", + startsWith: "Begins with", + endsWith: "Ends with", + lessThan: "Less than", + lessThanOrEquals: "Less than or equal to", + greaterThan: "Greater than", + greaterThanOrEquals: "Greater than or equal to", + inRange: "Between", + blank: "Blank", + notBlank: "Not blank", +}; + +export const sort = { + ascending: "asc", + descending: "desc", + none: "none", +}; + +export const filterTab = { + columns: "columns", + filter: "filter", + search: "search", + general: "menu", +}; + +function isElementNode(value) { + return Boolean(value && value.nodeType === 1); +} + +function getAttributeValue(element, attribute) { + const attributeNode = element?.attributes?.[attribute]; + + if (!attributeNode) { + return undefined; + } + + if (typeof attributeNode.value === "string") { + return attributeNode.value; + } + + if (typeof attributeNode.nodeValue === "string") { + return attributeNode.nodeValue; + } + + return undefined; +} + +function sortElementsByAttributeValue(attribute) { + return (a, b) => { + const contentA = parseInt(getAttributeValue(a, attribute), 10).valueOf(); + const contentB = parseInt(getAttributeValue(b, attribute), 10).valueOf(); + return contentA < contentB ? -1 : contentA > contentB ? 1 : 0; + }; +} + +function getTrimmedTextContent(element) { + return element?.textContent?.trim?.() ?? ""; +} + +export 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 async function waitForAgGridAnimation( + agGridRootElement, + options = {} +) { + if (!isElementNode(agGridRootElement)) { + throw new Error(`Couldn't find a valid AG Grid root element.`); + } + + const timeoutMs = options.timeoutMs ?? DEFAULT_ANIMATION_TIMEOUT_MS; + const animations = agGridRootElement.getAnimations?.({ subtree: true }) ?? []; + + const agGridAnimations = animations.filter((animation) => { + const animationTarget = animation.effect?.target; + + if ( + !isElementNode(animationTarget) || + !animationTarget.classList + ) { + return false; + } + + const hasAgGridClass = [...animationTarget.classList].some((className) => + className.startsWith("ag-") + ); + + return animationTarget === agGridRootElement || hasAgGridClass; + }); + + const finiteAnimations = agGridAnimations.filter((animation) => { + const iterations = animation.effect?.getTiming?.()?.iterations; + return iterations !== Infinity; + }); + + await Promise.race([ + Promise.all( + finiteAnimations.map(async (animation) => { + try { + await animation.finished; + } catch (error) { + if (error?.name === "AbortError") { + return; + } + + throw error; + } + }) + ), + new Promise((resolve) => { + setTimeout(resolve, timeoutMs); + }), + ]); +} + +export function getAgGridHeaders(tableElement) { + return [ + ...tableElement.querySelectorAll(".ag-header-row-column [aria-colindex]"), + ] + .sort(sortElementsByAttributeValue("aria-colindex")) + .map((headerElement) => { + const headerCells = [ + ...headerElement.querySelectorAll(".ag-header-cell-text"), + ]; + + if (headerCells.length === 0) { + return [getTrimmedTextContent(headerElement)]; + } + + return headerCells.map((element) => getTrimmedTextContent(element)); + }) + .flat(); +} + +function getRowCells(rowElement) { + const rowCells = [...rowElement.querySelectorAll(".ag-cell[aria-colindex]")]; + + if (rowCells.length > 0) { + return rowCells; + } + + return [...rowElement.querySelectorAll(".ag-cell")]; +} + +function getStructuredRows(allRows, returnElements) { + if (!allRows.length) { + return []; + } + + return allRows + .filter((rowCells) => rowCells.length) + .map((rowCells) => + rowCells + .sort(sortElementsByAttributeValue("aria-colindex")) + .map((element) => + returnElements ? element : getTrimmedTextContent(element) + ) + ); +} + +function mapRowsToObjects(headers, rows, options = {}) { + return rows.map((row) => + row.reduce((acc, curr, idx) => { + if ( + (options.onlyColumns && !options.onlyColumns.includes(headers[idx])) || + headers[idx] === undefined + ) { + return acc; + } + + return { ...acc, [headers[idx]]: curr }; + }, {}) + ); +} + +export function extractAgGrid(tableRootElement, options = {}) { + if (!isElementNode(tableRootElement)) { + throw new Error(`Couldn't find a valid AG Grid element.`); + } + + const returnElements = options.returnElements ?? false; + const tableElement = tableRootElement.querySelectorAll(".ag-root")[0]; + + if (!tableElement) { + throw new Error("The provided element does not contain an .ag-root node."); + } + + const headers = getAgGridHeaders(tableElement); + let allRows = []; + + AG_GRID_COLUMN_SELECTORS.forEach((selector) => { + [ + ...tableElement.querySelectorAll( + `${selector}:not(.ag-hidden) .ag-row:not(.ag-opacity-zero)` + ), + ] + .filter(isRowNotDestroyed) + .sort(sortElementsByAttributeValue("row-index")) + .forEach((rowElement) => { + const rowCells = getRowCells(rowElement); + const rowIndex = parseInt(getAttributeValue(rowElement, "row-index"), 10); + + if (allRows[rowIndex]) { + allRows[rowIndex] = [...allRows[rowIndex], ...rowCells]; + } else { + allRows[rowIndex] = rowCells; + } + }); + }); + + allRows = allRows + .filter((row) => row.length) + .map((row) => row.filter((cell, index) => row.indexOf(cell) === index)); + + const rows = getStructuredRows(allRows, returnElements); + + if (options.valuesArray) { + return { headers, rows }; + } + + return mapRowsToObjects(headers, rows, options); +} + +export async function extractAgGridData(agGridRootElement, options = {}) { + await waitForAgGridAnimation(agGridRootElement, options); + return extractAgGrid(agGridRootElement, options); +} + +export async function extractAgGridElements(agGridRootElement, options = {}) { + await waitForAgGridAnimation(agGridRootElement, options); + return extractAgGrid(agGridRootElement, { ...options, returnElements: true }); +} + +// Self-contained exports that can be serialized into a browser context by Playwright. +export async function browserWaitForAgGridAnimation( + agGridRootElement, + options = {} +) { + function isElementNode(value) { + return Boolean(value && value.nodeType === 1); + } + + if (!isElementNode(agGridRootElement)) { + throw new Error(`Couldn't find a valid AG Grid root element.`); + } + + const timeoutMs = options.timeoutMs ?? 5000; + const animations = agGridRootElement.getAnimations?.({ subtree: true }) ?? []; + + const agGridAnimations = animations.filter((animation) => { + const animationTarget = animation.effect?.target; + + if (!isElementNode(animationTarget) || !animationTarget.classList) { + return false; + } + + const hasAgGridClass = [...animationTarget.classList].some((className) => + className.startsWith("ag-") + ); + + return animationTarget === agGridRootElement || hasAgGridClass; + }); + + const finiteAnimations = agGridAnimations.filter((animation) => { + const iterations = animation.effect?.getTiming?.()?.iterations; + return iterations !== Infinity; + }); + + await Promise.race([ + Promise.all( + finiteAnimations.map(async (animation) => { + try { + await animation.finished; + } catch (error) { + if (error?.name === "AbortError") { + return; + } + + throw error; + } + }) + ), + new Promise((resolve) => setTimeout(resolve, timeoutMs)), + ]); +} + +export function browserExtractAgGrid(agGridRootElement, options = {}) { + const agGridColumnSelectors = [ + ".ag-pinned-left-cols-container", + ".ag-center-cols-clipper", + ".ag-center-cols-viewport", + ".ag-pinned-right-cols-container", + ]; + + function isElementNode(value) { + return Boolean(value && value.nodeType === 1); + } + + function getAttributeValue(element, attribute) { + const attributeNode = element?.attributes?.[attribute]; + + if (!attributeNode) { + return undefined; + } + + if (typeof attributeNode.value === "string") { + return attributeNode.value; + } + + if (typeof attributeNode.nodeValue === "string") { + return attributeNode.nodeValue; + } + + return undefined; + } + + function sortElementsByAttributeValue(attribute) { + return (a, b) => { + const contentA = parseInt(getAttributeValue(a, attribute), 10).valueOf(); + const contentB = parseInt(getAttributeValue(b, attribute), 10).valueOf(); + return contentA < contentB ? -1 : contentA > contentB ? 1 : 0; + }; + } + + function getTrimmedTextContent(element) { + return element?.textContent?.trim?.() ?? ""; + } + + function isRowNotDestroyedLocal(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 + ); + } + + function getRowCells(rowElement) { + const rowCells = [...rowElement.querySelectorAll(".ag-cell[aria-colindex]")]; + if (rowCells.length > 0) { + return rowCells; + } + + return [...rowElement.querySelectorAll(".ag-cell")]; + } + + if (!isElementNode(agGridRootElement)) { + throw new Error(`Couldn't find a valid AG Grid element.`); + } + + const returnElements = options.returnElements ?? false; + const tableElement = agGridRootElement.querySelectorAll(".ag-root")[0]; + + if (!tableElement) { + throw new Error("The provided element does not contain an .ag-root node."); + } + + const headers = [ + ...tableElement.querySelectorAll(".ag-header-row-column [aria-colindex]"), + ] + .sort(sortElementsByAttributeValue("aria-colindex")) + .map((headerElement) => { + const headerCells = [ + ...headerElement.querySelectorAll(".ag-header-cell-text"), + ]; + + if (headerCells.length === 0) { + return [getTrimmedTextContent(headerElement)]; + } + + return headerCells.map((element) => getTrimmedTextContent(element)); + }) + .flat(); + + let allRows = []; + + agGridColumnSelectors.forEach((selector) => { + [ + ...tableElement.querySelectorAll( + `${selector}:not(.ag-hidden) .ag-row:not(.ag-opacity-zero)` + ), + ] + .filter(isRowNotDestroyedLocal) + .sort(sortElementsByAttributeValue("row-index")) + .forEach((rowElement) => { + const rowCells = getRowCells(rowElement); + const rowIndex = parseInt(getAttributeValue(rowElement, "row-index"), 10); + + if (allRows[rowIndex]) { + allRows[rowIndex] = [...allRows[rowIndex], ...rowCells]; + } else { + allRows[rowIndex] = rowCells; + } + }); + }); + + allRows = allRows + .filter((row) => row.length) + .map((row) => row.filter((cell, index) => row.indexOf(cell) === index)); + + const rows = allRows + .filter((rowCells) => rowCells.length) + .map((rowCells) => + rowCells + .sort(sortElementsByAttributeValue("aria-colindex")) + .map((element) => + returnElements ? getAttributeValue(element, "col-id") ?? getTrimmedTextContent(element) : getTrimmedTextContent(element) + ) + ); + + if (options.valuesArray) { + return { headers, rows }; + } + + return rows.map((row) => + row.reduce((acc, curr, idx) => { + if ( + (options.onlyColumns && !options.onlyColumns.includes(headers[idx])) || + headers[idx] === undefined + ) { + return acc; + } + + return { ...acc, [headers[idx]]: curr }; + }, {}) + ); +} diff --git a/packages/ag-grid-core/src/index.test.js b/packages/ag-grid-core/src/index.test.js new file mode 100644 index 0000000..ed266d2 --- /dev/null +++ b/packages/ag-grid-core/src/index.test.js @@ -0,0 +1,203 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { + extractAgGridData, + extractAgGridElements, + waitForAgGridAnimation, +} from "./index.js"; + +function createAttributes(attributes = {}) { + return Object.fromEntries( + Object.entries(attributes).map(([key, value]) => [ + key, + { nodeValue: String(value), value: String(value) }, + ]) + ); +} + +class FakeElement { + constructor({ + textContent = "", + attributes = {}, + selectorMap = {}, + rect = { top: 0, left: 0, bottom: 10, right: 10 }, + classList = [], + nodeType = 1, + } = {}) { + this.textContent = textContent; + this.attributes = createAttributes(attributes); + this.selectorMap = selectorMap; + this._rect = rect; + this.classList = classList; + this.nodeType = nodeType; + this.parentElement = null; + } + + querySelectorAll(selector) { + return this.selectorMap[selector] ?? []; + } + + getBoundingClientRect() { + return this._rect; + } + + setSelectorMap(selectorMap) { + this.selectorMap = selectorMap; + return this; + } +} + +function connect(parent, children) { + children.forEach((child) => { + child.parentElement = parent; + }); +} + +function createCell(colIndex, text) { + return new FakeElement({ + textContent: text, + attributes: { "aria-colindex": colIndex }, + }); +} + +function createRow(rowIndex, cells, rect = { top: 0, left: 0, bottom: 10, right: 10 }) { + const row = new FakeElement({ + attributes: { "row-index": rowIndex }, + rect, + }); + + connect(row, cells); + row.setSelectorMap({ + ".ag-cell[aria-colindex]": cells, + ".ag-cell": cells, + }); + + const viewport = new FakeElement({ + rect: { top: 0, left: 0, bottom: 100, right: 100 }, + }); + row.parentElement = viewport; + + return row; +} + +function createHeader(colIndex, text) { + const textElement = new FakeElement({ textContent: text }); + const header = new FakeElement({ + attributes: { "aria-colindex": colIndex }, + }); + + connect(header, [textElement]); + header.setSelectorMap({ + ".ag-header-cell-text": [textElement], + }); + + return header; +} + +function createGridFixture() { + const headerYear = createHeader(1, "Year"); + const headerMake = createHeader(2, "Make"); + const headerModel = createHeader(3, "Model"); + + const pinnedLeftRow0 = createRow(0, [createCell(1, "2020")]); + const centerRow0 = createRow(0, [createCell(2, "Toyota"), createCell(3, "Celica")]); + const centerRow1 = createRow(1, [createCell(1, "2021"), createCell(2, "Ford"), createCell(3, "Mondeo")]); + const destroyedRow = createRow( + 2, + [createCell(1, "9999"), createCell(2, "Ghost"), createCell(3, "Row")], + { top: 200, left: 0, bottom: 210, right: 10 } + ); + + const agRoot = new FakeElement(); + agRoot.setSelectorMap({ + ".ag-header-row-column [aria-colindex]": [headerModel, headerYear, headerMake], + ".ag-pinned-left-cols-container:not(.ag-hidden) .ag-row:not(.ag-opacity-zero)": [pinnedLeftRow0], + ".ag-center-cols-clipper:not(.ag-hidden) .ag-row:not(.ag-opacity-zero)": [centerRow0, centerRow1, destroyedRow], + ".ag-center-cols-viewport:not(.ag-hidden) .ag-row:not(.ag-opacity-zero)": [], + ".ag-pinned-right-cols-container:not(.ag-hidden) .ag-row:not(.ag-opacity-zero)": [], + }); + + const gridRoot = new FakeElement(); + gridRoot.setSelectorMap({ + ".ag-root": [agRoot], + }); + + return { gridRoot, pinnedLeftRow0, centerRow0 }; +} + +test("extractAgGridData returns structured row data and filters destroyed rows", async () => { + const { gridRoot } = createGridFixture(); + gridRoot.getAnimations = () => []; + + const rows = await extractAgGridData(gridRoot); + + assert.deepEqual(rows, [ + { Year: "2020", Make: "Toyota", Model: "Celica" }, + { Year: "2021", Make: "Ford", Model: "Mondeo" }, + ]); +}); + +test("extractAgGridData supports onlyColumns and valuesArray", async () => { + const { gridRoot } = createGridFixture(); + gridRoot.getAnimations = () => []; + + const subset = await extractAgGridData(gridRoot, { onlyColumns: ["Make"] }); + const arrays = await extractAgGridData(gridRoot, { valuesArray: true }); + + assert.deepEqual(subset, [{ Make: "Toyota" }, { Make: "Ford" }]); + assert.deepEqual(arrays.headers, ["Year", "Make", "Model"]); + assert.deepEqual(arrays.rows, [ + ["2020", "Toyota", "Celica"], + ["2021", "Ford", "Mondeo"], + ]); +}); + +test("extractAgGridElements returns cell elements instead of text", async () => { + const { gridRoot, pinnedLeftRow0, centerRow0 } = createGridFixture(); + gridRoot.getAnimations = () => []; + + const rows = await extractAgGridElements(gridRoot); + + assert.equal(rows[0].Year, pinnedLeftRow0.querySelectorAll(".ag-cell")[0]); + assert.equal(rows[0].Make, centerRow0.querySelectorAll(".ag-cell")[0]); +}); + +test("waitForAgGridAnimation waits for AG Grid animations and ignores aborts", async () => { + let resolved = false; + + const target = new FakeElement({ classList: ["ag-root"] }); + const root = new FakeElement(); + root.getAnimations = () => [ + { + effect: { + target, + getTiming: () => ({ iterations: 1 }), + }, + finished: new Promise((resolve) => { + setTimeout(() => { + resolved = true; + resolve(); + }, 10); + }), + }, + { + effect: { + target, + getTiming: () => ({ iterations: 1 }), + }, + finished: Promise.reject(Object.assign(new Error("aborted"), { name: "AbortError" })), + }, + { + effect: { + target: new FakeElement({ classList: ["spinner"] }), + getTiming: () => ({ iterations: 1 }), + }, + finished: Promise.resolve(), + }, + ]; + + await waitForAgGridAnimation(root, { timeoutMs: 50 }); + + assert.equal(resolved, true); +}); diff --git a/packages/cypress-ag-grid/README.md b/packages/cypress-ag-grid/README.md new file mode 100644 index 0000000..3ff0512 --- /dev/null +++ b/packages/cypress-ag-grid/README.md @@ -0,0 +1,414 @@ +# cypress-ag-grid +Cypress plugin for interacting with and validating against ag grid. + +## Table of Contents + * [Installation](#installation) + * [Usage](#usage) + + [Grid Interaction](#) + - [Getting Data From the Grid](#getting-data-from-the-grid) + - [Getting Select Row Data](#getting-select-row-data) + - [Getting Elements From the Grid](#getting-elements-from-the-grid) + - [Sorting Columns](#sorting-columns) + - [Pinning Columns](#pinning-columns) + + [Grid Filtering](#) + - [Filter Options](#filter-options) + - [Filter by Text - Column Menu](#filter-by-text---column-menu) + - [Filterby Text - Floating Filter](#filterby-text---floating-filter) + - [Filter by Checkbox - Column Menu](#filter-by-checkbox---column-menu) + - [Filtering - Localization and Internationalization](#filtering---localization-and-internationalization) + - [Add or Remove Columns](#add-or-remove-columns) + + [Grid Validation](#) + - [Validate Paginated Table](#validate-paginated-table) + - [Validate Table in the Exact Order](#validate-table-in-the-exact-order) + - [Validate Subset of Table Data](#validate-subset-of-table-data) + - [Validate Empty Grid](#validate-empty-grid) + * [Limitations](#limitations) + * [Credit](#credit) +
+
+ +## Installation + +```bash +npm install cypress-ag-grid --save-dev +``` +Then include the following in your `support/index.js` file (Cypress v9 and below) or `support/e2e.(js|ts)` file (Cypress 10 and above): + +```javascript +import "cypress-ag-grid"; +``` +## Usage +Consider the ag grid example below: +![alt text](./ag-grid-example.png "AG Grid") + +With the following DOM structure: +![alt text](./ag-grid-example-dom.png "AG Grid Dom") +
+
+### Getting Data From the Grid: +To get the Ag Grid data, you must chain `.getAgGridData()` after the `cy.get()` command for the topmost level of the grid, including controls and headers (see selected DOM element in above image). + +Correct Usage: +```javascript +cy.get("#myGrid").getAgGridData() +``` + +Incorrect Usage: +```javascript +cy.getAgGridData(); +``` + +The correct command will return the following: +```json +[ + { "Year": "2020", "Make": "Toyota", "Model": "Celica" }, + { "Year": "2020", "Make": "Ford", "Model": "Mondeo" }, + { "Year": "2020", "Make": "Porsche", "Model": "Boxter" }, + { "Year": "2020", "Make": "BMW", "Model": "3-series" }, + { "Year": "2020", "Make": "Mercedes", "Model": "GLC300" }, +] +``` +
+
+ +### Getting Select Row Data +To only get certain rows of data, pass the header values into the `getAgGridData()` command, like so: + +```javascript +cy.get("#myGrid").getAgGridData({ onlyColumns: ["Year", "Make"] }) +``` + +The above command will return the follwoing: +```json +[ + { "Year": "2020", "Make": "Toyota"}, + { "Year": "2020", "Make": "Ford"}, + { "Year": "2020", "Make": "Porsche"}, + { "Year": "2020", "Make": "BMW"}, + { "Year": "2020", "Make": "Mercedes"}, +] +``` +
+
+ +### Getting Elements From the Grid +To get the Ag Grid data as elements (if you want to interact with the cells themselves), you must chain `.getAgGridElements()` after the `cy.get()` command for the topmost level of the grid, including controls and headers (see selected DOM element in above image). +```javascript + cy.get(agGridSelector) + .getAgGridElements() + .then((tableElements) => { + const porscheRow = tableElements.find( + (row) => row.Price.innerText === "72000" + ); + const priceCell = porscheRow.Price; + cy.wrap(priceCell).dblclick().type("66000{enter}"); + }); + +``` + +The above example will grab the table as elements, finds the row whose `Price` equals `72000`. It then gets the `Price` cell for that row, double clicks on it to enable an editable input, and changes the value of the cell. +
+
+ +### Sorting Columns +This command will sort the specified column by the sort direction specified. + +Defintion: +`.agGridSortColumn(columnName:String, sortDirection:String)` + +Example: + +```javascript +cy.get("#myGrid").agGridSortColumn("Model", "descending"); +``` +
+
+ +### Pinning Columns +This command will pin the specified column. +Definition +`.agGridPinColumn(columnName: string, pin: ['left', 'right', null])` + +Example: +```javascript +cy.get("#myGrid").agGridPinColumn("Model", "left") // Pins the "Model" column to the left +cy.get("#myGrid").agGridPinColumn("Model", "right") // Pins the "Model" column to the right +cy.get("#mGrid").agGridPinColumn("Model") // Removes the pin + +``` +
+
+ +### Filter Options + +The below filtering commands takes an `options` parameter comprised of the following properties: + +```javascript +options: { + searchCriteria: [{ + columnName: string; + filterValue: string; + operator?: string; + isMultiFilter?: boolean; + }]; + hasApplyButton?: boolean; + noMenuTabs?: boolean; + selectAllLocaleText: string; +} + +/** +- options.searchCriteria JSON with search properties and options +- options.searchCriteria.columnName name of the column to filter +- options.searchCriteria.filterValue value to input into the filter textbox +- options.searchCriteria.searchInputIndex [Optional] Uses 0 by default. Index of which filter box to use in event of having multiple search conditionals +- options.searchCriteria.operator [Optional] Use if using a search operator (i.e. Less Than, Equals, etc...use filterOperator.enum values). +- options.searchCriteria.isMultiFilter [Optional] Used when floating filter menu has checkbox options vs freeform text input. +- options.hasApplyButton [Optional] True if "Apply" button is used, false if filters by text input automatically. +- options.noMenuTabs [Optional] True if you use, for example, the community edition of ag-grid, which has no menu tabs +- options.selectAllLocaleText [Optional] Pass in the locale text value of "Select All" for when you are filtering by checkbox - this will first deselect the "Select All" option before selecting your filter value +*/ +``` + +### Filter by Text - Column Menu +This command will filter a column by a text value from its menu. In the options, you must specify a `searchCriteria` objects containing one or more objects with `columnName`, `filterValue`, and optionally `operator` (i.e. Contains, Not contains, Equals, etc.). + +![alt text](./ag-grid-example-filter-text-menu.png "AG Grid Dom - Filter by Text Menu") + +Definition: `.agGridColumnFilterTextMenu(options: {})` + +Example: +```javascript +cy.get("#myGrid").agGridColumnFilterTextMenu({ + searchCriteria:[{ + columnName: "Model", + filterValue: "GLC300", + operator:"Equals" + }, + { + columnName: "Make", + filterValue: "Mercedes", + operator:"Equals" + } + ], + hasApplyButton: false +}) +```` +The above command will filter the Model column for the value 'GLC300' and set the filter operator to 'Equals'. It will then apply a secondary filter on the Make column for 'Mercedes'. +
+
+### Filterby Text - Floating Filter +This command will filter a column by a text value from its floating filter (if applicable). This command will filter a column by a text value from its floating menu. In the options, you must specify a `searchCriteria` object with `columnName`, `filterValue`, and optionally `operator` (i.e. Contains, Not contains, Equals, etc.) and `searchInputIndex` in the event you wish to apply multiple text conditions (see below for multi-condition example). + +![alt text](./ag-grid-example-filter-text-floating.png "AG Grid Dom - Filter by Text Floating") + +Definition: .agGridColumnFilterTextFloating(options: {}) + +Example: +``` + cy.get(agGridSelector).agGridColumnFilterTextFloating({ + searchCriteria: { + columnName: "Make", + filterValue: "Ford", + }, + hasApplyButton: true, + }); +``` + +The above example will search for the Make `Ford` from the floating text menu filter. + +If you have the option for multiple conditions on the floating filter, you can do two searches, specifying the `searchInputIndex` parameter in the `searchCriteria` object. The below example will ssarch for any `Make` that contains `B` AND `MW`: + +Example: +``` + cy.get(agGridSelector).agGridColumnFilterTextFloating({ + searchCriteria: { + columnName: "Make", + filterValue: "B", + searchInputIndex: 0, + }, + hasApplyButton: true, + }); + cy.get(agGridSelector).agGridColumnFilterTextFloating({ + searchCriteria: { + columnName: "Make", + filterValue: "MW", + searchInputIndex: 1, + }, + hasApplyButton: true, + }); +``` +![alt text](./ag-grid-example-filter-text-floating-multi-condition.png "AG Grid Dom - Filter by Text Floating") + +For `Between`, pass the lower and upper bounds as two entries for the same column. The command will target the first and second visible inputs for that single `Between` condition: + +```javascript + cy.get(agGridSelector).agGridColumnFilterTextFloating({ + searchCriteria: [ + { + columnName: "Year", + filterValue: "1990", + operator: filterOperator.inRange, + }, + { + columnName: "Year", + filterValue: "2011", + operator: filterOperator.inRange, + }, + ], + hasApplyButton: true, + }); +``` + +
+
+ +### Filter by Checkbox - Column Menu +This command will filter a column by a checkbox text value from its menu. +![alt text](./ag-grid-example-filter-checkbox-menu.png "AG Grid Dom - Filter by Checkbox Menu") + +Definition: +```javascript +.agGridColumnFilterCheckboxMenu(options={}) +``` + +Example: +```javascript + cy.get("#myGrid").agGridColumnFilterCheckboxMenu({ + searchCriteria: { + columnName: "Model", + filterValue: "2002", + }, + hasApplyButton: true, + }); + +``` +
+ +### Filtering - Localization and Internationalization +When we filter by checkbox, we first deselect the Select All checkbox to ensure we ONLY select the specified checkbox. Since AG grid allows for localization, we need a way to be able to pass in the localeText for Select All. This is the only area of this plugin that has a hard-coded value, so no other localization accommodations are needed. + +``` + cy.get("#myGrid").agGridColumnFilterCheckboxMenu({ + searchCriteria: { + columnName: "Model", + filterValue: "2002", + }, + selectAllLocaleText: "Tout Sélectionner" + hasApplyButton: true, + }); +``` +
+ +### Add or Remove Columns +This command will toggle the specified column from the grid's sidebar. + +Definition:`.agGridToggleColumnsSideBar(columnName:String, doRemove:boolean)` + +Example: +```javascript +// This will remove the column "Year" from the grid +cy.get("#myGrid").agGridToggleColumnsSideBar("Year", true); +``` +
+
+ +### Validate Paginated Table +This command will validate the paginated grid's data. The supplied expectedPaginatedTableData must be paginated as it's shown in the grid. + +Definition: `agGridValidatePaginatedTable(expectedPaginatedTableData, onlyColumns = {})` + +Example: +```javascript + const expectedPaginatedTableData = [ + [ + { "Year": "2020", "Make": "Toyota", "Model": "Celica" }, + { "Year": "2020", "Make": "Ford", "Model": "Mondeo" }, + { "Year": "2020", "Make": "Porsche", "Model": "Boxter" }, + { "Year": "2020", "Make": "BMW", "Model": "3-series" }, + { "Year": "2020", "Make": "Mercedes", "Model": "GLC300" }, + ], + [ + { "Year": "2020", "Make": "Honda", "Model": "Civic" }, + { "Year": "2020", "Make": "Honda", "Model": "Accord" }, + { "Year": "2020", "Make": "Ford", "Model": "Taurus" }, + { "Year": "2020", "Make": "Hyundai", "Model": "Elantra" }, + { "Year": "2020", "Make": "Toyota", "Model": "Celica" }, + ], + ...other table data + ]; + cy.get("#myGrid").agGridValidatePaginatedTable( + expectedPaginatedTableData, onlyColumns ={"Year", "Make", "Model"} + ); + }); +``` +
+
+ +### Validate Table in the Exact Order +This command will verify the table data is displayed exactly in the same order as the supplied expected table data. This will ONLY validate the first page of a paginated table. + +Definition: `.agGridValidateRowsExactOrder(actualTableData, expectedTableData)` + +Example: +```javascript +cy.get("#myGrid") +.getAgGridData() +.then((actualTableData) => { + cy.agGridValidateRowsExactOrder(actualTableData, expectedTableData); +}); +``` +
+
+ +### Validate Subset of Table Data +This command will validate a subset of the table data. Ideal for verifying one or more records, or verify records without specified columns. + +Definition:: `agGridValidateRowsSubset(actualTableData, expectedTableData)` + +Example: +```javascript + const expectedTableData = [ + { "Year": "2020", "Make": "Toyota", "Model": "Celica" }, + { "Year": "2020", "Make": "Ford", "Model": "Mondeo" }, + { "Year": "2020", "Make": "Porsche", "Model": "Boxter" }, + { "Year": "2020", "Make": "BMW", "Model": "3-series" }, + { "Year": "2020", "Make": "Mercedes", "Model": "GLC300" }, + ]; + cy.get(agGridSelector) + .getAgGridData({ onlyColumns: ["Year", "Make", "Model"] }) + .then((actualTableData) => { + cy.agGridValidateRowsSubset(actualTableData, expectedTableData); + }); + }); +``` +
+
+ +### Validate Empty Grid +This will verify the table data is empty. + +Definition:`agGridValidateEmptyTable(actualTableData, expectedTableData)` + +Example: +```javascript + cy.get(agGridSelector) + .getAgGridData() + .then((actualTableData) => { + cy.agGridValidateEmptyTable(actualTableData); + }); +``` + +## Limitations +* ~~Unable to validate deeply nested row groups~~ As of v2.x, using `.getAgGridElements()` you should be able to accomplish this. +* ~~Unable to validate deeply nested column groups~~ As of v2.x, using `.getAgGridElements()` you should be able to accomplish this. +* Unable to validate the entirety of an unlimited scrolling grid. +* Unable to validate data that is out of view. The DOM will register the ag grid data as it's scrolled into view. + * To combat this, in your code where the ag grid is called, check if the Cypress window is controlling the app and set the ag grid object to `.sizeColumnsToFit()`. You can see an example of this in the `app/grid.js` file of this repository. Read more [here](https://www.ag-grid.com/javascript-grid/column-sizing/#size-columns-to-fit) + * Example: + ```javascript + if(window.Cypress){ + this.api.sizeColumnsToFit(); + } + ``` +## Credit +A portion of the logic to retrieve table data was expanded upon from the project [Cypress-Get-Table](https://github.com/roggerfe/cypress-get-table) by [Rogger Fernandez](https://github.com/roggerfe). diff --git a/ag-grid-example-dom.png b/packages/cypress-ag-grid/ag-grid-example-dom.png similarity index 100% rename from ag-grid-example-dom.png rename to packages/cypress-ag-grid/ag-grid-example-dom.png diff --git a/ag-grid-example-filter-checkbox-menu.png b/packages/cypress-ag-grid/ag-grid-example-filter-checkbox-menu.png similarity index 100% rename from ag-grid-example-filter-checkbox-menu.png rename to packages/cypress-ag-grid/ag-grid-example-filter-checkbox-menu.png diff --git a/ag-grid-example-filter-text-floating-multi-condition.png b/packages/cypress-ag-grid/ag-grid-example-filter-text-floating-multi-condition.png similarity index 100% rename from ag-grid-example-filter-text-floating-multi-condition.png rename to packages/cypress-ag-grid/ag-grid-example-filter-text-floating-multi-condition.png diff --git a/ag-grid-example-filter-text-floating.png b/packages/cypress-ag-grid/ag-grid-example-filter-text-floating.png similarity index 100% rename from ag-grid-example-filter-text-floating.png rename to packages/cypress-ag-grid/ag-grid-example-filter-text-floating.png diff --git a/ag-grid-example-filter-text-menu.png b/packages/cypress-ag-grid/ag-grid-example-filter-text-menu.png similarity index 100% rename from ag-grid-example-filter-text-menu.png rename to packages/cypress-ag-grid/ag-grid-example-filter-text-menu.png diff --git a/ag-grid-example.png b/packages/cypress-ag-grid/ag-grid-example.png similarity index 100% rename from ag-grid-example.png rename to packages/cypress-ag-grid/ag-grid-example.png diff --git a/app/ag-grid.css b/packages/cypress-ag-grid/app/ag-grid.css similarity index 100% rename from app/ag-grid.css rename to packages/cypress-ag-grid/app/ag-grid.css diff --git a/app/ag-theme-alpine.css b/packages/cypress-ag-grid/app/ag-theme-alpine.css similarity index 100% rename from app/ag-theme-alpine.css rename to packages/cypress-ag-grid/app/ag-theme-alpine.css diff --git a/app/animation-wait/ag-owned.html b/packages/cypress-ag-grid/app/animation-wait/ag-owned.html similarity index 100% rename from app/animation-wait/ag-owned.html rename to packages/cypress-ag-grid/app/animation-wait/ag-owned.html diff --git a/app/animation-wait/animation-grid.js b/packages/cypress-ag-grid/app/animation-wait/animation-grid.js similarity index 100% rename from app/animation-wait/animation-grid.js rename to packages/cypress-ag-grid/app/animation-wait/animation-grid.js diff --git a/app/animation-wait/third-party-subtree.html b/packages/cypress-ag-grid/app/animation-wait/third-party-subtree.html similarity index 100% rename from app/animation-wait/third-party-subtree.html rename to packages/cypress-ag-grid/app/animation-wait/third-party-subtree.html diff --git a/app/data.json b/packages/cypress-ag-grid/app/data.json similarity index 100% rename from app/data.json rename to packages/cypress-ag-grid/app/data.json diff --git a/app/grid-basic.js b/packages/cypress-ag-grid/app/grid-basic.js similarity index 100% rename from app/grid-basic.js rename to packages/cypress-ag-grid/app/grid-basic.js diff --git a/app/grid-grouped.js b/packages/cypress-ag-grid/app/grid-grouped.js similarity index 100% rename from app/grid-grouped.js rename to packages/cypress-ag-grid/app/grid-grouped.js diff --git a/app/index.html b/packages/cypress-ag-grid/app/index.html similarity index 100% rename from app/index.html rename to packages/cypress-ag-grid/app/index.html diff --git a/app/v33/index.html b/packages/cypress-ag-grid/app/v33/index.html similarity index 100% rename from app/v33/index.html rename to packages/cypress-ag-grid/app/v33/index.html diff --git a/app/v34/index.html b/packages/cypress-ag-grid/app/v34/index.html similarity index 100% rename from app/v34/index.html rename to packages/cypress-ag-grid/app/v34/index.html diff --git a/cypress.config.js b/packages/cypress-ag-grid/cypress.config.js similarity index 100% rename from cypress.config.js rename to packages/cypress-ag-grid/cypress.config.js diff --git a/cypress/e2e/ag-grid-animation-wait.v35.cy.js b/packages/cypress-ag-grid/cypress/e2e/ag-grid-animation-wait.v35.cy.js similarity index 100% rename from cypress/e2e/ag-grid-animation-wait.v35.cy.js rename to packages/cypress-ag-grid/cypress/e2e/ag-grid-animation-wait.v35.cy.js diff --git a/cypress/e2e/ag-grid-data.v33.cy.js b/packages/cypress-ag-grid/cypress/e2e/ag-grid-data.v33.cy.js similarity index 100% rename from cypress/e2e/ag-grid-data.v33.cy.js rename to packages/cypress-ag-grid/cypress/e2e/ag-grid-data.v33.cy.js diff --git a/cypress/e2e/ag-grid-data.v34.cy.js b/packages/cypress-ag-grid/cypress/e2e/ag-grid-data.v34.cy.js similarity index 100% rename from cypress/e2e/ag-grid-data.v34.cy.js rename to packages/cypress-ag-grid/cypress/e2e/ag-grid-data.v34.cy.js diff --git a/cypress/e2e/ag-grid-data.v35.cy.js b/packages/cypress-ag-grid/cypress/e2e/ag-grid-data.v35.cy.js similarity index 100% rename from cypress/e2e/ag-grid-data.v35.cy.js rename to packages/cypress-ag-grid/cypress/e2e/ag-grid-data.v35.cy.js diff --git a/cypress/e2e/ag-grid-elements.v33.cy.js b/packages/cypress-ag-grid/cypress/e2e/ag-grid-elements.v33.cy.js similarity index 100% rename from cypress/e2e/ag-grid-elements.v33.cy.js rename to packages/cypress-ag-grid/cypress/e2e/ag-grid-elements.v33.cy.js diff --git a/cypress/e2e/ag-grid-elements.v34.cy.js b/packages/cypress-ag-grid/cypress/e2e/ag-grid-elements.v34.cy.js similarity index 100% rename from cypress/e2e/ag-grid-elements.v34.cy.js rename to packages/cypress-ag-grid/cypress/e2e/ag-grid-elements.v34.cy.js diff --git a/cypress/e2e/ag-grid-elements.v35.cy.js b/packages/cypress-ag-grid/cypress/e2e/ag-grid-elements.v35.cy.js similarity index 100% rename from cypress/e2e/ag-grid-elements.v35.cy.js rename to packages/cypress-ag-grid/cypress/e2e/ag-grid-elements.v35.cy.js diff --git a/cypress/e2e/shared/run-ag-grid-data-suite.js b/packages/cypress-ag-grid/cypress/e2e/shared/run-ag-grid-data-suite.js similarity index 100% rename from cypress/e2e/shared/run-ag-grid-data-suite.js rename to packages/cypress-ag-grid/cypress/e2e/shared/run-ag-grid-data-suite.js diff --git a/cypress/e2e/shared/run-ag-grid-elements-suite.js b/packages/cypress-ag-grid/cypress/e2e/shared/run-ag-grid-elements-suite.js similarity index 100% rename from cypress/e2e/shared/run-ag-grid-elements-suite.js rename to packages/cypress-ag-grid/cypress/e2e/shared/run-ag-grid-elements-suite.js diff --git a/cypress/fixtures/cardata.json b/packages/cypress-ag-grid/cypress/fixtures/cardata.json similarity index 100% rename from cypress/fixtures/cardata.json rename to packages/cypress-ag-grid/cypress/fixtures/cardata.json diff --git a/cypress/plugins/index.js b/packages/cypress-ag-grid/cypress/plugins/index.js similarity index 100% rename from cypress/plugins/index.js rename to packages/cypress-ag-grid/cypress/plugins/index.js diff --git a/cypress/support/commands.js b/packages/cypress-ag-grid/cypress/support/commands.js similarity index 100% rename from cypress/support/commands.js rename to packages/cypress-ag-grid/cypress/support/commands.js diff --git a/cypress/support/e2e.js b/packages/cypress-ag-grid/cypress/support/e2e.js similarity index 100% rename from cypress/support/e2e.js rename to packages/cypress-ag-grid/cypress/support/e2e.js diff --git a/packages/cypress-ag-grid/package.json b/packages/cypress-ag-grid/package.json new file mode 100644 index 0000000..f130789 --- /dev/null +++ b/packages/cypress-ag-grid/package.json @@ -0,0 +1,39 @@ +{ + "name": "cypress-ag-grid", + "version": "3.3.5", + "description": "Cypress plugin to interact with ag grid", + "main": "src/index.js", + "repository": { + "type": "git", + "url": "git+https://github.com/kpmck/cypress-ag-grid.git" + }, + "keywords": [ + "aggrid", + "ag-grid", + "cypress", + "cypress-io", + "e2e testing", + "cypress table", + "cypress ag grid", + "cypress aggrid" + ], + "scripts": { + "test": "npm run test:all", + "test:all": "npx cypress run --headless --spec \"cypress/e2e/*.cy.js\"", + "test:v33": "npx cypress run --headless --spec \"cypress/e2e/*.v33.cy.js\"", + "test:v34": "npx cypress run --headless --spec \"cypress/e2e/*.v34.cy.js\"", + "test:v35": "npx cypress run --headless --spec \"cypress/e2e/*.v35.cy.js\"", + "test:watch": "npx cypress open" + }, + "publishConfig": { + "access": "public" + }, + "author": "Kerry McKeever ", + "license": "MIT", + "dependencies": { + "@kpmck/ag-grid-core": "0.1.0" + }, + "devDependencies": { + "cypress": "^15.12.0" + } +} diff --git a/src/agGrid/agGridInteractions.js b/packages/cypress-ag-grid/src/agGrid/agGridInteractions.js similarity index 75% rename from src/agGrid/agGridInteractions.js rename to packages/cypress-ag-grid/src/agGrid/agGridInteractions.js index a4e0d7f..9a31a51 100644 --- a/src/agGrid/agGridInteractions.js +++ b/packages/cypress-ag-grid/src/agGrid/agGridInteractions.js @@ -1,99 +1,41 @@ /// -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) { +import { + extractAgGridData, + extractAgGridElements, + filterOperator, + filterTab, + sort, + waitForAgGridAnimation, +} from "@kpmck/ag-grid-core"; + +function getSingleAgGridRootElement(agGridElement) { + const rootElements = agGridElement.get(); + + if (rootElements.length < 1) { throw new Error(`Couldn't find the element ${agGridElement}`); } - const AG_GRID_ANIMATION_TIMEOUT_MS = 5000; - const agGridRootElement = agGridElement.get()[0]; - const animations = agGridRootElement.getAnimations({ subtree: true }); - - const agGridAnimations = animations.filter((animation) => { - const animationTarget = animation.effect?.target; - if ( - !animationTarget || - animationTarget.nodeType !== 1 || - !animationTarget.classList - ) { - return false; - } - - const hasAgGridClass = [...animationTarget.classList].some((className) => - className.startsWith("ag-") - ); - - return ( - animationTarget === agGridRootElement || - hasAgGridClass + if (rootElements.length > 1) { + throw new Error( + `Selector "${agGridElement.selector}" returned more than 1 element.` ); - }); - - // Filter out infinite animations (e.g. loading spinners) whose .finished - // promise never resolves per the Web Animations API spec. - const finiteAnimations = agGridAnimations.filter((animation) => { - const iterations = animation.effect?.getTiming?.()?.iterations; - return iterations !== Infinity; - }); + } - await Promise.race([ - Promise.all( - finiteAnimations.map(async (animation) => { - try { - await animation.finished; - } catch (error) { - if (error.name === "AbortError") return; - console.error("error", error, error.name); - throw error; - } - }) - ), - new Promise((resolve) => { - setTimeout(resolve, AG_GRID_ANIMATION_TIMEOUT_MS); - }), - ]); + return rootElements[0]; +} +export const agGridWaitForAnimation = async (agGridElement) => { + await waitForAgGridAnimation(getSingleAgGridRootElement(agGridElement)); 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); + return extractAgGridData(getSingleAgGridRootElement(agGridElement), options); }; /** @@ -102,127 +44,11 @@ export const getAgGridData = async (agGridElement, options = {}) => { * @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 }; - }, {}) + return extractAgGridElements( + getSingleAgGridRootElement(agGridElement), + options ); -} +}; /** * Retrieve the ag grid column header element based on its column name value diff --git a/src/agGrid/agGridValidations.js b/packages/cypress-ag-grid/src/agGrid/agGridValidations.js similarity index 100% rename from src/agGrid/agGridValidations.js rename to packages/cypress-ag-grid/src/agGrid/agGridValidations.js diff --git a/src/agGrid/filterOperator.enum.js b/packages/cypress-ag-grid/src/agGrid/filterOperator.enum.js similarity index 100% rename from src/agGrid/filterOperator.enum.js rename to packages/cypress-ag-grid/src/agGrid/filterOperator.enum.js diff --git a/src/agGrid/menuTab.enum.js b/packages/cypress-ag-grid/src/agGrid/menuTab.enum.js similarity index 100% rename from src/agGrid/menuTab.enum.js rename to packages/cypress-ag-grid/src/agGrid/menuTab.enum.js diff --git a/src/agGrid/sort.enum.js b/packages/cypress-ag-grid/src/agGrid/sort.enum.js similarity index 100% rename from src/agGrid/sort.enum.js rename to packages/cypress-ag-grid/src/agGrid/sort.enum.js diff --git a/src/helpers/arrayHelpers.js b/packages/cypress-ag-grid/src/helpers/arrayHelpers.js similarity index 100% rename from src/helpers/arrayHelpers.js rename to packages/cypress-ag-grid/src/helpers/arrayHelpers.js diff --git a/src/index.d.ts b/packages/cypress-ag-grid/src/index.d.ts similarity index 100% rename from src/index.d.ts rename to packages/cypress-ag-grid/src/index.d.ts diff --git a/src/index.js b/packages/cypress-ag-grid/src/index.js similarity index 100% rename from src/index.js rename to packages/cypress-ag-grid/src/index.js diff --git a/packages/playwright-ag-grid/README.md b/packages/playwright-ag-grid/README.md new file mode 100644 index 0000000..cc1233a --- /dev/null +++ b/packages/playwright-ag-grid/README.md @@ -0,0 +1,387 @@ +# playwright-ag-grid + +Playwright helpers for interacting with and validating AG Grid. + +## Table of Contents + +- [Installation](#installation) +- [Usage](#usage) + - [Create the Grid Helper](#create-the-grid-helper) + - [Getting Data From the Grid](#getting-data-from-the-grid) + - [Getting Select Row Data](#getting-select-row-data) + - [Editing Grid Cells](#editing-grid-cells) + - [Sorting Columns](#sorting-columns) + - [Pinning Columns](#pinning-columns) + - [Filter Options](#filter-options) + - [Filter by Text - Column Menu](#filter-by-text---column-menu) + - [Filter by Text - Floating Filter](#filter-by-text---floating-filter) + - [Filter by Checkbox - Column Menu](#filter-by-checkbox---column-menu) + - [Filtering - Localization and Internationalization](#filtering---localization-and-internationalization) + - [Add or Remove Columns](#add-or-remove-columns) + - [Validation Examples](#validation-examples) + - [Validate Paginated Table](#validate-paginated-table) + - [Validate Table in the Exact Order](#validate-table-in-the-exact-order) + - [Validate Subset of Table Data](#validate-subset-of-table-data) + - [Validate Empty Grid](#validate-empty-grid) +- [Limitations](#limitations) + +## Installation + +```bash +npm install playwright-ag-grid --save-dev +``` + +Then import the helper in your Playwright tests: + +```javascript +import { createAgGrid, filterOperator, sort } from "playwright-ag-grid"; +``` + +## Usage + +### Create the Grid Helper + +Create a helper instance from the top-level AG Grid locator, including headers and controls. + +```javascript +import { createAgGrid } from "playwright-ag-grid"; + +const grid = createAgGrid(page.locator("#myGrid")); +``` + +### Getting Data From the Grid + +Use `getData()` to read the displayed AG Grid rows as structured objects. + +```javascript +const grid = createAgGrid(page.locator("#myGrid")); +const tableData = await grid.getData(); +``` + +The returned value looks like: + +```json +[ + { "Year": "2020", "Make": "Toyota", "Model": "Celica" }, + { "Year": "2020", "Make": "Ford", "Model": "Mondeo" }, + { "Year": "2020", "Make": "Porsche", "Model": "Boxter" }, + { "Year": "2020", "Make": "BMW", "Model": "3-series" }, + { "Year": "2020", "Make": "Mercedes", "Model": "GLC300" } +] +``` + +### Getting Select Row Data + +To only return certain columns, pass `onlyColumns`: + +```javascript +const grid = createAgGrid(page.locator("#myGrid")); +const tableData = await grid.getData({ onlyColumns: ["Year", "Make"] }); +``` + +You can also request the raw header/row arrays instead of mapped objects: + +```javascript +const grid = createAgGrid(page.locator("#myGrid")); +const tableData = await grid.getData({ valuesArray: true }); +``` + +### Editing Grid Cells + +Playwright does not expose AG Grid cells as Cypress-style command subjects, so the package provides `getCellLocator()` for targeted cell interaction. + +```javascript +const grid = createAgGrid(page.locator("#myGrid2")); + +await grid.filterTextFloating({ + searchCriteria: { + columnName: "Make", + filterValue: "Porsche", + operator: filterOperator.equals, + }, + hasApplyButton: true, +}); + +const priceCell = await grid.getCellLocator( + { Make: "Porsche", Price: "72000" }, + "Price" +); + +await priceCell.dblclick(); +await priceCell.locator("input").fill("66000"); +await priceCell.locator("input").press("Enter"); +``` + +### Sorting Columns + +Use `sortColumn(columnName, sortDirection)`: + +```javascript +const grid = createAgGrid(page.locator("#myGrid")); +await grid.sortColumn("Model", "descending"); +``` + +You can also use the exported enum values: + +```javascript +await grid.sortColumn("Model", sort.ascending); +``` + +### Pinning Columns + +Use `pinColumn(columnName, pin)` where `pin` is `"left"`, `"right"`, or `null`. + +```javascript +const grid = createAgGrid(page.locator("#myGrid")); + +await grid.pinColumn("Model", "left"); +await grid.pinColumn("Model", "right"); +await grid.pinColumn("Model", null); +``` + +### Filter Options + +The filtering commands accept an options object shaped like: + +```javascript +{ + searchCriteria: { + columnName: string; + filterValue: string; + operator?: string; + searchInputIndex?: number; + operatorIndex?: number; + isMultiFilter?: boolean; + } | Array<{ + columnName: string; + filterValue: string; + operator?: string; + searchInputIndex?: number; + operatorIndex?: number; + isMultiFilter?: boolean; + }>; + hasApplyButton?: boolean; + noMenuTabs?: boolean; + selectAllLocaleText?: string; +} +``` + +Option notes: + +- `searchCriteria.columnName`: column to filter. +- `searchCriteria.filterValue`: value to type or select. +- `searchCriteria.operator`: optional AG Grid operator text such as `Equals`, `Contains`, or `Between`. +- `searchCriteria.searchInputIndex`: optional input index when multiple visible inputs exist. +- `searchCriteria.operatorIndex`: optional operator picker index when multiple visible operators exist. +- `searchCriteria.isMultiFilter`: optional flag for floating filters using checkbox values rather than free-form text. +- `hasApplyButton`: use `true` when the filter UI has an explicit Apply button. +- `noMenuTabs`: use `true` for grids that render a menu list instead of filter tabs. +- `selectAllLocaleText`: localized text for the checkbox filter's Select All entry. + +### Filter by Text - Column Menu + +Use `filterTextMenu(options)` to filter via the column menu: + +```javascript +const grid = createAgGrid(page.locator("#myGrid")); + +await grid.filterTextMenu({ + searchCriteria: [ + { + columnName: "Model", + filterValue: "GLC300", + operator: filterOperator.equals, + }, + { + columnName: "Make", + filterValue: "Mercedes", + operator: filterOperator.equals, + }, + ], + hasApplyButton: true, +}); +``` + +### Filter by Text - Floating Filter + +Use `filterTextFloating(options)` to filter via a column's floating filter: + +```javascript +const grid = createAgGrid(page.locator("#myGrid")); + +await grid.filterTextFloating({ + searchCriteria: { + columnName: "Make", + filterValue: "Ford", + }, + hasApplyButton: true, +}); +``` + +For multiple conditions in the same floating filter: + +```javascript +await grid.filterTextFloating({ + searchCriteria: { + columnName: "Make", + filterValue: "B", + searchInputIndex: 0, + }, + hasApplyButton: true, +}); + +await grid.filterTextFloating({ + searchCriteria: { + columnName: "Make", + filterValue: "MW", + searchInputIndex: 1, + }, + hasApplyButton: true, +}); +``` + +For `Between`, pass two entries for the same column: + +```javascript +await grid.filterTextFloating({ + searchCriteria: [ + { + columnName: "Year", + filterValue: "1990", + operator: filterOperator.inRange, + }, + { + columnName: "Year", + filterValue: "2011", + operator: filterOperator.inRange, + }, + ], + hasApplyButton: true, +}); +``` + +### Filter by Checkbox - Column Menu + +Use `filterCheckboxMenu(options)` to filter by checkbox values from the column menu: + +```javascript +const grid = createAgGrid(page.locator("#myGrid")); + +await grid.filterCheckboxMenu({ + searchCriteria: { + columnName: "Model", + filterValue: "2002", + }, + hasApplyButton: true, +}); +``` + +Multiple checkbox values: + +```javascript +await grid.filterCheckboxMenu({ + searchCriteria: [ + { columnName: "Model", filterValue: "2002" }, + { columnName: "Model", filterValue: "3-series" }, + ], + hasApplyButton: true, +}); +``` + +### Filtering - Localization and Internationalization + +When filtering by checkbox, the helper first deselects the Select All entry so only the requested values remain selected. For localized grids, pass the localized Select All text: + +```javascript +await grid.filterCheckboxMenu({ + searchCriteria: { + columnName: "Model", + filterValue: "2002", + }, + selectAllLocaleText: "Tout Sélectionner", + hasApplyButton: true, +}); +``` + +### Add or Remove Columns + +Use `toggleColumnFromSideBar(columnName, doRemove)` to toggle columns from the sidebar: + +```javascript +const grid = createAgGrid(page.locator("#myGrid")); + +await grid.toggleColumnFromSideBar("Year", true); +await grid.toggleColumnFromSideBar("Year", false); +``` + +## Validation Examples + +Unlike the Cypress package, the Playwright package does not currently register assertion helpers. The intended pattern is to use `await grid.getData()` together with Playwright's `expect`. + +### Validate Paginated Table + +```javascript +import { expect } from "@playwright/test"; +import { createAgGrid } from "playwright-ag-grid"; + +const expectedPaginatedTableData = [ + [ + { Year: "2020", Make: "Toyota", Model: "Celica" }, + { Year: "2020", Make: "Ford", Model: "Mondeo" }, + ], + [ + { Year: "2020", Make: "Honda", Model: "Civic" }, + { Year: "2020", Make: "Honda", Model: "Accord" }, + ], +]; + +const grid = createAgGrid(page.locator("#myGrid")); + +for (const expectedPage of expectedPaginatedTableData) { + await expect(await grid.getData({ onlyColumns: ["Year", "Make", "Model"] })) + .toEqual(expectedPage); + await page.locator("#myGrid .ag-icon-next").click(); +} +``` + +### Validate Table in the Exact Order + +```javascript +import { expect } from "@playwright/test"; + +const grid = createAgGrid(page.locator("#myGrid")); +const actualTableData = await grid.getData(); + +await expect(actualTableData).toEqual(expectedTableData); +``` + +### Validate Subset of Table Data + +```javascript +import { expect } from "@playwright/test"; + +const grid = createAgGrid(page.locator("#myGrid")); +const actualTableData = await grid.getData({ onlyColumns: ["Year", "Make", "Model"] }); + +for (const expectedRow of expectedTableData) { + expect(actualTableData).toContainEqual(expectedRow); +} +``` + +### Validate Empty Grid + +```javascript +import { expect } from "@playwright/test"; + +const grid = createAgGrid(page.locator("#myGrid")); +const actualTableData = await grid.getData(); + +await expect(actualTableData).toEqual([]); +``` + +## Limitations + +- Validation helpers are not yet packaged as Playwright-specific assertion methods; use Playwright `expect` with `getData()`. +- `getCellLocator()` is the supported element-interaction path; there is not currently a `getAgGridElements()`-style API returning live DOM element maps. +- Unlimited scrolling grids are not fully supported when data is outside the rendered DOM. +- Data outside the current rendered viewport is not available until AG Grid renders it into the DOM. diff --git a/packages/playwright-ag-grid/package.json b/packages/playwright-ag-grid/package.json new file mode 100644 index 0000000..04e7be4 --- /dev/null +++ b/packages/playwright-ag-grid/package.json @@ -0,0 +1,41 @@ +{ + "name": "playwright-ag-grid", + "version": "0.1.0", + "description": "Playwright helpers for interacting with AG Grid", + "type": "module", + "main": "src/index.js", + "types": "src/index.d.ts", + "exports": { + ".": "./src/index.js" + }, + "scripts": { + "test": "npm run test:all", + "test:all": "playwright test", + "test:v33": "playwright test tests/ag-grid-data.v33.spec.js tests/ag-grid-elements.v33.spec.js", + "test:v34": "playwright test tests/ag-grid-data.v34.spec.js tests/ag-grid-elements.v34.spec.js", + "test:v35": "playwright test tests/ag-grid-data.v35.spec.js tests/ag-grid-elements.v35.spec.js tests/ag-grid-animation-wait.v35.spec.js", + "test:watch": "playwright test --ui" + }, + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/kpmck/cypress-ag-grid.git" + }, + "keywords": [ + "ag-grid", + "aggrid", + "playwright", + "testing", + "e2e" + ], + "author": "Kerry McKeever ", + "license": "MIT", + "dependencies": { + "@kpmck/ag-grid-core": "0.1.0" + }, + "devDependencies": { + "@playwright/test": "^1.52.0" + } +} diff --git a/packages/playwright-ag-grid/playwright.config.js b/packages/playwright-ag-grid/playwright.config.js new file mode 100644 index 0000000..0653717 --- /dev/null +++ b/packages/playwright-ag-grid/playwright.config.js @@ -0,0 +1,21 @@ +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { defineConfig } from "@playwright/test"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +export default defineConfig({ + testDir: path.join(__dirname, "tests"), + timeout: 30000, + use: { + baseURL: "http://127.0.0.1:4173", + headless: true, + }, + webServer: { + command: "node ./tests/server.mjs", + cwd: __dirname, + port: 4173, + reuseExistingServer: true, + }, +}); diff --git a/packages/playwright-ag-grid/src/index.d.ts b/packages/playwright-ag-grid/src/index.d.ts new file mode 100644 index 0000000..ca83ecc --- /dev/null +++ b/packages/playwright-ag-grid/src/index.d.ts @@ -0,0 +1,33 @@ +export declare const filterOperator: Record; +export declare const sort: Record; + +export interface SearchCriteria { + columnName: string; + filterValue: string; + operator?: string; + searchInputIndex?: number; + operatorIndex?: number; + isMultiFilter?: boolean; +} + +export interface FilterOptions { + searchCriteria: SearchCriteria | SearchCriteria[]; + hasApplyButton?: boolean; + noMenuTabs?: boolean; + selectAllLocaleText?: string; +} + +export declare class PlaywrightAgGrid { + constructor(rootLocator: any); + waitForAnimation(options?: object): Promise; + getData(options?: object): Promise; + sortColumn(columnName: string, sortDirection: string): Promise; + pinColumn(columnName: string, pin?: "left" | "right" | null): Promise; + filterTextMenu(options: FilterOptions): Promise; + filterTextFloating(options: FilterOptions): Promise; + filterCheckboxMenu(options: FilterOptions): Promise; + toggleColumnFromSideBar(columnName: string, doRemove: boolean): Promise; + getCellLocator(rowMatcher: Record, columnName: string): Promise; +} + +export declare function createAgGrid(rootLocator: any): PlaywrightAgGrid; diff --git a/packages/playwright-ag-grid/src/index.js b/packages/playwright-ag-grid/src/index.js new file mode 100644 index 0000000..3a2b81b --- /dev/null +++ b/packages/playwright-ag-grid/src/index.js @@ -0,0 +1,393 @@ +import { + browserExtractAgGrid, + browserWaitForAgGridAnimation, + filterOperator, + filterTab, + sort, +} from "@kpmck/ag-grid-core"; + +function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +async function getHeaderTextLocator(rootLocator, columnName) { + return rootLocator + .locator(".ag-header-cell-text") + .filter({ hasText: new RegExp(`^${escapeRegExp(columnName)}$`) }) + .first(); +} + +async function getColumnHeaderMeta(rootLocator, columnName) { + const metadata = await rootLocator.evaluate((root, targetColumnName) => { + const headerTexts = [...root.querySelectorAll(".ag-header-cell-text")]; + const matchingHeaderText = headerTexts.find( + (element) => element.textContent.trim() === targetColumnName + ); + + if (!matchingHeaderText) { + return null; + } + + const headerCell = matchingHeaderText.closest(".ag-header-cell"); + const visibleHeaderCells = [...root.querySelectorAll(".ag-header-row-column .ag-header-cell")] + .filter((element) => element.offsetParent !== null); + const headerPosition = headerCell + ? visibleHeaderCells.indexOf(headerCell) + : -1; + + return { + columnIndex: headerCell?.getAttribute("aria-colindex") ?? null, + headerPosition, + }; + }, columnName); + + if (!metadata) { + throw new Error(`Unable to find AG Grid column "${columnName}".`); + } + + return metadata; +} + +async function getHeaderCellLocator(rootLocator, columnName) { + const { columnIndex } = await getColumnHeaderMeta(rootLocator, columnName); + if (!columnIndex) { + throw new Error(`Unable to resolve a header cell for "${columnName}".`); + } + + return rootLocator.locator(`.ag-header-row-column .ag-header-cell[aria-colindex="${columnIndex}"]`).first(); +} + +async function getMenuTabLocator(rootLocator, tabName) { + return rootLocator.locator(".ag-tab").locator(`.ag-icon-${tabName}`).locator("xpath=ancestor::span[1]").first(); +} + +async function maybeCloseMenuTab(rootLocator, noMenuTabs = false) { + if (noMenuTabs) { + return; + } + + const tabs = rootLocator.locator(".ag-tab"); + if ((await tabs.count()) === 0) { + await rootLocator.evaluate(browserWaitForAgGridAnimation); + return; + } + + await (await getMenuTabLocator(rootLocator, filterTab.filter)).click(); +} + +async function getFloatingFilterButton(rootLocator, columnName) { + const { columnIndex, headerPosition } = await getColumnHeaderMeta(rootLocator, columnName); + + const usesV35FloatingFilterRow = + (await rootLocator.locator(".ag-header-row-filter").count()) > 0; + + let buttonLocator = usesV35FloatingFilterRow + ? rootLocator.locator( + `.ag-header-row-filter .ag-header-cell[aria-colindex="${columnIndex}"] .ag-floating-filter-button:visible` + ) + : rootLocator.locator( + `.ag-header-row-column-filter .ag-header-cell[aria-colindex="${columnIndex}"] .ag-floating-filter-button-button:visible` + ); + + if ((await buttonLocator.count()) === 0 && headerPosition > -1) { + buttonLocator = usesV35FloatingFilterRow + ? rootLocator.locator(".ag-header-row-filter .ag-floating-filter-button:visible").nth(headerPosition) + : rootLocator + .locator(".ag-header-row-column-filter .ag-floating-filter-button-button:visible") + .nth(headerPosition); + } else { + buttonLocator = buttonLocator.first(); + } + + return buttonLocator; +} + +async function getFilterColumnButton(rootLocator, columnName, isFloatingFilter = false) { + if (isFloatingFilter) { + return getFloatingFilterButton(rootLocator, columnName); + } + + const headerCell = await getHeaderCellLocator(rootLocator, columnName); + await headerCell.hover(); + return headerCell.locator(".ag-header-cell-filter-button").first(); +} + +async function toggleColumnCheckboxFilter(rootLocator, filterValue, doSelect) { + const label = rootLocator + .page() + .locator(".ag-popup .ag-input-field-label:visible") + .filter({ + hasText: new RegExp(`^${escapeRegExp(filterValue)}$`), + }) + .first(); + await label.waitFor({ state: "visible" }); + const toggle = label.locator("xpath=following-sibling::div[1]").first(); + const checkbox = label.locator("xpath=following-sibling::div[1]//input").first(); + + const isChecked = await checkbox.isChecked(); + if (isChecked !== doSelect) { + await toggle.click({ force: true }); + } +} + +async function filterBySearchTerm(rootLocator, options) { + const filterValue = options.searchCriteria.filterValue; + const operator = options.searchCriteria.operator; + const searchInputIndex = options.searchCriteria.searchInputIndex || 0; + const operatorIndex = + options.searchCriteria.operatorIndex ?? + (operator === filterOperator.inRange ? 0 : searchInputIndex); + const isMultiFilter = options.searchCriteria.isMultiFilter; + + if (operator) { + const picker = rootLocator + .locator(".ag-filter .ag-picker-field-wrapper:visible") + .nth(operatorIndex); + await rootLocator.evaluate(browserWaitForAgGridAnimation); + await picker.click(); + await rootLocator.locator(".ag-popup .ag-list span").filter({ + hasText: new RegExp(`^${escapeRegExp(operator)}$`), + }).first().click(); + } + + if (isMultiFilter) { + const selectAllText = options.selectAllLocaleText || "(Select All)"; + await toggleColumnCheckboxFilter(rootLocator, selectAllText, false); + const miniFilterInput = rootLocator + .page() + .locator(".ag-popup-child input:not([type='radio']):not([type='checkbox']):visible") + .first(); + if (await miniFilterInput.count()) { + await miniFilterInput.fill(""); + await miniFilterInput.type(`${filterValue}`); + } + } + + if ( + !isMultiFilter && + operator !== filterOperator.blank && + operator !== filterOperator.notBlank + ) { + const input = rootLocator + .locator(".ag-popup-child input:not([type='radio']):not([type='checkbox']):visible") + .nth(searchInputIndex); + await input.fill(""); + await input.type(`${filterValue}`); + await input.press("Enter"); + } + + if (isMultiFilter) { + await toggleColumnCheckboxFilter(rootLocator, filterValue, true); + } +} + +function normalizeFloatingFilterSearchCriteria(searchCriteria) { + const betweenInputIndexes = new Map(); + + return searchCriteria.map((criteria) => { + if ( + criteria.operator !== filterOperator.inRange || + criteria.searchInputIndex !== undefined + ) { + return criteria; + } + + const criteriaKey = `${criteria.columnName}::${criteria.operator}`; + const nextInputIndex = betweenInputIndexes.get(criteriaKey) || 0; + betweenInputIndexes.set(criteriaKey, nextInputIndex + 1); + + return { ...criteria, searchInputIndex: nextInputIndex }; + }); +} + +function groupFloatingFilterSearchCriteria(searchCriteria) { + const groupedCriteria = []; + + searchCriteria.forEach((criteria) => { + const lastGroup = groupedCriteria[groupedCriteria.length - 1]; + + if ( + criteria.operator === filterOperator.inRange && + lastGroup && + lastGroup[0].columnName === criteria.columnName && + lastGroup[0].operator === criteria.operator + ) { + lastGroup.push(criteria); + return; + } + + groupedCriteria.push([criteria]); + }); + + return groupedCriteria; +} + +async function applyColumnFilter(rootLocator, hasApplyButton, noMenuTabs) { + if (hasApplyButton) { + await rootLocator.locator(".ag-filter-apply-panel-button").getByText("Apply", { exact: true }).click(); + } + + await maybeCloseMenuTab(rootLocator, noMenuTabs); + await rootLocator.page().keyboard.press("Escape"); +} + +export class PlaywrightAgGrid { + constructor(rootLocator) { + this.rootLocator = rootLocator; + } + + async waitForAnimation(options = {}) { + await this.rootLocator.evaluate(browserWaitForAgGridAnimation, options); + } + + async getData(options = {}) { + await this.rootLocator.evaluate(browserWaitForAgGridAnimation, options); + return this.rootLocator.evaluate(browserExtractAgGrid, options); + } + + async sortColumn(columnName, sortDirection) { + let normalized = sortDirection; + + if (normalized.toLowerCase() === "ascending") { + normalized = "asc"; + } else if (normalized.toLowerCase() === "descending") { + normalized = "desc"; + } + + if (normalized !== sort.ascending && normalized !== sort.descending) { + throw new Error("sortDirection must be either 'asc' or 'desc'."); + } + + const headerText = await getHeaderTextLocator(this.rootLocator, columnName); + const container = headerText.locator( + "xpath=ancestor::*[contains(@class, 'ag-cell-label-container')][1]" + ); + + for (let attempts = 0; attempts < 3; attempts += 1) { + const className = (await container.getAttribute("class")) || ""; + if (className.includes(`ag-header-cell-sorted-${normalized}`)) { + return; + } + await headerText.click(); + } + } + + async pinColumn(columnName, pin) { + const headerCell = await getHeaderCellLocator(this.rootLocator, columnName); + await headerCell.hover(); + await headerCell.locator(".ag-header-cell-menu-button").click(); + if ((await this.rootLocator.locator(".ag-tab").count()) > 0) { + await (await getMenuTabLocator(this.rootLocator, filterTab.general)).click(); + } + await this.rootLocator.locator(".ag-menu-option").getByText("Pin Column", { exact: true }).click(); + + const selectedOption = + pin === "left" ? "Pin Left" : pin === "right" ? "Pin Right" : "No Pin"; + + await this.rootLocator.locator(".ag-menu-option").getByText(selectedOption, { exact: true }).click(); + } + + async filterTextMenu(options) { + const criteriaList = Array.isArray(options.searchCriteria) + ? options.searchCriteria + : [options.searchCriteria]; + + for (const searchCriteria of criteriaList) { + const optionSet = { ...options, searchCriteria }; + await (await getFilterColumnButton(this.rootLocator, searchCriteria.columnName)).click(); + await filterBySearchTerm(this.rootLocator, optionSet); + await applyColumnFilter(this.rootLocator, options.hasApplyButton, options.noMenuTabs); + } + } + + async filterTextFloating(options) { + const criteriaList = Array.isArray(options.searchCriteria) + ? normalizeFloatingFilterSearchCriteria(options.searchCriteria) + : [options.searchCriteria]; + + const groups = groupFloatingFilterSearchCriteria(criteriaList); + + for (const group of groups) { + await (await getFilterColumnButton(this.rootLocator, group[0].columnName, true)).click(); + + for (let index = 0; index < group.length; index += 1) { + const criteria = group[index]; + await filterBySearchTerm(this.rootLocator, { + ...options, + searchCriteria: index === 0 ? criteria : { ...criteria, operator: undefined }, + }); + } + + await applyColumnFilter(this.rootLocator, options.hasApplyButton, options.noMenuTabs); + } + } + + async filterCheckboxMenu(options) { + const criteriaList = Array.isArray(options.searchCriteria) + ? options.searchCriteria + : [options.searchCriteria]; + + for (const searchCriteria of criteriaList) { + await (await getFilterColumnButton(this.rootLocator, searchCriteria.columnName)).click(); + await toggleColumnCheckboxFilter( + this.rootLocator, + options.selectAllLocaleText || "(Select All)", + false + ); + await toggleColumnCheckboxFilter(this.rootLocator, searchCriteria.filterValue, true); + await applyColumnFilter(this.rootLocator, options.hasApplyButton, options.noMenuTabs); + } + } + + async toggleColumnFromSideBar(columnName, doRemove) { + const columnFilterInput = this.rootLocator.locator( + ".ag-column-select-header-filter-wrapper input" + ).first(); + + if (!(await columnFilterInput.isVisible())) { + await this.rootLocator.page().locator(".ag-side-buttons span").getByText("Columns", { exact: true }).click(); + } + + await this.waitForAnimation(); + await columnFilterInput.fill(columnName); + const checkbox = this.rootLocator.page() + .locator(".ag-column-select-column-label") + .filter({ hasText: new RegExp(`^${escapeRegExp(columnName)}$`) }) + .first() + .locator("xpath=ancestor::*[1]//input") + .first(); + + if (doRemove) { + await checkbox.uncheck({ force: true }); + } else { + await checkbox.check({ force: true }); + } + } + + async getCellLocator(rowMatcher, columnName) { + const rows = await this.getData(); + const rowIndex = rows.findIndex((row) => + Object.entries(rowMatcher).every(([key, value]) => row[key] === value) + ); + + if (rowIndex === -1) { + throw new Error(`Unable to find row matching ${JSON.stringify(rowMatcher)}.`); + } + + const valuesArray = await this.getData({ valuesArray: true }); + const columnIndex = valuesArray.headers.indexOf(columnName); + + if (columnIndex === -1) { + throw new Error(`Unable to find column "${columnName}".`); + } + + const visibleRows = this.rootLocator.locator(".ag-center-cols-clipper .ag-row:not(.ag-opacity-zero), .ag-center-cols-viewport .ag-row:not(.ag-opacity-zero)"); + return visibleRows.nth(rowIndex).locator(".ag-cell").nth(columnIndex); + } +} + +export function createAgGrid(rootLocator) { + return new PlaywrightAgGrid(rootLocator); +} + +export { filterOperator, sort }; diff --git a/packages/playwright-ag-grid/tests/ag-grid-animation-wait.v35.spec.js b/packages/playwright-ag-grid/tests/ag-grid-animation-wait.v35.spec.js new file mode 100644 index 0000000..97e04a6 --- /dev/null +++ b/packages/playwright-ag-grid/tests/ag-grid-animation-wait.v35.spec.js @@ -0,0 +1,52 @@ +import { expect, test } from "@playwright/test"; + +import { createAgGrid } from "../src/index.js"; + +test.describe("agGridWaitForAnimation", () => { + test("waits for AG Grid-owned animations to finish", async ({ page }) => { + await page.goto("/animation-wait/ag-owned.html"); + await page.locator(".ag-cell").first().waitFor({ state: "visible" }); + + await page.evaluate(() => { + window.startAnimationWaitScenario(); + window.__animationProbe.waitStartedAt = Date.now(); + }); + + const grid = createAgGrid(page.locator("#myGrid")); + await grid.waitForAnimation(); + + const probe = await page.evaluate(() => ({ + ...window.__animationProbe, + elapsedMs: Date.now() - window.__animationProbe.waitStartedAt, + })); + + expect(probe.agStarted).toBe(true); + expect(probe.agFinished).toBe(true); + expect(probe.elapsedMs).toBeGreaterThan(200); + expect(probe.elapsedMs).toBeLessThan(2000); + }); + + test("ignores third-party subtree animations whose finished promise never resolves", async ({ page }) => { + await page.goto("/animation-wait/third-party-subtree.html"); + await page.locator(".ag-cell").first().waitFor({ state: "visible" }); + + await page.evaluate(() => { + window.startAnimationWaitScenario(); + window.__animationProbe.waitStartedAt = Date.now(); + }); + + expect(await page.locator("#myGrid .os-scrollbar-handle").count()).toBeGreaterThan(0); + + const grid = createAgGrid(page.locator("#myGrid")); + await grid.waitForAnimation(); + + const probe = await page.evaluate(() => ({ + ...window.__animationProbe, + elapsedMs: Date.now() - window.__animationProbe.waitStartedAt, + })); + + expect(probe.agFinished).toBe(true); + expect(probe.thirdPartyInstalled).toBe(true); + expect(probe.elapsedMs).toBeLessThan(2000); + }); +}); diff --git a/packages/playwright-ag-grid/tests/ag-grid-data.v33.spec.js b/packages/playwright-ag-grid/tests/ag-grid-data.v33.spec.js new file mode 100644 index 0000000..4fbdf55 --- /dev/null +++ b/packages/playwright-ag-grid/tests/ag-grid-data.v33.spec.js @@ -0,0 +1,6 @@ +import { runAgGridDataSuite } from "./shared/run-ag-grid-data-suite.js"; + +runAgGridDataSuite({ + pagePath: "/v33/index.html", + versionLabel: "v33", +}); diff --git a/packages/playwright-ag-grid/tests/ag-grid-data.v34.spec.js b/packages/playwright-ag-grid/tests/ag-grid-data.v34.spec.js new file mode 100644 index 0000000..4e8acf2 --- /dev/null +++ b/packages/playwright-ag-grid/tests/ag-grid-data.v34.spec.js @@ -0,0 +1,6 @@ +import { runAgGridDataSuite } from "./shared/run-ag-grid-data-suite.js"; + +runAgGridDataSuite({ + pagePath: "/v34/index.html", + versionLabel: "v34", +}); diff --git a/packages/playwright-ag-grid/tests/ag-grid-data.v35.spec.js b/packages/playwright-ag-grid/tests/ag-grid-data.v35.spec.js new file mode 100644 index 0000000..2020b05 --- /dev/null +++ b/packages/playwright-ag-grid/tests/ag-grid-data.v35.spec.js @@ -0,0 +1,6 @@ +import { runAgGridDataSuite } from "./shared/run-ag-grid-data-suite.js"; + +runAgGridDataSuite({ + pagePath: "/index.html", + versionLabel: "v35", +}); diff --git a/packages/playwright-ag-grid/tests/ag-grid-elements.v33.spec.js b/packages/playwright-ag-grid/tests/ag-grid-elements.v33.spec.js new file mode 100644 index 0000000..12a3e31 --- /dev/null +++ b/packages/playwright-ag-grid/tests/ag-grid-elements.v33.spec.js @@ -0,0 +1,6 @@ +import { runAgGridElementsSuite } from "./shared/run-ag-grid-elements-suite.js"; + +runAgGridElementsSuite({ + pagePath: "/v33/index.html", + versionLabel: "v33", +}); diff --git a/packages/playwright-ag-grid/tests/ag-grid-elements.v34.spec.js b/packages/playwright-ag-grid/tests/ag-grid-elements.v34.spec.js new file mode 100644 index 0000000..c188e23 --- /dev/null +++ b/packages/playwright-ag-grid/tests/ag-grid-elements.v34.spec.js @@ -0,0 +1,6 @@ +import { runAgGridElementsSuite } from "./shared/run-ag-grid-elements-suite.js"; + +runAgGridElementsSuite({ + pagePath: "/v34/index.html", + versionLabel: "v34", +}); diff --git a/packages/playwright-ag-grid/tests/ag-grid-elements.v35.spec.js b/packages/playwright-ag-grid/tests/ag-grid-elements.v35.spec.js new file mode 100644 index 0000000..3ac2c18 --- /dev/null +++ b/packages/playwright-ag-grid/tests/ag-grid-elements.v35.spec.js @@ -0,0 +1,6 @@ +import { runAgGridElementsSuite } from "./shared/run-ag-grid-elements-suite.js"; + +runAgGridElementsSuite({ + pagePath: "/index.html", + versionLabel: "v35", +}); diff --git a/packages/playwright-ag-grid/tests/server.mjs b/packages/playwright-ag-grid/tests/server.mjs new file mode 100644 index 0000000..a4905be --- /dev/null +++ b/packages/playwright-ag-grid/tests/server.mjs @@ -0,0 +1,47 @@ +import http from "node:http"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const appRoot = path.resolve(__dirname, "../../cypress-ag-grid/app"); +const port = 4173; + +const contentTypes = { + ".css": "text/css; charset=utf-8", + ".html": "text/html; charset=utf-8", + ".js": "application/javascript; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".png": "image/png", +}; + +http + .createServer((req, res) => { + const requestPath = req.url === "/" ? "/index.html" : req.url; + const safePath = path.normalize(decodeURIComponent(requestPath)).replace(/^(\.\.[/\\])+/, ""); + const filePath = path.join(appRoot, safePath); + + if (!filePath.startsWith(appRoot)) { + res.writeHead(403); + res.end("Forbidden"); + return; + } + + fs.readFile(filePath, (error, data) => { + if (error) { + res.writeHead(error.code === "ENOENT" ? 404 : 500); + res.end(error.code === "ENOENT" ? "Not found" : "Server error"); + return; + } + + res.writeHead(200, { + "Content-Type": + contentTypes[path.extname(filePath)] || "application/octet-stream", + }); + res.end(data); + }); + }) + .listen(port, "127.0.0.1", () => { + console.log(`Playwright AG Grid test server listening on ${port}`); + }); diff --git a/packages/playwright-ag-grid/tests/shared/fixtures.js b/packages/playwright-ag-grid/tests/shared/fixtures.js new file mode 100644 index 0000000..3e92b6e --- /dev/null +++ b/packages/playwright-ag-grid/tests/shared/fixtures.js @@ -0,0 +1,109 @@ +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const cardataPath = path.resolve( + __dirname, + "../../../cypress-ag-grid/cypress/fixtures/cardata.json" +); + +export const agGridSelector = "#myGrid"; +export const agGridElementsSelector = "#myGrid2"; + +export const pageSize = 5; + +export const cardataFixture = JSON.parse( + fs.readFileSync(cardataPath, "utf-8") +); + +export const expectedPaginatedTableData = [ + [ + { Year: "2020", Make: "Toyota", Model: "Celica", Condition: "fair", Price: "35000" }, + { Year: "2020", Make: "Ford", Model: "Mondeo", Condition: "excellent", Price: "32000" }, + { Year: "2020", Make: "Porsche", Model: "Boxter", Condition: "good", Price: "72000" }, + { Year: "2020", Make: "BMW", Model: "3-series", Condition: "fair", Price: "45000" }, + { Year: "2020", Make: "Mercedes", Model: "GLC300", Condition: "good", Price: "53000" }, + ], + [ + { Year: "2020", Make: "Honda", Model: "Civic", Condition: "poor", Price: "22000" }, + { Year: "2020", Make: "Honda", Model: "Accord", Condition: "poor", Price: "32000" }, + { Year: "2020", Make: "Ford", Model: "Taurus", Condition: "excellent", Price: "19000" }, + { Year: "2020", Make: "Hyundai", Model: "Elantra", Condition: "good", Price: "22000" }, + { Year: "2020", Make: "Toyota", Model: "Celica", Condition: "poor", Price: "5000" }, + ], + [ + { Year: "2020", Make: "Ford", Model: "Mondeo", Condition: "good", Price: "25000" }, + { Year: "2020", Make: "Porsche", Model: "Boxter", Condition: "good", Price: "99000" }, + { Year: "2020", Make: "BMW", Model: "3-series", Condition: "poor", Price: "32000" }, + { Year: "2020", Make: "Mercedes", Model: "GLC300", Condition: "excellent", Price: "35000" }, + { Year: "2011", Make: "Honda", Model: "Civic", Condition: "good", Price: "9000" }, + ], + [ + { Year: "2020", Make: "Honda", Model: "Accord", Condition: "good", Price: "34000" }, + { Year: "1990", Make: "Ford", Model: "Taurus", Condition: "excellent", Price: "900" }, + { Year: "2020", Make: "Hyundai", Model: "Elantra", Condition: "fair", Price: "3000" }, + { Year: "2020", Make: "BMW", Model: "2002", Condition: "excellent", Price: "88001" }, + { Year: "2023", Make: "Hyundai", Model: "Santa Fe", Condition: "excellent", Price: "" }, + ], +]; + +export const expectedFirstPageTableData = expectedPaginatedTableData[0]; + +export const expectedPorscheRowsBeforeEditing = [ + { Year: "2020", Make: "Porsche", Model: "Boxter", Price: "72000" }, + { Year: "2020", Make: "Porsche", Model: "Boxter", Price: "99000" }, +]; + +export const expectedPorscheRowsAfterEditing = [ + { Year: "2020", Make: "Porsche", Model: "Boxter", Price: "66000" }, + { Year: "2020", Make: "Porsche", Model: "Boxter", Price: "99000" }, +]; + +export function clone(value) { + return structuredClone(value); +} + +export function removePropertyFromCollection(collection, columnsToExclude) { + const cloned = clone(collection); + + if (!columnsToExclude) { + return cloned; + } + + for (const excludedColumn of columnsToExclude) { + for (const row of cloned) { + delete row[excludedColumn]; + } + } + + return cloned; +} + +export function sortedCollectionByProperty(collection, columnName, sortedBy, pageSizeLimit = pageSize) { + const cloned = clone(collection); + const direction = sortedBy === "desc" ? -1 : 1; + + return cloned + .sort((a, b) => { + const valueA = String(a[columnName] ?? "").toUpperCase(); + const valueB = String(b[columnName] ?? "").toUpperCase(); + if (valueA < valueB) return -1 * direction; + if (valueA > valueB) return 1 * direction; + return 0; + }) + .slice(0, pageSizeLimit); +} + +export function getSortedMileage(actualTableData) { + return actualTableData + .map((row) => row.Mileage) + .sort((a, b) => Number(a) - Number(b)); +} + +export function expectRowsSubset(expect, actualRows, expectedRows) { + for (const expectedRow of expectedRows) { + expect(actualRows).toContainEqual(expectedRow); + } +} diff --git a/packages/playwright-ag-grid/tests/shared/run-ag-grid-data-suite.js b/packages/playwright-ag-grid/tests/shared/run-ag-grid-data-suite.js new file mode 100644 index 0000000..018f63f --- /dev/null +++ b/packages/playwright-ag-grid/tests/shared/run-ag-grid-data-suite.js @@ -0,0 +1,598 @@ +import { expect, test } from "@playwright/test"; + +import { createAgGrid, filterOperator, sort } from "../../src/index.js"; +import { + agGridSelector, + cardataFixture, + clone, + expectRowsSubset, + expectedFirstPageTableData, + expectedPaginatedTableData, + getSortedMileage, + pageSize, + removePropertyFromCollection, + sortedCollectionByProperty, +} from "./fixtures.js"; + +async function enableMileageNumberFilter(page, floatingFilter = false) { + await page.evaluate(({ field, filter, floatingFilterValue, hide }) => { + window.setColumnFilter(field, filter, floatingFilterValue, hide); + }, { + field: "mileage", + filter: "agNumberColumnFilter", + floatingFilterValue: floatingFilter, + hide: false, + }); + await page.locator(".ag-cell").first().waitFor({ state: "visible" }); +} + +async function validatePaginatedTable(page, grid, expectedPages, options) { + for (let index = 0; index < expectedPages.length; index += 1) { + await expect(await grid.getData(options)).toEqual(expectedPages[index]); + if (index < expectedPages.length - 1) { + await page.locator(`${agGridSelector} .ag-icon-next`).click(); + } + } +} + +function sortRowsByMileage(rows) { + return clone(rows).sort((a, b) => Number(a.Mileage) - Number(b.Mileage)); +} + +export function runAgGridDataSuite({ pagePath, versionLabel }) { + test.describe(`playwright ag-grid data scenarios (${versionLabel})`, () => { + test.beforeEach(async ({ page }) => { + await page.goto(pagePath); + await expect(page.locator(".example-version")).toContainText(`AG Grid ${versionLabel}`); + await page.locator(".ag-cell").first().waitFor({ state: "visible" }); + await page.locator("#floating").click(); + }); + + test("verify paginated table data - any order - include all columns", async ({ page }) => { + const grid = createAgGrid(page.locator(agGridSelector)); + await validatePaginatedTable(page, grid, expectedPaginatedTableData); + }); + + test("verify paginated table data - exact order - include all columns", async ({ page }) => { + const grid = createAgGrid(page.locator(agGridSelector)); + await expect(await grid.getData()).toEqual(expectedPaginatedTableData[0]); + }); + + test("verify exact order table data when columns are not in order - include all columns", async ({ page }) => { + const grid = createAgGrid(page.locator(agGridSelector)); + await grid.pinColumn("Price", "left"); + await expect(await grid.getData()).toEqual(expectedPaginatedTableData[0]); + }); + + test("verify paginated table data - excluding columns", async ({ page }) => { + const grid = createAgGrid(page.locator(agGridSelector)); + const expectedSubset = expectedPaginatedTableData.map((pageRows) => + pageRows.map(({ Year, Make, Model }) => ({ Year, Make, Model })) + ); + + await validatePaginatedTable(page, grid, expectedSubset, { + onlyColumns: ["Year", "Make", "Model"], + }); + }); + + test("able to filter by checkbox", async ({ page }) => { + const grid = createAgGrid(page.locator(agGridSelector)); + await grid.filterTextFloating({ + searchCriteria: { + columnName: "Model", + filterValue: "2002", + }, + selectAllLocaleText: "(Select All)", + hasApplyButton: true, + }); + await expect(await grid.getData()).toEqual([ + { Year: "2020", Make: "BMW", Model: "2002", Condition: "excellent", Price: "88001" }, + ]); + }); + + test("able to filter by checkbox - multiple columns", async ({ page }) => { + const grid = createAgGrid(page.locator(agGridSelector)); + await page.locator("#nonFloating").click(); + await grid.filterCheckboxMenu({ + searchCriteria: [ + { columnName: "Model", filterValue: "2002" }, + { columnName: "Model", filterValue: "3-series" }, + ], + hasApplyButton: true, + }); + await expect(await grid.getData()).toEqual([ + { Year: "2020", Make: "BMW", Model: "3-series", Condition: "fair", Price: "45000" }, + { Year: "2020", Make: "BMW", Model: "3-series", Condition: "poor", Price: "32000" }, + { Year: "2020", Make: "BMW", Model: "2002", Condition: "excellent", Price: "88001" }, + ]); + }); + + test("able to filter by text - menu", async ({ page }) => { + const grid = createAgGrid(page.locator(agGridSelector)); + await grid.sortColumn("Model", sort.ascending); + await grid.filterTextMenu({ + searchCriteria: { + columnName: "Price", + filterValue: "32000", + operator: filterOperator.equals, + }, + hasApplyButton: true, + }); + await expect(await grid.getData()).toEqual([ + { Year: "2020", Make: "BMW", Model: "3-series", Condition: "poor", Price: "32000" }, + { Year: "2020", Make: "Honda", Model: "Accord", Condition: "poor", Price: "32000" }, + { Year: "2020", Make: "Ford", Model: "Mondeo", Condition: "excellent", Price: "32000" }, + ]); + }); + + test("able to filter by text - menu - multiple columns", async ({ page }) => { + const grid = createAgGrid(page.locator(agGridSelector)); + await page.locator("#nonFloating").click(); + await grid.sortColumn("Model", sort.ascending); + await grid.filterTextMenu({ + searchCriteria: [ + { + columnName: "Price", + filterValue: "32000", + operator: filterOperator.equals, + }, + { + columnName: "Make", + filterValue: "BMW", + operator: filterOperator.equals, + }, + ], + hasApplyButton: true, + }); + await expect(await grid.getData()).toEqual([ + { Year: "2020", Make: "BMW", Model: "3-series", Condition: "poor", Price: "32000" }, + ]); + }); + + test("able to filter by text - menu - contains operator", async ({ page }) => { + const grid = createAgGrid(page.locator(agGridSelector)); + await grid.sortColumn("Model", sort.ascending); + await grid.filterTextFloating({ + searchCriteria: { + columnName: "Make", + filterValue: "ord", + operator: filterOperator.contains, + }, + hasApplyButton: true, + }); + await expect(await grid.getData()).toEqual([ + { Year: "2020", Make: "Ford", Model: "Mondeo", Condition: "excellent", Price: "32000" }, + { Year: "2020", Make: "Ford", Model: "Mondeo", Condition: "good", Price: "25000" }, + { Year: "2020", Make: "Ford", Model: "Taurus", Condition: "excellent", Price: "19000" }, + { Year: "1990", Make: "Ford", Model: "Taurus", Condition: "excellent", Price: "900" }, + ]); + }); + + test("able to filter by text - menu - does not contain operator", async ({ page }) => { + const grid = createAgGrid(page.locator(agGridSelector)); + await grid.filterTextFloating({ + searchCriteria: { + columnName: "Make", + filterValue: "ord", + operator: filterOperator.notContains, + }, + hasApplyButton: true, + }); + + const actualTableData = await grid.getData(); + expect(actualTableData.length).toBeGreaterThan(0); + for (const row of actualTableData) { + expect(row.Make).not.toContain("ord"); + } + }); + + test("able to filter by text - menu - does not equal operator", async ({ page }) => { + const grid = createAgGrid(page.locator(agGridSelector)); + await grid.filterTextFloating({ + searchCriteria: { + columnName: "Make", + filterValue: "Ford", + operator: filterOperator.notEquals, + }, + hasApplyButton: true, + }); + + const actualTableData = await grid.getData(); + expect(actualTableData.length).toBeGreaterThan(0); + for (const row of actualTableData) { + expect(row.Make).not.toBe("Ford"); + } + }); + + test("able to filter by text - menu - less than operator", async ({ page }) => { + const grid = createAgGrid(page.locator(agGridSelector)); + await enableMileageNumberFilter(page); + await grid.filterTextMenu({ + searchCriteria: { + columnName: "Mileage", + filterValue: "5000", + operator: filterOperator.lessThan, + }, + hasApplyButton: true, + }); + expect(getSortedMileage(await grid.getData())).toEqual(["250", "1000", "3500", "4500"]); + }); + + test("able to filter by text - menu - less than or equal operator", async ({ page }) => { + const grid = createAgGrid(page.locator(agGridSelector)); + await enableMileageNumberFilter(page); + await grid.filterTextMenu({ + searchCriteria: { + columnName: "Mileage", + filterValue: "5000", + operator: filterOperator.lessThanOrEquals, + }, + hasApplyButton: true, + }); + expect(getSortedMileage(await grid.getData())).toEqual(["250", "1000", "3500", "4500", "5000"]); + }); + + test("able to filter by text - menu - greater than operator", async ({ page }) => { + const grid = createAgGrid(page.locator(agGridSelector)); + await enableMileageNumberFilter(page); + await grid.filterTextMenu({ + searchCriteria: { + columnName: "Mileage", + filterValue: "50000", + operator: filterOperator.greaterThan, + }, + hasApplyButton: true, + }); + expect(getSortedMileage(await grid.getData())).toEqual(["52000", "60000", "70000", "90000"]); + }); + + test("able to filter by text - menu - greater than or equal operator", async ({ page }) => { + const grid = createAgGrid(page.locator(agGridSelector)); + await enableMileageNumberFilter(page); + await grid.filterTextMenu({ + searchCriteria: { + columnName: "Mileage", + filterValue: "50000", + operator: filterOperator.greaterThanOrEquals, + }, + hasApplyButton: true, + }); + expect(getSortedMileage(await grid.getData())).toEqual(["52000", "60000", "70000", "90000"]); + }); + + test("able to filter by text - floating filter", async ({ page }) => { + const grid = createAgGrid(page.locator(agGridSelector)); + await grid.sortColumn("Model", sort.ascending); + await grid.filterTextFloating({ + searchCriteria: { + columnName: "Make", + filterValue: "Ford", + }, + hasApplyButton: true, + }); + await expect(await grid.getData()).toEqual([ + { Year: "2020", Make: "Ford", Model: "Mondeo", Condition: "excellent", Price: "32000" }, + { Year: "2020", Make: "Ford", Model: "Mondeo", Condition: "good", Price: "25000" }, + { Year: "2020", Make: "Ford", Model: "Taurus", Condition: "excellent", Price: "19000" }, + { Year: "1990", Make: "Ford", Model: "Taurus", Condition: "excellent", Price: "900" }, + ]); + }); + + test("able to filter by text - floating filter - multiple conditions", async ({ page }) => { + const grid = createAgGrid(page.locator(agGridSelector)); + await grid.sortColumn("Model", sort.ascending); + await grid.filterTextFloating({ + searchCriteria: { + columnName: "Make", + filterValue: "B", + searchInputIndex: 0, + }, + hasApplyButton: true, + }); + await grid.filterTextFloating({ + searchCriteria: { + columnName: "Make", + filterValue: "MW", + searchInputIndex: 1, + }, + hasApplyButton: true, + }); + await expect(await grid.getData()).toEqual([ + { Year: "2020", Make: "BMW", Model: "2002", Condition: "excellent", Price: "88001" }, + { Year: "2020", Make: "BMW", Model: "3-series", Condition: "fair", Price: "45000" }, + { Year: "2020", Make: "BMW", Model: "3-series", Condition: "poor", Price: "32000" }, + ]); + }); + + test("able to filter by text - floating filter - multiple columns", async ({ page }) => { + const grid = createAgGrid(page.locator(agGridSelector)); + await grid.sortColumn("Model", sort.ascending); + await grid.filterTextFloating({ + searchCriteria: [ + { columnName: "Make", filterValue: "Ford" }, + { columnName: "Year", filterValue: "1990" }, + ], + hasApplyButton: true, + }); + await expect(await grid.getData()).toEqual([ + { Year: "1990", Make: "Ford", Model: "Taurus", Condition: "excellent", Price: "900" }, + ]); + }); + + test("able to filter by text - floating filter - between operator", async ({ page }) => { + const grid = createAgGrid(page.locator(agGridSelector)); + await enableMileageNumberFilter(page, true); + await grid.filterTextFloating({ + searchCriteria: [ + { + columnName: "Mileage", + filterValue: "0", + operator: filterOperator.inRange, + }, + { + columnName: "Mileage", + filterValue: "5000", + operator: filterOperator.inRange, + }, + ], + hasApplyButton: true, + }); + + const expectedTableData = [ + { Year: "2023", Make: "Hyundai", Model: "Santa Fe", Condition: "excellent", Mileage: "250", Price: "" }, + { Year: "2020", Make: "Porsche", Model: "Boxter", Condition: "good", Mileage: "1000", Price: "99000" }, + { Year: "2020", Make: "Hyundai", Model: "Elantra", Condition: "fair", Mileage: "3500", Price: "3000" }, + { Year: "2020", Make: "BMW", Model: "2002", Condition: "excellent", Mileage: "4500", Price: "88001" }, + ]; + + expect(sortRowsByMileage(await grid.getData())).toEqual(sortRowsByMileage(expectedTableData)); + }); + + test("able to filter by text - floating filter - between operator with explicit indexes", async ({ page }) => { + const grid = createAgGrid(page.locator(agGridSelector)); + await enableMileageNumberFilter(page, true); + + if (versionLabel === "v33") { + await grid.filterTextFloating({ + searchCriteria: [ + { + columnName: "Mileage", + filterValue: "0", + operator: filterOperator.inRange, + searchInputIndex: 0, + operatorIndex: 0, + }, + { + columnName: "Mileage", + filterValue: "5000", + operator: filterOperator.inRange, + searchInputIndex: 1, + operatorIndex: 0, + }, + ], + hasApplyButton: true, + }); + } else { + await grid.filterTextFloating({ + searchCriteria: { + columnName: "Mileage", + filterValue: "0", + operator: filterOperator.inRange, + searchInputIndex: 0, + operatorIndex: 0, + }, + hasApplyButton: true, + }); + await grid.filterTextFloating({ + searchCriteria: { + columnName: "Mileage", + filterValue: "5000", + operator: filterOperator.inRange, + searchInputIndex: 1, + operatorIndex: 0, + }, + hasApplyButton: true, + }); + } + + expect(getSortedMileage(await grid.getData())).toEqual(["250", "1000", "3500", "4500"]); + }); + + test("able to filter by text - floating filter - between operator with mixed criteria", async ({ page }) => { + const grid = createAgGrid(page.locator(agGridSelector)); + await enableMileageNumberFilter(page, true); + await grid.filterTextFloating({ + searchCriteria: [ + { + columnName: "Mileage", + filterValue: "0", + operator: filterOperator.inRange, + }, + { + columnName: "Mileage", + filterValue: "500", + operator: filterOperator.inRange, + }, + { + columnName: "Make", + filterValue: "Ford", + }, + ], + hasApplyButton: true, + }); + await expect(await grid.getData()).toEqual([]); + }); + + test("able to filter by text - floating filter - between operator without apply button", async ({ page }) => { + const grid = createAgGrid(page.locator(agGridSelector)); + await enableMileageNumberFilter(page, true); + await grid.filterTextFloating({ + searchCriteria: [ + { + columnName: "Mileage", + filterValue: "0", + operator: filterOperator.inRange, + }, + { + columnName: "Mileage", + filterValue: "5000", + operator: filterOperator.inRange, + }, + ], + hasApplyButton: false, + noMenuTabs: true, + }); + expect(getSortedMileage(await grid.getData())).toEqual(["250", "1000", "3500", "4500"]); + }); + + test("able to filter by text - floating filter - multi filter", async ({ page }) => { + const grid = createAgGrid(page.locator(agGridSelector)); + await grid.sortColumn("Model", sort.ascending); + await grid.filterTextFloating({ + searchCriteria: [ + { + columnName: "Model", + filterValue: "Taurus", + isMultiFilter: true, + }, + ], + hasApplyButton: true, + }); + await expect(await grid.getData()).toEqual([ + { Year: "2020", Make: "Ford", Model: "Taurus", Condition: "excellent", Price: "19000" }, + { Year: "1990", Make: "Ford", Model: "Taurus", Condition: "excellent", Price: "900" }, + ]); + }); + + test("able to validate empty table", async ({ page }) => { + const grid = createAgGrid(page.locator(agGridSelector)); + await grid.filterTextMenu({ + searchCriteria: { + columnName: "Price", + filterValue: "0", + operator: filterOperator.equals, + }, + hasApplyButton: true, + }); + await expect(await grid.getData()).toEqual([]); + }); + + test("able to sort by ascending order", async ({ page }) => { + const grid = createAgGrid(page.locator(agGridSelector)); + await grid.sortColumn("Make", sort.ascending); + const expectedDataSortedByAscending = sortedCollectionByProperty( + removePropertyFromCollection(cardataFixture, ["Mileage"]), + "Make", + sort.ascending, + pageSize + ); + await expect(await grid.getData()).toEqual(expectedDataSortedByAscending); + }); + + test("able to sort by descending order", async ({ page }) => { + const grid = createAgGrid(page.locator(agGridSelector)); + await grid.sortColumn("Make", sort.descending); + const expectedDataSortedByDescending = sortedCollectionByProperty( + removePropertyFromCollection(cardataFixture, ["Mileage"]), + "Make", + sort.descending, + pageSize + ); + await expect(await grid.getData()).toEqual(expectedDataSortedByDescending); + }); + + test("remove column from grid and verify select column data", async ({ page }) => { + const grid = createAgGrid(page.locator(agGridSelector)); + await grid.toggleColumnFromSideBar("Year", true); + const expectedData = removePropertyFromCollection( + removePropertyFromCollection(cardataFixture, ["Mileage"]), + ["Year"] + ).slice(0, pageSize); + await expect(await grid.getData()).toEqual(expectedData); + }); + + test("remove single pinned column from grid and verify select column data", async ({ page }) => { + const grid = createAgGrid(page.locator(agGridSelector)); + await grid.toggleColumnFromSideBar("Price", true); + const expectedData = removePropertyFromCollection( + removePropertyFromCollection(cardataFixture, ["Mileage"]), + ["Price"] + ).slice(0, pageSize); + await expect(await grid.getData()).toEqual(expectedData); + }); + + test("remove multiple columns from grid and verify select column data", async ({ page }) => { + const grid = createAgGrid(page.locator(agGridSelector)); + await grid.toggleColumnFromSideBar("Price", true); + await grid.toggleColumnFromSideBar("Make", true); + const expectedData = removePropertyFromCollection( + removePropertyFromCollection(cardataFixture, ["Mileage"]), + ["Price", "Make"] + ).slice(0, pageSize); + await expect(await grid.getData()).toEqual(expectedData); + }); + + test("only validate select column data", async ({ page }) => { + const grid = createAgGrid(page.locator(agGridSelector)); + const actualTableData = await grid.getData({ onlyColumns: ["Year", "Make", "Model"] }); + expectRowsSubset(expect, actualTableData, expectedFirstPageTableData.map(({ Year, Make, Model }) => ({ Year, Make, Model }))); + }); + + test("able to filter by 'Blank'", async ({ page }) => { + const grid = createAgGrid(page.locator(agGridSelector)); + await grid.filterTextMenu({ + searchCriteria: { + columnName: "Price", + operator: filterOperator.blank, + }, + hasApplyButton: true, + }); + expectRowsSubset(expect, await grid.getData(), [ + { Year: "2023", Make: "Hyundai", Model: "Santa Fe", Condition: "excellent", Price: "" }, + ]); + }); + + test("able to filter by 'Not blank'", async ({ page }) => { + const grid = createAgGrid(page.locator(agGridSelector)); + await grid.filterTextMenu({ + searchCriteria: { + columnName: "Price", + operator: filterOperator.notBlank, + }, + hasApplyButton: true, + }); + const actualTableData = await grid.getData(); + expect(actualTableData.length).toBeGreaterThan(0); + for (const row of actualTableData) { + expect(row.Price).not.toBe(""); + } + }); + + test("able to filter by agTextColumnFilter with join operator", async ({ page }) => { + const grid = createAgGrid(page.locator(agGridSelector)); + await grid.filterTextFloating({ + searchCriteria: { + columnName: "Condition", + operator: filterOperator.startsWith, + filterValue: "f", + searchInputIndex: 0, + }, + hasApplyButton: true, + }); + await grid.filterTextFloating({ + searchCriteria: { + columnName: "Condition", + operator: filterOperator.endsWith, + filterValue: "ir", + searchInputIndex: 1, + }, + hasApplyButton: true, + }); + expectRowsSubset(expect, await grid.getData(), [ + { Year: "2020", Make: "Toyota", Model: "Celica", Condition: "fair", Price: "35000" }, + { Year: "2020", Make: "BMW", Model: "3-series", Condition: "fair", Price: "45000" }, + { Year: "2020", Make: "Hyundai", Model: "Elantra", Condition: "fair", Price: "3000" }, + ]); + }); + }); +} diff --git a/packages/playwright-ag-grid/tests/shared/run-ag-grid-elements-suite.js b/packages/playwright-ag-grid/tests/shared/run-ag-grid-elements-suite.js new file mode 100644 index 0000000..d30ae5f --- /dev/null +++ b/packages/playwright-ag-grid/tests/shared/run-ag-grid-elements-suite.js @@ -0,0 +1,49 @@ +import { expect, test } from "@playwright/test"; + +import { createAgGrid, filterOperator } from "../../src/index.js"; +import { + agGridElementsSelector, + expectedPorscheRowsAfterEditing, + expectedPorscheRowsBeforeEditing, +} from "./fixtures.js"; + +function expectRowsSubset(actualRows, expectedRows) { + for (const expectedRow of expectedRows) { + expect(actualRows).toContainEqual(expectedRow); + } +} + +export function runAgGridElementsSuite({ pagePath, versionLabel }) { + test.describe(`playwright ag-grid elements scenarios (${versionLabel})`, () => { + test.beforeEach(async ({ page }) => { + await page.goto(pagePath); + await expect(page.locator(".example-version")).toContainText(`AG Grid ${versionLabel}`); + await page.locator(".ag-cell").first().waitFor({ state: "visible" }); + }); + + test("updates a grid cell value", async ({ page }) => { + const grid = createAgGrid(page.locator(agGridElementsSelector)); + + await grid.filterTextFloating({ + searchCriteria: { + columnName: "Make", + filterValue: "Porsche", + operator: filterOperator.equals, + }, + hasApplyButton: true, + }); + + expectRowsSubset(await grid.getData(), expectedPorscheRowsBeforeEditing); + + const priceCell = await grid.getCellLocator( + { Make: "Porsche", Price: "72000" }, + "Price" + ); + await priceCell.dblclick(); + await priceCell.locator("input").fill("66000"); + await priceCell.locator("input").press("Enter"); + + expectRowsSubset(await grid.getData(), expectedPorscheRowsAfterEditing); + }); + }); +} diff --git a/test.ts b/test.ts new file mode 100644 index 0000000..fee5994 --- /dev/null +++ b/test.ts @@ -0,0 +1,60 @@ +type TestResult = { + testName: string; + suite: string; + status: "passed" | "failed" | "skipped"; + durationMs: number; + retries: number; + errorMessage?: string; +} + +const results: TestResult[] = [ + { testName: "login valid user", suite: "auth", status: "passed", durationMs: 1200, retries: 0 }, + { testName: "login invalid password", suite: "auth", status: "failed", durationMs: 900, retries: 0, errorMessage: "401 !== 200" }, + { testName: "reset password", suite: "auth", status: "passed", durationMs: 1500, retries: 1 }, + { testName: "create order", suite: "orders", status: "passed", durationMs: 2200, retries: 0 }, + { testName: "cancel order", suite: "orders", status: "skipped", durationMs: 0, retries: 0 }, + { testName: "refund order", suite: "orders", status: "failed", durationMs: 1800, retries: 2, errorMessage: "500 !== 200" }, + { testName: "reset password", suite: "auth", status: "passed", durationMs: 1400, retries: 2 } +]; + +type Result = { + total: number; + passed: number; + failed: number; + skipped: number; + passRate: number; // percentage from 0 to 100, rounded to 2 decimals + flakyTests: string[]; // unique test names where retries > 0 and final status is passed + slowestTest?: { testName: string; durationMs: number }; + failuresBySuite: Record; +} + +function summarizeResults(results: TestResult[]): Result { + const passed = results.filter((r)=>r.status === "passed").length; + const total = results.length; + const failed = results.filter((r)=>r.status === "failed").length; + const skipped = results.filter((r)=>r.status === "skipped").length; + const slowestTest = results.sort((a,b) => a.durationMs - b.durationMs)[0]; + const failuresBySuite: Record = {}; + + for (const result of results) { + if(result.status === "failed"){ + if(!failuresBySuite){ + + } + } + } + console.log("FAILING SUITES:", failingSuites) + + return{ + flakyTests: results.filter((r) => r.retries >0 && r.status === "passed").map((r)=>r.testName), + passed, + total, + failed, + skipped, + passRate: passed / (passed + failed) * 100, + slowestTest: slowestTest ? { ...slowestTest} : undefined, + failuresBySuite: undefined + } +} + +summarizeResults(results);