Skip to content

Commit b4fd401

Browse files
committed
syntax checking in editor fixes
1 parent a842036 commit b4fd401

34 files changed

Lines changed: 1753 additions & 135 deletions

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

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,172 @@ test.describe('7. Test Data Generation', () => {
5656
expectNoPageErrors(pageErrors);
5757
});
5858

59+
test('invalid domain command text preserves the domain row type in the app editor', async ({ page }) => {
60+
const { appPage, pageErrors } = await openApp(page);
61+
62+
await appPage.testDataPanel.expand();
63+
await appPage.testDataPanel.expectExpanded();
64+
65+
await appPage.testDataPanel.setSchemaText('Name\nperson.fullName');
66+
await expect.poll(async () => appPage.testDataPanel.getSchemaRowCount()).toBe(1);
67+
await expect.poll(async () => appPage.testDataPanel.getSchemaCell(0, 'type')).toBe('person.fullName');
68+
await expect
69+
.poll(async () =>
70+
appPage.testDataPanel.page
71+
.locator('#testDataSchemaRows .generator-schema-row')
72+
.first()
73+
.locator('select[data-field="sourceType"]')
74+
.inputValue()
75+
)
76+
.toBe('domain');
77+
78+
await appPage.testDataPanel.setSchemaText('Name\nperson.fullNam');
79+
80+
await expect.poll(async () => appPage.testDataPanel.getSchemaCell(0, 'type')).toBe('person.fullNam');
81+
await expect
82+
.poll(async () =>
83+
appPage.testDataPanel.page
84+
.locator('#testDataSchemaRows .generator-schema-row')
85+
.first()
86+
.locator('select[data-field="sourceType"]')
87+
.inputValue()
88+
)
89+
.toBe('domain');
90+
91+
await expect(appPage.testDataPanel.page.locator('#testDataSchemaRows .generator-schema-row').first()).toHaveClass(
92+
/generator-schema-row-invalid/
93+
);
94+
95+
await appPage.testDataPanel.clickGenerate();
96+
await expect
97+
.poll(async () => appPage.testDataPanel.getSchemaErrorText())
98+
.toContain('Row 1: unknown domain command "person.fullNam".');
99+
100+
expectNoPageErrors(pageErrors);
101+
});
102+
103+
test('domain rows with invalid params stay domain after text mode round-trip in the app editor', async ({ page }) => {
104+
const { appPage, pageErrors } = await openApp(page);
105+
106+
await appPage.testDataPanel.expand();
107+
await appPage.testDataPanel.expectExpanded();
108+
109+
await appPage.testDataPanel.addSchemaRow();
110+
await expect.poll(async () => appPage.testDataPanel.getSchemaRowCount()).toBe(1);
111+
await appPage.testDataPanel.setSchemaCell(0, 'columnName', 'Name');
112+
await appPage.testDataPanel.setSchemaTypeValue(0, 'person.fullName');
113+
await appPage.testDataPanel.setSchemaCell(0, 'params', '(10)');
114+
115+
await appPage.testDataPanel.setSchemaTextMode(true);
116+
await appPage.testDataPanel.setSchemaTextMode(false);
117+
118+
await expect.poll(async () => appPage.testDataPanel.getSchemaCell(0, 'type')).toBe('person.fullName');
119+
await expect
120+
.poll(async () =>
121+
appPage.testDataPanel.page
122+
.locator('#testDataSchemaRows .generator-schema-row')
123+
.first()
124+
.locator('select[data-field="sourceType"]')
125+
.inputValue()
126+
)
127+
.toBe('domain');
128+
await expect.poll(async () => appPage.testDataPanel.getSchemaCell(0, 'params')).toBe('(10)');
129+
await expect(
130+
appPage.testDataPanel.page
131+
.locator('#testDataSchemaRows .generator-schema-row')
132+
.first()
133+
.locator('.generator-schema-row-validation')
134+
).toContainText('invalid domain params');
135+
136+
expectNoPageErrors(pageErrors);
137+
});
138+
139+
test('debounced semantic validation appears after typing stops in the app editor', async ({ page }) => {
140+
const { appPage, pageErrors } = await openApp(page);
141+
142+
await appPage.testDataPanel.expand();
143+
await appPage.testDataPanel.expectExpanded();
144+
145+
await appPage.testDataPanel.addSchemaRow();
146+
await expect.poll(async () => appPage.testDataPanel.getSchemaRowCount()).toBe(1);
147+
await appPage.testDataPanel.setSchemaCell(0, 'columnName', 'Name');
148+
await appPage.testDataPanel.setSchemaTypeValue(0, 'person.fullName');
149+
150+
const paramsInput = appPage.testDataPanel.page
151+
.locator('#testDataSchemaRows .generator-schema-row')
152+
.first()
153+
.locator('input[data-field="params"]');
154+
await paramsInput.fill('(10)');
155+
156+
await expect
157+
.poll(
158+
async () =>
159+
await appPage.testDataPanel.page
160+
.locator('#testDataSchemaRows .generator-schema-row')
161+
.first()
162+
.locator('.generator-schema-row-validation')
163+
.textContent(),
164+
{ timeout: 2500 }
165+
)
166+
.toContain('invalid domain params');
167+
168+
expectNoPageErrors(pageErrors);
169+
});
170+
171+
test('row validation clears when name editing blurs into the next field in the app editor', async ({ page }) => {
172+
const { appPage, pageErrors } = await openApp(page);
173+
174+
await appPage.testDataPanel.expand();
175+
await appPage.testDataPanel.expectExpanded();
176+
177+
await appPage.testDataPanel.addSchemaRow();
178+
await expect.poll(async () => appPage.testDataPanel.getSchemaRowCount()).toBe(1);
179+
const row = appPage.testDataPanel.page.locator('#testDataSchemaRows .generator-schema-row').first();
180+
await appPage.testDataPanel.setSchemaTypeValue(0, 'literal');
181+
await appPage.testDataPanel.setSchemaCell(0, 'value', 'Active');
182+
await appPage.testDataPanel.clickGenerate();
183+
await expect(row.locator('.generator-schema-row-validation')).toContainText('column name is required');
184+
185+
const nameInput = row.locator('input[data-field="name"]');
186+
const valueInput = row.locator('input[data-field="value"]');
187+
await nameInput.fill('Status');
188+
await valueInput.click();
189+
190+
await expect(row.locator('.generator-schema-row-validation')).toHaveCount(0);
191+
expectNoPageErrors(pageErrors);
192+
});
193+
194+
test('domain params without brackets are normalized and preserved in the app editor', async ({ page }) => {
195+
const { appPage, pageErrors } = await openApp(page);
196+
197+
await appPage.testDataPanel.expand();
198+
await appPage.testDataPanel.expectExpanded();
199+
200+
await appPage.testDataPanel.addSchemaRow();
201+
await expect.poll(async () => appPage.testDataPanel.getSchemaRowCount()).toBe(1);
202+
await appPage.testDataPanel.setSchemaCell(0, 'columnName', 'Phone');
203+
await appPage.testDataPanel.setSchemaTypeValue(0, 'phone.number');
204+
await appPage.testDataPanel.setSchemaCell(0, 'params', 'style=13');
205+
206+
await appPage.testDataPanel.setSchemaTextMode(true);
207+
await expect.poll(async () => appPage.testDataPanel.getSchemaText()).toContain('phone.number(style=13)');
208+
await appPage.testDataPanel.setSchemaTextMode(false);
209+
210+
await expect.poll(async () => appPage.testDataPanel.getSchemaCell(0, 'type')).toBe('phone.number');
211+
await expect
212+
.poll(async () =>
213+
appPage.testDataPanel.page
214+
.locator('#testDataSchemaRows .generator-schema-row')
215+
.first()
216+
.locator('select[data-field="sourceType"]')
217+
.inputValue()
218+
)
219+
.toBe('domain');
220+
await expect.poll(async () => appPage.testDataPanel.getSchemaCell(0, 'params')).toBe('(style=13)');
221+
222+
expectNoPageErrors(pageErrors);
223+
});
224+
59225
test('Schema grid edits after initial sync are reflected in the text schema', async ({ page }) => {
60226
const { appPage, pageErrors } = await openApp(page);
61227

@@ -78,6 +244,27 @@ test.describe('7. Test Data Generation', () => {
78244
expectNoPageErrors(pageErrors);
79245
});
80246

247+
test('switching enum rows to domain datatype.enum preserves the enum params in the app editor', async ({ page }) => {
248+
const { appPage, pageErrors } = await openApp(page);
249+
250+
await appPage.testDataPanel.expand();
251+
await appPage.testDataPanel.expectExpanded();
252+
253+
await appPage.testDataPanel.addSchemaRow();
254+
await expect.poll(async () => appPage.testDataPanel.getSchemaRowCount()).toBe(1);
255+
await appPage.testDataPanel.setSchemaCell(0, 'columnName', 'Status');
256+
await appPage.testDataPanel.setSchemaTypeValue(0, 'enum', { assertSchemaTextIncludesType: false });
257+
await appPage.testDataPanel.setSchemaCell(0, 'value', 'active,inactive,pending');
258+
259+
await appPage.testDataPanel.setSchemaTypeValue(0, 'datatype.enum', { assertSchemaTextIncludesType: false });
260+
261+
await expect.poll(async () => appPage.testDataPanel.getSchemaCell(0, 'type')).toBe('datatype.enum');
262+
await expect.poll(async () => appPage.testDataPanel.getSchemaCell(0, 'value')).toBe('active,inactive,pending');
263+
await expect.poll(async () => appPage.testDataPanel.getSchemaText()).toContain('enum(active,inactive,pending)');
264+
265+
expectNoPageErrors(pageErrors);
266+
});
267+
81268
test('empty schema text can switch back to schema mode without locking', async ({ page }) => {
82269
const { appPage, pageErrors } = await openApp(page);
83270

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

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,136 @@ test.describe('Generator Schema Editing', () => {
6161
expectNoPageErrors(pageErrors);
6262
});
6363

64+
test('switching enum rows to domain datatype.enum preserves the enum params in the generator editor', async ({
65+
page,
66+
}) => {
67+
const { generatorPage, pageErrors } = await openGenerator(page);
68+
69+
await generatorPage.schema.setTextMode(false);
70+
await generatorPage.schema.setRowName(0, 'Status');
71+
await generatorPage.schema.setRowSourceType(0, 'enum');
72+
await generatorPage.schema.setRowValue(0, 'active,inactive,pending');
73+
74+
await generatorPage.schema.editor.setRowTypeValue(0, 'datatype.enum');
75+
76+
await expect(generatorPage.schema.row(0).locator('[data-action="pick-command"]')).toHaveText('datatype.enum');
77+
await expect(generatorPage.schema.row(0).locator('input[data-field="params"]')).toHaveValue(
78+
'active,inactive,pending'
79+
);
80+
await expect.poll(async () => generatorPage.schema.getSchemaText()).toContain('enum(active,inactive,pending)');
81+
82+
expectNoPageErrors(pageErrors);
83+
});
84+
85+
test('invalid domain command text preserves the domain row type in the generator editor', async ({ page }) => {
86+
const { generatorPage, pageErrors } = await openGenerator(page);
87+
88+
await generatorPage.schema.setSchemaText('Name\nperson.fullName');
89+
await generatorPage.schema.setTextMode(false);
90+
91+
await expect(generatorPage.schema.rows).toHaveCount(1);
92+
await expect(generatorPage.schema.row(0).locator('select[data-field="sourceType"]')).toHaveValue('domain');
93+
await expect(generatorPage.schema.row(0).locator('[data-action="pick-command"]')).toHaveText('person.fullName');
94+
95+
await generatorPage.schema.setSchemaText('Name\nperson.fullNam');
96+
await generatorPage.schema.setTextMode(false);
97+
98+
await expect(generatorPage.schema.row(0).locator('select[data-field="sourceType"]')).toHaveValue('domain');
99+
await expect(generatorPage.schema.row(0).locator('[data-action="pick-command"]')).toHaveText('person.fullNam');
100+
await expect(generatorPage.schema.row(0)).toHaveClass(/generator-schema-row-invalid/);
101+
await expect(generatorPage.schema.row(0).locator('.generator-schema-row-validation')).toContainText(
102+
'Row 1: unknown domain command "person.fullNam".'
103+
);
104+
105+
await generatorPage.preview.clickPreview();
106+
await expect(page.locator('#generatorSchemaErrorText')).toContainText(
107+
'Row 1: unknown domain command "person.fullNam".'
108+
);
109+
110+
expectNoPageErrors(pageErrors);
111+
});
112+
113+
test('domain rows with invalid params stay domain after text mode round-trip in the generator editor', async ({
114+
page,
115+
}) => {
116+
const { generatorPage, pageErrors } = await openGenerator(page);
117+
118+
await generatorPage.schema.setTextMode(false);
119+
await generatorPage.schema.setRowName(0, 'Name');
120+
await generatorPage.schema.editor.setRowTypeValue(0, 'person.fullName');
121+
await generatorPage.schema.editor.setRowField(0, 'params', '(10)');
122+
123+
await generatorPage.schema.setTextMode(true);
124+
await generatorPage.schema.setTextMode(false);
125+
126+
await expect(generatorPage.schema.row(0).locator('select[data-field="sourceType"]')).toHaveValue('domain');
127+
await expect(generatorPage.schema.row(0).locator('[data-action="pick-command"]')).toHaveText('person.fullName');
128+
await expect(generatorPage.schema.row(0).locator('input[data-field="params"]')).toHaveValue('(10)');
129+
await expect(generatorPage.schema.row(0).locator('.generator-schema-row-validation')).toContainText(
130+
'invalid domain params'
131+
);
132+
133+
expectNoPageErrors(pageErrors);
134+
});
135+
136+
test('focus change triggers semantic validation in the generator editor', async ({ page }) => {
137+
const { generatorPage, pageErrors } = await openGenerator(page);
138+
139+
await generatorPage.schema.setTextMode(false);
140+
await generatorPage.schema.setRowName(0, 'Name');
141+
await generatorPage.schema.editor.setRowTypeValue(0, 'person.fullName');
142+
143+
const paramsInput = generatorPage.schema.row(0).locator('input[data-field="params"]');
144+
await paramsInput.fill('(10)');
145+
await generatorPage.schema.row(0).locator('input[data-field="name"]').click();
146+
147+
await expect(generatorPage.schema.row(0).locator('.generator-schema-row-validation')).toContainText(
148+
'invalid domain params',
149+
{ timeout: 2000 }
150+
);
151+
152+
expectNoPageErrors(pageErrors);
153+
});
154+
155+
test('row validation clears when name editing blurs into the next field in the generator editor', async ({
156+
page,
157+
}) => {
158+
const { generatorPage, pageErrors } = await openGenerator(page);
159+
160+
const row = generatorPage.page.locator('.generator-schema-row').first();
161+
await generatorPage.schema.setRowSourceType(0, 'literal');
162+
await generatorPage.schema.setRowValue(0, 'Active');
163+
await generatorPage.preview.clickPreview();
164+
await expect(row.locator('.generator-schema-row-validation')).toContainText('column name is required');
165+
166+
const nameInput = row.locator('input[data-field="name"]');
167+
const valueInput = row.locator('input[data-field="value"]');
168+
await nameInput.fill('Status');
169+
await valueInput.click();
170+
171+
await expect(row.locator('.generator-schema-row-validation')).toHaveCount(0);
172+
expectNoPageErrors(pageErrors);
173+
});
174+
175+
test('domain params without brackets are normalized and preserved in the generator editor', async ({ page }) => {
176+
const { generatorPage, pageErrors } = await openGenerator(page);
177+
178+
await generatorPage.schema.setTextMode(false);
179+
await generatorPage.schema.setRowName(0, 'Phone');
180+
await generatorPage.schema.editor.setRowTypeValue(0, 'phone.number');
181+
await generatorPage.schema.editor.setRowField(0, 'params', 'style=13');
182+
183+
await generatorPage.schema.setTextMode(true);
184+
await expect.poll(async () => generatorPage.schema.getSchemaText()).toContain('phone.number(style=13)');
185+
await generatorPage.schema.setTextMode(false);
186+
187+
await expect(generatorPage.schema.row(0).locator('select[data-field="sourceType"]')).toHaveValue('domain');
188+
await expect(generatorPage.schema.row(0).locator('[data-action="pick-command"]')).toHaveText('phone.number');
189+
await expect(generatorPage.schema.row(0).locator('input[data-field="params"]')).toHaveValue('(style=13)');
190+
191+
expectNoPageErrors(pageErrors);
192+
});
193+
64194
test('schema edit buttons states are correct across top middle and bottom rows', async ({ page }) => {
65195
const { generatorPage, pageErrors } = await openGenerator(page);
66196

apps/web/styles.css

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -720,6 +720,25 @@ body.theme-dark .generator-status-text {
720720
align-items: center;
721721
}
722722

723+
.generator-schema-row-invalid {
724+
padding: 0.35rem 0.45rem;
725+
border: 1px solid #d64545;
726+
border-radius: 10px;
727+
background: #fff6f6;
728+
}
729+
730+
.generator-schema-field-invalid {
731+
border-color: #d64545 !important;
732+
box-shadow: 0 0 0 1px color-mix(in srgb, #d64545 45%, transparent);
733+
}
734+
735+
.generator-schema-row-validation {
736+
grid-column: 1 / -1;
737+
color: #8d1b1b;
738+
font-size: 0.82rem;
739+
font-weight: 600;
740+
}
741+
723742
.generator-schema-row-faker {
724743
grid-template-columns: auto minmax(220px, 0.9fr) 120px minmax(180px, 1fr) max-content minmax(260px, 1.45fr);
725744
}
@@ -812,6 +831,15 @@ body.theme-dark #generatorOutputPreview {
812831
border: 1px solid #3a3b3c !important;
813832
}
814833

834+
body.theme-dark .generator-schema-row-invalid {
835+
background: #341b1d;
836+
border-color: #f06a6a;
837+
}
838+
839+
body.theme-dark .generator-schema-row-validation {
840+
color: #ffb4b4;
841+
}
842+
815843
.generator-data-table-preview {
816844
margin-top: 0.6rem;
817845
}

0 commit comments

Comments
 (0)