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
59 changes: 58 additions & 1 deletion apps/web/src/stories/shared-schema-definition.stories.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { expect, userEvent, within } from 'storybook/test';
import { expect, userEvent, waitFor, within } from 'storybook/test';
import RandExp from 'randexp';
import { faker } from '@faker-js/faker';
import {
Expand Down Expand Up @@ -87,6 +87,7 @@ function renderSharedSchemaDefinitionStory(args) {
const fakerCommands = getFakerCommands().filter((command) => command !== 'RegEx' && command.startsWith('helpers.'));
const domainCommands = getDomainCommands();
const root = document.createElement('section');
root.style.minHeight = args.storyMinHeight || 'auto';
const ids = createIds(args.idPrefix || 'shared-schema');
const createBlankRow = createBlankRowFactory(args.idPrefix || 'story-schema-row');
const component = createSharedSchemaDefinitionComponent({
Expand Down Expand Up @@ -201,6 +202,7 @@ const meta = {
initialText: '',
showErrors: false,
startInTextMode: false,
storyMinHeight: 'auto',
},
};

Expand Down Expand Up @@ -423,3 +425,58 @@ export const CommandPicker = {
await expect(commandInput?.value).toBe('helpers.fake');
},
};

export const ParamsDialog = {
render: renderSharedSchemaDefinitionStory,
args: {
storyMinHeight: '820px',
initialRows: [
{
id: 'sequence-row',
name: 'SequenceId',
sourceType: 'domain',
command: 'autoIncrement.sequence',
params: '',
value: '',
comments: '',
leadingTextLines: [],
},
],
},
parameters: {
layout: 'fullscreen',
docs: {
description: {
story:
'Guided params editing flow for documented command params. This story demonstrates the value-only editor: required state appears as read-only checkboxes, documented defaults are prefilled into the value inputs, each param row exposes a help tippy with descriptions/examples/rules, and string values are auto-quoted when the generated schema params text is built. Hover a row help icon to review the metadata before editing and applying the params back into the shared row editor.',
},
},
},
play: async ({ canvasElement }) => {
expectSchemaModeVisible(canvasElement);
const initialRow = canvasElement.querySelector('.shared-schema-row');
const paramsButton = initialRow.querySelector('[data-action="edit-params"]');
expect(paramsButton).not.toBeNull();
await userEvent.click(paramsButton);

const dialog = within(document.body).getByRole('dialog', { name: /edit params for .*autoincrement\.sequence/i });
const dialogScope = within(dialog);
const firstHelpIcon = dialog.querySelector('[data-role="params-editor-param-help"]');
expect(dialog.querySelector('[data-role="params-editor-mode"]')).toBeNull();
await expect(firstHelpIcon).toHaveAttribute('data-help-text', expect.stringContaining('<strong>Rules:</strong>'));
await expect(firstHelpIcon).toHaveAttribute('data-help-text', expect.stringContaining('Optional.'));
await expect(firstHelpIcon).toHaveAttribute('data-help-text', expect.stringContaining('Default: 1'));
await expect(dialogScope.getByRole('textbox', { name: /start value/i }).value).toBe('1');
await expect(dialogScope.getByRole('textbox', { name: /step value/i }).value).toBe('1');
const prefixInput = dialogScope.getByRole('textbox', { name: /prefix value/i });
await userEvent.type(prefixInput, 'filename');
await expect(dialogScope.getByText('(start=1,step=1,prefix="filename",zeropadding=0)')).toBeTruthy();
await userEvent.click(dialogScope.getByRole('button', { name: /^apply$/i }));

await waitFor(() =>
expect(canvasElement.querySelector('.shared-schema-row [data-field="params"]').value).toBe(
'(start=1,step=1,prefix="filename",zeropadding=0)'
)
);
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,25 @@ test.describe('Generator Schema Editing', () => {
expectNoPageErrors(pageErrors);
});

test('documented command params can be edited through the guided params dialog', async ({ page }) => {
const { generatorPage, pageErrors } = await openGenerator(page);

await generatorPage.schema.setTextMode(false);
await generatorPage.schema.setRowName(0, 'Status');
await generatorPage.schema.editor.setRowTypeValue(0, 'datatype.enum');
await generatorPage.schema.editor.editRowParamsWithDialog(0, {
values: 'active,inactive,pending',
});

await expect(generatorPage.schema.row(0).locator('[data-action="pick-command"]')).toHaveText('datatype.enum');
await expect(generatorPage.schema.row(0).locator('input[data-field="params"]')).toHaveValue(
'(active,inactive,pending)'
);
await expect.poll(async () => generatorPage.schema.getSchemaText()).toContain('enum(active,inactive,pending)');

expectNoPageErrors(pageErrors);
});

test('schema edit buttons states are correct across top middle and bottom rows', async ({ page }) => {
const { generatorPage, pageErrors } = await openGenerator(page);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const { expect } = require('@playwright/test');

function escapeRegExp(value) {
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

class ParamsEditorDialogComponent {
constructor(page) {
this.page = page;
this.overlay = page.locator('[data-role="params-editor-overlay"]');
this.dialog = page.getByRole('dialog', { name: /^edit params for /i });
this.applyButton = this.dialog.getByRole('button', { name: /^apply$/i });
}

async expectOpen() {
await expect(this.dialog).toBeVisible();
}

valueInput(name) {
return this.dialog.getByRole('textbox', { name: new RegExp(`^${escapeRegExp(name)} value$`, 'i') });
}
Comment thread
eviltester marked this conversation as resolved.

async setValue(name, value) {
await this.expectOpen();
await this.valueInput(name).fill(String(value));
}

async apply() {
await this.expectOpen();
await this.applyButton.click();
await expect(this.overlay).toHaveCount(0);
}
}

module.exports = { ParamsEditorDialogComponent };
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const { expect } = require('@playwright/test');
const { MethodPickerDialogComponent } = require('./method-picker-dialog.component');
const { OverlaySafeActivationComponent } = require('./overlay-safe-activation.component');
const { ParamsEditorDialogComponent } = require('./params-editor-dialog.component');

class SchemaEditorComponent {
constructor(page, config) {
Expand Down Expand Up @@ -31,6 +32,7 @@ class SchemaEditorComponent {
? page.locator(this.config.addFieldSelector)
: this.root.locator('[data-role="schema-add-field"]');
this.methodPicker = new MethodPickerDialogComponent(page);
this.paramsEditor = new ParamsEditorDialogComponent(page);
}

row(index) {
Expand Down Expand Up @@ -171,6 +173,16 @@ class SchemaEditorComponent {
await this.row(index).locator(`button[data-action="${action}"]`).click();
}

async editRowParamsWithDialog(index, valuesByName) {
await this.ensureSchemaMode();
await this.dismissOpenHelpTooltips();
await this.row(index).locator('[data-action="edit-params"]').click();
for (const [name, value] of Object.entries(valuesByName || {})) {
await this.paramsEditor.setValue(name, value);
}
await this.paramsEditor.apply();
}

async dragRowToIndex(fromIndex, toIndex, { placement = 'before' } = {}) {
await this.ensureSchemaMode();
const source = this.row(fromIndex).locator('[data-action="drag"]');
Expand Down
19 changes: 18 additions & 1 deletion apps/web/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1865,7 +1865,7 @@ body.theme-dark .shared-schema-row-validation {
align-self: center;
margin: 0 0.2rem;
}
.shared-schema-row > input[data-field='params'] {
.shared-schema-row > .shared-schema-params-control {
grid-column: 1 / span 3;
grid-row: 4;
min-width: 0;
Expand Down Expand Up @@ -1899,6 +1899,23 @@ body.theme-dark .shared-schema-row-validation {
position: relative;
}

.shared-schema-params-control {
display: grid;
grid-template-columns: minmax(0, 1fr) max-content;
gap: 0.35rem;
align-items: center;
}

.shared-schema-params-control > input[data-field='params'] {
min-width: 0;
width: 100%;
}

.shared-schema-params-button[disabled] {
opacity: 0.55;
cursor: not-allowed;
}

.shared-schema-command-picker-shadow-select {
position: absolute;
left: -9999px;
Expand Down
2 changes: 2 additions & 0 deletions docs/frontend-component-migration-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,8 @@ Current status:

- `SharedSchemaDefinition` now lives under `shared/schema-definition/` with a controller, view, and create-component factory.
- The new component reuses the existing shared schema parsing, validation, row-editing, command-picker, drag/drop, and text-mode logic from `shared/test-data/schema/` instead of duplicating those rules.
- The shared schema row editor now also exposes a documented params-edit dialog for commands with parameter metadata, so app and generator hosts share the same guided param-entry flow and semantic validation path instead of leaving params as raw punctuation-only text entry.
- The shared params-edit dialog now normalizes command metadata so optional/defaulted params stay visibly optional, shows reviewer-facing `Req` checkboxes instead of text labels, and auto-quotes string values while keeping the dialog focused on value entry rather than quote-format selection.
- The app test-data panel now mounts its schema editor through `SharedSchemaDefinition`, keeping the same DOM IDs so the rest of the app flow and browser tests continue to treat the schema surface as a black box.
- The generator page now also mounts its live schema editor through `SharedSchemaDefinition`, while preserving the page-level `#generatorSchemaSection` host contract, row-level browser interactions, and text-mode generate/pairwise flows.
- Generator runtime adoption also moved the shared text-mode syncing, method-picker command selection, semantic-validation caret preservation, and pairwise-button visibility behavior onto the shared component path.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,16 @@ const SYNTHETIC_DOMAIN_HELP = Object.freeze({
examples: ['datatype.enum(active,inactive,pending)'],
exampleReturnValues: ['active', 'inactive', 'pending'],
returnType: 'string',
args: [],
args: [
{
name: 'values',
type: 'comma-separated list',
variadic: true,
optional: false,
description: 'List of allowed enum values chosen at random during generation.',
example: 'active,inactive,pending',
},
],
},
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ const HELP_URLS = Object.freeze({
enum: 'https://anywaydata.com/docs/category/generating-data',
});

const ENUM_VALUE_PARAM = Object.freeze({
name: 'values',
type: 'comma-separated list',
variadic: true,
optional: false,
description: 'List of allowed values chosen at random during generation.',
example: 'active,inactive,pending',
});

function resolveFakerDocsUrl(command, docsUrl) {
const normalizedCommand = String(command || '').trim();
if (normalizedCommand.startsWith('helpers.')) {
Expand All @@ -42,6 +51,54 @@ function cleanParamText(text) {
.trim();
}

function extractSimpleDefaultValue(param = {}) {
if (Object.prototype.hasOwnProperty.call(param, 'defaultValue')) {
return param.defaultValue;
}
if (Object.prototype.hasOwnProperty.call(param, 'default')) {
return param.default;
}

const description = cleanParamText(param.description);
const match = description.match(/defaults?\s+to\s+("[^"]*"|'[^']*'|-?\d+(?:\.\d+)?|true|false|null)\.?$/iu);
if (!match) {
return '';
}

const value = match[1];
if (value.startsWith('"') || value.startsWith("'")) {
return value.slice(1, -1);
}
return value;
}

function normalizeHelpParam(param = {}) {
return {
...param,
optional: param.optional === true || param.required === false,
defaultValue: extractSimpleDefaultValue(param),
};
}

function normalizeHelpParams(params = []) {
return (Array.isArray(params) ? params : []).map((param) => normalizeHelpParam(param));
}

function resolveDomainHelpParams(command, commandHelp) {
const normalized = normalizeHelpParams(commandHelp?.args || []);
if (normalized.length > 0) {
return normalized;
}
if (
String(command || '')
.trim()
.toLowerCase() === 'datatype.enum'
) {
return normalizeHelpParams([ENUM_VALUE_PARAM]);
}
return [];
}

function buildCallSignature(heading, params) {
if (!Array.isArray(params) || params.length === 0) {
return `${heading}()`;
Expand Down Expand Up @@ -169,11 +226,8 @@ function buildSchemaHelpModel(sourceType, commandValue) {
{
params: [
{
name: 'values',
type: 'comma-separated list',
optional: false,
...ENUM_VALUE_PARAM,
description: 'List of allowed values randomly selected during generation.',
example: 'active,inactive,pending',
},
],
example: 'enum active,inactive,pending',
Expand All @@ -198,7 +252,7 @@ function buildSchemaHelpModel(sourceType, commandValue) {
heading: `faker.${command}`,
summary: commandHelp?.summary || `Generates data using faker.${command}.`,
docsUrl: resolveFakerDocsUrl(command, commandHelp?.docsUrl),
params: commandHelp?.params || [],
params: normalizeHelpParams(commandHelp?.params || []),
example: commandHelp?.example || '',
examples: Array.isArray(commandHelp?.examples) ? commandHelp.examples : [],
exampleReturnValues: Array.isArray(commandHelp?.exampleReturnValues)
Expand Down Expand Up @@ -226,7 +280,7 @@ function buildSchemaHelpModel(sourceType, commandValue) {
heading: commandHelp?.canonical || command,
summary: commandHelp?.summary || `Generates data using ${commandHelp?.canonical || command}.`,
docsUrl: commandHelp?.docsUrl || HELP_URLS.domain,
params: commandHelp?.args || [],
params: resolveDomainHelpParams(command, commandHelp),
example: commandHelp?.example || '',
examples: Array.isArray(commandHelp?.examples) ? commandHelp.examples : [],
exampleReturnValues: Array.isArray(commandHelp?.exampleReturnValues)
Expand Down
Loading