Skip to content
Merged
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
18 changes: 16 additions & 2 deletions apps/web/src/stories/population-actions.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ function renderPopulationActionsStory(args) {
generatePairwiseBusy: args.generatePairwiseBusy,
generateLabel: args.generateLabel,
generatePairwiseLabel: args.generatePairwiseLabel,
generateSchemaLabel: 'Grid to Enum Schema',
generateHelpHtml: args.generateHelpHtml,
generatePairwiseHelpHtml: args.generatePairwiseHelpHtml,
generateSchemaHelpHtml: '<p>Scan the current grid and build an enum-only schema from visible values.</p>',
},
callbacks: {
onGenerate: () => {
Expand All @@ -38,6 +40,10 @@ function renderPopulationActionsStory(args) {
args.onGeneratePairwise?.();
result.textContent = 'action:generate-secondary';
},
onGenerateSchemaFromGrid: () => {
args.onGenerateSchemaFromGrid?.();
result.textContent = 'action:generate-schema';
},
},
});

Expand Down Expand Up @@ -79,6 +85,7 @@ const meta = {
'<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>',
onGenerate: fn(),
onGeneratePairwise: fn(),
onGenerateSchemaFromGrid: fn(),
},
argTypes: {
pairwiseVisible: {
Expand Down Expand Up @@ -117,6 +124,10 @@ const meta = {
description: 'Storybook action fired when the secondary action button is clicked.',
table: { category: 'Events' },
},
onGenerateSchemaFromGrid: {
description: 'Storybook action fired when the Grid to Enum Schema button is clicked.',
table: { category: 'Events' },
},
},
render: renderPopulationActionsStory,
};
Expand All @@ -137,7 +148,8 @@ export const Default = {
const canvas = within(canvasElement);
await expect(canvas.getByRole('button', { name: 'Generate', exact: true })).toBeEnabled();
await expect(canvas.queryByRole('button', { name: 'Generate Combinations' })).toBeNull();
await expect(canvas.getAllByRole('button', { name: /show .* help/i })).toHaveLength(1);
await expect(canvas.getByRole('button', { name: 'Grid to Enum Schema' })).toBeVisible();
await expect(canvas.getAllByRole('button', { name: /show .* help/i })).toHaveLength(2);
await userEvent.click(canvas.getByRole('button', { name: 'Generate', exact: true }));
await expect(canvas.getByText('action:generate')).toBeVisible();
},
Expand All @@ -160,7 +172,8 @@ export const PairwiseAvailable = {
const canvas = within(canvasElement);
await expect(canvas.getByRole('button', { name: 'Generate Combinations' })).toBeVisible();
await expect(canvas.getByRole('button', { name: 'Generate Combinations' })).toBeEnabled();
await expect(canvas.getAllByRole('button', { name: /show .* help/i })).toHaveLength(2);
await expect(canvas.getByRole('button', { name: 'Grid to Enum Schema' })).toBeVisible();
await expect(canvas.getAllByRole('button', { name: /show .* help/i })).toHaveLength(3);
await userEvent.click(canvas.getByRole('button', { name: 'Generate Combinations' }));
await expect(canvas.getByText('action:generate-secondary')).toBeVisible();
},
Expand All @@ -185,6 +198,7 @@ export const BusyStates = {
const canvas = within(canvasElement);
await expect(canvas.getByRole('button', { name: 'Generate', exact: true })).toBeDisabled();
await expect(canvas.getByRole('button', { name: 'Generate Combinations' })).toBeDisabled();
await expect(canvas.getByRole('button', { name: 'Grid to Enum Schema' })).toBeEnabled();
await expect(canvas.getByRole('button', { name: 'Generate', exact: true })).toHaveAttribute(
'aria-disabled',
'true'
Expand Down
14 changes: 14 additions & 0 deletions apps/web/src/stories/test-data-population-toolbar.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ function renderToolbarStory(args) {
pairwiseVisible: args.pairwiseVisible,
generateBusy: args.generateBusy,
generatePairwiseBusy: args.generatePairwiseBusy,
generateSchemaBusy: args.generateSchemaBusy,
modeOptions: [
{ value: 'new-table', label: 'New Table' },
{ value: 'amend-table', label: 'Amend Table' },
Expand All @@ -49,6 +50,10 @@ function renderToolbarStory(args) {
args.onGenerateCombinations?.();
log.textContent = 'action:generate-combinations';
},
onGenerateSchemaFromGrid: () => {
args.onGenerateSchema?.();
log.textContent = 'action:generate-schema';
},
onModeChange: (mode) => {
args.onModeChange?.(mode);
log.textContent = `selected:${mode}`;
Expand Down Expand Up @@ -87,10 +92,12 @@ const meta = {
pairwiseVisible: false,
generateBusy: false,
generatePairwiseBusy: false,
generateSchemaBusy: false,
rowCount: 1,
maxWidth: '',
onGenerate: fn(),
onGenerateCombinations: fn(),
onGenerateSchema: fn(),
onModeChange: fn(),
},
argTypes: {
Expand All @@ -111,6 +118,10 @@ const meta = {
control: 'boolean',
description: 'Disables the combinations action when true.',
},
generateSchemaBusy: {
control: 'boolean',
description: 'Disables the Grid to Enum Schema action when true.',
},
rowCount: {
control: 'number',
description: 'Initial count shown by the shared row-count control.',
Expand All @@ -137,6 +148,7 @@ export const Default = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByRole('button', { name: 'Generate' })).toBeVisible();
await expect(canvas.getByRole('button', { name: 'Grid to Enum Schema' })).toBeVisible();
await expect(canvas.getByRole('spinbutton', { name: 'How Many?' })).toHaveValue(1);
await expect(canvas.getByRole('radio', { name: 'New Table' })).toBeChecked();
await userEvent.click(canvas.getByRole('button', { name: 'Generate' }));
Expand All @@ -149,6 +161,7 @@ export const PairwiseAndBusy = {
pairwiseVisible: true,
generateBusy: true,
generatePairwiseBusy: true,
generateSchemaBusy: true,
selectedMode: 'amend-selected',
rowCount: 3,
},
Expand All @@ -164,6 +177,7 @@ export const PairwiseAndBusy = {
const canvas = within(canvasElement);
await expect(canvas.getByRole('button', { name: 'Generate' })).toBeDisabled();
await expect(canvas.getByRole('button', { name: 'Generate Combinations' })).toBeDisabled();
await expect(canvas.getByRole('button', { name: 'Grid to Enum Schema' })).toBeDisabled();
await expect(canvas.getByRole('radio', { name: 'Amend Selected' })).toBeChecked();
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ class ConfirmDialogComponent {
await expect(this.backdrop).toBeHidden();
}

async confirmIfVisible() {
async confirmIfVisible(options = {}) {
if (!(await this.isVisible())) {
return false;
}
await this.confirm();
await this.confirm(options);
return true;
}

Expand All @@ -34,9 +34,9 @@ class ConfirmDialogComponent {
return true;
}

async confirm() {
async confirm({ confirmLabel = /^ok$/i } = {}) {
await this.expectVisible();
await this.backdrop.getByRole('button', { name: /^ok$/i }).click();
await this.backdrop.getByRole('button', { name: confirmLabel }).click();
await this.expectHidden();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
const { expect } = require('@playwright/test');
const { GridRendererComponent } = require('./grid-renderer.component');
const { ConfirmDialogComponent } = require('./confirm-dialog.component');
const { TextInputDialogComponent } = require('./text-input-dialog.component');
const { SchemaEditorComponent } = require('../../../shared/abstractions/components/schema-editor.component');
const {
OverlaySafeActivationComponent,
Expand All @@ -14,6 +16,7 @@ class TestDataPanelComponent {
this.heading = this.container.getByText('Test Data', { exact: true });
this.generateButton = this.container.getByRole('button', { name: 'Generate', exact: true });
this.generatePairwiseButton = this.container.getByRole('button', { name: 'Generate Combinations' });
this.generateSchemaButton = this.container.getByRole('button', { name: 'Grid to Enum Schema', exact: true });
this.combinationsDialog = page.getByRole('dialog', { name: 'Generate Combinations' });
this.combinationsDialogStrengthSelect = this.combinationsDialog.getByLabel('n');
this.combinationsDialogCancelButton = this.combinationsDialog.getByRole('button', { name: 'Cancel' });
Expand All @@ -26,6 +29,8 @@ class TestDataPanelComponent {
this.deleteSelectedSchemaRowsButton = this.container.getByRole('button', { name: '- Delete Selected' });
this.selectedSchemaRowIndex = 0;
this.overlaySafeActivation = new OverlaySafeActivationComponent(page);
this.confirmDialog = new ConfirmDialogComponent(page);
this.textInputDialog = new TextInputDialogComponent(page);
this.schemaEditor = new SchemaEditorComponent(page, {
rootSelector: '[data-role="data-population-panel-root"]',
fieldMap: {
Expand Down Expand Up @@ -134,6 +139,15 @@ class TestDataPanelComponent {
await expect(this.combinationsDialog).toBeVisible();
}

async openGridToEnumSchemaDialog() {
await this.overlaySafeActivation.activateButton(this.generateSchemaButton);
await this.textInputDialog.expectVisible();
}

async submitGridToEnumSchemaLimit(value) {
await this.textInputDialog.submit(value, { submitLabel: /build schema/i });
}

async cancelGenerateCombinationsDialog() {
await this.combinationsDialogCancelButton.click();
await expect(this.combinationsDialog).toBeHidden();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ class TextInputDialogComponent {
constructor(page) {
this.page = page;
this.backdrop = page.locator('#text-input-modal-backdrop');
this.input = this.backdrop.getByRole('textbox');
this.input = this.backdrop.locator('[data-role="text-input-dialog-field"]');
}

async expectVisible() {
Expand All @@ -15,10 +15,10 @@ class TextInputDialogComponent {
await expect(this.backdrop).toBeHidden();
}

async submit(value) {
async submit(value, { submitLabel = /^ok$/i } = {}) {
await this.expectVisible();
await this.input.fill(String(value ?? ''));
await this.backdrop.getByRole('button', { name: /^ok$/i }).click();
await this.backdrop.getByRole('button', { name: submitLabel }).click();
await this.expectHidden();
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
const { test } = require('@playwright/test');
const { openApp, expectNoPageErrors, expect } = require('../../abstractions/helpers/scenario-helpers');

async function seedEnumGrid(appPage) {
await appPage.gridEditor.resetTable();
await expect.poll(async () => appPage.gridEditor.renderer.countRows()).toBe(0);

await appPage.gridEditor.header.renameColumn('~rename-me', 'Status');
await appPage.gridEditor.header.addColumnRight('Status', 'Priority');

for (let rowIndex = 0; rowIndex < 4; rowIndex += 1) {
await appPage.gridEditor.addRow();
}
await expect.poll(async () => appPage.gridEditor.renderer.countRows()).toBe(4);

const statusValues = ['active', 'pending', 'active', 'inactive'];
const priorityValues = ['high', 'low', 'low', 'high'];

for (let rowIndex = 0; rowIndex < statusValues.length; rowIndex += 1) {
await appPage.gridEditor.renderer.setCellTextByColumnName('Status', rowIndex, statusValues[rowIndex]);
await appPage.gridEditor.renderer.setCellTextByColumnName('Priority', rowIndex, priorityValues[rowIndex]);
}

await appPage.gridEditor.renderer.waitForGridSettle({ columnName: 'Status', sampleSize: 4 });
}

test.describe('7. Test Data Generation', () => {
test('builds enum schema rows from the current grid', async ({ page }) => {
const { appPage, pageErrors } = await openApp(page);

await seedEnumGrid(appPage);
await appPage.testDataPanel.expand();
await appPage.testDataPanel.openGridToEnumSchemaDialog();
await appPage.testDataPanel.submitGridToEnumSchemaLimit(256);

await expect.poll(async () => appPage.testDataPanel.getSchemaRowCount()).toBe(2);
await expect
.poll(async () => appPage.testDataPanel.getSchemaText())
.toContain('Status\nenum(active,pending,inactive)');
await expect(await appPage.testDataPanel.getSchemaCell(0, 'columnName')).toBe('Status');
await expect(await appPage.testDataPanel.getSchemaCell(1, 'columnName')).toBe('Priority');
expectNoPageErrors(pageErrors);
});

test('confirms truncation when the requested enum limit is lower than current unique counts', async ({ page }) => {
const { appPage, pageErrors } = await openApp(page);

await seedEnumGrid(appPage);
await appPage.testDataPanel.expand();
await appPage.testDataPanel.openGridToEnumSchemaDialog();
await appPage.testDataPanel.submitGridToEnumSchemaLimit(2);
await appPage.testDataPanel.confirmDialog.confirm({ confirmLabel: /truncate schema/i });

await expect.poll(async () => appPage.testDataPanel.getSchemaText()).toContain('Status\nenum(active,pending)');
await expect.poll(async () => appPage.testDataPanel.getSchemaText()).not.toContain('inactive');
expectNoPageErrors(pageErrors);
});
});
37 changes: 34 additions & 3 deletions docs-src/docs/040-test-data/015-data-grid-editable.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,40 @@ The definition grid acts as a schema/template:

Generated rows are inserted into the main editable data grid.

## Grid to Enum Schema

The `Grid to Enum Schema` action scans the current main data grid and builds a schema definition from the values already in the table.

Use this when you already have example rows in the editable grid and want to quickly turn those values into `Enum` generation rules.

The generated schema uses:

- one schema row per grid column
- the current column header as the schema row name
- `Enum` as the source type for every generated row
- unique values collected in first-seen row order

### How it works

1. Open the `Test Data` section in `app.html`.
2. Press `Grid to Enum Schema`.
3. Enter a maximum number of enum values to keep for each column.
4. Confirm truncation if any column has more unique values than the chosen limit.
5. Review the generated schema and press `Generate` or `Generate Pairwise` as needed.

The limit prompt shows the largest unique-value count found in the scanned grid and uses a numeric input field to reduce entry mistakes.

The default value is:

- the largest column unique-value count when it is `256` or less
- `256` when the largest column has more than `256` unique values

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.

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.

If the grid is empty, or no usable values can be extracted, then the existing schema is left unchanged.

## Generate Modes in the Grid

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

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.




Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class DataPopulationPanelController {
rowCountProps: { ...(props.rowCountProps || {}) },
actionIds: { ...(props.actionIds || {}) },
schemaDefinitionProps: { ...(props.schemaDefinitionProps || {}) },
generateSchemaBusy: props.generateSchemaBusy === true,
};
}

Expand All @@ -24,6 +25,7 @@ class DataPopulationPanelController {
schemaDefinitionProps: nextProps.schemaDefinitionProps
? { ...this.state.schemaDefinitionProps, ...nextProps.schemaDefinitionProps }
: this.state.schemaDefinitionProps,
generateSchemaBusy: nextProps.generateSchemaBusy ?? this.state.generateSchemaBusy,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,12 @@ class DataPopulationPanelView {
modeOptions: state.modeOptions,
rowCountProps: state.rowCountProps,
actionIds: state.actionIds,
generateSchemaBusy: state.generateSchemaBusy,
},
callbacks: {
onGenerate: this.callbacks.onGenerate,
onGeneratePairwise: this.callbacks.onGeneratePairwise,
onGenerateSchemaFromGrid: this.callbacks.onGenerateSchemaFromGrid,
onModeChange: (mode) => this.controller.handleModeChange(mode),
},
});
Expand Down Expand Up @@ -79,6 +81,7 @@ class DataPopulationPanelView {
modeOptions: state.modeOptions,
rowCountProps: state.rowCountProps,
actionIds: state.actionIds,
generateSchemaBusy: state.generateSchemaBusy,
});
this.schemaPanel?.update?.({
className: 'test-data-schema-edit-zone shared-schema-section',
Expand Down Expand Up @@ -123,6 +126,10 @@ class DataPopulationPanelView {
this.toolbar?.setGeneratePairwiseBusy?.(isBusy);
}

setGenerateSchemaBusy(isBusy) {
this.toolbar?.setGenerateSchemaBusy?.(isBusy);
}

getSchemaDefinition() {
return this.schemaPanel?.getSchemaDefinition?.() || null;
}
Expand Down
Loading