Skip to content

Commit da8841f

Browse files
authored
Merge pull request #184 from eviltester/168-enum-to-schema
Issue 168: add grid to enum schema workflow closes #168
2 parents bde250a + 9efb5e7 commit da8841f

31 files changed

Lines changed: 1032 additions & 23 deletions

apps/web/src/stories/population-actions.stories.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@ function renderPopulationActionsStory(args) {
2626
generatePairwiseBusy: args.generatePairwiseBusy,
2727
generateLabel: args.generateLabel,
2828
generatePairwiseLabel: args.generatePairwiseLabel,
29+
generateSchemaLabel: 'Grid to Enum Schema',
2930
generateHelpHtml: args.generateHelpHtml,
3031
generatePairwiseHelpHtml: args.generatePairwiseHelpHtml,
32+
generateSchemaHelpHtml: '<p>Scan the current grid and build an enum-only schema from visible values.</p>',
3133
},
3234
callbacks: {
3335
onGenerate: () => {
@@ -38,6 +40,10 @@ function renderPopulationActionsStory(args) {
3840
args.onGeneratePairwise?.();
3941
result.textContent = 'action:generate-secondary';
4042
},
43+
onGenerateSchemaFromGrid: () => {
44+
args.onGenerateSchemaFromGrid?.();
45+
result.textContent = 'action:generate-schema';
46+
},
4147
},
4248
});
4349

@@ -79,6 +85,7 @@ const meta = {
7985
'<p>Generate n-wise combinations from enum columns in the current schema directly into the grid.</p><p><a class="helplink" href="/docs/test-data/n-wise-testing" target="_blank" rel="noopener noreferrer">N-wise generation docs</a></p>',
8086
onGenerate: fn(),
8187
onGeneratePairwise: fn(),
88+
onGenerateSchemaFromGrid: fn(),
8289
},
8390
argTypes: {
8491
pairwiseVisible: {
@@ -117,6 +124,10 @@ const meta = {
117124
description: 'Storybook action fired when the secondary action button is clicked.',
118125
table: { category: 'Events' },
119126
},
127+
onGenerateSchemaFromGrid: {
128+
description: 'Storybook action fired when the Grid to Enum Schema button is clicked.',
129+
table: { category: 'Events' },
130+
},
120131
},
121132
render: renderPopulationActionsStory,
122133
};
@@ -137,7 +148,8 @@ export const Default = {
137148
const canvas = within(canvasElement);
138149
await expect(canvas.getByRole('button', { name: 'Generate', exact: true })).toBeEnabled();
139150
await expect(canvas.queryByRole('button', { name: 'Generate Combinations' })).toBeNull();
140-
await expect(canvas.getAllByRole('button', { name: /show .* help/i })).toHaveLength(1);
151+
await expect(canvas.getByRole('button', { name: 'Grid to Enum Schema' })).toBeVisible();
152+
await expect(canvas.getAllByRole('button', { name: /show .* help/i })).toHaveLength(2);
141153
await userEvent.click(canvas.getByRole('button', { name: 'Generate', exact: true }));
142154
await expect(canvas.getByText('action:generate')).toBeVisible();
143155
},
@@ -160,7 +172,8 @@ export const PairwiseAvailable = {
160172
const canvas = within(canvasElement);
161173
await expect(canvas.getByRole('button', { name: 'Generate Combinations' })).toBeVisible();
162174
await expect(canvas.getByRole('button', { name: 'Generate Combinations' })).toBeEnabled();
163-
await expect(canvas.getAllByRole('button', { name: /show .* help/i })).toHaveLength(2);
175+
await expect(canvas.getByRole('button', { name: 'Grid to Enum Schema' })).toBeVisible();
176+
await expect(canvas.getAllByRole('button', { name: /show .* help/i })).toHaveLength(3);
164177
await userEvent.click(canvas.getByRole('button', { name: 'Generate Combinations' }));
165178
await expect(canvas.getByText('action:generate-secondary')).toBeVisible();
166179
},
@@ -185,6 +198,7 @@ export const BusyStates = {
185198
const canvas = within(canvasElement);
186199
await expect(canvas.getByRole('button', { name: 'Generate', exact: true })).toBeDisabled();
187200
await expect(canvas.getByRole('button', { name: 'Generate Combinations' })).toBeDisabled();
201+
await expect(canvas.getByRole('button', { name: 'Grid to Enum Schema' })).toBeEnabled();
188202
await expect(canvas.getByRole('button', { name: 'Generate', exact: true })).toHaveAttribute(
189203
'aria-disabled',
190204
'true'

apps/web/src/stories/test-data-population-toolbar.stories.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ function renderToolbarStory(args) {
2727
pairwiseVisible: args.pairwiseVisible,
2828
generateBusy: args.generateBusy,
2929
generatePairwiseBusy: args.generatePairwiseBusy,
30+
generateSchemaBusy: args.generateSchemaBusy,
3031
modeOptions: [
3132
{ value: 'new-table', label: 'New Table' },
3233
{ value: 'amend-table', label: 'Amend Table' },
@@ -49,6 +50,10 @@ function renderToolbarStory(args) {
4950
args.onGenerateCombinations?.();
5051
log.textContent = 'action:generate-combinations';
5152
},
53+
onGenerateSchemaFromGrid: () => {
54+
args.onGenerateSchema?.();
55+
log.textContent = 'action:generate-schema';
56+
},
5257
onModeChange: (mode) => {
5358
args.onModeChange?.(mode);
5459
log.textContent = `selected:${mode}`;
@@ -87,10 +92,12 @@ const meta = {
8792
pairwiseVisible: false,
8893
generateBusy: false,
8994
generatePairwiseBusy: false,
95+
generateSchemaBusy: false,
9096
rowCount: 1,
9197
maxWidth: '',
9298
onGenerate: fn(),
9399
onGenerateCombinations: fn(),
100+
onGenerateSchema: fn(),
94101
onModeChange: fn(),
95102
},
96103
argTypes: {
@@ -111,6 +118,10 @@ const meta = {
111118
control: 'boolean',
112119
description: 'Disables the combinations action when true.',
113120
},
121+
generateSchemaBusy: {
122+
control: 'boolean',
123+
description: 'Disables the Grid to Enum Schema action when true.',
124+
},
114125
rowCount: {
115126
control: 'number',
116127
description: 'Initial count shown by the shared row-count control.',
@@ -137,6 +148,7 @@ export const Default = {
137148
play: async ({ canvasElement }) => {
138149
const canvas = within(canvasElement);
139150
await expect(canvas.getByRole('button', { name: 'Generate' })).toBeVisible();
151+
await expect(canvas.getByRole('button', { name: 'Grid to Enum Schema' })).toBeVisible();
140152
await expect(canvas.getByRole('spinbutton', { name: 'How Many?' })).toHaveValue(1);
141153
await expect(canvas.getByRole('radio', { name: 'New Table' })).toBeChecked();
142154
await userEvent.click(canvas.getByRole('button', { name: 'Generate' }));
@@ -149,6 +161,7 @@ export const PairwiseAndBusy = {
149161
pairwiseVisible: true,
150162
generateBusy: true,
151163
generatePairwiseBusy: true,
164+
generateSchemaBusy: true,
152165
selectedMode: 'amend-selected',
153166
rowCount: 3,
154167
},
@@ -164,6 +177,7 @@ export const PairwiseAndBusy = {
164177
const canvas = within(canvasElement);
165178
await expect(canvas.getByRole('button', { name: 'Generate' })).toBeDisabled();
166179
await expect(canvas.getByRole('button', { name: 'Generate Combinations' })).toBeDisabled();
180+
await expect(canvas.getByRole('button', { name: 'Grid to Enum Schema' })).toBeDisabled();
167181
await expect(canvas.getByRole('radio', { name: 'Amend Selected' })).toBeChecked();
168182
},
169183
};

apps/web/src/tests/browser/app/abstractions/components/confirm-dialog.component.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ class ConfirmDialogComponent {
1818
await expect(this.backdrop).toBeHidden();
1919
}
2020

21-
async confirmIfVisible() {
21+
async confirmIfVisible(options = {}) {
2222
if (!(await this.isVisible())) {
2323
return false;
2424
}
25-
await this.confirm();
25+
await this.confirm(options);
2626
return true;
2727
}
2828

@@ -34,9 +34,9 @@ class ConfirmDialogComponent {
3434
return true;
3535
}
3636

37-
async confirm() {
37+
async confirm({ confirmLabel = /^ok$/i } = {}) {
3838
await this.expectVisible();
39-
await this.backdrop.getByRole('button', { name: /^ok$/i }).click();
39+
await this.backdrop.getByRole('button', { name: confirmLabel }).click();
4040
await this.expectHidden();
4141
}
4242

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
const { expect } = require('@playwright/test');
22
const { GridRendererComponent } = require('./grid-renderer.component');
3+
const { ConfirmDialogComponent } = require('./confirm-dialog.component');
4+
const { TextInputDialogComponent } = require('./text-input-dialog.component');
35
const { SchemaEditorComponent } = require('../../../shared/abstractions/components/schema-editor.component');
46
const {
57
OverlaySafeActivationComponent,
@@ -14,6 +16,7 @@ class TestDataPanelComponent {
1416
this.heading = this.container.getByText('Test Data', { exact: true });
1517
this.generateButton = this.container.getByRole('button', { name: 'Generate', exact: true });
1618
this.generatePairwiseButton = this.container.getByRole('button', { name: 'Generate Combinations' });
19+
this.generateSchemaButton = this.container.getByRole('button', { name: 'Grid to Enum Schema', exact: true });
1720
this.combinationsDialog = page.getByRole('dialog', { name: 'Generate Combinations' });
1821
this.combinationsDialogStrengthSelect = this.combinationsDialog.getByLabel('n');
1922
this.combinationsDialogCancelButton = this.combinationsDialog.getByRole('button', { name: 'Cancel' });
@@ -26,6 +29,8 @@ class TestDataPanelComponent {
2629
this.deleteSelectedSchemaRowsButton = this.container.getByRole('button', { name: '- Delete Selected' });
2730
this.selectedSchemaRowIndex = 0;
2831
this.overlaySafeActivation = new OverlaySafeActivationComponent(page);
32+
this.confirmDialog = new ConfirmDialogComponent(page);
33+
this.textInputDialog = new TextInputDialogComponent(page);
2934
this.schemaEditor = new SchemaEditorComponent(page, {
3035
rootSelector: '[data-role="data-population-panel-root"]',
3136
fieldMap: {
@@ -134,6 +139,15 @@ class TestDataPanelComponent {
134139
await expect(this.combinationsDialog).toBeVisible();
135140
}
136141

142+
async openGridToEnumSchemaDialog() {
143+
await this.overlaySafeActivation.activateButton(this.generateSchemaButton);
144+
await this.textInputDialog.expectVisible();
145+
}
146+
147+
async submitGridToEnumSchemaLimit(value) {
148+
await this.textInputDialog.submit(value, { submitLabel: /build schema/i });
149+
}
150+
137151
async cancelGenerateCombinationsDialog() {
138152
await this.combinationsDialogCancelButton.click();
139153
await expect(this.combinationsDialog).toBeHidden();

apps/web/src/tests/browser/app/abstractions/components/text-input-dialog.component.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ class TextInputDialogComponent {
44
constructor(page) {
55
this.page = page;
66
this.backdrop = page.locator('#text-input-modal-backdrop');
7-
this.input = this.backdrop.getByRole('textbox');
7+
this.input = this.backdrop.locator('[data-role="text-input-dialog-field"]');
88
}
99

1010
async expectVisible() {
@@ -15,10 +15,10 @@ class TextInputDialogComponent {
1515
await expect(this.backdrop).toBeHidden();
1616
}
1717

18-
async submit(value) {
18+
async submit(value, { submitLabel = /^ok$/i } = {}) {
1919
await this.expectVisible();
2020
await this.input.fill(String(value ?? ''));
21-
await this.backdrop.getByRole('button', { name: /^ok$/i }).click();
21+
await this.backdrop.getByRole('button', { name: submitLabel }).click();
2222
await this.expectHidden();
2323
}
2424
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
const { test } = require('@playwright/test');
2+
const { openApp, expectNoPageErrors, expect } = require('../../abstractions/helpers/scenario-helpers');
3+
4+
async function seedEnumGrid(appPage) {
5+
await appPage.gridEditor.resetTable();
6+
await expect.poll(async () => appPage.gridEditor.renderer.countRows()).toBe(0);
7+
8+
await appPage.gridEditor.header.renameColumn('~rename-me', 'Status');
9+
await appPage.gridEditor.header.addColumnRight('Status', 'Priority');
10+
11+
for (let rowIndex = 0; rowIndex < 4; rowIndex += 1) {
12+
await appPage.gridEditor.addRow();
13+
}
14+
await expect.poll(async () => appPage.gridEditor.renderer.countRows()).toBe(4);
15+
16+
const statusValues = ['active', 'pending', 'active', 'inactive'];
17+
const priorityValues = ['high', 'low', 'low', 'high'];
18+
19+
for (let rowIndex = 0; rowIndex < statusValues.length; rowIndex += 1) {
20+
await appPage.gridEditor.renderer.setCellTextByColumnName('Status', rowIndex, statusValues[rowIndex]);
21+
await appPage.gridEditor.renderer.setCellTextByColumnName('Priority', rowIndex, priorityValues[rowIndex]);
22+
}
23+
24+
await appPage.gridEditor.renderer.waitForGridSettle({ columnName: 'Status', sampleSize: 4 });
25+
}
26+
27+
test.describe('7. Test Data Generation', () => {
28+
test('builds enum schema rows from the current grid', async ({ page }) => {
29+
const { appPage, pageErrors } = await openApp(page);
30+
31+
await seedEnumGrid(appPage);
32+
await appPage.testDataPanel.expand();
33+
await appPage.testDataPanel.openGridToEnumSchemaDialog();
34+
await appPage.testDataPanel.submitGridToEnumSchemaLimit(256);
35+
36+
await expect.poll(async () => appPage.testDataPanel.getSchemaRowCount()).toBe(2);
37+
await expect
38+
.poll(async () => appPage.testDataPanel.getSchemaText())
39+
.toContain('Status\nenum(active,pending,inactive)');
40+
await expect(await appPage.testDataPanel.getSchemaCell(0, 'columnName')).toBe('Status');
41+
await expect(await appPage.testDataPanel.getSchemaCell(1, 'columnName')).toBe('Priority');
42+
expectNoPageErrors(pageErrors);
43+
});
44+
45+
test('confirms truncation when the requested enum limit is lower than current unique counts', async ({ page }) => {
46+
const { appPage, pageErrors } = await openApp(page);
47+
48+
await seedEnumGrid(appPage);
49+
await appPage.testDataPanel.expand();
50+
await appPage.testDataPanel.openGridToEnumSchemaDialog();
51+
await appPage.testDataPanel.submitGridToEnumSchemaLimit(2);
52+
await appPage.testDataPanel.confirmDialog.confirm({ confirmLabel: /truncate schema/i });
53+
54+
await expect.poll(async () => appPage.testDataPanel.getSchemaText()).toContain('Status\nenum(active,pending)');
55+
await expect.poll(async () => appPage.testDataPanel.getSchemaText()).not.toContain('inactive');
56+
expectNoPageErrors(pageErrors);
57+
});
58+
});

docs-src/docs/040-test-data/015-data-grid-editable.md

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,40 @@ The definition grid acts as a schema/template:
3232

3333
Generated rows are inserted into the main editable data grid.
3434

35+
## Grid to Enum Schema
36+
37+
The `Grid to Enum Schema` action scans the current main data grid and builds a schema definition from the values already in the table.
38+
39+
Use this when you already have example rows in the editable grid and want to quickly turn those values into `Enum` generation rules.
40+
41+
The generated schema uses:
42+
43+
- one schema row per grid column
44+
- the current column header as the schema row name
45+
- `Enum` as the source type for every generated row
46+
- unique values collected in first-seen row order
47+
48+
### How it works
49+
50+
1. Open the `Test Data` section in `app.html`.
51+
2. Press `Grid to Enum Schema`.
52+
3. Enter a maximum number of enum values to keep for each column.
53+
4. Confirm truncation if any column has more unique values than the chosen limit.
54+
5. Review the generated schema and press `Generate` or `Generate Pairwise` as needed.
55+
56+
The limit prompt shows the largest unique-value count found in the scanned grid and uses a numeric input field to reduce entry mistakes.
57+
58+
The default value is:
59+
60+
- the largest column unique-value count when it is `256` or less
61+
- `256` when the largest column has more than `256` unique values
62+
63+
If the chosen limit is lower than the number of unique values in one or more columns, then AnyWayData will ask for confirmation before truncating the schema. Truncation keeps the first values seen in current grid row order.
64+
65+
When accepted, the generated enum schema replaces the existing schema definition in the Test Data editor. The schema grid, schema text area, validation state, and pairwise button visibility are all refreshed automatically.
66+
67+
If the grid is empty, or no usable values can be extracted, then the existing schema is left unchanged.
68+
3569
## Generate Modes in the Grid
3670

3771
When generating in `app.html`, you can choose whether generation replaces data or amends existing rows:
@@ -103,6 +137,3 @@ The schema in the Column Definition Data Grid will be used to generate the data.
103137

104138
All data generation happens in the browser so the amount of data you can generate is limited only by the performance and memory of your computer.
105139

106-
107-
108-

packages/core-ui/js/gui_components/app/data-population-panel/data-population-panel-controller.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ class DataPopulationPanelController {
88
rowCountProps: { ...(props.rowCountProps || {}) },
99
actionIds: { ...(props.actionIds || {}) },
1010
schemaDefinitionProps: { ...(props.schemaDefinitionProps || {}) },
11+
generateSchemaBusy: props.generateSchemaBusy === true,
1112
};
1213
}
1314

@@ -24,6 +25,7 @@ class DataPopulationPanelController {
2425
schemaDefinitionProps: nextProps.schemaDefinitionProps
2526
? { ...this.state.schemaDefinitionProps, ...nextProps.schemaDefinitionProps }
2627
: this.state.schemaDefinitionProps,
28+
generateSchemaBusy: nextProps.generateSchemaBusy ?? this.state.generateSchemaBusy,
2729
};
2830
}
2931

packages/core-ui/js/gui_components/app/data-population-panel/data-population-panel-view.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,12 @@ class DataPopulationPanelView {
4646
modeOptions: state.modeOptions,
4747
rowCountProps: state.rowCountProps,
4848
actionIds: state.actionIds,
49+
generateSchemaBusy: state.generateSchemaBusy,
4950
},
5051
callbacks: {
5152
onGenerate: this.callbacks.onGenerate,
5253
onGeneratePairwise: this.callbacks.onGeneratePairwise,
54+
onGenerateSchemaFromGrid: this.callbacks.onGenerateSchemaFromGrid,
5355
onModeChange: (mode) => this.controller.handleModeChange(mode),
5456
},
5557
});
@@ -79,6 +81,7 @@ class DataPopulationPanelView {
7981
modeOptions: state.modeOptions,
8082
rowCountProps: state.rowCountProps,
8183
actionIds: state.actionIds,
84+
generateSchemaBusy: state.generateSchemaBusy,
8285
});
8386
this.schemaPanel?.update?.({
8487
className: 'test-data-schema-edit-zone shared-schema-section',
@@ -123,6 +126,10 @@ class DataPopulationPanelView {
123126
this.toolbar?.setGeneratePairwiseBusy?.(isBusy);
124127
}
125128

129+
setGenerateSchemaBusy(isBusy) {
130+
this.toolbar?.setGenerateSchemaBusy?.(isBusy);
131+
}
132+
126133
getSchemaDefinition() {
127134
return this.schemaPanel?.getSchemaDefinition?.() || null;
128135
}

0 commit comments

Comments
 (0)