Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .rulesync/rules/ag-grid.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,10 @@ For detailed information about preferred technologies and architectural constrai
- `yarn nx build:package <package>` – create ESM/CJS bundles to validate publishable output.
- `yarn nx build:umd <package>` – produce UMD bundles for browser distribution smoke-tests.
- `yarn nx run-many -t build` – rebuild all packages when changes span the dependency graph.
- `yarn nx test ag-behavioural-testing --run` – run behavioural tests in `testing/behavioural/` (primary test suite, uses Vitest).
- `yarn nx test ag-behavioural-testing --run "<file-pattern>"` – run specific behavioural test file.
- `yarn nx test ag-behavioural-testing --run "<file-pattern>" -t "<test-name>"` – run specific behavioural test by name.
- `./behave.sh` – run behavioural tests in `testing/behavioural/` (primary test suite, uses Vitest).
- `./behave.sh "<file-pattern>"` – run specific behavioural test file.
- `./behave.sh "<file-pattern>" -t "<test-name>"` – run specific behavioural test by name.
- `./behave.sh --watch` – run behavioural tests in watch mode.
- `yarn nx test <package>` – execute Jest unit tests for the affected package.
- `yarn nx test <package> --testPathPattern="<file-name>"` - test specific test file
- `yarn nx test <package> --testPathPattern="<file-name>" --testNamePattern="<test-name>"` - test specific test name in a specific test file
Expand Down
11 changes: 7 additions & 4 deletions .rulesync/rules/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,20 @@ packages/ag-grid-community/src/

### Behavioural Tests (Vitest) – Primary Test Suite

Behavioural tests in `testing/behavioural/` are the primary test suite for verifying grid behaviour. They use Vitest via Nx. Use `--run` to execute once (without watch mode):
Behavioural tests in `testing/behavioural/` are the primary test suite for verifying grid behaviour. They use Vitest. Watch mode is disabled by default:

```bash
# Run all behavioural tests
yarn nx test ag-behavioural-testing --run
./behave.sh

# Run specific test file
yarn nx test ag-behavioural-testing --run "cell-editing-regression"
./behave.sh "cell-editing-regression"

# Run specific test by name
yarn nx test ag-behavioural-testing --run "cell-editing-regression" -t "should handle"
./behave.sh "cell-editing-regression" -t "should handle"

# Run in watch mode
./behave.sh --watch
```

### Benchmarks
Expand Down
10 changes: 7 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,13 @@ For detailed information about preferred technologies and architectural constrai
- `yarn nx build:package <package>` – create ESM/CJS bundles to validate publishable output.
- `yarn nx build:umd <package>` – produce UMD bundles for browser distribution smoke-tests.
- `yarn nx run-many -t build` – rebuild all packages when changes span the dependency graph.
- `yarn nx test ag-behavioural-testing --run` – run behavioural tests in `testing/behavioural/` (primary test suite, uses Vitest).
- `yarn nx test ag-behavioural-testing --run "<file-pattern>"` – run specific behavioural test file.
- `yarn nx test ag-behavioural-testing --run "<file-pattern>" -t "<test-name>"` – run specific behavioural test by name.
- `./behave.sh` – run behavioural tests in `testing/behavioural/` (primary test suite, uses Vitest).
- `./behave.sh "<file-pattern>"` – run specific behavioural test file.
- `./behave.sh "<file-pattern>" -t "<test-name>"` – run specific behavioural test by name.
- `./behave.sh --watch` – run behavioural tests in watch mode.
- `./behave.sh --update-grid-rows` – update GridRows inline snapshots after diagram format changes.
- `./behave.sh --update-grid-rows "<pattern>"` – update snapshots in matching test files only.
- `./behave.sh --update-grid-rows=dry` – dry run, shows what would change without writing files.
- `yarn nx test <package>` – execute Jest unit tests for the affected package.
- `yarn nx test <package> --testPathPattern="<file-name>"` - test specific test file
- `yarn nx test <package> --testPathPattern="<file-name>" --testNamePattern="<test-name>"` - test specific test name in a specific test file
Expand Down
41 changes: 41 additions & 0 deletions behave.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
# Runs behavioural tests directly via Vitest, bypassing Nx.
# All arguments are forwarded to vitest. Watch mode is disabled by default.
#
# Usage:
# ./behave.sh # Run all tests
# ./behave.sh "file-pattern" # Run tests matching pattern
# ./behave.sh "file-pattern" -t "name" # Run specific test by name
# ./behave.sh -w # Run in watch mode
# ./behave.sh --watch # Run in watch mode
# ./behave.sh --update # Update vitest snapshots
# ./behave.sh --update-grid-rows # Update GridRows inline snapshots
# ./behave.sh --update-grid-rows=dry # Dry-run: show what would change

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# Parse --update-grid-rows flag
args=()
for arg in "$@"; do
case "$arg" in
--update-grid-rows)
export UPDATE_GRID_ROWS_SNAPSHOTS=1
;;
--update-grid-rows=dry)
export UPDATE_GRID_ROWS_SNAPSHOTS=dry
;;
--update-grid-rows=*)
echo "Unknown value: $arg (expected --update-grid-rows or --update-grid-rows=dry)" >&2
exit 1
;;
*)
args+=("$arg")
;;
esac
done

cd "$SCRIPT_DIR/testing/behavioural"

exec npx vitest "${args[@]+"${args[@]}"}"
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"setup-prompts": "./external/ag-shared/scripts/setup-prompts/setup-prompts.sh --postinstall",
"bootstrap": "SHARP_IGNORE_GLOBAL_LIBVIPS=true YARN_REGISTRY=\"https://registry.ag-grid.com\" yarn",
"git-clean": "git clean -fdx && git reset --hard HEAD",
"git-purge-local-only": "git fetch -p && for branch in $(git for-each-ref --format '%(refname) %(upstream:track)' refs/heads | awk '$2 == \"[gone]\" {sub(\"refs/heads/\", \"\", $1); print $1}'); do git branch -D $branch; done"
"git-purge-local-only": "git fetch -p && for branch in $(git for-each-ref --format '%(refname) %(upstream:track)' refs/heads | awk '$2 == \"[gone]\" {sub(\"refs/heads/\", \"\", $1); print $1}'); do git branch -D $branch; done",
"behave": "./behave.sh"
},
"private": true,
"engines": {
Expand Down Expand Up @@ -51,6 +52,7 @@
"@types/prompts": "^2.4.9",
"@typescript-eslint/eslint-plugin": "7.18.0",
"@typescript-eslint/parser": "7.18.0",
"@vitejs/plugin-react-swc": "^4.2.3",
"@vue/compiler-sfc": "3.5.13",
"JSONStream": "1.3.5",
"autoprefixer": "^10.4.20",
Expand Down
1 change: 1 addition & 0 deletions packages/ag-grid-community/src/main-internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ export {
AgElementParams as _AgElementParams,
_clearElement,
_createAgElement,
_addOrRemoveAttribute,
_getAbsoluteHeight,
_getAbsoluteWidth,
_getInnerHeight,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,10 +210,12 @@ export class GroupFloatingFilterComp extends Component implements IFloatingFilte
eFloatingFilterText.setDisplayed(true);
}
};
if (this.gos.get('enableFilterHandlers')) {
updateText(colFilter.getHandler(column!));
if (!column) {
updateText();
} else if (this.gos.get('enableFilterHandlers')) {
updateText(colFilter.getHandler(column));
} else {
colFilter.getOrCreateFilterUi(column!)?.then((filter) => {
colFilter.getOrCreateFilterUi(column)?.then((filter) => {
updateText(filter);
});
}
Expand Down
2 changes: 2 additions & 0 deletions packages/ag-grid-enterprise/src/widgets/agRichSelect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ describe('AgRichSelect', () => {
const richSelect = createRichSelect<string>();
const listComponent = {
getScrollTop: jest.fn(() => 500),
offsetHoveredIndexOnPrependedRows: jest.fn(),
setCurrentList: jest.fn(),
restoreScrollOnPrependedRows: jest.fn(),
refresh: jest.fn(),
Expand All @@ -296,6 +297,7 @@ describe('AgRichSelect', () => {
});

expect(listComponent.getScrollTop).toHaveBeenCalled();
expect(listComponent.offsetHoveredIndexOnPrependedRows).toHaveBeenCalledWith(100);
expect(listComponent.restoreScrollOnPrependedRows).toHaveBeenCalledWith(500, 100);
});

Expand Down
46 changes: 22 additions & 24 deletions packages/ag-grid-enterprise/src/widgets/agRichSelect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
KeyCode,
RefPlaceholder,
_addGridCommonParams,
_addOrRemoveAttribute,
_clearElement,
_createIconNoSpan,
_debounce,
Expand Down Expand Up @@ -123,8 +124,10 @@ export class AgRichSelect<TValue = any> extends AgPickerField<
private skipWrapperAnnouncement?: boolean = false;
private tooltipFeature?: TooltipFeature;
private shouldDisplayTooltip?: () => boolean;
private readonly valueFormatter: (value: TValue | TValue[] | null | undefined) => string;

constructor(config?: RichSelectParams<TValue>) {
const valueFormatter = resolveRichSelectValueFormatter<TValue>(config?.valueFormatter);
const resolvedAgComponents = config?.agComponents?.includes(AgInputTextFieldSelector)
? config.agComponents
: [AgInputTextFieldSelector, ...(config?.agComponents ?? [])];
Expand All @@ -142,10 +145,10 @@ export class AgRichSelect<TValue = any> extends AgPickerField<
template: config?.template ?? AgRichSelectElement,
agComponents: resolvedAgComponents,
modalPicker: config?.modalPicker ?? false,
valueFormatter:
config?.valueFormatter ?? ((value: TValue | TValue[] | null | undefined) => String(value ?? '')),
valueFormatter,
maxPickerHeight: config?.maxPickerHeight ?? 'calc(var(--ag-row-height) * 6.5)',
});
this.valueFormatter = valueFormatter;

const { value, valueList, searchStringCreator, onSearch } = config ?? {};

Expand Down Expand Up @@ -263,11 +266,10 @@ export class AgRichSelect<TValue = any> extends AgPickerField<
multiSelect,
suppressDeselectAll,
suppressMultiSelectPillRenderer,
valueFormatter,
onSearch,
} = config;

const valueFormatted = formatValueFn(value, valueFormatter);
const valueFormatted = this.valueFormatter(value);
const isTypingMultiSelect = !!(allowTyping && multiSelect);

if (allowTyping) {
Expand Down Expand Up @@ -435,6 +437,10 @@ export class AgRichSelect<TValue = any> extends AgPickerField<

const previousScrollTop = prependedRowCount > 0 ? listComponent.getScrollTop() : undefined;

if (prependedRowCount > 0) {
listComponent.offsetHoveredIndexOnPrependedRows(prependedRowCount);
}

// we need to update the list component even if the 'values' is undefined
listComponent.setCurrentList(valueList);

Expand All @@ -450,8 +456,6 @@ export class AgRichSelect<TValue = any> extends AgPickerField<
listComponent.refresh(true);
const hasCurrentValueInLoadedList = value != null && listComponent.getIndicesForValues(value).length > 0;
if (isPickerDisplayed && hasCurrentValueInLoadedList && scrollToCurrentValue) {
// keep async/paged behaviour aligned with sync lists: when the loaded page contains
// the current value, select and scroll it into view.
listComponent.selectValue(value);
}
} else if (isPickerDisplayed) {
Expand Down Expand Up @@ -536,11 +540,10 @@ export class AgRichSelect<TValue = any> extends AgPickerField<
container.appendChild(pillContainer.getGui());

const { config, eWrapper, ariaDeleteSelection } = this;
const { valueFormatter } = config;

pillContainer.init({
eWrapper,
valueFormatter,
valueFormatter: this.valueFormatter,
onPillMouseDown: (e: MouseEvent) => {
e.stopImmediatePropagation();
},
Expand Down Expand Up @@ -687,14 +690,11 @@ export class AgRichSelect<TValue = any> extends AgPickerField<
}

private getSearchStringsFromValues(values: TValue[]): string[] | undefined {
const {
config: { valueFormatter },
} = this;
if (typeof values[0] === 'object' && this.searchStringCreator) {
return this.searchStringCreator(values);
}

return values.map((value) => (value === '' ? '' : formatValueFn(value, valueFormatter)));
return values.map((value) => (value === '' ? '' : this.valueFormatter(value)));
}

private filterListModel(filteredValues: TValue[]): void {
Expand Down Expand Up @@ -766,7 +766,7 @@ export class AgRichSelect<TValue = any> extends AgPickerField<
// active-descendant is managed on the focusable element (wrapper/input),
// so clear it there to avoid stale references after filtering to no results.
const eAriaEl = this.getFocusableElement();
eAriaEl.removeAttribute('data-active-option');
_addOrRemoveAttribute(eAriaEl, 'data-active-option', null);
_setAriaActiveDescendant(eAriaEl, null);
}
}
Expand Down Expand Up @@ -925,9 +925,7 @@ export class AgRichSelect<TValue = any> extends AgPickerField<
}

if (typeof left === 'object' && typeof right === 'object' && left != null && right != null) {
const valueFormatter =
this.config.valueFormatter ?? ((value: TValue | TValue[] | null | undefined) => String(value ?? ''));
return valueFormatter(left) === valueFormatter(right);
return this.valueFormatter(left) === this.valueFormatter(right);
}

return false;
Expand Down Expand Up @@ -1310,14 +1308,6 @@ export class AgRichSelect<TValue = any> extends AgPickerField<
}
}

// helper function that users a provided value formatter or
// converts the value to a string, or to '' if the original
// value is `null` or `undefined`
const formatValueFn = <TValue>(
value: TValue | null | undefined,
valueFormatter?: ((value: TValue | TValue[] | null | undefined) => string) | undefined
) => valueFormatter?.(value) ?? String(value ?? '');

/**
* cell renderers are used in a few places. they bind to dom slightly differently to other cell renders as they
* can return back strings (instead of html element) in the getGui() method. common code placed here to handle that.
Expand All @@ -1336,3 +1326,11 @@ export function _bindCellRendererToHtmlElement(
}
});
}

type RichSelectValueFormatter<TValue> = (value: TValue | TValue[] | null | undefined) => string;

export function resolveRichSelectValueFormatter<TValue>(
valueFormatter?: RichSelectParams<TValue>['valueFormatter']
): RichSelectValueFormatter<TValue> {
return (value) => valueFormatter?.(value) ?? String(value ?? '');
}
54 changes: 54 additions & 0 deletions packages/ag-grid-enterprise/src/widgets/agRichSelectList.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,17 @@ describe('AgRichSelectList', () => {
expect(indices).toEqual([0, 1]);
});

it('matches primitive current values against complex list items via formatted text', () => {
const { list } = createList<ComplexValue>({
valueFormatter: ((value: ComplexValue) => value.label) as any,
});
const pink = { id: 1, label: 'Pink' };
const blue = { id: 2, label: 'Blue' };
list.setCurrentList([pink, blue]);

expect(list.getIndicesForValues('Pink' as any)).toEqual([0]);
});

it('matches selected complex objects by reference first, then formatter', () => {
const { list } = createList<ComplexValue>({
valueFormatter: ((value: ComplexValue) => `id-${value.id}`) as any,
Expand All @@ -61,6 +72,42 @@ describe('AgRichSelectList', () => {
expect((list as any).findItemInSelected({ id: 2, label: 'two-copy' })).toBe(selectedByFormatter);
});

it('keeps highlight state when selected rows render after selection', () => {
const { list, wrapper } = createList<string>();
list.setCurrentList(['Pink', 'Blue']);

let rendered = false;
const row = {
getCompId: () => '123',
getValue: () => 'Pink',
toggleHighlighted: jest.fn(),
updateSelected: jest.fn(),
};

(list as any).forEachRenderedRow = (callback: (cmp: any, idx: number) => void) => {
if (rendered) {
callback(row, 0);
}
};
(list as any).refresh = jest.fn();
(list as any).ensureIndexVisible = jest.fn();

list.selectValue('Pink');
expect(row.toggleHighlighted).not.toHaveBeenCalled();

const virtualListPrototype = Object.getPrototypeOf(Object.getPrototypeOf(list));
const drawVirtualRowsSpy = jest.spyOn(virtualListPrototype, 'drawVirtualRows').mockImplementation(() => {});
try {
rendered = true;
(list as any).drawVirtualRows(true);

expect(row.toggleHighlighted).toHaveBeenCalledWith(true);
expect(wrapper.getAttribute('data-active-option')).toBe('ag-rich-select-row-123');
} finally {
drawVirtualRowsSpy.mockRestore();
}
});

it('clamps mouse-derived row index to zero for positions above the list', () => {
const { list } = createList<string>();
const gui = list.getGui() as HTMLElement;
Expand Down Expand Up @@ -92,6 +139,13 @@ describe('AgRichSelectList', () => {
expect(list.getIndicesForValues(null)).toEqual([1]);
});

it('does not match null sentinel to empty-string options', () => {
const { list } = createList<string | null>();
list.setCurrentList(['', 'Open', 'Closed']);

expect(list.getIndicesForValues(null)).toEqual([]);
});

it('requests more rows when viewport is close to the end', () => {
const { list } = createList<string>();
const callback = jest.fn();
Expand Down
Loading
Loading