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
18 changes: 18 additions & 0 deletions dev/ag-grid-angular/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,24 @@ if (isDevMode()) {
path: 'row-actions',
loadComponent: async () => import('./tests/row-actions.ng').then((m) => m.DevRowActions)
},
{
path: 'row-selection-state',
loadComponent: async () => import('./tests/row-selection-state.ng').then((m) => m.DevRowSelectionState)
},
{
path: 'row-selection-state-query-params',
loadComponent: async () =>
import('./tests/row-selection-state.ng').then((m) => m.DevRowSelectionStateQueryParams)
},
{
path: 'row-focus-state',
loadComponent: async () => import('./tests/row-focus-state.ng').then((m) => m.DevRowFocusState)
},
{
path: 'row-focus-state-query-params',
loadComponent: async () =>
import('./tests/row-focus-state.ng').then((m) => m.DevRowFocusStateQueryParams)
},
{
path: 'column-menu',
loadComponent: async () => import('./tests/column-menu.ng').then((m) => m.DevColumnMenu)
Expand Down
100 changes: 100 additions & 0 deletions dev/ag-grid-angular/src/tests/row-focus-state.ng.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import {
KbqAgGridRowFocusStateLocalStorageStore,
KbqAgGridRowFocusStateQueryParamsStore,
KbqAgGridThemeModule
} from '@koobiq/ag-grid-angular-theme';
import { AgGridModule } from 'ag-grid-angular';
import { AllCommunityModule, ColDef, GetRowIdFunc, ModuleRegistry } from 'ag-grid-community';
import { DevRowData, devInjectRowData } from '../row-data';

ModuleRegistry.registerModules([AllCommunityModule]);

const COLUMN_DEFS: ColDef[] = [
{ field: 'athlete', headerName: 'Athlete' },
{ field: 'age', headerName: 'Age' },
{ field: 'country', headerName: 'Country' },
{ field: 'year', headerName: 'Year' },
{ field: 'date', headerName: 'Date' },
{ field: 'sport', headerName: 'Sport' }
];

const DEFAULT_COL_DEF: ColDef = {
editable: true
};

const GET_ROW_ID: GetRowIdFunc<DevRowData> = (params) => params.data.id;

const STATE_KEY = 'dev-ag-grid-row-focus-state';

@Component({
standalone: true,
imports: [AgGridModule, KbqAgGridThemeModule],
selector: 'dev-row-focus-state',
template: `
<button type="button" (click)="rowFocusState.reset()">Reset state</button>
<ag-grid-angular
#rowFocusState="kbqAgGridRowFocusState"
data-testid="e2eScreenshotTarget"
kbqAgGridTheme
animateRows="false"
Comment thread
artembelik marked this conversation as resolved.
[getRowId]="getRowId"
[kbqAgGridRowFocusState]="stateKey"
[kbqAgGridRowFocusStateStore]="store"
[rowData]="rowData()"
[columnDefs]="columnDefs"
[defaultColDef]="defaultColDef"
/>
`,
styles: `
ag-grid-angular {
height: 100%;
max-width: 2036px;
}
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DevRowFocusState {
readonly rowData = devInjectRowData();
readonly store = inject(KbqAgGridRowFocusStateLocalStorageStore);
readonly stateKey = STATE_KEY;
readonly columnDefs = COLUMN_DEFS;
readonly defaultColDef = DEFAULT_COL_DEF;
readonly getRowId = GET_ROW_ID;
}

@Component({
standalone: true,
imports: [AgGridModule, KbqAgGridThemeModule],
selector: 'dev-row-focus-state-query-params',
template: `
<button type="button" (click)="rowFocusState.reset()">Reset state</button>
<ag-grid-angular
#rowFocusState="kbqAgGridRowFocusState"
data-testid="e2eScreenshotTarget"
kbqAgGridTheme
animateRows="false"
Comment thread
artembelik marked this conversation as resolved.
[getRowId]="getRowId"
[kbqAgGridRowFocusState]="stateKey"
[kbqAgGridRowFocusStateStore]="store"
[rowData]="rowData()"
[columnDefs]="columnDefs"
[defaultColDef]="defaultColDef"
/>
`,
styles: `
ag-grid-angular {
height: 100%;
max-width: 2036px;
}
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DevRowFocusStateQueryParams {
readonly rowData = devInjectRowData();
readonly store = inject(KbqAgGridRowFocusStateQueryParamsStore);
readonly stateKey = STATE_KEY;
readonly columnDefs = COLUMN_DEFS;
readonly defaultColDef = DEFAULT_COL_DEF;
readonly getRowId = GET_ROW_ID;
}
105 changes: 105 additions & 0 deletions dev/ag-grid-angular/src/tests/row-focus-state.playwright-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { expect, Page, test } from '@playwright/test';
import { getCell, isCellFocused } from './utils/helpers';

const rowFocusStateStorageKey = 'dev-ag-grid-row-focus-state';
const rowFocusStateQueryParamKey = 'dev-ag-grid-row-focus-state';

type StoredRowFocusState = { rowId: string; colId: string };

const clearRowFocusState = async (page: Page): Promise<void> => {
await page.evaluate((key: string) => localStorage.removeItem(key), rowFocusStateStorageKey);
};

const getRowFocusState = async (page: Page): Promise<StoredRowFocusState | null> => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return page.evaluate((key) => {
const stored = localStorage.getItem(key);
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return stored ? JSON.parse(stored) : null;
}, rowFocusStateStorageKey);
};

const getRowFocusStateFromUrl = async (page: Page): Promise<StoredRowFocusState | null> => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return page.evaluate((key) => {
const item = new URLSearchParams(window.location.search).get(key);
if (!item) return null;

try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return JSON.parse(item);
} catch {
return null;
}
}, rowFocusStateQueryParamKey);
};

const buildStateUrl = (state: StoredRowFocusState): string => {
const encoded = encodeURIComponent(JSON.stringify(state));
return `/e2e/row-focus-state-query-params?${rowFocusStateQueryParamKey}=${encoded}`;
};

test.describe('KbqAgGridRowFocusState', () => {
test.describe('KbqAgGridRowFocusStateLocalStorageStore', () => {
test('saves the active cell to localStorage when a cell is clicked', async ({ page }) => {
await page.goto('/e2e/row-focus-state');
await clearRowFocusState(page);

await getCell(page, 0, 'athlete').click();

await expect.poll(async () => (await getRowFocusState(page))?.colId).toBe('athlete');
});

test('restores the active cell from localStorage on page reload', async ({ page }) => {
await page.goto('/e2e/row-focus-state');
await clearRowFocusState(page);

await getCell(page, 2, 'country').click();
await expect.poll(async () => (await getRowFocusState(page))?.colId).toBe('country');

await page.reload();

await expect.poll(async () => isCellFocused(page, 2, 'country')).toBe(true);
});

test('reset clears the active cell and stored state', async ({ page }) => {
await page.goto('/e2e/row-focus-state');
await clearRowFocusState(page);

await getCell(page, 0, 'athlete').click();
await expect.poll(async () => isCellFocused(page, 0, 'athlete')).toBe(true);

await page.getByRole('button', { name: 'Reset state' }).click();

await expect.poll(async () => isCellFocused(page, 0, 'athlete')).toBe(false);
await expect.poll(async () => getRowFocusState(page)).toBeNull();
});
});

test.describe('KbqAgGridRowFocusStateQueryParamsStore', () => {
test('saves the active cell to URL when a cell is clicked', async ({ page }) => {
await page.goto('/e2e/row-focus-state-query-params');

await getCell(page, 0, 'athlete').click();

await expect.poll(async () => (await getRowFocusStateFromUrl(page))?.colId).toBe('athlete');
});

test('restores the active cell from URL on page reload', async ({ page }) => {
await page.goto('/e2e/row-focus-state-query-params');

await getCell(page, 2, 'country').click();
await expect.poll(async () => (await getRowFocusStateFromUrl(page))?.colId).toBe('country');

await page.reload();

await expect.poll(async () => isCellFocused(page, 2, 'country')).toBe(true);
});

test('applies pre-existing active cell from URL on page load', async ({ page }) => {
await page.goto(buildStateUrl({ rowId: '4f32fc42-54ea-4afe-bcb3-d3e15fff11a5', colId: 'athlete' }));

await expect.poll(async () => isCellFocused(page, 0, 'athlete')).toBe(true);
});
});
});
116 changes: 116 additions & 0 deletions dev/ag-grid-angular/src/tests/row-group.playwright-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,35 @@ const naturalCompare = (a: string, b: string): number => a.localeCompare(b, unde

const getUseCustomCellContentCheckbox = (page: Page): Locator => page.getByTestId('useCustomCellContentCheckbox');

const rowGroupCollapsedStateStorageKey = 'dev-ag-grid-row-group-collapsed-state';
const rowGroupSelectionStateStorageKey = 'dev-ag-grid-row-group-selection-state';

const clearRowGroupPersistedState = async (page: Page): Promise<void> => {
await page.evaluate(
({ collapseKey, selectionKey }) => {
localStorage.removeItem(collapseKey);
localStorage.removeItem(selectionKey);
},
{ collapseKey: rowGroupCollapsedStateStorageKey, selectionKey: rowGroupSelectionStateStorageKey }
);
};

const getStoredCollapsedPaths = async (page: Page): Promise<string[] | null> =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
page.evaluate((key) => {
const stored = localStorage.getItem(key);
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return stored ? JSON.parse(stored) : null;
}, rowGroupCollapsedStateStorageKey);

const getStoredSelectedIds = async (page: Page): Promise<string[] | null> =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
page.evaluate((key) => {
const stored = localStorage.getItem(key);
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return stored ? JSON.parse(stored) : null;
}, rowGroupSelectionStateStorageKey);

// The custom kbqAgGridRowGroupCellContent demo (DevRowGroupCustomCellContent) renders its own
// count markup instead of the default `.kbq-ag-grid-group-cell-renderer__count` span.
const getCustomCellContentCount = (page: Page, rowIndex: number): Locator =>
Expand Down Expand Up @@ -723,4 +752,91 @@ test.describe('KbqAgGridRowGroup', () => {
await expect(getGroupCellInner(page, 0)).toHaveAttribute('aria-expanded', 'false');
});
});

test.describe('state persistence', () => {
test('persists collapse state to localStorage and restores it on reload', async ({ page }) => {
await page.goto('/e2e/row-group');
await clearRowGroupPersistedState(page);
await page.reload();
await waitForGroupsVisible(page);

// Expand the first top-level group — row 1 becomes its first (still-collapsed)
// nested Sport sub-group, not the *next* top-level country header.
const firstGroupKey =
(await page.locator('.kbq-ag-grid-group-cell-renderer__key').first().textContent()) ?? '';
await getGroupCellInner(page, 0).click();
await expect(page.locator('.ag-row[row-index="1"]')).toBeVisible();

await expect.poll(async () => getStoredCollapsedPaths(page)).not.toBeNull();

await page.reload();
await waitForGroupsVisible(page);

// The same top-level group must still be expanded after reload.
await expect(getGroupCellInner(page, 0)).toContainText(firstGroupKey);
await expect(getGroupCellInner(page, 0)).toHaveAttribute('aria-expanded', 'true');
await expect(page.locator('.ag-row[row-index="1"]')).toBeVisible();
});

test('reset group state clears the persisted collapse state', async ({ page }) => {
await page.goto('/e2e/row-group');
await clearRowGroupPersistedState(page);
await page.reload();
await waitForGroupsVisible(page);

await getGroupCellInner(page, 0).click();
await expect.poll(async () => getStoredCollapsedPaths(page)).not.toBeNull();

await page.getByTestId('resetRowGroupStateBtn').click();

await expect.poll(async () => getStoredCollapsedPaths(page)).toBeNull();
});

test('persists selection to localStorage and restores it on reload, even hidden inside a collapsed group', async ({
page
}) => {
await page.goto('/e2e/row-group');
await clearRowGroupPersistedState(page);
await page.reload();
await waitForGroupsVisible(page);

// Select Ilya Zakharov while Russia > Diving is still fully collapsed.
await page.getByTestId('selectIlyaZakharovBtn').click();
await expect(page.getByTestId('selectedCount')).toHaveText('1');
await expect.poll(async () => getStoredSelectedIds(page)).not.toBeNull();

await page.reload();
await waitForGroupsVisible(page);

// The full-dataset selection count is restored — including a row not yet loaded
// into AG's row model — once the (async-fetched) row data has arrived.
await expect(page.getByTestId('selectedCount')).toHaveText('1', { timeout: 10_000 });

// Expand down to the row — it comes back visually selected too.
await page.getByTestId('expandRussiaDivingBtn').click();
const athleteRow = getDataRowByText(page, 'Ilya Zakharov');
await expect(athleteRow).toBeVisible();
await expect(athleteRow).toHaveClass(/ag-row-selected/);
});

test('reset group state does not clear the persisted selection — selection is independent of grouping', async ({
page
}) => {
await page.goto('/e2e/row-group');
await clearRowGroupPersistedState(page);
await page.reload();
await waitForGroupsVisible(page);

await page.getByTestId('selectIlyaZakharovBtn').click();
await expect.poll(async () => getStoredSelectedIds(page)).not.toBeNull();

await page.getByTestId('resetRowGroupStateBtn').click();

// Collapse state is cleared by the reset...
await expect.poll(async () => getStoredCollapsedPaths(page)).toBeNull();
// ...but the selection itself — both in storage and in the reported count — remains.
await expect.poll(async () => getStoredSelectedIds(page)).not.toBeNull();
await expect(page.getByTestId('selectedCount')).toHaveText('1');
});
});
});
Loading