Skip to content

Commit bc24ee0

Browse files
committed
Merge branch '42-amend-calls-in-api-and-mcp-and-cli'
2 parents 393e932 + c0c47c9 commit bc24ee0

33 files changed

Lines changed: 502 additions & 110 deletions

apps/web/src/tests/browser/abstraction-smoke-tests/app-page-abstractions-smoke.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ test('add row button creates row in grid', async ({ page }) => {
4747
await appPage.gridEditor.addRow();
4848
await expect.poll(async () => appPage.gridEditor.renderer.countRows()).toBe(initialRowCount + 1);
4949
const rowCountAfterAdd = await appPage.gridEditor.renderer.countRows();
50-
await expect.poll(async () => (await appPage.gridEditor.header.getColumnNames()).length).toBeGreaterThan(0);
50+
await appPage.gridEditor.header.expectHasAnyColumns();
5151
const [primaryColumnName] = await appPage.gridEditor.header.getColumnNames();
5252
expect(primaryColumnName).toBeTruthy();
5353

apps/web/src/tests/browser/abstraction-smoke-tests/grid-editor-controls.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ test('quick filter and clear filters update visible rows', async ({ page }) => {
6060
await appPage.gridEditor.addRow();
6161
await appPage.gridEditor.addRow();
6262
await expect.poll(async () => appPage.gridEditor.renderer.countRows()).toBe(2);
63-
await expect.poll(async () => (await appPage.gridEditor.header.getColumnNames()).length).toBeGreaterThan(0);
63+
await appPage.gridEditor.header.expectHasAnyColumns();
6464
const [primaryColumnName] = await appPage.gridEditor.header.getColumnNames();
6565
expect(primaryColumnName).toBeTruthy();
6666
await appPage.gridEditor.renderer.setCellTextByColumnName(primaryColumnName, 0, 'Filter Match');

apps/web/src/tests/browser/abstraction-smoke-tests/grid-header-controls.spec.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,12 @@ test('grid header sorting and per-column filter change visible results', async (
3939

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

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

5050
await appPage.gridEditor.header.setColumnFilter(targetColumnName, 'Cherry');
@@ -56,7 +56,7 @@ test('grid header sorting and per-column filter change visible results', async (
5656
await appPage.gridEditor.header.setColumnFilter(targetColumnName, '');
5757
await expect.poll(async () => appPage.gridEditor.header.getColumnFilterValue(targetColumnName)).toBe('');
5858
await expect
59-
.poll(async () => (await appPage.gridEditor.renderer.getColumnTextsByName(targetColumnName)).slice(0, 3))
59+
.poll(async () => appPage.gridEditor.renderer.getTopActiveColumnTextsByName(targetColumnName, 3))
6060
.toEqual(['Apple', 'Banana', 'Cherry']);
6161
expect(pageErrors).toEqual([]);
6262
});

apps/web/src/tests/browser/abstraction-smoke-tests/tabbed-formats-options.spec.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,9 @@ test('all format options become dirty and apply cleanly', async ({ page }) => {
6969
expect(await appPage.formatOptionsPanel.isApplyEnabled()).toBe(false);
7070

7171
await appPage.formatOptionsPanel.setFirstEditableOption();
72-
await expect.poll(async () => appPage.formatOptionsPanel.isApplyEnabled()).toBe(true);
72+
await appPage.formatOptionsPanel.expectApplyEnabled(true);
7373
await appPage.formatOptionsPanel.apply();
74-
await expect.poll(async () => appPage.formatOptionsPanel.isApplyEnabled()).toBe(false);
74+
await appPage.formatOptionsPanel.expectApplyEnabled(false);
7575
}
7676

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

8888
await appPage.tabbedText.togglePreviewEdit(false);
89-
await expect.poll(async () => appPage.tabbedText.getPreviewEditLabel()).toBe('Edit');
89+
await appPage.tabbedText.expectPreviewEditLabel('Edit');
9090
expect(await appPage.importExportControls.isSetGridFromTextEnabled()).toBe(true);
9191

9292
await appPage.tabbedText.togglePreviewEdit();
93-
await expect.poll(async () => appPage.tabbedText.getPreviewEditLabel()).toContain('Preview');
93+
await appPage.tabbedText.expectPreviewEditLabelContains('Preview');
9494
expect(await appPage.importExportControls.isSetGridFromTextEnabled()).toBe(false);
9595

9696
expect(pageErrors).toEqual([]);

apps/web/src/tests/browser/abstractions/app.page.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
const { expect } = require('@playwright/test');
12
const { TopNavigationComponent } = require('./components/top-navigation.component');
23
const { GridEditorComponent } = require('./components/grid-editor.component');
34
const { ImportExportControlsComponent } = require('./components/import-export-controls.component');
@@ -25,7 +26,7 @@ class AppPage {
2526
}
2627

2728
async waitUntilReady() {
28-
await this.initialLoading.waitFor({ state: 'hidden' });
29+
await expect(this.initialLoading).toBeHidden();
2930
await this.topNavigation.expectReady();
3031
await this.gridEditor.expectReady();
3132
await this.importExportControls.expectReady();

apps/web/src/tests/browser/abstractions/components/format-options-panel.component.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
1+
const { expect } = require('@playwright/test');
2+
13
class FormatOptionsPanelComponent {
24
constructor(page) {
35
this.page = page;
46
this.container = page.locator('#tabbedTextArea .options-parent');
57
}
68

79
async expectVisible() {
8-
await this.container.waitFor({ state: 'visible' });
10+
await expect(this.container).toBeVisible();
911
}
1012

1113
async expectReady() {
1214
await this.expectVisible();
13-
await this.container.locator('.apply-options').first().waitFor({ state: 'visible' });
15+
await expect(this.container.locator('.apply-options').first()).toBeVisible();
1416
}
1517

1618
async hasApplyButton() {
@@ -22,6 +24,15 @@ class FormatOptionsPanelComponent {
2224
return button.isEnabled();
2325
}
2426

27+
async expectApplyEnabled(enabled = true) {
28+
const button = this.container.locator('.apply-options').first();
29+
if (enabled) {
30+
await expect(button).toBeEnabled();
31+
return;
32+
}
33+
await expect(button).toBeDisabled();
34+
}
35+
2536
async apply() {
2637
await this.container.locator('.apply-options').first().click();
2738
}

apps/web/src/tests/browser/abstractions/components/grid-editor.component.js

Lines changed: 99 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
const { expect } = require('@playwright/test');
12
const { GridRendererComponent } = require('./grid-renderer.component');
23
const { GridHeaderComponent } = require('./grid-header.component');
34

@@ -7,7 +8,7 @@ class GridEditorComponent {
78
this.container = page.locator('#main-grid-view');
89
this.grid = page.locator('#myGrid');
910
this.renderer = new GridRendererComponent(page, this.grid);
10-
this.header = new GridHeaderComponent(page, this.grid);
11+
this.header = new GridHeaderComponent(page, this.grid, this.renderer);
1112
this.addRowButton = page.getByRole('button', { name: 'Add Row', exact: true });
1213
this.addRowsAboveButton = page.getByRole('button', { name: 'Add Rows Above' });
1314
this.addRowsBelowButton = page.getByRole('button', { name: 'Add Rows Below' });
@@ -19,21 +20,21 @@ class GridEditorComponent {
1920
}
2021

2122
async expectVisible() {
22-
await this.container.waitFor({ state: 'visible' });
23-
await this.grid.waitFor({ state: 'visible' });
24-
await this.addRowButton.waitFor({ state: 'visible' });
25-
await this.addRowsAboveButton.waitFor({ state: 'visible' });
26-
await this.addRowsBelowButton.waitFor({ state: 'visible' });
27-
await this.deleteSelectedRowsButton.waitFor({ state: 'visible' });
28-
await this.quickFilterInput.waitFor({ state: 'visible' });
29-
await this.clearFiltersButton.waitFor({ state: 'visible' });
30-
await this.clearSortButton.waitFor({ state: 'visible' });
31-
await this.resetTableButton.waitFor({ state: 'visible' });
23+
await expect(this.container).toBeVisible();
24+
await expect(this.grid).toBeVisible();
25+
await expect(this.addRowButton).toBeVisible();
26+
await expect(this.addRowsAboveButton).toBeVisible();
27+
await expect(this.addRowsBelowButton).toBeVisible();
28+
await expect(this.deleteSelectedRowsButton).toBeVisible();
29+
await expect(this.quickFilterInput).toBeVisible();
30+
await expect(this.clearFiltersButton).toBeVisible();
31+
await expect(this.clearSortButton).toBeVisible();
32+
await expect(this.resetTableButton).toBeVisible();
3233
}
3334

3435
async expectReady() {
3536
await this.expectVisible();
36-
await this.grid.locator('.tabulator-col-title').first().waitFor({ state: 'visible' });
37+
await expect(this.grid.locator('.tabulator-col-title').first()).toBeVisible();
3738
}
3839

3940
async addRow() {
@@ -84,21 +85,93 @@ class GridEditorComponent {
8485
await this.renderer.selectRows(rowIndexes);
8586
}
8687

87-
async clearFilters() {
88-
for (let attempt = 0; attempt < 3; attempt += 1) {
89-
await this.clearFiltersButton.click();
90-
for (let check = 0; check < 20; check += 1) {
91-
const quickFilterValue = await this.quickFilterInput.inputValue();
92-
const hasActiveColumnFilter = await this.grid
93-
.locator('.tabulator-header-filter input')
94-
.evaluateAll((inputs) => inputs.some((input) => String(input.value || '').trim().length > 0));
95-
if (quickFilterValue === '' && !hasActiveColumnFilter) {
96-
return;
97-
}
98-
await this.page.waitForTimeout(50);
99-
}
88+
async clearFilters({ expectedActiveRowCount } = {}) {
89+
const context = await this._buildClearFiltersContext(expectedActiveRowCount);
90+
91+
try {
92+
await expect(async () => {
93+
await this._attemptClearFilters(context);
94+
}).toPass({ timeout: 15000, intervals: [100, 200, 400, 800] });
95+
return;
96+
} catch (error) {
97+
// fall through to detailed diagnostics
10098
}
101-
throw new Error('Failed to clear all filters.');
99+
100+
await this._throwClearFiltersDiagnostics(context);
101+
}
102+
103+
async _attemptClearFilters(context) {
104+
await this.clearFiltersButton.click();
105+
await expect(this.quickFilterInput).toHaveValue('');
106+
await this._expectHeaderFiltersCleared();
107+
await this._expectActiveRowCountRecovered(context);
108+
await this.renderer.waitForGridSettle({ columnName: context.diagnosticColumn, stableForMs: 2000, timeoutMs: 7000 });
109+
await this._expectHeaderFiltersCleared();
110+
await this._expectActiveRowCountRecovered(context);
111+
}
112+
113+
async _buildClearFiltersContext(expectedActiveRowCount) {
114+
const initialActiveRowCount = await this.renderer.getActiveRowCount();
115+
const columnNames = await this.header.getColumnNames();
116+
const diagnosticColumn = columnNames[0];
117+
const hadActiveFiltersBeforeClear = await this._hasActiveFilters();
118+
const minRecoveredRowCount =
119+
hadActiveFiltersBeforeClear && !Number.isFinite(expectedActiveRowCount)
120+
? initialActiveRowCount + 1
121+
: initialActiveRowCount;
122+
return {
123+
expectedActiveRowCount,
124+
initialActiveRowCount,
125+
diagnosticColumn,
126+
minRecoveredRowCount,
127+
};
128+
}
129+
130+
async _hasActiveFilters() {
131+
const quickFilterValue = await this.quickFilterInput.inputValue();
132+
if (String(quickFilterValue || '').trim().length > 0) {
133+
return true;
134+
}
135+
const headerFilterValues = await this._getHeaderFilterValues();
136+
return headerFilterValues.some((value) => value.length > 0);
137+
}
138+
139+
async _getHeaderFilterValues() {
140+
return this.grid
141+
.locator('.tabulator-header-filter input')
142+
.evaluateAll((inputs) => inputs.map((input) => String(input.value || '').trim()));
143+
}
144+
145+
async _expectHeaderFiltersCleared() {
146+
const headerFilterValues = await this._getHeaderFilterValues();
147+
expect(headerFilterValues.some((value) => value.length > 0)).toBe(false);
148+
}
149+
150+
async _expectActiveRowCountRecovered(context) {
151+
const activeRowCount = await this.renderer.getActiveRowCount();
152+
if (Number.isFinite(context.expectedActiveRowCount)) {
153+
expect(activeRowCount).toBe(context.expectedActiveRowCount);
154+
} else {
155+
expect(activeRowCount).toBeGreaterThanOrEqual(context.minRecoveredRowCount);
156+
}
157+
}
158+
159+
async _throwClearFiltersDiagnostics(context) {
160+
const quickFilterValue = await this.quickFilterInput.inputValue();
161+
const headerFilterValues = await this._getHeaderFilterValues();
162+
const activeRowCount = await this.renderer.getActiveRowCount();
163+
const snapshot = await this.renderer.getActiveTableSnapshot(context.diagnosticColumn, 3);
164+
throw new Error(
165+
[
166+
'Failed to clear all filters and restore active rows.',
167+
`quickFilter="${quickFilterValue}"`,
168+
`headerFilters=${JSON.stringify(headerFilterValues)}`,
169+
`activeRowCount=${activeRowCount}`,
170+
`expectedActiveRowCount=${Number.isFinite(context.expectedActiveRowCount) ? context.expectedActiveRowCount : 'n/a'}`,
171+
`diagnosticColumn=${context.diagnosticColumn || 'n/a'}`,
172+
`topValues=${JSON.stringify(snapshot.topValues || [])}`,
173+
].join(' ')
174+
);
102175
}
103176

104177
async clearSort() {

apps/web/src/tests/browser/abstractions/components/grid-header.component.js

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
const { expect } = require('@playwright/test');
2+
const { GridRendererComponent } = require('./grid-renderer.component');
3+
14
class GridHeaderComponent {
2-
constructor(page, gridRootLocator) {
5+
constructor(page, gridRootLocator, renderer = undefined) {
36
this.page = page;
47
this.gridRoot = gridRootLocator;
58
this.headers = this.gridRoot.locator('.tabulator-col');
9+
this.renderer = renderer || new GridRendererComponent(page, gridRootLocator);
610
}
711

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

26+
async expectHasAnyColumns() {
27+
await expect(this.gridRoot.locator('.tabulator-col .tabulator-col-title').first()).toBeVisible();
28+
}
29+
2230
async clickAction(columnName, action) {
2331
const headerTitle = this._headerTitleByName(columnName);
2432
const actionTitleMap = {
@@ -74,10 +82,12 @@ class GridHeaderComponent {
7482

7583
async sortAsc(columnName) {
7684
await this._ensureSortState(columnName, 'asc', 'sort-asc');
85+
await this.renderer.waitForGridSettle({ columnName });
7786
}
7887

7988
async sortDesc(columnName) {
8089
await this._ensureSortState(columnName, 'desc', 'sort-desc');
90+
await this.renderer.waitForGridSettle({ columnName });
8191
}
8292

8393
async clearSort(columnName) {
@@ -105,17 +115,11 @@ class GridHeaderComponent {
105115
}
106116

107117
async _ensureSortState(columnName, expectedState, action) {
108-
for (let attempt = 0; attempt < 3; attempt += 1) {
118+
await expect(async () => {
109119
await this.clickAction(columnName, action);
110-
for (let check = 0; check < 10; check += 1) {
111-
const state = await this.getColumnSortState(columnName);
112-
if (String(state).includes(expectedState)) {
113-
return;
114-
}
115-
await this.page.waitForTimeout(50);
116-
}
117-
}
118-
throw new Error(`Failed to set sort state '${expectedState}' for column '${columnName}'.`);
120+
const state = await this.getColumnSortState(columnName);
121+
expect(String(state)).toContain(expectedState);
122+
}).toPass({ timeout: 3000, intervals: [100, 200, 400] });
119123
}
120124

121125
_headerTitleByName(columnName) {

0 commit comments

Comments
 (0)