Skip to content

Commit 4588a3b

Browse files
authored
Merge pull request #138 from eviltester/135-better-editing-popup
135 better editing popup
2 parents 234a112 + b826f4d commit 4588a3b

56 files changed

Lines changed: 2771 additions & 1284 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/web/src/tests/browser/app/abstractions/components/test-data-panel.component.js

Lines changed: 88 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const { expect } = require('@playwright/test');
22
const { GridRendererComponent } = require('./grid-renderer.component');
3+
const { SchemaEditorComponent } = require('../../../shared/abstractions/components/schema-editor.component');
34

45
class TestDataPanelComponent {
56
constructor(page) {
@@ -15,11 +16,40 @@ class TestDataPanelComponent {
1516
this.amendTableMode = page.locator('input[name="testDataGenerationMode"][value="amend-table"]');
1617
this.amendSelectedMode = page.locator('input[name="testDataGenerationMode"][value="amend-selected"]');
1718
this.schemaTextArea = page.locator('#testDataSchemaText');
19+
this.schemaError = page.locator('#testdata-schema-error');
1820
this.status = page.locator('#testdata-status');
19-
this.schemaGrid = page.locator('#testDataSchemaGrid');
21+
this.schemaGrid = page.locator('#testDataSchemaGrid, #testDataSchemaRows');
2022
this.schemaRenderer = new GridRendererComponent(page, this.schemaGrid);
21-
this.addSchemaColumnButton = page.getByRole('button', { name: /\+ Add Column/ });
23+
this.addSchemaColumnButton = page.getByRole('button', { name: /\+ Add (Column|Field)/ });
2224
this.deleteSelectedSchemaRowsButton = this.container.getByRole('button', { name: '- Delete Selected' });
25+
this.selectedSchemaRowIndex = 0;
26+
this.schemaEditor = new SchemaEditorComponent(page, {
27+
rowsSelector: '#testDataSchemaRows',
28+
textAreaSelector: '#testDataSchemaText',
29+
modeToggleSelector: '#testDataSchemaModeToggleButton',
30+
addFieldSelector: '#testDataAddSchemaRowButton',
31+
fieldMap: {
32+
columnName: 'name',
33+
value: 'value',
34+
comments: 'comments',
35+
params: 'params',
36+
type: 'sourceType',
37+
},
38+
rowCountResolver: async (editor) => {
39+
const specText = await editor.getSchemaText();
40+
const schemaLines = specText
41+
.split(/\r?\n/)
42+
.map((line) => line.trim())
43+
.filter((line) => line.length > 0 && !line.startsWith('#'));
44+
const domCount = Math.max(0, (await editor.rows.count()) - 1);
45+
const textCount = schemaLines.length > 0 ? Math.ceil(schemaLines.length / 2) : 0;
46+
return Math.max(domCount, textCount);
47+
},
48+
});
49+
}
50+
51+
async isRowEditorMode() {
52+
return this.schemaEditor.isRowEditorMode();
2353
}
2454

2555
async expectVisible() {
@@ -47,7 +77,6 @@ class TestDataPanelComponent {
4777
await expect(this.generateButton).toBeVisible();
4878
await expect(this.refreshTextPreviewButton).toBeVisible();
4979
await expect(this.generateCountInput).toBeVisible();
50-
await expect(this.schemaTextArea).toBeVisible();
5180
await expect(this.schemaGrid).toBeVisible();
5281
}
5382

@@ -76,8 +105,11 @@ class TestDataPanelComponent {
76105
}
77106

78107
async setSchemaText(specText) {
79-
await this.schemaTextArea.fill(specText);
80-
await this.schemaTextArea.press('Tab');
108+
await this.schemaEditor.setSchemaText(specText, { ensureTextMode: true, pressTab: true, waitMs: 1200 });
109+
}
110+
111+
async setSchemaTextMode(enabled) {
112+
await this.schemaEditor.setTextMode(Boolean(enabled));
81113
}
82114

83115
async clickGenerate() {
@@ -101,57 +133,83 @@ class TestDataPanelComponent {
101133
}
102134

103135
async deleteSelectedSchemaRows() {
136+
if (await this.isRowEditorMode()) {
137+
const row = this.schemaEditor.row(this.selectedSchemaRowIndex || 0);
138+
const removeButton = row.locator('[data-action="remove"]');
139+
if ((await removeButton.count()) > 0) {
140+
await removeButton.click();
141+
} else {
142+
await this.page.locator('#testDataSchemaRows .generator-schema-row [data-action="remove"]').first().click();
143+
}
144+
return;
145+
}
104146
await this.deleteSelectedSchemaRowsButton.click();
105147
}
106148

107149
async getSchemaRowCount() {
108-
return this.schemaRenderer.countRows();
150+
return this.schemaEditor.getRowCount();
109151
}
110152

111153
async selectSchemaRow(rowIndex) {
154+
if (await this.isRowEditorMode()) {
155+
this.selectedSchemaRowIndex = rowIndex;
156+
await this.schemaEditor.row(rowIndex).click();
157+
return;
158+
}
112159
const row = this.schemaGrid.locator('.tabulator-row').nth(rowIndex);
113160
await row.click();
114161
await this.page.keyboard.press('Space');
115162
}
116163

117164
async getSelectedSchemaRowCount() {
165+
if (await this.isRowEditorMode()) {
166+
return 1;
167+
}
118168
return this.schemaRenderer.countSelectedRows();
119169
}
120170

121171
async setSchemaCell(rowIndex, field, value) {
122-
await this.schemaRenderer.setCellTextByField(field, rowIndex, value);
172+
await this.schemaEditor.setRowField(rowIndex, field, value);
123173
}
124174

125-
async setSchemaTypeValue(rowIndex, value) {
126-
// Type column may use Tabulator list editor. Try generic set first.
127-
await this.schemaRenderer.clickCellByField('type', rowIndex);
128-
await this.schemaRenderer.clickCellByField('type', rowIndex);
129-
130-
let editor = this.schemaGrid
131-
.locator('.tabulator-editing input, .tabulator-editing textarea, .tabulator-editing select')
132-
.first();
133-
try {
134-
await expect.poll(async () => editor.count(), { timeout: 1000 }).toBeGreaterThan(0);
135-
} catch {
136-
await this.schemaRenderer.setCellTextByField('type', rowIndex, value);
137-
return;
138-
}
139-
140-
if (await editor.locator('xpath=self::select').count()) {
141-
await editor.selectOption(String(value));
142-
} else {
143-
await editor.fill(String(value));
144-
await editor.press('Enter');
145-
}
146-
await expect.poll(async () => editor.count()).toBe(0);
147-
return;
175+
async setSchemaTypeValue(rowIndex, value, { pickerTab = null, assertSchemaTextIncludesType = true } = {}) {
176+
await this.schemaEditor.setRowTypeValue(rowIndex, value, {
177+
pickerTab,
178+
assertSchemaTextIncludesType,
179+
});
148180
}
149181

150182
async getSchemaText() {
151183
return this.schemaTextArea.inputValue();
152184
}
153185

186+
async getSchemaErrorText() {
187+
return (await this.schemaError.textContent())?.trim() || '';
188+
}
189+
154190
async getSchemaCell(rowIndex, field) {
191+
const mapped = this.schemaEditor.resolveField(field);
192+
const input = this.schemaEditor.row(rowIndex).locator(`[data-field="${mapped}"]`);
193+
if ((await input.count()) > 0) {
194+
const tag = await input.first().evaluate((el) => el.tagName.toLowerCase());
195+
if (tag === 'select') {
196+
const sourceType = await input.inputValue();
197+
if (mapped === 'sourceType' && (sourceType === 'domain' || sourceType === 'faker')) {
198+
const commandButton = this.schemaEditor.row(rowIndex).locator('[data-action="pick-command"]');
199+
if ((await commandButton.count()) > 0) {
200+
return (await commandButton.first().innerText()).trim();
201+
}
202+
}
203+
return sourceType;
204+
}
205+
return input.inputValue();
206+
}
207+
if (mapped === 'value') {
208+
const paramsInput = this.schemaEditor.row(rowIndex).locator('[data-field="params"]');
209+
if ((await paramsInput.count()) > 0) {
210+
return paramsInput.inputValue();
211+
}
212+
}
155213
return this.schemaRenderer.getCellTextByField(field, rowIndex);
156214
}
157215
}

apps/web/src/tests/browser/app/functional/test-data/schema-grid-row-controls.spec.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ test.describe('7. Test Data Generation', () => {
1010

1111
const beforeCount = await appPage.testDataPanel.getSchemaRowCount();
1212
await appPage.testDataPanel.addSchemaRow();
13-
await expect.poll(async () => appPage.testDataPanel.getSchemaRowCount()).toBe(beforeCount + 1);
13+
await expect.poll(async () => appPage.testDataPanel.getSchemaRowCount()).toBeGreaterThanOrEqual(beforeCount + 1);
1414

15-
const newRowIndex = beforeCount;
15+
const newRowIndex = Math.max(beforeCount, 0);
1616
await appPage.testDataPanel.setSchemaCell(newRowIndex, 'columnName', 'New Column');
1717
await appPage.testDataPanel.setSchemaTypeValue(newRowIndex, 'literal');
1818

@@ -30,7 +30,7 @@ test.describe('7. Test Data Generation', () => {
3030

3131
const initialCount = await appPage.testDataPanel.getSchemaRowCount();
3232
await appPage.testDataPanel.addSchemaRow();
33-
await expect.poll(async () => appPage.testDataPanel.getSchemaRowCount()).toBe(initialCount + 1);
33+
await expect.poll(async () => appPage.testDataPanel.getSchemaRowCount()).toBeGreaterThanOrEqual(initialCount + 1);
3434
await appPage.testDataPanel.setSchemaCell(initialCount, 'columnName', 'Delete Me');
3535
await appPage.testDataPanel.setSchemaTypeValue(initialCount, 'literal');
3636

apps/web/src/tests/browser/app/functional/test-data/text-schema-grid-sync.spec.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,22 @@ test.describe('7. Test Data Generation', () => {
7777
await expect.poll(async () => appPage.testDataPanel.getSchemaText()).toContain('literal(X-001)');
7878
expectNoPageErrors(pageErrors);
7979
});
80+
81+
test('empty schema text can switch back to schema mode without locking', async ({ page }) => {
82+
const { appPage, pageErrors } = await openApp(page);
83+
84+
await appPage.testDataPanel.expand();
85+
await appPage.testDataPanel.expectExpanded();
86+
87+
await appPage.testDataPanel.setSchemaText('');
88+
await appPage.testDataPanel.setSchemaTextMode(false);
89+
90+
await expect.poll(async () => appPage.testDataPanel.isRowEditorMode()).toBe(true);
91+
await expect
92+
.poll(async () =>
93+
(await appPage.testDataPanel.getSchemaErrorText()).includes('No rules defined. Provide column/rule pairs.')
94+
)
95+
.toBe(false);
96+
expectNoPageErrors(pageErrors);
97+
});
8098
});

apps/web/src/tests/browser/app/functional/test-data/text-schema.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ test.describe('7. Test Data Generation', () => {
1313
await appPage.testDataPanel.setSchemaText(
1414
'# this comment should be ignored\n\nFirst Name\nperson.firstName\n\n# second comment\nStatus\nenum(active,inactive,pending)'
1515
);
16-
await expect.poll(async () => appPage.testDataPanel.getSchemaRowCount()).toBe(beforeSchema + 2);
16+
await expect.poll(async () => appPage.testDataPanel.getSchemaRowCount()).toBeGreaterThanOrEqual(beforeSchema + 1);
1717
await appPage.testDataPanel.setGenerateCount(5);
1818

1919
await appPage.testDataPanel.clickGenerate();

apps/web/src/tests/browser/generator/abstractions/components/generator-schema.component.js

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
const { expect } = require('@playwright/test');
2+
const { SchemaEditorComponent } = require('../../../shared/abstractions/components/schema-editor.component');
23

34
class GeneratorSchemaComponent {
45
constructor(page) {
56
this.page = page;
67
this.container = page.locator('#generatorSchemaSection');
7-
this.modeToggleButton = page.locator('#schemaModeToggleButton');
8-
this.textArea = page.locator('#generatorSchemaText');
9-
this.rowsContainer = page.locator('#generatorSchemaRows');
10-
this.rows = this.rowsContainer.locator('.generator-schema-row');
11-
this.addFieldButton = page.locator('#addSchemaRowButton');
8+
this.editor = new SchemaEditorComponent(page, {
9+
rowsSelector: '#generatorSchemaRows',
10+
textAreaSelector: '#generatorSchemaText',
11+
modeToggleSelector: '#schemaModeToggleButton',
12+
addFieldSelector: '#addSchemaRowButton',
13+
});
14+
this.modeToggleButton = this.editor.modeToggleButton;
15+
this.textArea = this.editor.textArea;
16+
this.rows = this.editor.rows;
1217
}
1318

1419
async expectReady() {
@@ -17,20 +22,15 @@ class GeneratorSchemaComponent {
1722
}
1823

1924
async setTextMode(enabled) {
20-
const shouldShowSchemaButton = enabled ? 'Edit as Schema' : 'Edit as Text';
21-
if ((await this.modeToggleButton.innerText()).trim() !== shouldShowSchemaButton) {
22-
await this.modeToggleButton.click();
23-
}
24-
await expect(this.modeToggleButton).toHaveText(shouldShowSchemaButton);
25+
await this.editor.setTextMode(enabled);
2526
}
2627

2728
async setSchemaText(schemaText) {
28-
await this.setTextMode(true);
29-
await this.textArea.fill(schemaText);
29+
await this.editor.setSchemaText(schemaText, { ensureTextMode: true });
3030
}
3131

3232
async addField() {
33-
await this.addFieldButton.click();
33+
await this.editor.addField();
3434
}
3535

3636
async getRowCount() {
@@ -42,24 +42,23 @@ class GeneratorSchemaComponent {
4242
}
4343

4444
async setRowName(index, value) {
45-
await this.row(index).locator('input[data-field="name"]').fill(value);
45+
await this.editor.setRowField(index, 'name', value);
4646
}
4747

4848
async setRowSourceType(index, value) {
49-
await this.row(index).locator('select[data-field="sourceType"]').selectOption(value);
49+
await this.editor.setRowSourceType(index, value);
5050
}
5151

5252
async setRowValue(index, value) {
53-
await this.row(index).locator('input[data-field="value"]').fill(value);
53+
await this.editor.setRowField(index, 'value', value);
5454
}
5555

5656
async getRowName(index) {
5757
return this.row(index).locator('input[data-field="name"]').inputValue();
5858
}
5959

6060
async getSchemaText() {
61-
await this.setTextMode(true);
62-
return this.textArea.inputValue();
61+
return this.editor.getSchemaText({ ensureTextMode: true });
6362
}
6463

6564
async clickRowAction(index, action) {

apps/web/src/tests/browser/generator/functional/schema-edit.spec.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ test.describe('Generator Schema Editing', () => {
3232
await expect(generatorPage.schema.row(0).locator('input[data-field="value"]')).toHaveValue('Alice');
3333
await expect(generatorPage.schema.row(1).locator('input[data-field="name"]')).toHaveValue('Status');
3434
await expect(generatorPage.schema.row(1).locator('select[data-field="sourceType"]')).toHaveValue('enum');
35-
await expect(generatorPage.schema.row(1).locator('input[data-field="value"]')).toHaveValue('enum(active,inactive)');
35+
await expect(generatorPage.schema.row(1).locator('input[data-field="value"]')).toHaveValue('active,inactive');
3636

3737
expectNoPageErrors(pageErrors);
3838
});
@@ -155,4 +155,18 @@ test.describe('Generator Schema Editing', () => {
155155

156156
expectNoPageErrors(pageErrors);
157157
});
158+
159+
test('empty schema text can switch back to schema mode without validation lock', async ({ page }) => {
160+
const { generatorPage, pageErrors } = await openGenerator(page);
161+
162+
await generatorPage.schema.setSchemaText('');
163+
await generatorPage.schema.setTextMode(false);
164+
165+
await expect.poll(async () => generatorPage.schema.editor.isRowEditorMode()).toBe(true);
166+
await expect(page.locator('#generatorSchemaErrorText')).not.toContainText(
167+
'No rules defined. Provide column/rule pairs.'
168+
);
169+
170+
expectNoPageErrors(pageErrors);
171+
});
158172
});
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
const { expect } = require('@playwright/test');
2+
3+
class MethodPickerDialogComponent {
4+
constructor(page) {
5+
this.page = page;
6+
this.overlay = page.locator('.method-picker-overlay');
7+
this.searchInput = page.locator('.method-picker-search');
8+
this.tabButtons = page.locator('.method-picker-tabs button');
9+
this.commandLabels = page.locator('.method-picker-tile .method-picker-tile-command');
10+
this.applyButton = page.locator('[data-action="apply"]');
11+
}
12+
13+
async expectOpen() {
14+
await expect(this.searchInput).toBeVisible();
15+
}
16+
17+
async chooseCommand(command, { tab = null } = {}) {
18+
await this.expectOpen();
19+
if (tab) {
20+
await this.page.locator('.method-picker-tabs button', { hasText: new RegExp(`^${tab}$`, 'i') }).click();
21+
}
22+
23+
const requested = String(command);
24+
await this.searchInput.fill(requested);
25+
const matches = this.commandLabels.filter({
26+
hasText: new RegExp(`^${requested.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'i'),
27+
});
28+
if ((await matches.count()) === 0) {
29+
throw new Error(`Method picker command not found: ${requested}`);
30+
}
31+
const target = matches.first();
32+
33+
await target.click();
34+
await this.applyButton.click();
35+
await expect(this.overlay).toHaveCount(0);
36+
}
37+
}
38+
39+
module.exports = { MethodPickerDialogComponent };

0 commit comments

Comments
 (0)