Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ test('add row button creates row in grid', async ({ page }) => {
await appPage.gridEditor.addRow();
await expect.poll(async () => appPage.gridEditor.renderer.countRows()).toBe(initialRowCount + 1);
const rowCountAfterAdd = await appPage.gridEditor.renderer.countRows();
await expect.poll(async () => (await appPage.gridEditor.header.getColumnNames()).length).toBeGreaterThan(0);
await appPage.gridEditor.header.expectHasAnyColumns();
const [primaryColumnName] = await appPage.gridEditor.header.getColumnNames();
expect(primaryColumnName).toBeTruthy();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ test('quick filter and clear filters update visible rows', async ({ page }) => {
await appPage.gridEditor.addRow();
await appPage.gridEditor.addRow();
await expect.poll(async () => appPage.gridEditor.renderer.countRows()).toBe(2);
await expect.poll(async () => (await appPage.gridEditor.header.getColumnNames()).length).toBeGreaterThan(0);
await appPage.gridEditor.header.expectHasAnyColumns();
const [primaryColumnName] = await appPage.gridEditor.header.getColumnNames();
expect(primaryColumnName).toBeTruthy();
await appPage.gridEditor.renderer.setCellTextByColumnName(primaryColumnName, 0, 'Filter Match');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ test('grid header sorting and per-column filter change visible results', async (

await appPage.gridEditor.header.sortDesc(targetColumnName);
await expect
.poll(async () => (await appPage.gridEditor.renderer.getColumnTextsByName(targetColumnName)).slice(0, 3))
.poll(async () => appPage.gridEditor.renderer.getTopActiveColumnTextsByName(targetColumnName, 3))
.toEqual(['Cherry', 'Banana', 'Apple']);

await appPage.gridEditor.header.sortAsc(targetColumnName);
await expect
.poll(async () => (await appPage.gridEditor.renderer.getColumnTextsByName(targetColumnName)).slice(0, 3))
.poll(async () => appPage.gridEditor.renderer.getTopActiveColumnTextsByName(targetColumnName, 3))
.toEqual(['Apple', 'Banana', 'Cherry']);

await appPage.gridEditor.header.setColumnFilter(targetColumnName, 'Cherry');
Expand All @@ -56,7 +56,7 @@ test('grid header sorting and per-column filter change visible results', async (
await appPage.gridEditor.header.setColumnFilter(targetColumnName, '');
await expect.poll(async () => appPage.gridEditor.header.getColumnFilterValue(targetColumnName)).toBe('');
await expect
.poll(async () => (await appPage.gridEditor.renderer.getColumnTextsByName(targetColumnName)).slice(0, 3))
.poll(async () => appPage.gridEditor.renderer.getTopActiveColumnTextsByName(targetColumnName, 3))
.toEqual(['Apple', 'Banana', 'Cherry']);
expect(pageErrors).toEqual([]);
});
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@ test('all format options become dirty and apply cleanly', async ({ page }) => {
expect(await appPage.formatOptionsPanel.isApplyEnabled()).toBe(false);

await appPage.formatOptionsPanel.setFirstEditableOption();
await expect.poll(async () => appPage.formatOptionsPanel.isApplyEnabled()).toBe(true);
await appPage.formatOptionsPanel.expectApplyEnabled(true);
await appPage.formatOptionsPanel.apply();
await expect.poll(async () => appPage.formatOptionsPanel.isApplyEnabled()).toBe(false);
await appPage.formatOptionsPanel.expectApplyEnabled(false);
}

expect(pageErrors).toEqual([]);
Expand All @@ -86,11 +86,11 @@ test('preview edit mode and set-grid-from-text state transitions are correct', a
expect(await appPage.tabbedText.getPreviewEditLabel()).toContain('Preview');

await appPage.tabbedText.togglePreviewEdit(false);
await expect.poll(async () => appPage.tabbedText.getPreviewEditLabel()).toBe('Edit');
await appPage.tabbedText.expectPreviewEditLabel('Edit');
expect(await appPage.importExportControls.isSetGridFromTextEnabled()).toBe(true);

await appPage.tabbedText.togglePreviewEdit();
await expect.poll(async () => appPage.tabbedText.getPreviewEditLabel()).toContain('Preview');
await appPage.tabbedText.expectPreviewEditLabelContains('Preview');
expect(await appPage.importExportControls.isSetGridFromTextEnabled()).toBe(false);

expect(pageErrors).toEqual([]);
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/tests/browser/abstractions/app.page.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const { expect } = require('@playwright/test');
const { TopNavigationComponent } = require('./components/top-navigation.component');
const { GridEditorComponent } = require('./components/grid-editor.component');
const { ImportExportControlsComponent } = require('./components/import-export-controls.component');
Expand Down Expand Up @@ -25,7 +26,7 @@ class AppPage {
}

async waitUntilReady() {
await this.initialLoading.waitFor({ state: 'hidden' });
await expect(this.initialLoading).toBeHidden();
await this.topNavigation.expectReady();
await this.gridEditor.expectReady();
await this.importExportControls.expectReady();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
const { expect } = require('@playwright/test');

class FormatOptionsPanelComponent {
constructor(page) {
this.page = page;
this.container = page.locator('#tabbedTextArea .options-parent');
}

async expectVisible() {
await this.container.waitFor({ state: 'visible' });
await expect(this.container).toBeVisible();
}

async expectReady() {
await this.expectVisible();
await this.container.locator('.apply-options').first().waitFor({ state: 'visible' });
await expect(this.container.locator('.apply-options').first()).toBeVisible();
}

async hasApplyButton() {
Expand All @@ -22,6 +24,15 @@ class FormatOptionsPanelComponent {
return button.isEnabled();
}

async expectApplyEnabled(enabled = true) {
const button = this.container.locator('.apply-options').first();
if (enabled) {
await expect(button).toBeEnabled();
return;
}
await expect(button).toBeDisabled();
}

async apply() {
await this.container.locator('.apply-options').first().click();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const { expect } = require('@playwright/test');
const { GridRendererComponent } = require('./grid-renderer.component');
const { GridHeaderComponent } = require('./grid-header.component');

Expand All @@ -7,7 +8,7 @@ class GridEditorComponent {
this.container = page.locator('#main-grid-view');
this.grid = page.locator('#myGrid');
this.renderer = new GridRendererComponent(page, this.grid);
this.header = new GridHeaderComponent(page, this.grid);
this.header = new GridHeaderComponent(page, this.grid, this.renderer);
this.addRowButton = page.getByRole('button', { name: 'Add Row', exact: true });
this.addRowsAboveButton = page.getByRole('button', { name: 'Add Rows Above' });
this.addRowsBelowButton = page.getByRole('button', { name: 'Add Rows Below' });
Expand All @@ -19,21 +20,21 @@ class GridEditorComponent {
}

async expectVisible() {
await this.container.waitFor({ state: 'visible' });
await this.grid.waitFor({ state: 'visible' });
await this.addRowButton.waitFor({ state: 'visible' });
await this.addRowsAboveButton.waitFor({ state: 'visible' });
await this.addRowsBelowButton.waitFor({ state: 'visible' });
await this.deleteSelectedRowsButton.waitFor({ state: 'visible' });
await this.quickFilterInput.waitFor({ state: 'visible' });
await this.clearFiltersButton.waitFor({ state: 'visible' });
await this.clearSortButton.waitFor({ state: 'visible' });
await this.resetTableButton.waitFor({ state: 'visible' });
await expect(this.container).toBeVisible();
await expect(this.grid).toBeVisible();
await expect(this.addRowButton).toBeVisible();
await expect(this.addRowsAboveButton).toBeVisible();
await expect(this.addRowsBelowButton).toBeVisible();
await expect(this.deleteSelectedRowsButton).toBeVisible();
await expect(this.quickFilterInput).toBeVisible();
await expect(this.clearFiltersButton).toBeVisible();
await expect(this.clearSortButton).toBeVisible();
await expect(this.resetTableButton).toBeVisible();
}

async expectReady() {
await this.expectVisible();
await this.grid.locator('.tabulator-col-title').first().waitFor({ state: 'visible' });
await expect(this.grid.locator('.tabulator-col-title').first()).toBeVisible();
}

async addRow() {
Expand Down Expand Up @@ -84,21 +85,93 @@ class GridEditorComponent {
await this.renderer.selectRows(rowIndexes);
}

async clearFilters() {
for (let attempt = 0; attempt < 3; attempt += 1) {
await this.clearFiltersButton.click();
for (let check = 0; check < 20; check += 1) {
const quickFilterValue = await this.quickFilterInput.inputValue();
const hasActiveColumnFilter = await this.grid
.locator('.tabulator-header-filter input')
.evaluateAll((inputs) => inputs.some((input) => String(input.value || '').trim().length > 0));
if (quickFilterValue === '' && !hasActiveColumnFilter) {
return;
}
await this.page.waitForTimeout(50);
}
async clearFilters({ expectedActiveRowCount } = {}) {
const context = await this._buildClearFiltersContext(expectedActiveRowCount);

try {
await expect(async () => {
await this._attemptClearFilters(context);
}).toPass({ timeout: 15000, intervals: [100, 200, 400, 800] });
return;
} catch (error) {
// fall through to detailed diagnostics
}
throw new Error('Failed to clear all filters.');

await this._throwClearFiltersDiagnostics(context);
}

async _attemptClearFilters(context) {
await this.clearFiltersButton.click();
await expect(this.quickFilterInput).toHaveValue('');
await this._expectHeaderFiltersCleared();
await this._expectActiveRowCountRecovered(context);
await this.renderer.waitForGridSettle({ columnName: context.diagnosticColumn, stableForMs: 2000, timeoutMs: 7000 });
await this._expectHeaderFiltersCleared();
await this._expectActiveRowCountRecovered(context);
}

async _buildClearFiltersContext(expectedActiveRowCount) {
const initialActiveRowCount = await this.renderer.getActiveRowCount();
const columnNames = await this.header.getColumnNames();
const diagnosticColumn = columnNames[0];
const hadActiveFiltersBeforeClear = await this._hasActiveFilters();
const minRecoveredRowCount =
hadActiveFiltersBeforeClear && !Number.isFinite(expectedActiveRowCount)
? initialActiveRowCount + 1
: initialActiveRowCount;
return {
expectedActiveRowCount,
initialActiveRowCount,
diagnosticColumn,
minRecoveredRowCount,
};
}

async _hasActiveFilters() {
const quickFilterValue = await this.quickFilterInput.inputValue();
if (String(quickFilterValue || '').trim().length > 0) {
return true;
}
const headerFilterValues = await this._getHeaderFilterValues();
return headerFilterValues.some((value) => value.length > 0);
}

async _getHeaderFilterValues() {
return this.grid
.locator('.tabulator-header-filter input')
.evaluateAll((inputs) => inputs.map((input) => String(input.value || '').trim()));
}

async _expectHeaderFiltersCleared() {
const headerFilterValues = await this._getHeaderFilterValues();
expect(headerFilterValues.some((value) => value.length > 0)).toBe(false);
}

async _expectActiveRowCountRecovered(context) {
const activeRowCount = await this.renderer.getActiveRowCount();
if (Number.isFinite(context.expectedActiveRowCount)) {
expect(activeRowCount).toBe(context.expectedActiveRowCount);
} else {
expect(activeRowCount).toBeGreaterThanOrEqual(context.minRecoveredRowCount);
}
}

async _throwClearFiltersDiagnostics(context) {
const quickFilterValue = await this.quickFilterInput.inputValue();
const headerFilterValues = await this._getHeaderFilterValues();
const activeRowCount = await this.renderer.getActiveRowCount();
const snapshot = await this.renderer.getActiveTableSnapshot(context.diagnosticColumn, 3);
throw new Error(
[
'Failed to clear all filters and restore active rows.',
`quickFilter="${quickFilterValue}"`,
`headerFilters=${JSON.stringify(headerFilterValues)}`,
`activeRowCount=${activeRowCount}`,
`expectedActiveRowCount=${Number.isFinite(context.expectedActiveRowCount) ? context.expectedActiveRowCount : 'n/a'}`,
`diagnosticColumn=${context.diagnosticColumn || 'n/a'}`,
`topValues=${JSON.stringify(snapshot.topValues || [])}`,
].join(' ')
);
}

async clearSort() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
const { expect } = require('@playwright/test');
const { GridRendererComponent } = require('./grid-renderer.component');

class GridHeaderComponent {
constructor(page, gridRootLocator) {
constructor(page, gridRootLocator, renderer = undefined) {
this.page = page;
this.gridRoot = gridRootLocator;
this.headers = this.gridRoot.locator('.tabulator-col');
this.renderer = renderer || new GridRendererComponent(page, gridRootLocator);
}

async getColumnNames() {
Expand All @@ -19,6 +23,10 @@ class GridHeaderComponent {
return this.gridRoot.locator('.tabulator-col .tabulator-col-title').count();
}

async expectHasAnyColumns() {
await expect(this.gridRoot.locator('.tabulator-col .tabulator-col-title').first()).toBeVisible();
}

async clickAction(columnName, action) {
const headerTitle = this._headerTitleByName(columnName);
const actionTitleMap = {
Expand Down Expand Up @@ -74,10 +82,12 @@ class GridHeaderComponent {

async sortAsc(columnName) {
await this._ensureSortState(columnName, 'asc', 'sort-asc');
await this.renderer.waitForGridSettle({ columnName });
}

async sortDesc(columnName) {
await this._ensureSortState(columnName, 'desc', 'sort-desc');
await this.renderer.waitForGridSettle({ columnName });
}

async clearSort(columnName) {
Expand Down Expand Up @@ -105,17 +115,11 @@ class GridHeaderComponent {
}

async _ensureSortState(columnName, expectedState, action) {
for (let attempt = 0; attempt < 3; attempt += 1) {
await expect(async () => {
await this.clickAction(columnName, action);
for (let check = 0; check < 10; check += 1) {
const state = await this.getColumnSortState(columnName);
if (String(state).includes(expectedState)) {
return;
}
await this.page.waitForTimeout(50);
}
}
throw new Error(`Failed to set sort state '${expectedState}' for column '${columnName}'.`);
const state = await this.getColumnSortState(columnName);
expect(String(state)).toContain(expectedState);
}).toPass({ timeout: 3000, intervals: [100, 200, 400] });
}

_headerTitleByName(columnName) {
Expand Down
Loading
Loading