From 364c70f496590c5b76cde98dfe02e6164672deb7 Mon Sep 17 00:00:00 2001 From: Tak Tran Date: Thu, 15 Jan 2026 16:03:43 +0000 Subject: [PATCH 1/3] AG-3390 - Fix landing pages sitemap + clean up landing pages (#12893) * AG-3390 - Add landing pages to sitemap * AG-3390 - Use astro native way to merge charts sitemap * AG-3390 - Remove unused import * AG-3390 - Fix enterprise landing data validation --- documentation/ag-grid-docs/astro.config.mjs | 7 +- documentation/ag-grid-docs/package.json | 1 - .../ag-grid-docs/plugins/agMergeSitemap.ts | 66 ------------------- .../landing-pages/LandingPage.astro | 1 - .../ag-grid-docs/src/content.config.ts | 5 +- .../landing-pages/enterprise-data-grid.json | 2 +- .../ag-grid-docs/src/utils/sitemap.ts | 11 ++-- yarn.lock | 43 +----------- 8 files changed, 14 insertions(+), 122 deletions(-) delete mode 100644 documentation/ag-grid-docs/plugins/agMergeSitemap.ts diff --git a/documentation/ag-grid-docs/astro.config.mjs b/documentation/ag-grid-docs/astro.config.mjs index 778961a2f4b..e877743456f 100644 --- a/documentation/ag-grid-docs/astro.config.mjs +++ b/documentation/ag-grid-docs/astro.config.mjs @@ -12,7 +12,6 @@ import agLinkChecker from '../../external/ag-website-shared/plugins/agLinkChecke import buildTime from './plugins/agBuildTime'; import agHotModuleReload from './plugins/agHotModuleReload'; import agHtaccessGen from './plugins/agHtaccessGen'; -import agMergeSitemap from './plugins/agMergeSitemap'; import agRedirectsChecker from './plugins/agRedirectsChecker'; import { getSitemapConfig } from './src/utils/sitemap'; import { urlWithBaseUrl } from './src/utils/urlWithBaseUrl'; @@ -198,15 +197,11 @@ export default defineConfig({ buildTime(), react(), markdoc(), - sitemap(getSitemapConfig()), + sitemap(getSitemapConfig({ chartsSitemap: CHARTS_SITEMAP_INDEX_URL })), agHtaccessGen({ include: HTACCESS === 'true' }), agRedirectsChecker({ skip: CHECK_REDIRECTS !== 'true', }), agLinkChecker({ include: CHECK_LINKS === 'true' }), - agMergeSitemap({ - // Merge charts sitemap - sitemapIndexUrl: CHARTS_SITEMAP_INDEX_URL, - }), ], }); diff --git a/documentation/ag-grid-docs/package.json b/documentation/ag-grid-docs/package.json index cd8c99c2726..6d54f621667 100644 --- a/documentation/ag-grid-docs/package.json +++ b/documentation/ag-grid-docs/package.json @@ -71,7 +71,6 @@ "codesandbox-import-utils": "2.2.3", "dompurify": "^3.2.6", "downshift": "^9.0.6", - "fast-xml-parser": "^4.4.1", "gif-frames": "^1.0.1", "he": "^1.2.0", "history": "^5.3.0", diff --git a/documentation/ag-grid-docs/plugins/agMergeSitemap.ts b/documentation/ag-grid-docs/plugins/agMergeSitemap.ts deleted file mode 100644 index a5eef3647bd..00000000000 --- a/documentation/ag-grid-docs/plugins/agMergeSitemap.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type { AstroIntegration } from 'astro'; -import { XMLBuilder, XMLParser } from 'fast-xml-parser'; -import { readFileSync, writeFileSync } from 'node:fs'; -import { join } from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const GRID_SITEMAP_INDEX_FILE = 'sitemap-index.xml'; - -interface Options { - sitemapIndexUrl: string; -} - -/** - * Merge sitemap from another Astro sitemap index - */ -export default function createPlugin(options: Options): AstroIntegration { - return { - name: 'ag-merge-sitemap', - hooks: { - 'astro:build:done': async ({ dir, logger }) => { - if (!options?.sitemapIndexUrl) { - logger.info('No sitemapIndexUrl specified, skipping'); - return; - } - - const destDir = fileURLToPath(dir.href); - - const parser = new XMLParser({ ignoreAttributes: false }); - const gridSitemapIndexFile = join(destDir, GRID_SITEMAP_INDEX_FILE); - const gridSitemapIndex = readFileSync(gridSitemapIndexFile); - - const otherSitemapIndexPromise = await fetch(options.sitemapIndexUrl); - const otherSitemapIndex = await otherSitemapIndexPromise.text(); - - const gridSitemapParsed = parser.parse(gridSitemapIndex); - const otherSitemapParsed = parser.parse(otherSitemapIndex); - - if (!gridSitemapParsed.sitemapindex || !otherSitemapParsed.sitemapindex) { - logger.error('Sitemap index not found in either grid or other sitemap index'); - logger.error(`gridSitemapIndex (${GRID_SITEMAP_INDEX_FILE}):`); - logger.error(gridSitemapIndex.toString()); - logger.error(`otherSitemapIndex (${options.sitemapIndexUrl}):`); - logger.error(otherSitemapIndex.toString()); - - throw new Error('Sitemap index not found'); - } - if (Array.isArray(gridSitemapParsed.sitemapindex.sitemap)) { - gridSitemapParsed.sitemapindex.sitemap = gridSitemapParsed.sitemapindex.sitemap.concat( - otherSitemapParsed.sitemapindex.sitemap - ); - } else if (typeof gridSitemapParsed.sitemapindex.sitemap === 'object') { - gridSitemapParsed.sitemapindex.sitemap = [gridSitemapParsed.sitemapindex.sitemap].concat( - otherSitemapParsed.sitemapindex.sitemap - ); - } - - const builder = new XMLBuilder({ ignoreAttributes: false, format: true }); - const updatedSitemapIndex = builder.build(gridSitemapParsed); - - // Overwrite sitemap - writeFileSync(gridSitemapIndexFile, updatedSitemapIndex); - logger.info(`Merged sitemap from ${options.sitemapIndexUrl} into ${GRID_SITEMAP_INDEX_FILE}`); - }, - }, - }; -} diff --git a/documentation/ag-grid-docs/src/components/landing-pages/LandingPage.astro b/documentation/ag-grid-docs/src/components/landing-pages/LandingPage.astro index 833f8ac84c4..470d3245bbd 100644 --- a/documentation/ag-grid-docs/src/components/landing-pages/LandingPage.astro +++ b/documentation/ag-grid-docs/src/components/landing-pages/LandingPage.astro @@ -41,7 +41,6 @@ import { Video } from '@ag-website-shared/components/video/Video'; import { InstallText } from '@ag-website-shared/components/install-text'; import { AutomatedIntegratedCharts } from '@ag-website-shared/components/automated-examples/AutomatedIntegratedCharts'; import { TrialButton } from '@ag-website-shared/components/trial-licence-modal/TrialButton'; -import { EnterpriseTrial } from '@ag-website-shared/components/license-pricing/EnterpriseTrial'; // Site-specific components import { getHeadingWithLogo } from '@utils/framework-landing-page-utils'; diff --git a/documentation/ag-grid-docs/src/content.config.ts b/documentation/ag-grid-docs/src/content.config.ts index 0570eb83dcc..97d9f791c87 100644 --- a/documentation/ag-grid-docs/src/content.config.ts +++ b/documentation/ag-grid-docs/src/content.config.ts @@ -1,10 +1,11 @@ -import { FRAMEWORKS } from '@constants'; +import { ALL_INTERNAL_FRAMEWORKS, FRAMEWORKS } from '@constants'; // NOTE: Use glob, instead of file for single object files unless the file is an // array of objects import { glob } from 'astro/loaders'; import { defineCollection, z } from 'astro:content'; const framework = z.enum(FRAMEWORKS as any); +const internalFramework = z.enum(ALL_INTERNAL_FRAMEWORKS as any); const docs = defineCollection({ loader: glob({ pattern: '**/[^_]*.mdoc', base: './src/content/docs' }), @@ -328,7 +329,7 @@ const landingPages = defineCollection({ title: z.string(), description: z.string(), }), - framework: z.string().optional(), + framework: internalFramework.optional(), packageName: z.string().optional(), docsPath: z.string(), analyticsPrefix: z.string(), diff --git a/documentation/ag-grid-docs/src/content/landing-pages/enterprise-data-grid.json b/documentation/ag-grid-docs/src/content/landing-pages/enterprise-data-grid.json index 87482fd146a..99b163d4973 100644 --- a/documentation/ag-grid-docs/src/content/landing-pages/enterprise-data-grid.json +++ b/documentation/ag-grid-docs/src/content/landing-pages/enterprise-data-grid.json @@ -3,7 +3,7 @@ "title": "AG Grid Enterprise: Advanced Data Grid Features for Professional Applications", "description": "Unlock AG Grid's full potential with Enterprise features including AI Toolkit, Server-Side Row Model, Row Grouping, Master/Detail, and Integrated Charts. Start your free trial today." }, - "framework": "react", + "framework": "reactFunctionalTs", "packageName": "ag-grid-enterprise", "docsPath": "data-grid", "analyticsPrefix": "enterprise", diff --git a/documentation/ag-grid-docs/src/utils/sitemap.ts b/documentation/ag-grid-docs/src/utils/sitemap.ts index 56720017301..9b342192447 100644 --- a/documentation/ag-grid-docs/src/utils/sitemap.ts +++ b/documentation/ag-grid-docs/src/utils/sitemap.ts @@ -34,10 +34,10 @@ export const isTestPage = (page: string) => { const isRedirectPage = (page: string) => { return ( page.endsWith('/documentation/') || - page.endsWith('/react-data-grid/') || - page.endsWith('/angular-data-grid/') || - page.endsWith('/javascript-data-grid/') || - page.endsWith('/vue-data-grid/') || + (!page.endsWith('/landing-pages/react-data-grid/') && page.endsWith('/react-data-grid/')) || + (!page.endsWith('/landing-pages/angular-data-grid/') && page.endsWith('/angular-data-grid/')) || + (!page.endsWith('/landing-pages/javascript-data-grid/') && page.endsWith('/javascript-data-grid/')) || + (!page.endsWith('/landing-pages/vue-data-grid/') && page.endsWith('/vue-data-grid/')) || page.includes(`/${FRAMEWORK_REDIRECT_PATH}/`) ); }; @@ -60,8 +60,9 @@ const filterIgnoredPages = (page: string) => { ); }; -export function getSitemapConfig() { +export function getSitemapConfig({ chartsSitemap }: { chartsSitemap?: string }) { return { + customSitemaps: chartsSitemap ? [chartsSitemap] : [], filter: filterIgnoredPages, changefreq: 'daily', priority: 0.7, diff --git a/yarn.lock b/yarn.lock index 1a27181f12d..61e51045b08 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13282,13 +13282,6 @@ fast-uri@^3.0.1: resolved "http://52.50.158.57:4873/fast-uri/-/fast-uri-3.1.0.tgz#66eecff6c764c0df9b762e62ca7edcfb53b4edfa" integrity sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA== -fast-xml-parser@^4.4.1: - version "4.5.3" - resolved "http://52.50.158.57:4873/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz#c54d6b35aa0f23dc1ea60b6c884340c006dc6efb" - integrity sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig== - dependencies: - strnum "^1.1.1" - fastest-levenshtein@^1.0.12, fastest-levenshtein@^1.0.16: version "1.0.16" resolved "http://52.50.158.57:4873/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" @@ -22930,16 +22923,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "http://52.50.158.57:4873/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "http://52.50.158.57:4873/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -23044,7 +23028,7 @@ stringify-entities@^4.0.0: character-entities-html4 "^2.0.0" character-entities-legacy "^3.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "http://52.50.158.57:4873/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -23058,13 +23042,6 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "http://52.50.158.57:4873/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1, strip-ansi@^7.1.0: version "7.1.2" resolved "http://52.50.158.57:4873/strip-ansi/-/strip-ansi-7.1.2.tgz#132875abde678c7ea8d691533f2e7e22bb744dba" @@ -23134,11 +23111,6 @@ strip-outer@^2.0.0: resolved "http://52.50.158.57:4873/strip-outer/-/strip-outer-2.0.0.tgz#c45c724ed9b1ff6be5f660503791404f4714084b" integrity sha512-A21Xsm1XzUkK0qK1ZrytDUvqsQWict2Cykhvi0fBQntGG5JSprESasEyV1EZ/4CiR5WB5KjzLTrP/bO37B0wPg== -strnum@^1.1.1: - version "1.1.2" - resolved "http://52.50.158.57:4873/strnum/-/strnum-1.1.2.tgz#57bca4fbaa6f271081715dbc9ed7cee5493e28e4" - integrity sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA== - strtok3@^10.3.4: version "10.3.4" resolved "https://registry.ag-grid.com/strtok3/-/strtok3-10.3.4.tgz#793ebd0d59df276a085586134b73a406e60be9c1" @@ -25501,7 +25473,7 @@ wordwrap@^1.0.0: resolved "http://52.50.158.57:4873/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "http://52.50.158.57:4873/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -25537,15 +25509,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "http://52.50.158.57:4873/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^9.0.0: version "9.0.2" resolved "http://52.50.158.57:4873/wrap-ansi/-/wrap-ansi-9.0.2.tgz#956832dea9494306e6d209eb871643bb873d7c98" From 52fd686bf0703c3f786d257506800c34177ba5fb Mon Sep 17 00:00:00 2001 From: Stephen Cooper Date: Thu, 15 Jan 2026 17:16:15 +0000 Subject: [PATCH 2/3] Bundle size report updates (#12894) * Update the report with percentages * Run on latest to keep track of aggregate change * Highlight 5% changes * test making FindModule bigger. * remove test logic --- .github/workflows/ci.yml | 55 +-------- .github/workflows/module-size-vs-release.yml | 7 +- scripts/ci/compare-module-sizes.mjs | 113 ++++++++++++++----- 3 files changed, 90 insertions(+), 85 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2e2fbc4dc98..b7b5892deeb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,11 +18,6 @@ on: type: boolean default: false required: true - skip_bundle_size: - description: 'Skip bundle size tests' - type: boolean - default: true - required: true run_package_tests: description: 'Run package tests' type: boolean @@ -245,48 +240,6 @@ jobs: path: | reports/ - bundle_size: - runs-on: ubuntu-latest - name: Bundle Size Tests - needs: calc_matrix - if: (needs.init.outputs.build_type == 'latest' && needs.calc_matrix.result == 'success' && !inputs.skip_bundle_size) || (needs.init.outputs.build_type == 'release' && inputs.run_bundle_tests) - strategy: - matrix: ${{ fromJson(needs.calc_matrix.outputs.bundle_test_matrix )}} - fail-fast: false - env: - NX_PARALLEL: 1 - NX_BASE: ${{ needs.init.outputs.nx_base }} - steps: - - name: Checkout - id: checkout - uses: actions/checkout@v4 - with: - fetch-depth: 1 # shallow copy - - - name: Setup - id: setup - uses: ./.github/actions/setup-nx - with: - cache_mode: ro - - # Disable now that module size tests are reported on PRs - # - name: module-size tests (non-sharded) - # if: matrix.shard == 0 - # id: test-no-shard - # run: yarn nx ${{ github.event.inputs.nx_command || 'affected' }} -t test:e2e -c staging --exclude '!tag:module-size' --exclude 'tag:sharding' --exclude all - # - name: module-size tests - # if: matrix.shard != 0 - # id: test - # run: yarn nx ${{ github.event.inputs.nx_command || 'affected' }} -t test:e2e -c staging --exclude '!tag:module-size' --exclude 'tag:non-sharding' --exclude all --shard=${{ matrix.shard }}/$((${{ strategy.job-total }} - 1)) - - - name: Persist test results - if: always() && matrix.shard != 0 - uses: actions/upload-artifact@v4 - with: - name: test-results-bundle-shard-${{matrix.shard}} - path: | - reports/ - format_lint_build: runs-on: ubuntu-latest name: Format, Lint and Build @@ -420,7 +373,7 @@ jobs: report: runs-on: ubuntu-24.04 - needs: [ init, test, e2e, format_lint_build, docs, calc_matrix, bundle_size, fw_pkg_test, sonar_community, sonar_enterprise ] + needs: [ init, test, e2e, format_lint_build, docs, calc_matrix, fw_pkg_test, sonar_community, sonar_enterprise ] if: cancelled() != true && github.event.pull_request.draft == false permissions: contents: write @@ -461,8 +414,7 @@ jobs: - name: Test Report uses: dorny/test-reporter@v1 if: needs.test.result == 'success' || needs.test.result == 'failure' || - needs.e2e.result == 'success' || needs.e2e.result == 'failure' || - needs.bundle_size.result == 'success' || needs.bundle_size.result == 'failure' + needs.e2e.result == 'success' || needs.e2e.result == 'failure' id: testReport continue-on-error: true with: @@ -489,8 +441,6 @@ jobs: WORKFLOW_STATUS="failure" elif [ "${{ needs.fw_pkg_test.result }}" == "failure" ] ; then WORKFLOW_STATUS="failure" - elif [ "${{ needs.bundle_size.result }}" == "failure" ] ; then - WORKFLOW_STATUS="failure" elif [ "${{ needs.sonar_community.result }}" == "failure" ] ; then WORKFLOW_STATUS="failure" elif [ "${{ needs.sonar_enterprise.result }}" == "failure" ] ; then @@ -568,7 +518,6 @@ jobs: "FW_Pkg": "${{ needs.fw_pkg_test.result }}", "e2e": "${{ needs.e2e.result }}", "Docs": "${{ needs.docs.result }}", - "Bundle": "${{ needs.bundle_size.result }}", "SonarCommunity": "${{ needs.sonar_community.result }}", "SonarEnterprise": "${{ needs.sonar_enterprise.result }}" } diff --git a/.github/workflows/module-size-vs-release.yml b/.github/workflows/module-size-vs-release.yml index 73ef9a3852b..f95586ceaa5 100644 --- a/.github/workflows/module-size-vs-release.yml +++ b/.github/workflows/module-size-vs-release.yml @@ -1,15 +1,16 @@ name: Module Size vs Release on: + push: + branches: + - 'latest' + - 'b[0-9][0-9]?.[0-9][0-9]?.[0-9][0-9]?' workflow_dispatch: inputs: release_tag: description: 'Release tag to compare against (e.g., b35.1.0)' type: string required: true - schedule: - # Run weekly on Monday at 9am UTC - - cron: '0 9 * * 1' env: NX_NO_CLOUD: true diff --git a/scripts/ci/compare-module-sizes.mjs b/scripts/ci/compare-module-sizes.mjs index dcfcdc6b545..c174626d6b9 100644 --- a/scripts/ci/compare-module-sizes.mjs +++ b/scripts/ci/compare-module-sizes.mjs @@ -1,7 +1,8 @@ #!/usr/bin/env node import fs from 'node:fs'; -const THRESHOLD_KB = 1; // Report modules with size changes +const THRESHOLD_PERCENT = 0.5; // Ignore changes below this percentage +const HIGHLIGHT_PERCENT = 5; // Highlight table rows when change exceeds this percentage /** * Compare two module-size-results.json files and generate a diff report @@ -41,12 +42,28 @@ function formatDiff(diff) { return `${sign}${diff.toFixed(2)}`; } +function calcPercent(base, diff) { + if (base === 0) { + return diff === 0 ? 0 : 100; + } + return (diff / base) * 100; +} + +function formatPercent(percent) { + const sign = percent >= 0 ? '+' : ''; + return `${sign}${percent.toFixed(1)}%`; +} + +function applyHighlight(text, shouldHighlight) { + return shouldHighlight ? `${text}` : text; +} + function getChangeEmoji(diff) { if (diff > 0) { - return '📈'; + return '🔺'; } if (diff < 0) { - return '📉'; + return '🟢'; } return '➖'; } @@ -77,6 +94,8 @@ for (const key of allKeys) { const selfSizeDiff = pr.selfSize - base.selfSize; const fileSizeDiff = pr.fileSize - base.fileSize; const gzipSizeDiff = pr.gzipSize - base.gzipSize; + const selfSizePercent = calcPercent(base.selfSize, selfSizeDiff); + const gzipSizePercent = calcPercent(base.gzipSize, gzipSizeDiff); diffs.push({ modules: pr.modules, @@ -84,12 +103,14 @@ for (const key of allKeys) { baseSelfSize: base.selfSize, prSelfSize: pr.selfSize, selfSizeDiff, + selfSizePercent, baseFileSize: base.fileSize, prFileSize: pr.fileSize, fileSizeDiff, baseGzipSize: base.gzipSize, prGzipSize: pr.gzipSize, gzipSizeDiff, + gzipSizePercent, isNew: false, isRemoved: false, }); @@ -100,12 +121,14 @@ for (const key of allKeys) { baseSelfSize: 0, prSelfSize: pr.selfSize, selfSizeDiff: pr.selfSize, + selfSizePercent: 100, baseFileSize: 0, prFileSize: pr.fileSize, fileSizeDiff: pr.fileSize, baseGzipSize: 0, prGzipSize: pr.gzipSize, gzipSizeDiff: pr.gzipSize, + gzipSizePercent: 100, isNew: true, isRemoved: false, }); @@ -116,12 +139,14 @@ for (const key of allKeys) { baseSelfSize: base.selfSize, prSelfSize: 0, selfSizeDiff: -base.selfSize, + selfSizePercent: -100, baseFileSize: base.fileSize, prFileSize: 0, fileSizeDiff: -base.fileSize, baseGzipSize: base.gzipSize, prGzipSize: 0, gzipSizeDiff: -base.gzipSize, + gzipSizePercent: -100, isNew: false, isRemoved: true, }); @@ -136,7 +161,7 @@ const maxIncrease = diffs.reduce((max, d) => (d.selfSizeDiff > max.selfSizeDiff const maxDecrease = diffs.reduce((min, d) => (d.selfSizeDiff < min.selfSizeDiff ? d : min), diffs[0]); // Filter significant changes -const significantChanges = diffs.filter((d) => Math.abs(d.selfSizeDiff) >= THRESHOLD_KB); +const significantChanges = diffs.filter((d) => Math.abs(d.selfSizePercent) >= THRESHOLD_PERCENT); // Generate markdown report let report = ''; @@ -144,38 +169,56 @@ let report = ''; // Header report += '## Module Size Comparison\n\n'; -// Extremes section +// Extremes section - only show if change is >= THRESHOLD_PERCENT report += '### Extreme Values\n\n'; -if (maxIncrease && maxIncrease.selfSizeDiff > 0) { +const showMaxIncrease = maxIncrease && maxIncrease.selfSizePercent >= THRESHOLD_PERCENT; +const showMaxDecrease = maxDecrease && maxDecrease.selfSizePercent <= -THRESHOLD_PERCENT; + +if (showMaxIncrease) { const moduleName = maxIncrease.modules.length === 0 ? 'Base (no modules)' : maxIncrease.modules.join(', '); - report += `📈 **Largest Increase:** ${moduleName}\n`; - report += ` - Self Size: ${formatSize(maxIncrease.baseSelfSize)} KB → ${formatSize(maxIncrease.prSelfSize)} KB (**${formatDiff(maxIncrease.selfSizeDiff)} KB**)\n\n`; + report += `🔺 **Largest Increase:** ${moduleName}\n`; + report += ` - Self Size: ${formatSize(maxIncrease.baseSelfSize)} KB → ${formatSize(maxIncrease.prSelfSize)} KB (**${formatDiff(maxIncrease.selfSizeDiff)} KB**, ${formatPercent(maxIncrease.selfSizePercent)})\n\n`; } -if (maxDecrease && maxDecrease.selfSizeDiff < 0) { +if (showMaxDecrease) { const moduleName = maxDecrease.modules.length === 0 ? 'Base (no modules)' : maxDecrease.modules.join(', '); - report += `📉 **Largest Decrease:** ${moduleName}\n`; - report += ` - Self Size: ${formatSize(maxDecrease.baseSelfSize)} KB → ${formatSize(maxDecrease.prSelfSize)} KB (**${formatDiff(maxDecrease.selfSizeDiff)} KB**)\n\n`; + report += `🟢 **Largest Decrease:** ${moduleName}\n`; + report += ` - Self Size: ${formatSize(maxDecrease.baseSelfSize)} KB → ${formatSize(maxDecrease.prSelfSize)} KB (**${formatDiff(maxDecrease.selfSizeDiff)} KB**, ${formatPercent(maxDecrease.selfSizePercent)})\n\n`; +} + +if (!showMaxIncrease && !showMaxDecrease) { + report += `✅ No significant changes (all changes < ${THRESHOLD_PERCENT}%)\n\n`; } // Significant changes table if (significantChanges.length > 0) { - report += `### Significant Changes (≥ ${THRESHOLD_KB} KB)\n\n`; - report += '| Module(s) | Base (KB) | PR (KB) | Diff (KB) | Gzip Diff (KB) |\n'; - report += '|-----------|-----------|---------|-----------|----------------|\n'; + report += `### Significant Changes (≥ ${THRESHOLD_PERCENT}%)\n\n`; + report += '| Module(s) | Base (KB) | PR (KB) | Diff (KB) | Diff % | Gzip Diff (KB) | Gzip % |\n'; + report += '|-----------|-----------|---------|-----------|--------|----------------|--------|\n'; for (const diff of significantChanges) { const moduleName = diff.modules.length === 0 ? 'Base (no modules)' : diff.modules.join(', '); const emoji = getChangeEmoji(diff.selfSizeDiff); const status = diff.isNew ? ' 🆕' : diff.isRemoved ? ' 🗑️' : ''; - report += `| ${emoji} ${moduleName}${status} | ${formatSize(diff.baseSelfSize)} | ${formatSize(diff.prSelfSize)} | **${formatDiff(diff.selfSizeDiff)}** | ${formatDiff(diff.gzipSizeDiff)} |\n`; + const shouldHighlight = Math.abs(diff.selfSizePercent) >= HIGHLIGHT_PERCENT; + const cells = [ + applyHighlight(`${emoji} ${moduleName}${status}`, shouldHighlight), + applyHighlight(formatSize(diff.baseSelfSize), shouldHighlight), + applyHighlight(formatSize(diff.prSelfSize), shouldHighlight), + applyHighlight(`**${formatDiff(diff.selfSizeDiff)}**`, shouldHighlight), + applyHighlight(formatPercent(diff.selfSizePercent), shouldHighlight), + applyHighlight(formatDiff(diff.gzipSizeDiff), shouldHighlight), + applyHighlight(formatPercent(diff.gzipSizePercent), shouldHighlight), + ]; + + report += `| ${cells.join(' | ')} |\n`; } report += '\n'; } else { report += '### Significant Changes\n\n'; - report += `✅ No modules changed by more than ${THRESHOLD_KB} KB.\n\n`; + report += `✅ No modules changed by more than ${THRESHOLD_PERCENT}%.\n\n`; } // New/Removed modules @@ -211,15 +254,26 @@ report += `- **Modules unchanged:** ${diffs.filter((d) => d.selfSizeDiff === 0). const allChanges = diffs.filter((d) => d.selfSizeDiff !== 0); if (allChanges.length > 0) { report += '#### All Module Changes\n\n'; - report += '| Module(s) | Base (KB) | PR (KB) | Diff (KB) | Gzip Diff (KB) |\n'; - report += '|-----------|-----------|---------|-----------|----------------|\n'; + report += '| Module(s) | Base (KB) | PR (KB) | Diff (KB) | Diff % | Gzip Diff (KB) | Gzip % |\n'; + report += '|-----------|-----------|---------|-----------|--------|----------------|--------|\n'; for (const diff of allChanges) { const moduleName = diff.modules.length === 0 ? 'Base (no modules)' : diff.modules.join(', '); const emoji = getChangeEmoji(diff.selfSizeDiff); const status = diff.isNew ? ' 🆕' : diff.isRemoved ? ' 🗑️' : ''; - report += `| ${emoji} ${moduleName}${status} | ${formatSize(diff.baseSelfSize)} | ${formatSize(diff.prSelfSize)} | **${formatDiff(diff.selfSizeDiff)}** | ${formatDiff(diff.gzipSizeDiff)} |\n`; + const shouldHighlight = Math.abs(diff.selfSizePercent) >= HIGHLIGHT_PERCENT; + const cells = [ + applyHighlight(`${emoji} ${moduleName}${status}`, shouldHighlight), + applyHighlight(formatSize(diff.baseSelfSize), shouldHighlight), + applyHighlight(formatSize(diff.prSelfSize), shouldHighlight), + applyHighlight(`**${formatDiff(diff.selfSizeDiff)}**`, shouldHighlight), + applyHighlight(formatPercent(diff.selfSizePercent), shouldHighlight), + applyHighlight(formatDiff(diff.gzipSizeDiff), shouldHighlight), + applyHighlight(formatPercent(diff.gzipSizePercent), shouldHighlight), + ]; + + report += `| ${cells.join(' | ')} |\n`; } report += '\n'; } @@ -232,18 +286,19 @@ console.log(`Comparison report written to ${outputFile}`); // Output summary for CI console.log('\n--- Summary ---'); -console.log(`Significant changes (>= ${THRESHOLD_KB} KB): ${significantChanges.length}`); -let hasChanges = false; -if (maxIncrease && maxIncrease.selfSizeDiff > 0) { +console.log(`Significant changes (>= ${THRESHOLD_PERCENT}%): ${significantChanges.length}`); +if (showMaxIncrease) { const moduleName = maxIncrease.modules.length === 0 ? 'Base (no modules)' : maxIncrease.modules.join(', '); - console.log(`Largest increase: ${moduleName} (+${formatSize(maxIncrease.selfSizeDiff)} KB)`); - hasChanges = true; + console.log( + `Largest increase: ${moduleName} (+${formatSize(maxIncrease.selfSizeDiff)} KB, ${formatPercent(maxIncrease.selfSizePercent)})` + ); } -if (maxDecrease && maxDecrease.selfSizeDiff < 0) { +if (showMaxDecrease) { const moduleName = maxDecrease.modules.length === 0 ? 'Base (no modules)' : maxDecrease.modules.join(', '); - console.log(`Largest decrease: ${moduleName} (${formatSize(maxDecrease.selfSizeDiff)} KB)`); - hasChanges = true; + console.log( + `Largest decrease: ${moduleName} (${formatSize(maxDecrease.selfSizeDiff)} KB, ${formatPercent(maxDecrease.selfSizePercent)})` + ); } -if (!hasChanges) { - console.log('No module size changes detected.'); +if (!showMaxIncrease && !showMaxDecrease) { + console.log(`No significant module size changes detected (all < ${THRESHOLD_PERCENT}%).`); } From 6f43ff257c2e285068eb425b655e58d6eeb89816 Mon Sep 17 00:00:00 2001 From: Salvatore Previti Date: Thu, 15 Jan 2026 20:10:03 +0000 Subject: [PATCH 3/3] AG-16541 avoid double cell editable callback call (#12888) * AG-16541-avoid-double-cell-editable-callback-call --- .../ag-grid-community/src/edit/editApi.ts | 1 + .../ag-grid-community/src/edit/editService.ts | 4 +- .../src/edit/strategy/fullRowEditStrategy.ts | 8 +- .../edit/strategy/singleCellEditStrategy.ts | 8 +- .../src/edit/utils/editors.ts | 111 ++++++++++-------- .../src/interfaces/iEditService.ts | 2 + .../src/rendering/cell/cellCtrl.ts | 9 +- .../cell/cellKeyboardListenerFeature.ts | 2 +- .../cell-editing-regression.test.ts | 36 ++++++ 9 files changed, 123 insertions(+), 58 deletions(-) diff --git a/packages/ag-grid-community/src/edit/editApi.ts b/packages/ag-grid-community/src/edit/editApi.ts index 1db0332546c..41d436b69b6 100644 --- a/packages/ag-grid-community/src/edit/editApi.ts +++ b/packages/ag-grid-community/src/edit/editApi.ts @@ -126,6 +126,7 @@ export function startEditingCell(beans: BeanCollection, params: StartEditingCell { event: key ? new KeyboardEvent('keydown', { key }) : undefined, source: 'api', + editable: true, } ); } diff --git a/packages/ag-grid-community/src/edit/editService.ts b/packages/ag-grid-community/src/edit/editService.ts index 627af749d12..8b60178af8a 100644 --- a/packages/ag-grid-community/src/edit/editService.ts +++ b/packages/ag-grid-community/src/edit/editService.ts @@ -256,7 +256,8 @@ export class EditService extends BeanStub implements NamedBean, IEditService { this.strategy ??= this.createStrategy(); - if (!this.isCellEditable(position, 'api')) { + const editable = params.editable ?? this.isCellEditable(position, 'api'); + if (!editable) { return; } @@ -264,6 +265,7 @@ export class EditService extends BeanStub implements NamedBean, IEditService { // yet to initialise the cell, so we re-schedule this operation for when celLComp is attached const cellCtrl = _getCellCtrl(this.beans, position)!; if (cellCtrl && !cellCtrl.comp) { + params.editable = undefined; // So we re-evaluate editable later cellCtrl.onCompAttachedFuncs.push(() => this.startEditing(position, params)); return; } diff --git a/packages/ag-grid-community/src/edit/strategy/fullRowEditStrategy.ts b/packages/ag-grid-community/src/edit/strategy/fullRowEditStrategy.ts index e5fb19b3b48..85bce5cd1ba 100644 --- a/packages/ag-grid-community/src/edit/strategy/fullRowEditStrategy.ts +++ b/packages/ag-grid-community/src/edit/strategy/fullRowEditStrategy.ts @@ -268,7 +268,13 @@ export class FullRowEditStrategy extends BaseEditStrategy { if (suppressStartEditOnTab) { nextCell.focusCell(true, event); } else { - this.editSvc.startEditing(nextCell, { startedEdit: true, event, source, ignoreEventKey: true }); + this.editSvc.startEditing(nextCell, { + startedEdit: true, + event, + source, + ignoreEventKey: true, + editable: nextEditable || undefined, + }); } } diff --git a/packages/ag-grid-community/src/edit/strategy/singleCellEditStrategy.ts b/packages/ag-grid-community/src/edit/strategy/singleCellEditStrategy.ts index 16b94110d9e..2709af2e5fb 100644 --- a/packages/ag-grid-community/src/edit/strategy/singleCellEditStrategy.ts +++ b/packages/ag-grid-community/src/edit/strategy/singleCellEditStrategy.ts @@ -208,7 +208,13 @@ export class SingleCellEditStrategy extends BaseEditStrategy { if (suppressStartEditOnTab) { nextCell.focusCell(true, event); } else { - this.editSvc.startEditing(nextCell, { startedEdit: true, event, source, ignoreEventKey: true }); + this.editSvc.startEditing(nextCell, { + startedEdit: true, + event, + source, + ignoreEventKey: true, + editable: nextEditable, + }); } } diff --git a/packages/ag-grid-community/src/edit/utils/editors.ts b/packages/ag-grid-community/src/edit/utils/editors.ts index 2a70cdb34e6..995fd0b05f7 100644 --- a/packages/ag-grid-community/src/edit/utils/editors.ts +++ b/packages/ag-grid-community/src/edit/utils/editors.ts @@ -25,32 +25,24 @@ import { _getCellCtrl } from './controllers'; export const UNEDITED = Symbol('unedited'); -function getCellEditorInstanceMap( +/** public api getCellEditorInstances */ +export const getCellEditorInstances = ( beans: BeanCollection, params: GetCellEditorInstancesParams = {} -): { ctrl: CellCtrl; editor: ICellEditor }[] { - const res: { ctrl: CellCtrl; editor: ICellEditor }[] = []; - +): ICellEditor[] => { const ctrls = beans.rowRenderer.getCellCtrls(params.rowNodes, params.columns as AgColumn[]); - - for (const ctrl of ctrls) { + const editors: ICellEditor[] = new Array(ctrls.length); + let count = 0; + for (let i = 0, len = ctrls.length; i < len; ++i) { + const ctrl = ctrls[i]; const cellEditor = ctrl.comp?.getCellEditor(); - if (cellEditor) { - res.push({ - ctrl, - editor: _unwrapUserComp(cellEditor), - }); + editors[count++] = _unwrapUserComp(cellEditor); } } - - return res; -} - -export const getCellEditorInstances = ( - beans: BeanCollection, - params: GetCellEditorInstancesParams = {} -): ICellEditor[] => getCellEditorInstanceMap(beans, params).map((res) => res.editor); + editors.length = count; + return editors; +}; export function _setupEditors( beans: BeanCollection, @@ -549,30 +541,50 @@ function dispatchEditingStopped( } } -function _hasValidationRules(beans: BeanCollection): boolean { - const { gos, colModel } = beans; - const getFullRowEditValidationErrors = !!gos.get('getFullRowEditValidationErrors'); - const columnsHaveRules = colModel - .getColumnDefs() - ?.filter((c: ColDef) => c.editable) - .some(({ cellEditorParams }: ColDef) => { - const { minLength, maxLength, getValidationErrors, min, max } = cellEditorParams || {}; - - return ( - minLength !== undefined || - maxLength !== undefined || - getValidationErrors !== undefined || - min !== undefined || - max !== undefined - ); - }); +function _columnDefsRequireValidation(columnDefs?: ColDef[]): boolean { + if (!columnDefs) { + return false; + } + for (let i = 0, len = columnDefs.length; i < len; ++i) { + const colDef = columnDefs[i]; + const params = colDef.cellEditorParams; + if (!params || !colDef.editable) { + continue; + } + if ( + params.minLength !== undefined || + params.maxLength !== undefined || + params.getValidationErrors !== undefined || + params.min !== undefined || + params.max !== undefined + ) { + return true; + } + } + return false; +} - const editorsHaveRules = beans.gridApi - .getCellEditorInstances() - // Check if either method was provided in the editor - .some((editor) => editor.getValidationElement || editor.getValidationErrors); +function _editorsRequireValidation(beans: BeanCollection): boolean { + const ctrls = beans.rowRenderer.getCellCtrls(); + for (let i = 0, len = ctrls.length; i < len; ++i) { + const ctrl = ctrls[i]; + const cellEditor = ctrl.comp?.getCellEditor(); + if (cellEditor) { + const editor = _unwrapUserComp(cellEditor); + if (editor.getValidationElement || editor.getValidationErrors) { + return true; + } + } + } + return false; +} - return columnsHaveRules || getFullRowEditValidationErrors || editorsHaveRules; +function _hasValidationRules(beans: BeanCollection): boolean { + return ( + !!beans.gos.get('getFullRowEditValidationErrors') || + _columnDefsRequireValidation(beans.colModel.getColumnDefs()) || + _editorsRequireValidation(beans) + ); } export function _populateModelValidationErrors(beans: BeanCollection, force?: boolean): void { @@ -580,16 +592,20 @@ export function _populateModelValidationErrors(beans: BeanCollection, force?: bo return; } - const mappedEditors = getCellEditorInstanceMap(beans); const cellValidationModel = new EditCellValidationModel(); const { ariaAnnounce, localeSvc, editModelSvc, gos } = beans; const includeRows = gos.get('editType') === 'fullRow'; const translate = _getLocaleTextFunc(localeSvc); const ariaValidationErrorPrefix = translate('ariaValidationErrorPrefix', 'Cell Editor Validation'); + const rowCtrlSet = new Set(); + for (const ctrl of beans.rowRenderer.getCellCtrls()) { + const cellEditorComp = ctrl.comp?.getCellEditor(); + if (!cellEditorComp) { + continue; + } - for (const mappedEditor of mappedEditors) { - const { ctrl, editor } = mappedEditor; + const editor = _unwrapUserComp(cellEditorComp); const { rowNode, column } = ctrl; const errorMessages = editor.getValidationErrors?.() ?? []; const el = editor.getValidationElement?.(false) || (!editor.isPopup?.() && ctrl.eGui); @@ -621,6 +637,7 @@ export function _populateModelValidationErrors(beans: BeanCollection, force?: bo } ); } + rowCtrlSet.add(ctrl.rowCtrl); } _syncFromEditors(beans, { persist: false }); @@ -629,12 +646,6 @@ export function _populateModelValidationErrors(beans: BeanCollection, force?: bo // the second loop over mappedEditor below editModelSvc?.setCellValidationModel(cellValidationModel); - const rowCtrlSet = new Set(); - - for (const { ctrl } of mappedEditors) { - rowCtrlSet.add(ctrl.rowCtrl); - } - if (includeRows) { const rowValidations = _generateRowValidationErrors(beans); editModelSvc?.setRowValidationModel(rowValidations); diff --git a/packages/ag-grid-community/src/interfaces/iEditService.ts b/packages/ag-grid-community/src/interfaces/iEditService.ts index 08c24a85c63..934730ee187 100644 --- a/packages/ag-grid-community/src/interfaces/iEditService.ts +++ b/packages/ag-grid-community/src/interfaces/iEditService.ts @@ -30,6 +30,8 @@ export type StartEditParams = { ignoreEventKey?: boolean; silent?: boolean; continueEditing?: boolean; + /** If true, skip checking if the cell is editable by invoking the editable user callback */ + editable?: boolean; }; export type StopEditParams = { diff --git a/packages/ag-grid-community/src/rendering/cell/cellCtrl.ts b/packages/ag-grid-community/src/rendering/cell/cellCtrl.ts index 9ea5f5aff63..93473a92b0b 100644 --- a/packages/ag-grid-community/src/rendering/cell/cellCtrl.ts +++ b/packages/ag-grid-community/src/rendering/cell/cellCtrl.ts @@ -274,15 +274,16 @@ export class CellCtrl extends BeanStub { this.rangeFeature?.setComp(comp); this.rowResizeFeature?.refreshRowResizer(); - if ( - (startEditing && this.isCellEditable()) || - (this.hasEdit && this.editSvc?.isEditing(this, { withOpenEditor: true })) - ) { + const editable = startEditing ? this.isCellEditable() : undefined; + const continuingEdit = !editable && this.hasEdit && this.editSvc?.isEditing(this, { withOpenEditor: true }); + + if (editable || continuingEdit) { this.editSvc?.startEditing(this, { startedEdit: false, source: 'api', silent: true, continueEditing: true, + editable, }); } else { // We can skip refreshing the range handle as this is done in this.rangeFeature.setComp above diff --git a/packages/ag-grid-community/src/rendering/cell/cellKeyboardListenerFeature.ts b/packages/ag-grid-community/src/rendering/cell/cellKeyboardListenerFeature.ts index c79331286ff..211a432ff68 100644 --- a/packages/ag-grid-community/src/rendering/cell/cellKeyboardListenerFeature.ts +++ b/packages/ag-grid-community/src/rendering/cell/cellKeyboardListenerFeature.ts @@ -268,7 +268,7 @@ export class CellKeyboardListenerFeature extends BeanStub { return; } - editSvc?.startEditing(cellCtrl, { startedEdit: true, event, source: 'api' }); + editSvc?.startEditing(cellCtrl, { startedEdit: true, event, source: 'api', editable: true }); // if we don't prevent default, then the event also gets applied to the text field // (at least when doing the default editor), but we need to allow the editor to decide // what it wants to do. we only do this IF editing was started - otherwise it messes diff --git a/testing/behavioural/src/cell-editing/cell-editing-regression.test.ts b/testing/behavioural/src/cell-editing/cell-editing-regression.test.ts index a5c5c4199d2..8287d41d848 100644 --- a/testing/behavioural/src/cell-editing/cell-editing-regression.test.ts +++ b/testing/behavioural/src/cell-editing/cell-editing-regression.test.ts @@ -70,6 +70,42 @@ describe('Cell Editing Regression', () => { }); }); + test('full-row editing tab to next row starts editors when focusing read-only cell', async () => { + const api = await gridMgr.createGridAndWait('myGrid', { + columnDefs: [ + { field: 'readOnly', headerName: 'Read Only', editable: false }, + { field: 'make' }, + { field: 'model' }, + ], + defaultColDef: { + editable: true, + }, + editType: 'fullRow', + rowData: [ + { readOnly: 'RO-0', make: 'Toyota', model: 'Celica' }, + { readOnly: 'RO-1', make: 'Ford', model: 'Mondeo' }, + ], + }); + + const gridDiv = getGridElement(api)! as HTMLElement; + await asyncSetTimeout(1); + + const makeCellRow0 = getByTestId(gridDiv, agTestIdFor.cell('0', 'make')); + await userEvent.dblClick(makeCellRow0); + await waitForInput(gridDiv, makeCellRow0, { popup: false }); + + await userEvent.keyboard('{Tab}{Tab}'); + await asyncSetTimeout(1); + + const modelCellRow1 = getByTestId(gridDiv, agTestIdFor.cell('1', 'model')); + const editor = await waitForInput(gridDiv, modelCellRow1, { popup: false }); + await userEvent.clear(editor); + await userEvent.type(editor, 'Updated'); + await userEvent.keyboard('{Enter}'); + + expect(modelCellRow1).toHaveTextContent('Updated'); + }); + // AG-15698 - row doesn't rerender after value is selected in rich select editor test('cell not refreshed after richSelectEditor select', async () => { // virtualList doesn't add option elements if the offsetHeight is 0, so we need to fake it