diff --git a/README.md b/README.md index 74d9e63b..6c8f6d49 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ name rule # compact pict-style alternative -status: enum(active,inactive) +status: active,inactive IF [name] = "Bob" THEN [status] = "active" ENDIF ``` diff --git a/apps/api/src/fromschema.route.test.js b/apps/api/src/fromschema.route.test.js index 90b42a99..76a0086b 100644 --- a/apps/api/src/fromschema.route.test.js +++ b/apps/api/src/fromschema.route.test.js @@ -181,7 +181,7 @@ test('/v1/generate/fromschema accepts comments and blank lines in schema text', const response = await fetch(url('/v1/generate/fromschema?rowCount=2&outputFormat=json'), { method: 'POST', headers: { 'content-type': 'text/plain' }, - body: '# skip me\n\nPriority\nenum(high,medium,low)\n\n# and me\nStatus\nenum(active,inactive,pending)', + body: '# skip me\n\nPriority\nhigh,medium,low\n\n# and me\nStatus\nactive,inactive,pending', }); expect(response.status).toBe(200); diff --git a/apps/cli/src/tests/run-cli.test.js b/apps/cli/src/tests/run-cli.test.js index 2febdc2a..432667b7 100644 --- a/apps/cli/src/tests/run-cli.test.js +++ b/apps/cli/src/tests/run-cli.test.js @@ -239,7 +239,7 @@ test('generates deterministic pairwise output in buffered mode', async () => { test('supports comments and blank lines in input schema', async () => { const platform = makePlatform({ - textSpec: '# comment\n\nPriority\nenum(high,medium,low)\n\nStatus\nenum(active,inactive,pending)', + textSpec: '# comment\n\nPriority\nhigh,medium,low\n\nStatus\nactive,inactive,pending', }); const code = await runCliCommand({ platform, diff --git a/apps/mcp/src/mcp.test.js b/apps/mcp/src/mcp.test.js index 7ae75276..d33cfaa3 100644 --- a/apps/mcp/src/mcp.test.js +++ b/apps/mcp/src/mcp.test.js @@ -82,7 +82,7 @@ test('MCP server accepts comments and blank lines in textSpec', () => { params: { name: 'generate_data_from_spec', arguments: { - textSpec: '# comment\n\nPriority\nenum(high,medium,low)\nStatus\nenum(active,inactive,pending)', + textSpec: '# comment\n\nPriority\nhigh,medium,low\nStatus\nactive,inactive,pending', rowCount: 1, outputFormat: 'json', }, diff --git a/apps/web/src/stories/generator-page.stories.js b/apps/web/src/stories/generator-page.stories.js index 858c9065..d2565920 100644 --- a/apps/web/src/stories/generator-page.stories.js +++ b/apps/web/src/stories/generator-page.stories.js @@ -11,7 +11,7 @@ import { createGeneratorPageComponent } from '../../../../packages/core-ui/js/gu import { createGeneratorPageShellComponent } from '../../../../packages/core-ui/js/gui_components/generator/page/create-generator-page-shell-component.js'; import { GENERATOR_DEFAULT_EXAMPLE_SCHEMA_TEXT } from '../../../../packages/core-ui/js/gui_components/shared/test-data/schema/schema-examples.js'; import { validateSchemaRows as validateSharedSchemaRows } from '../../../../packages/core-ui/js/gui_components/shared/test-data/schema/schema-editor-core.js'; -import { getKnownFakerCommandsAlphabetical } from '../../../../packages/core-ui/js/gui_components/shared/faker-commands.js'; +import { getAllowedFakerCommandsAlphabetical } from '../../../../packages/core-ui/js/gui_components/shared/faker-commands.js'; import { getKnownDomainCommandsAlphabetical } from '../../../../packages/core-ui/js/gui_components/shared/domain-commands.js'; import { buildSchemaModeHelpHtml } from '../../../../packages/core-ui/js/gui_components/shared/test-data/help/schema-mode-help-builder.js'; import { createGeneratorSchemaDefinitionSupport } from '../../../../packages/core-ui/js/gui_components/generator/schema-support/create-generator-schema-definition-support.js'; @@ -50,7 +50,7 @@ function getSchemaHelpText(rootElement) { function createGeneratorSchemaStoryProps(ids = {}) { let rowCounter = 1; - const fakerCommands = getKnownFakerCommandsAlphabetical().filter( + const fakerCommands = getAllowedFakerCommandsAlphabetical().filter( (command) => command !== 'RegEx' && command.startsWith('helpers.') ); const domainCommands = getKnownDomainCommandsAlphabetical(); diff --git a/apps/web/src/stories/generator-schema-panel.stories.js b/apps/web/src/stories/generator-schema-panel.stories.js index f39e7eed..d1861574 100644 --- a/apps/web/src/stories/generator-schema-panel.stories.js +++ b/apps/web/src/stories/generator-schema-panel.stories.js @@ -8,7 +8,7 @@ import { } from '@anywaydata/core/data_generation/schema-rules-adapter.js'; import { createSchemaPanelComponent } from '../../../../packages/core-ui/js/gui_components/shared/schema-panel/index.js'; import { validateSchemaRows as validateSharedSchemaRows } from '../../../../packages/core-ui/js/gui_components/shared/test-data/schema/schema-editor-core.js'; -import { getKnownFakerCommandsAlphabetical } from '../../../../packages/core-ui/js/gui_components/shared/faker-commands.js'; +import { getAllowedFakerCommandsAlphabetical } from '../../../../packages/core-ui/js/gui_components/shared/faker-commands.js'; import { getKnownDomainCommandsAlphabetical } from '../../../../packages/core-ui/js/gui_components/shared/domain-commands.js'; import { buildSchemaModeHelpHtml } from '../../../../packages/core-ui/js/gui_components/shared/test-data/help/schema-mode-help-builder.js'; import { createGeneratorSchemaDefinitionSupport } from '../../../../packages/core-ui/js/gui_components/generator/schema-support/create-generator-schema-definition-support.js'; @@ -16,7 +16,7 @@ import { GENERATOR_DEFAULT_EXAMPLE_SCHEMA_TEXT } from '../../../../packages/core function createGeneratorSchemaStoryProps() { let rowCounter = 1; - const fakerCommands = getKnownFakerCommandsAlphabetical().filter( + const fakerCommands = getAllowedFakerCommandsAlphabetical().filter( (command) => command !== 'RegEx' && command.startsWith('helpers.') ); const domainCommands = getKnownDomainCommandsAlphabetical(); diff --git a/apps/web/src/stories/import-export-download-control.stories.js b/apps/web/src/stories/import-export-download-control.stories.js index 053df001..bccaedef 100644 --- a/apps/web/src/stories/import-export-download-control.stories.js +++ b/apps/web/src/stories/import-export-download-control.stories.js @@ -79,7 +79,7 @@ export const Default = { }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); - const downloadButton = canvas.getByRole('button', { name: /\.csv Download/i }); + const downloadButton = canvas.getByRole('button', { name: 'Download file' }); await expect(downloadButton).toBeEnabled(); await userEvent.click(downloadButton); await expect(canvas.getByText('action:download')).toBeVisible(); @@ -101,7 +101,7 @@ export const Busy = { }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); - await expect(canvas.getByRole('button', { name: /\.csv Download/i })).toBeDisabled(); + await expect(canvas.getByRole('button', { name: 'Download file' })).toBeDisabled(); await expect(canvas.getByText('Generating export text...')).toBeVisible(); }, }; diff --git a/apps/web/src/stories/import-export-toolbar.stories.js b/apps/web/src/stories/import-export-toolbar.stories.js index 7d7a2ce2..01b65af3 100644 --- a/apps/web/src/stories/import-export-toolbar.stories.js +++ b/apps/web/src/stories/import-export-toolbar.stories.js @@ -202,7 +202,7 @@ export const Default = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); const helpButtons = canvas.getAllByRole('button', { name: 'Show help' }); - const downloadButton = canvas.getByRole('button', { name: /\.csv Download/i }); + const downloadButton = canvas.getByRole('button', { name: 'Download file' }); await expect(helpButtons).toHaveLength(2); await expect(helpButtons[0]).toHaveAttribute('data-help', 'import-export-import'); @@ -259,7 +259,7 @@ export const BusyAndStatus = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); await expect(canvas.getByRole('button', { name: 'Import From Clipboard' })).toBeDisabled(); - await expect(canvas.getByRole('button', { name: /\.csv Download/i })).toBeDisabled(); + await expect(canvas.getByRole('button', { name: 'Download file' })).toBeDisabled(); await expect(canvas.getByText('Importing full data into grid...')).toBeVisible(); await expect(canvas.getByText('Generating export text...')).toBeVisible(); }, diff --git a/apps/web/src/stories/shared-schema-definition.stories.js b/apps/web/src/stories/shared-schema-definition.stories.js index 9e2703f4..2934bd7a 100644 --- a/apps/web/src/stories/shared-schema-definition.stories.js +++ b/apps/web/src/stories/shared-schema-definition.stories.js @@ -306,7 +306,7 @@ export const SampleSchema = { export const ValidationError = { render: renderSharedSchemaDefinitionStory, args: { - initialText: 'Status\nenum(active,inactive)', + initialText: 'Status\nactive,inactive', showErrors: true, startInTextMode: false, }, @@ -335,7 +335,7 @@ export const ValidationError = { export const TextMode = { render: renderSharedSchemaDefinitionStory, args: { - initialText: 'Status\nenum(active,inactive,pending)', + initialText: 'Status\nactive,inactive,pending', showErrors: true, startInTextMode: true, }, @@ -351,7 +351,7 @@ export const TextMode = { expectTextModeVisible(canvasElement); const textArea = within(canvasElement).getByRole('textbox', { name: /schema text/i }); await expect(textArea.value).toContain('Status'); - await expect(textArea.value).toContain('enum(active,inactive,pending)'); + await expect(textArea.value).toContain('active,inactive,pending'); const helpIcon = canvasElement.querySelector('[data-role="schema-mode-help"]'); await userEvent.hover(helpIcon); await expectSchemaHelpContent(helpIcon, { helpHeadingText: 'Edit as Schema' }); @@ -363,9 +363,9 @@ export const ConstrainedTextMode = { render: renderSharedSchemaDefinitionStory, args: { initialText: `Priority -enum(high,low) +enum("high","low") Status -enum(open,closed) +enum("open","closed") IF [Priority] = "high" THEN [Status] = "open" ENDIF`, showErrors: true, startInTextMode: true, diff --git a/apps/web/src/stories/stored-schemas-manager.stories.js b/apps/web/src/stories/stored-schemas-manager.stories.js index 3f3563d2..ab8c9bc1 100644 --- a/apps/web/src/stories/stored-schemas-manager.stories.js +++ b/apps/web/src/stories/stored-schemas-manager.stories.js @@ -16,7 +16,7 @@ function createInMemoryStorageState(args) { { id: 'last-1', name: 'last used - 2026-06-16T10:05:00.000Z', - schemaText: 'Status\nenum(active,inactive)', + schemaText: 'Status\nactive,inactive', updatedAt: '2026-06-16T10:05:00.000Z', }, ] @@ -26,7 +26,7 @@ function createInMemoryStorageState(args) { { id: 'saved-1', name: 'Regression Smoke', - schemaText: 'Browser\nenum(chrome,firefox)', + schemaText: 'Browser\nchrome,firefox', updatedAt: '2026-06-16T10:06:00.000Z', }, ] @@ -203,7 +203,7 @@ export const DraftAndHistory = { const canvas = within(canvasElement); await userEvent.click(canvas.getByText('Managed Stored Schemas (0)')); await userEvent.selectOptions(canvas.getByLabelText('Last Used'), 'last-1'); - await userEvent.click(canvas.getByRole('button', { name: /^load$/i })); + await userEvent.click(canvas.getByRole('button', { name: /load last used schema/i })); await expect(canvas.getByText(/Loaded last used/i)).toBeVisible(); }, }; diff --git a/apps/web/src/tests/browser/app/abstractions/components/test-data-panel.component.js b/apps/web/src/tests/browser/app/abstractions/components/test-data-panel.component.js index c24ea564..704f858a 100644 --- a/apps/web/src/tests/browser/app/abstractions/components/test-data-panel.component.js +++ b/apps/web/src/tests/browser/app/abstractions/components/test-data-panel.component.js @@ -57,8 +57,8 @@ class TestDataPanelComponent { this.storedSchemasSummary = this.panelRoot.getByText(/Managed Stored Schemas/); this.storedSchemasSaveAsButton = this.panelRoot.getByRole('button', { name: 'Save Schema As' }); this.storedSchemasRecoverDraftButton = this.panelRoot.getByRole('button', { name: 'Recover Draft' }); - this.storedSchemasLastUsedSelect = this.panelRoot.getByLabel('Last Used'); - this.storedSchemasLoadLastUsedButton = this.panelRoot.getByRole('button', { name: /^Load$/ }); + this.storedSchemasLastUsedSelect = this.panelRoot.getByRole('combobox', { name: 'Last Used' }); + this.storedSchemasLoadLastUsedButton = this.panelRoot.getByRole('button', { name: 'Load last used schema' }); this.storedSchemasLoadSavedButton = this.panelRoot.getByRole('button', { name: 'Load Saved Schema' }); this.storedSchemasDialog = page.getByRole('dialog', { name: 'Saved Schemas' }); this.schemaGrid = this.schemaEditor.rowsContainer; diff --git a/apps/web/src/tests/browser/app/functional/test-data/grid-to-enum-schema.spec.js b/apps/web/src/tests/browser/app/functional/test-data/grid-to-enum-schema.spec.js index e4eb9f24..175d4432 100644 --- a/apps/web/src/tests/browser/app/functional/test-data/grid-to-enum-schema.spec.js +++ b/apps/web/src/tests/browser/app/functional/test-data/grid-to-enum-schema.spec.js @@ -36,7 +36,7 @@ test.describe('7. Test Data Generation', () => { await expect.poll(async () => appPage.testDataPanel.getSchemaRowCount()).toBe(2); await expect .poll(async () => appPage.testDataPanel.getSchemaText()) - .toContain('Status\nenum(active,pending,inactive)'); + .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); @@ -51,7 +51,7 @@ test.describe('7. Test Data Generation', () => { 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()).toContain('Status\nenum("active","pending")'); await expect.poll(async () => appPage.testDataPanel.getSchemaText()).not.toContain('inactive'); expectNoPageErrors(pageErrors); }); diff --git a/apps/web/src/tests/browser/app/functional/test-data/pairwise-generation.spec.js b/apps/web/src/tests/browser/app/functional/test-data/pairwise-generation.spec.js index eadbe96b..ecec4d4c 100644 --- a/apps/web/src/tests/browser/app/functional/test-data/pairwise-generation.spec.js +++ b/apps/web/src/tests/browser/app/functional/test-data/pairwise-generation.spec.js @@ -10,7 +10,7 @@ test.describe('7. Test Data Generation', () => { await appPage.testDataPanel.expectExpanded(); await appPage.testDataPanel.setSchemaText( - 'Browser\nenum(chrome,firefox,safari)\n\nPlan\nenum(free,pro,enterprise)\n\nFixed\nliteral(CONSTANT)' + 'Browser\nchrome,firefox,safari\n\nPlan\nfree,pro,enterprise\n\nFixed\nliteral(CONSTANT)' ); const initialRowCount = await appPage.gridEditor.renderer.countRows(); @@ -29,7 +29,7 @@ test.describe('7. Test Data Generation', () => { await appPage.testDataPanel.expectExpanded(); await appPage.testDataPanel.setSchemaText( - 'Browser\nenum(chrome,firefox,safari)\n\nPlan\nenum(free,pro,enterprise)\n\nFixed\nliteral(CONSTANT)\n\nCode\n[A-Z]{2}[0-9]{2}\n\nEnabled\ndatatype.boolean' + 'Browser\nchrome,firefox,safari\n\nPlan\nfree,pro,enterprise\n\nFixed\nliteral(CONSTANT)\n\nCode\n[A-Z]{2}[0-9]{2}\n\nEnabled\ndatatype.boolean' ); await appPage.testDataPanel.clickGeneratePairwise(); @@ -86,7 +86,7 @@ test.describe('7. Test Data Generation', () => { await appPage.testDataPanel.expectExpanded(); await appPage.testDataPanel.setSchemaText( - 'Browser\nenum(chrome,firefox,safari)\n\nPlan\nenum(free,pro,enterprise)\n\nRegion\nenum(amer,emea,apac)\n\nFixed\nliteral(CONSTANT)' + 'Browser\nchrome,firefox,safari\n\nPlan\nfree,pro,enterprise\n\nRegion\namer,emea,apac\n\nFixed\nliteral(CONSTANT)' ); await appPage.testDataPanel.openGenerateCombinationsDialog(); @@ -127,7 +127,7 @@ test.describe('7. Test Data Generation', () => { await appPage.testDataPanel.expectExpanded(); await appPage.testDataPanel.setSchemaText( - 'A\nenum(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)\n\nB\nenum(b1,b2,b3,b4,b5,b6,b7,b8,b9,b10,b11)\n\nC\nenum(c1,c2,c3,c4,c5,c6,c7,c8,c9,c10,c11)\n\nD\nenum(d1,d2,d3,d4,d5,d6,d7,d8,d9,d10,d11)' + 'A\na1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11\n\nB\nb1,b2,b3,b4,b5,b6,b7,b8,b9,b10,b11\n\nC\nc1,c2,c3,c4,c5,c6,c7,c8,c9,c10,c11\n\nD\nd1,d2,d3,d4,d5,d6,d7,d8,d9,d10,d11' ); const initialRowCount = await appPage.gridEditor.renderer.countRows(); diff --git a/apps/web/src/tests/browser/app/functional/test-data/schema-file-load-save.spec.js b/apps/web/src/tests/browser/app/functional/test-data/schema-file-load-save.spec.js index fb0c4e7b..414bdcff 100644 --- a/apps/web/src/tests/browser/app/functional/test-data/schema-file-load-save.spec.js +++ b/apps/web/src/tests/browser/app/functional/test-data/schema-file-load-save.spec.js @@ -10,7 +10,7 @@ test.describe('Test Data Schema File Load Save', () => { await appPage.testDataPanel.loadSchemaFile({ name: 'schema.txt', mimeType: 'text/plain', - buffer: Buffer.from('Loaded Name\nliteral(Ada)\nLoaded Status\nenum(active,inactive)'), + buffer: Buffer.from('Loaded Name\nliteral(Ada)\nLoaded Status\nactive,inactive'), }); await expect.poll(async () => appPage.testDataPanel.getSchemaText()).toContain('Loaded Name'); diff --git a/apps/web/src/tests/browser/app/functional/test-data/text-schema-grid-sync.spec.js b/apps/web/src/tests/browser/app/functional/test-data/text-schema-grid-sync.spec.js index 45b8b4bd..94a374a3 100644 --- a/apps/web/src/tests/browser/app/functional/test-data/text-schema-grid-sync.spec.js +++ b/apps/web/src/tests/browser/app/functional/test-data/text-schema-grid-sync.spec.js @@ -43,11 +43,11 @@ test.describe('7. Test Data Generation', () => { await appPage.testDataPanel.expand(); await appPage.testDataPanel.expectExpanded(); - await appPage.testDataPanel.setSchemaText('Priority\nenum(low,medium,high)'); + await appPage.testDataPanel.setSchemaText('Priority\nlow,medium,high'); await expect.poll(async () => appPage.testDataPanel.getSchemaRowCount()).toBe(1); await expect.poll(async () => appPage.testDataPanel.getSchemaCell(0, 'columnName')).toBe('Priority'); - await appPage.testDataPanel.setSchemaText('Priority\nenum(low,medium,high)\nEnabled\ndatatype.boolean'); + await appPage.testDataPanel.setSchemaText('Priority\nlow,medium,high\nEnabled\ndatatype.boolean'); await expect.poll(async () => appPage.testDataPanel.getSchemaRowCount()).toBe(2); await expect.poll(async () => appPage.testDataPanel.getSchemaCell(1, 'columnName')).toBe('Enabled'); @@ -82,7 +82,49 @@ test.describe('7. Test Data Generation', () => { expectNoPageErrors(pageErrors); }); - test('domain rows with invalid params stay domain after text mode round-trip in the app editor', async ({ page }) => { + test('invalid enum text shows a schema error and remains in text mode when editing as schema in the app', async ({ + page, + }) => { + const { appPage, pageErrors } = await openApp(page); + + await appPage.testDataPanel.expand(); + await appPage.testDataPanel.expectExpanded(); + + await appPage.testDataPanel.setSchemaText('Status\ndatatype.enum(values="active,pending)'); + await appPage.testDataPanel.schemaEditor.modeToggleButton.click(); + + await expect + .poll(async () => appPage.testDataPanel.getSchemaErrorText()) + .toContain('Status failed domain validation - Invalid keyword arguments: unbalanced expression'); + await expect.poll(async () => appPage.testDataPanel.isRowEditorMode()).toBe(false); + await expect(appPage.testDataPanel.schemaEditor.modeToggleButton).toHaveText('Edit as Schema'); + + expectNoPageErrors(pageErrors); + }); + + test('invalid enum text shows a schema error and leaves the grid unchanged when generating in the app', async ({ + page, + }) => { + const { appPage, pageErrors } = await openApp(page); + + await appPage.testDataPanel.expand(); + await appPage.testDataPanel.expectExpanded(); + + const totalRowsBefore = await appPage.gridEditor.totalRows.textContent(); + await appPage.testDataPanel.setSchemaText('Status\ndatatype.enum()'); + await appPage.testDataPanel.clickGenerate(); + + await expect + .poll(async () => appPage.testDataPanel.getSchemaErrorText()) + .toContain('Status failed domain validation - Invalid keyword arguments: argument "values" is required'); + await expect(appPage.gridEditor.totalRows).toHaveText(totalRowsBefore || ''); + + expectNoPageErrors(pageErrors); + }); + + test('domain rows with invalid params show a schema error after text mode round-trip in the app editor', async ({ + page, + }) => { const { appPage, pageErrors } = await openApp(page); await appPage.testDataPanel.expand(); @@ -95,12 +137,12 @@ test.describe('7. Test Data Generation', () => { await appPage.testDataPanel.setSchemaCell(0, 'params', '(10)'); await appPage.testDataPanel.setSchemaTextMode(true); - await appPage.testDataPanel.setSchemaTextMode(false); + await appPage.testDataPanel.schemaEditor.modeToggleButton.click(); - await expect.poll(async () => appPage.testDataPanel.getSchemaCell(0, 'type')).toBe('person.fullName'); - await expect.poll(async () => appPage.testDataPanel.getSchemaSourceType(0)).toBe('domain'); - await expect.poll(async () => appPage.testDataPanel.getSchemaCell(0, 'params')).toBe('(10)'); - await expect(appPage.testDataPanel.getSchemaValidationMessage(0)).toContainText('invalid domain params'); + await expect + .poll(async () => appPage.testDataPanel.getSchemaErrorText()) + .toContain('Name failed domain validation'); + await expect.poll(async () => appPage.testDataPanel.isRowEditorMode()).toBe(false); expectNoPageErrors(pageErrors); }); @@ -157,17 +199,17 @@ test.describe('7. Test Data Generation', () => { await appPage.testDataPanel.addSchemaRow(); await expect.poll(async () => appPage.testDataPanel.getSchemaRowCount()).toBe(1); - await appPage.testDataPanel.setSchemaCell(0, 'columnName', 'Phone'); - await appPage.testDataPanel.setSchemaTypeValue(0, 'phone.number'); - await appPage.testDataPanel.setSchemaCell(0, 'params', 'style=13'); + await appPage.testDataPanel.setSchemaCell(0, 'columnName', 'Code'); + await appPage.testDataPanel.setSchemaTypeValue(0, 'string.alpha'); + await appPage.testDataPanel.setSchemaCell(0, 'params', 'length=4'); await appPage.testDataPanel.setSchemaTextMode(true); - await expect.poll(async () => appPage.testDataPanel.getSchemaText()).toContain('phone.number(style=13)'); + await expect.poll(async () => appPage.testDataPanel.getSchemaText()).toContain('string.alpha(length=4)'); await appPage.testDataPanel.setSchemaTextMode(false); - await expect.poll(async () => appPage.testDataPanel.getSchemaCell(0, 'type')).toBe('phone.number'); + await expect.poll(async () => appPage.testDataPanel.getSchemaCell(0, 'type')).toBe('string.alpha'); await expect.poll(async () => appPage.testDataPanel.getSchemaSourceType(0)).toBe('domain'); - await expect.poll(async () => appPage.testDataPanel.getSchemaCell(0, 'params')).toBe('(style=13)'); + await expect.poll(async () => appPage.testDataPanel.getSchemaCell(0, 'params')).toBe('(length=4)'); expectNoPageErrors(pageErrors); }); @@ -233,7 +275,9 @@ test.describe('7. Test Data Generation', () => { await expect.poll(async () => appPage.testDataPanel.getSchemaCell(0, 'type')).toBe('datatype.enum'); await expect.poll(async () => appPage.testDataPanel.getSchemaCell(0, 'value')).toBe('active,inactive,pending'); - await expect.poll(async () => appPage.testDataPanel.getSchemaText()).toContain('enum(active,inactive,pending)'); + await expect + .poll(async () => appPage.testDataPanel.getSchemaText()) + .toContain('enum("active","inactive","pending")'); expectNoPageErrors(pageErrors); }); diff --git a/apps/web/src/tests/browser/app/functional/test-data/text-schema.spec.js b/apps/web/src/tests/browser/app/functional/test-data/text-schema.spec.js index 57b1c747..f282889d 100644 --- a/apps/web/src/tests/browser/app/functional/test-data/text-schema.spec.js +++ b/apps/web/src/tests/browser/app/functional/test-data/text-schema.spec.js @@ -11,7 +11,7 @@ test.describe('7. Test Data Generation', () => { const beforeSchema = await appPage.testDataPanel.getSchemaRowCount(); await appPage.testDataPanel.setSchemaText( - '# this comment should be ignored\n\nFirst Name\nperson.firstName\n\n# second comment\nStatus\nenum(active,inactive,pending)' + '# this comment should be ignored\n\nFirst Name\nperson.firstName\n\n# second comment\nStatus\nactive,inactive,pending' ); await expect.poll(async () => appPage.testDataPanel.getSchemaRowCount()).toBeGreaterThanOrEqual(beforeSchema + 1); await appPage.testDataPanel.setGenerateCount(5); diff --git a/apps/web/src/tests/browser/generator/abstractions/components/generator-schema.component.js b/apps/web/src/tests/browser/generator/abstractions/components/generator-schema.component.js index cb1cff49..d0340218 100644 --- a/apps/web/src/tests/browser/generator/abstractions/components/generator-schema.component.js +++ b/apps/web/src/tests/browser/generator/abstractions/components/generator-schema.component.js @@ -17,8 +17,8 @@ class GeneratorSchemaComponent { this.storedSchemasSummary = this.container.getByText(/Managed Stored Schemas/); this.storedSchemasSaveAsButton = this.container.getByRole('button', { name: 'Save Schema As' }); this.storedSchemasRecoverDraftButton = this.container.getByRole('button', { name: 'Recover Draft' }); - this.storedSchemasLastUsedSelect = this.container.getByLabel('Last Used'); - this.storedSchemasLoadLastUsedButton = this.container.getByRole('button', { name: /^Load$/ }); + this.storedSchemasLastUsedSelect = this.container.getByRole('combobox', { name: 'Last Used' }); + this.storedSchemasLoadLastUsedButton = this.container.getByRole('button', { name: 'Load last used schema' }); this.storedSchemasLoadSavedButton = this.container.getByRole('button', { name: 'Load Saved Schema' }); this.storedSchemasDialog = page.getByRole('dialog', { name: 'Saved Schemas' }); this.textInputDialog = new TextInputDialogComponent(page); diff --git a/apps/web/src/tests/browser/generator/functional/generate-pairwise-data.spec.js b/apps/web/src/tests/browser/generator/functional/generate-pairwise-data.spec.js index 2d885710..7241ce56 100644 --- a/apps/web/src/tests/browser/generator/functional/generate-pairwise-data.spec.js +++ b/apps/web/src/tests/browser/generator/functional/generate-pairwise-data.spec.js @@ -45,7 +45,7 @@ function validatePairwiseRows(rows) { async function setPairwiseSchema(generatorPage) { await generatorPage.schema.setSchemaText( - 'Browser\nenum(chrome,firefox,safari)\n\nPlan\nenum(free,pro,enterprise)\n\nFixed\nliteral(CONSTANT)\n\nCode\n[A-Z]{2}[0-9]{2}\n\nEnabled\ndatatype.boolean' + 'Browser\nchrome,firefox,safari\n\nPlan\nfree,pro,enterprise\n\nFixed\nliteral(CONSTANT)\n\nCode\n[A-Z]{2}[0-9]{2}\n\nEnabled\ndatatype.boolean' ); } diff --git a/apps/web/src/tests/browser/generator/functional/schema-edit.spec.js b/apps/web/src/tests/browser/generator/functional/schema-edit.spec.js index 2a515a78..fe3d1a5a 100644 --- a/apps/web/src/tests/browser/generator/functional/schema-edit.spec.js +++ b/apps/web/src/tests/browser/generator/functional/schema-edit.spec.js @@ -23,7 +23,7 @@ test.describe('Generator Schema Editing', () => { test('can edit as text then edit as schema', async ({ page }) => { const { generatorPage, pageErrors } = await openGenerator(page); - await generatorPage.schema.setSchemaText('First Name\nliteral(Alice)\n\nStatus\nenum(active,inactive)'); + await generatorPage.schema.setSchemaText('First Name\nliteral(Alice)\n\nStatus\nactive,inactive'); await generatorPage.schema.setTextMode(false); await expect(generatorPage.schema.rows).toHaveCount(2); @@ -77,7 +77,9 @@ test.describe('Generator Schema Editing', () => { 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)'); + await expect + .poll(async () => generatorPage.schema.getSchemaText()) + .toContain('enum("active","inactive","pending")'); expectNoPageErrors(pageErrors); }); @@ -108,6 +110,48 @@ test.describe('Generator Schema Editing', () => { expectNoPageErrors(pageErrors); }); + test('invalid enum text shows a schema error and remains in text mode when editing as schema', async ({ page }) => { + const { generatorPage, pageErrors } = await openGenerator(page); + + await generatorPage.schema.setSchemaText('Status\ndatatype.enum(values="active,pending)'); + await generatorPage.schema.modeToggleButton.click(); + + await expect(generatorPage.schema.errorStatus).toContainText( + 'Status failed domain validation - Invalid keyword arguments: unbalanced expression' + ); + await expect.poll(async () => generatorPage.schema.editor.isRowEditorMode()).toBe(false); + await expect(generatorPage.schema.modeToggleButton).toHaveText('Edit as Schema'); + + expectNoPageErrors(pageErrors); + }); + + test('invalid enum text shows a schema error when previewing generator data', async ({ page }) => { + const { generatorPage, pageErrors } = await openGenerator(page); + + await generatorPage.schema.setSchemaText('Status\ndatatype.enum(values="")'); + await generatorPage.preview.clickPreview(); + + await expect(generatorPage.schema.errorStatus).toContainText( + 'Status failed domain validation - Invalid keyword arguments: argument "values" is required' + ); + await expect.poll(async () => generatorPage.preview.getOutputPreviewText()).toBe(''); + + expectNoPageErrors(pageErrors); + }); + + test('invalid enum text shows a schema error when generating data', async ({ page }) => { + const { generatorPage, pageErrors } = await openGenerator(page); + + await generatorPage.schema.setSchemaText('Status\ndatatype.enum()'); + await generatorPage.generateOptions.clickGenerateData(); + + await expect(generatorPage.schema.errorStatus).toContainText( + 'Status failed domain validation - Invalid keyword arguments: argument "values" is required' + ); + + expectNoPageErrors(pageErrors); + }); + test('schema help shows tippy tooltip content for faker and literal', async ({ page }) => { const { generatorPage, pageErrors } = await openGenerator(page); @@ -131,7 +175,7 @@ test.describe('Generator Schema Editing', () => { expectNoPageErrors(pageErrors); }); - test('domain rows with invalid params stay domain after text mode round-trip in the generator editor', async ({ + test('domain rows with invalid params show a schema error after text mode round-trip in the generator editor', async ({ page, }) => { const { generatorPage, pageErrors } = await openGenerator(page); @@ -142,14 +186,10 @@ test.describe('Generator Schema Editing', () => { await generatorPage.schema.editor.setRowField(0, 'params', '(10)'); await generatorPage.schema.setTextMode(true); - await generatorPage.schema.setTextMode(false); + await generatorPage.schema.modeToggleButton.click(); - await expect(generatorPage.schema.row(0).locator('select[data-field="sourceType"]')).toHaveValue('domain'); - await expect(generatorPage.schema.row(0).locator('[data-action="pick-command"]')).toHaveText('person.fullName'); - await expect(generatorPage.schema.row(0).locator('input[data-field="params"]')).toHaveValue('(10)'); - await expect(generatorPage.schema.row(0).locator('.shared-schema-row-validation')).toContainText( - 'invalid domain params' - ); + await expect(generatorPage.schema.errorStatus).toContainText('Name failed domain validation'); + await expect.poll(async () => generatorPage.schema.editor.isRowEditorMode()).toBe(false); expectNoPageErrors(pageErrors); }); @@ -198,16 +238,16 @@ test.describe('Generator Schema Editing', () => { await generatorPage.schema.setTextMode(false); await generatorPage.schema.setRowName(0, 'Phone'); - await generatorPage.schema.editor.setRowTypeValue(0, 'phone.number'); - await generatorPage.schema.editor.setRowField(0, 'params', 'style=13'); + await generatorPage.schema.editor.setRowTypeValue(0, 'string.alpha'); + await generatorPage.schema.editor.setRowField(0, 'params', 'length=4'); await generatorPage.schema.setTextMode(true); - await expect.poll(async () => generatorPage.schema.getSchemaText()).toContain('phone.number(style=13)'); + await expect.poll(async () => generatorPage.schema.getSchemaText()).toContain('string.alpha(length=4)'); await generatorPage.schema.setTextMode(false); await expect(generatorPage.schema.row(0).locator('select[data-field="sourceType"]')).toHaveValue('domain'); - await expect(generatorPage.schema.row(0).locator('[data-action="pick-command"]')).toHaveText('phone.number'); - await expect(generatorPage.schema.row(0).locator('input[data-field="params"]')).toHaveValue('(style=13)'); + await expect(generatorPage.schema.row(0).locator('[data-action="pick-command"]')).toHaveText('string.alpha'); + await expect(generatorPage.schema.row(0).locator('input[data-field="params"]')).toHaveValue('(length=4)'); expectNoPageErrors(pageErrors); }); @@ -226,7 +266,9 @@ test.describe('Generator Schema Editing', () => { 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)'); + await expect + .poll(async () => generatorPage.schema.getSchemaText()) + .toContain('enum("active","inactive","pending")'); expectNoPageErrors(pageErrors); }); diff --git a/apps/web/src/tests/browser/generator/functional/schema-file-load-save.spec.js b/apps/web/src/tests/browser/generator/functional/schema-file-load-save.spec.js index 607526a0..b62f9240 100644 --- a/apps/web/src/tests/browser/generator/functional/schema-file-load-save.spec.js +++ b/apps/web/src/tests/browser/generator/functional/schema-file-load-save.spec.js @@ -9,7 +9,7 @@ test.describe('Generator Schema File Load Save', () => { await generatorPage.schema.loadSchemaFile({ name: 'schema.txt', mimeType: 'text/plain', - buffer: Buffer.from('Generated Name\nliteral(Ada)\nGenerated Status\nenum(active,inactive)'), + buffer: Buffer.from('Generated Name\nliteral(Ada)\nGenerated Status\nactive,inactive'), }); await expect.poll(async () => generatorPage.schema.getSchemaText()).toContain('Generated Name'); diff --git a/docs-src/blog/2026-05-18-domain-abstraction-over-raw-faker.md b/docs-src/blog/2026-05-18-domain-abstraction-over-raw-faker.md index 58cb7eb4..46265ad2 100644 --- a/docs-src/blog/2026-05-18-domain-abstraction-over-raw-faker.md +++ b/docs-src/blog/2026-05-18-domain-abstraction-over-raw-faker.md @@ -100,7 +100,7 @@ The second version is shorter, easier to read, and less sensitive to upstream li ```text # pairwise enums HTTP Method -enum(GET,POST,PUT,DELETE) +GET,POST,PUT,DELETE # pairwise enums Content Type diff --git a/docs-src/docs/040-test-data/010-test-data-generation.md b/docs-src/docs/040-test-data/010-test-data-generation.md index 9815f20e..b9b79d2e 100644 --- a/docs-src/docs/040-test-data/010-test-data-generation.md +++ b/docs-src/docs/040-test-data/010-test-data-generation.md @@ -1,13 +1,13 @@ --- sidebar_position: 1 title: "Test Data Generation" -description: "Overview of test-data generation workflows in app.html and generate.html." +description: "Overview of test-data generation workflows in app.html and generator.html." --- AnyWayData offers two main web UI workflows for generating and working with test data: - **Data Grid Editable** (`app.html`) for interactive grid-first editing and generation -- **Generate to File** (`generate.html`) for schema-driven generation and direct file output +- **Generate to File** (`generator.html`) for schema-driven generation and direct file output ## Choose a Workflow @@ -21,7 +21,7 @@ Use this when you want to: See [Data Grid Editable](/docs/test-data/data-grid-editable). -### Generate to File (`generate.html`) +### Generate to File (`generator.html`) Use this when you want to: diff --git a/docs-src/docs/040-test-data/016-generate-to-file.md b/docs-src/docs/040-test-data/016-generate-to-file.md index 65317399..19e95cb7 100644 --- a/docs-src/docs/040-test-data/016-generate-to-file.md +++ b/docs-src/docs/040-test-data/016-generate-to-file.md @@ -1,12 +1,12 @@ --- sidebar_position: 1.6 title: "Generate to File" -description: "Use generate.html for schema-first data generation with output preview and direct file download." +description: "Use generator.html for schema-first data generation with output preview and direct file download." --- The **Generate to File** workflow is available at: -- `https://anywaydata.com/generate.html` +- `https://anywaydata.com/generator.html` It is designed for schema-driven generation where your main goal is to produce output files quickly. @@ -50,7 +50,7 @@ See [All Pairs Combinatorial Testing](/docs/test-data/pairwise-testing) for deta ## Relationship to `app.html` -Use `generate.html` when file output speed and schema-driven generation are the priority. +Use `generator.html` when file output speed and schema-driven generation are the priority. Use `app.html` when you need richer interactive table editing before or after generation. diff --git a/docs-src/docs/040-test-data/018-Schema-Definition.md b/docs-src/docs/040-test-data/018-Schema-Definition.md index 1ee8296b..e23b0b7f 100644 --- a/docs-src/docs/040-test-data/018-Schema-Definition.md +++ b/docs-src/docs/040-test-data/018-Schema-Definition.md @@ -4,7 +4,7 @@ title: "Schema Definition" description: "Full reference for schema text format, field rules, and IF ... THEN schema constraints." --- -The schema editor in `app.html` and `generate.html` uses a plain text format. +The schema editor in `app.html` and `generator.html` uses a plain text format. This page explains: diff --git a/docs-src/docs/040-test-data/035-auto-increment-sequences.md b/docs-src/docs/040-test-data/035-auto-increment-sequences.md index 2f0150ac..5bacdb30 100644 --- a/docs-src/docs/040-test-data/035-auto-increment-sequences.md +++ b/docs-src/docs/040-test-data/035-auto-increment-sequences.md @@ -65,9 +65,9 @@ This command is especially useful when you generate rows under constraints. Ticket autoIncrement.sequence(prefix="T-", zeropadding=4) Priority -enum(High,Low) +High,Low Status -enum(Open,Closed) +Open,Closed IF [Priority] = "High" THEN [Status] = "Open"; ``` diff --git a/docs-src/docs/040-test-data/050-pairwise-testing.md b/docs-src/docs/040-test-data/050-pairwise-testing.md index c3f72665..a1872672 100644 --- a/docs-src/docs/040-test-data/050-pairwise-testing.md +++ b/docs-src/docs/040-test-data/050-pairwise-testing.md @@ -96,33 +96,33 @@ For more complex scenarios, you can use function-based formats: ``` # function style enum Priority -enum(high,medium,low) +high,medium,low # another function style enum Status -enum(active,inactive,pending) +active,inactive,pending ``` #### Datatype enum() Function ``` # datatype enum form Priority -datatype.enum(high,medium,low) +datatype.enum(csv="high,medium,low") # second datatype enum Status -datatype.enum(active,inactive,pending) +datatype.enum(csv="active,inactive,pending") ``` #### Full AWD enum() Function ``` # fully-qualified enum form Priority -awd.datatype.enum(high,medium,low) +awd.datatype.enum(csv="high,medium,low") # fully-qualified enum form Status -awd.datatype.enum(active,inactive,pending) +awd.datatype.enum(csv="active,inactive,pending") ``` ### Quoted Values for Complex Data @@ -171,7 +171,7 @@ Consider testing an API with both categorical parameters (that need pairwise cov ``` # pairwise enums HTTP Method -enum(GET,POST,PUT,DELETE) +GET,POST,PUT,DELETE # pairwise enums Content Type diff --git a/docs-src/docs/040-test-data/domain/100-datatype.md b/docs-src/docs/040-test-data/domain/100-datatype.md index a1d932a5..d39753d6 100644 --- a/docs-src/docs/040-test-data/domain/100-datatype.md +++ b/docs-src/docs/040-test-data/domain/100-datatype.md @@ -42,3 +42,31 @@ datatype.boolean(probability=0.5) ``` Returns: `true` + +### `datatype.enum` + +Enum helper accepts CSV values or a string array and returns one value at random. Bare CSV is supported as schema shorthand; function calls use quoted strings, arrays, or named arguments. + +- Canonical: `awd.domain.datatype.enum` + +| Arg | Type | Required | Description | +| --- | --- | --- | --- | +| `values` | `comma-separated list\|array` | yes | List of allowed enum values chosen at random during generation. Named csv="..." is also accepted as a CSV-string alias for this argument. | + +Examples: + +Shows the canonical datatype enum helper using a named CSV argument. The same public enum can also be authored as enum("active","inactive","pending") or the schema shorthand active,inactive,pending. + +```txt +datatype.enum(csv="active,inactive,pending") +``` + +Returns: `inactive` + +Shows the string-array form for values that should be parsed directly instead of as CSV text. The fully-qualified compatibility alias awd.datatype.enum(...) also normalizes to this same datatype.enum command internally. + +```txt +datatype.enum(values=["GET","POST","PUT","PATCH"]) +``` + +Returns: `PUT` diff --git a/docs-src/docs/040-test-data/domain/190-location.md b/docs-src/docs/040-test-data/domain/190-location.md index bf65240c..b7607a17 100644 --- a/docs-src/docs/040-test-data/domain/190-location.md +++ b/docs-src/docs/040-test-data/domain/190-location.md @@ -97,18 +97,28 @@ Returns a random cardinal direction (north, east, south, west). - Canonical: `awd.domain.location.cardinalDirection` - Faker docs: [https://fakerjs.dev/api/location](https://fakerjs.dev/api/location) -No parameters. +| Arg | Type | Required | Description | +| --- | --- | --- | --- | +| `abbreviated` | `boolean` | no | If true this will return abbreviated cardinal directions (N, E, S, W). Otherwise this will return the long name. | Examples: -Shows the default location.cardinalDirection call. +Shows location.cardinalDirection when optional params are omitted. ```txt -location.cardinalDirection +location.cardinalDirection() ``` Returns: `East` +Shows location.cardinalDirection using abbreviated. + +```txt +location.cardinalDirection(abbreviated=true) +``` + +Returns: `E` + ### `location.city` Generates a random localized city name. @@ -344,18 +354,28 @@ Returns a random ordinal direction (northwest, southeast, etc). - Canonical: `awd.domain.location.ordinalDirection` - Faker docs: [https://fakerjs.dev/api/location](https://fakerjs.dev/api/location) -No parameters. +| Arg | Type | Required | Description | +| --- | --- | --- | --- | +| `abbreviated` | `boolean` | no | If true this will return abbreviated ordinal directions (NW, SE, etc). Otherwise this will return the long name. | Examples: -Shows the default location.ordinalDirection call. +Shows location.ordinalDirection when optional params are omitted. ```txt -location.ordinalDirection +location.ordinalDirection() ``` Returns: `Northwest` +Shows location.ordinalDirection using abbreviated. + +```txt +location.ordinalDirection(abbreviated=true) +``` + +Returns: `NW` + ### `location.secondaryAddress` Generates a random localized secondary address. This refers to a specific location at a given address diff --git a/docs-src/docs/040-test-data/faker/000-faker-based-data.mdx b/docs-src/docs/040-test-data/faker/000-faker-based-data.mdx index 070f69dc..229b95ab 100644 --- a/docs-src/docs/040-test-data/faker/000-faker-based-data.mdx +++ b/docs-src/docs/040-test-data/faker/000-faker-based-data.mdx @@ -45,7 +45,7 @@ Use domain docs for domain-first schemas: [Domain Test Data](/docs/test-data/dom ```txt Sentence -helpers.mustache("I found {{count}} instances.", { count: () => `${this.number.int()}` }) +helpers.mustache("Hello {{name}}", { name: "Ada" }) ``` ```txt @@ -55,10 +55,10 @@ helpers.fake("Hi, my name is {{person.firstName}} {{person.lastName}}!") ```txt Direction -faker.location.cardinalDirection({ abbreviated: true }) +location.direction(abbreviated=true) ``` -Note: non-helper faker calls may be superseded by domain mappings over time. +Note: non-helper faker calls may be superseded by domain mappings over time. Prefer the documented domain syntax when a domain equivalent exists. ## See Domain Examples diff --git a/docs-src/docs/040-test-data/faker/010-helpers.md b/docs-src/docs/040-test-data/faker/010-helpers.md index 65fb67ca..3f995521 100644 --- a/docs-src/docs/040-test-data/faker/010-helpers.md +++ b/docs-src/docs/040-test-data/faker/010-helpers.md @@ -35,7 +35,7 @@ helpers.fake("Hi, my name is {{person.firstName}} {{person.lastName}}!") ``` ```txt -helpers.mustache("I found {{count}} items.", { count: () => `${this.number.int()}` }) +helpers.mustache("Hello {{name}}", { name: "Ada" }) ``` ```txt @@ -114,7 +114,7 @@ helpers.multiple(() => this.person.firstName(), { count: 3 }) - Many helper functions can return arrays or objects depending on method and inputs. - Prefer scalar-returning helpers when using grid/display flows that expect single values. -- Helper methods support callback/object shapes from Faker docs and are intentionally not represented in `domain.*`. +- Some Faker helper callback shapes are not supported in browser schema text. Use the executable schema examples on this page when copying examples into AnyWayData. ## Faker Reference diff --git a/docs/frontend-ui-matrix-rationalization-plan.md b/docs/frontend-ui-matrix-rationalization-plan.md index 711c99ba..d7c239eb 100644 --- a/docs/frontend-ui-matrix-rationalization-plan.md +++ b/docs/frontend-ui-matrix-rationalization-plan.md @@ -70,7 +70,7 @@ The frontend test stack should follow these rules: ## Current State -Current matrix suites under `packages/core-ui/src/tests/interaction/matrix/` cover three different concerns: +Current matrix suites under `packages/core-ui/src/tests/interaction/matrix/` cover two retained concerns: - `schema-interaction-runtime-matrix.test.js` - scenario execution against the runtime/data-generation layer @@ -78,10 +78,8 @@ Current matrix suites under `packages/core-ui/src/tests/interaction/matrix/` cov - standalone generator UI scenario execution in JSDOM - `app-schema-interaction-matrix.test.js` - embedded app test-data panel scenario execution in JSDOM -- `ui-schema-interaction-parity.test.js` - - parity comparison between app and generator scenario outputs -The current issue is not that matrix coverage is useless. The issue is that the app/generator UI and parity matrices now overlap heavily with shared component tests and browser flows, while still being expensive and brittle. +The app/generator UI matrices are intentionally small page-wiring smoke suites. Broad app-vs-generator parity coverage was removed because both pages now exercise the same shared internals, while runtime semantics, focused component tests, and browser workflows provide clearer protection. ## Keep / Reduce / Remove Guidance @@ -150,14 +148,14 @@ Exit criteria: - There is a documented target layer for every major test-data behavior. - We have a replacement destination for any matrix assertion we plan to remove. -## Phase 3: Shrink Cross-Page Parity To A Representative Contract +## Phase 3: Retire Broad Cross-Page Parity -Goal: keep only the parity checks that still prove something unique. +Goal: keep cross-page checks only where they prove something unique. -- [ ] Replace the large `ui-schema-interaction-parity.test.js` sweep with a much smaller representative parity set. -- [ ] Keep scenarios only for composition points that can still diverge between app and generator. -- [ ] Prefer exact parity checks for a few canonical scenarios rather than structural parity checks for many scenarios. -- [ ] Remove parity cases that merely re-prove shared component output through both shells. +- [x] Remove the large `ui-schema-interaction-parity.test.js` sweep. +- [x] Remove parity fixture generation and static parity/report artifacts. +- [x] Keep app and generator page coverage as separate smoke suites so failures identify the broken shell directly. +- [x] Rely on runtime matrix coverage for broad scenario semantics and browser tests for user-visible workflows. Candidate parity survivors: @@ -169,8 +167,8 @@ Candidate parity survivors: Exit criteria: -- Cross-page parity coverage is small, fast, and easy to interpret. -- Failing parity tests point to real app-vs-generator divergence, not broad duplicated behavior. +- Cross-page parity coverage no longer duplicates shared component output through both shells. +- Failing page-shell tests point to the app or generator wiring that actually regressed. ## Phase 4: Move Shared Behavior Coverage Down To Components @@ -218,9 +216,9 @@ Exit criteria: Goal: reduce maintenance overhead in the test infrastructure itself. -- [ ] Remove harness code that exists only for broad parity sweeps no longer required. +- [x] Remove harness code that exists only for broad parity sweeps no longer required. - [ ] Simplify app/generator interaction harnesses to the smaller retained scenario sets. -- [ ] Delete helper utilities and fixture complexity that no longer serve a retained suite. +- [x] Delete helper utilities and fixture complexity that no longer serve a retained suite. - [ ] Update contributor docs so the intended role of runtime matrix, component tests, and browser tests is explicit. Exit criteria: @@ -248,7 +246,7 @@ Exit criteria: This plan is complete when: -- broad duplicated app-vs-generator parity coverage has been reduced to a small intentional contract +- broad duplicated app-vs-generator parity coverage has been retired in favor of separate smoke suites - shared behavior is primarily covered by component tests - page-specific behavior is primarily covered by focused integration and browser tests - runtime semantics remain covered independently of page-shell duplication @@ -260,7 +258,7 @@ The safest first implementation slice is: 1. Complete Phase 0 and Phase 1 as a paper audit. 2. Complete Phase 2 as the replacement coverage model. -3. Shrink `ui-schema-interaction-parity.test.js` to a representative subset. +3. Retire `ui-schema-interaction-parity.test.js` once app/generator smoke coverage is in place. 4. Keep the runtime matrix unchanged. 5. Re-run coverage review before shrinking the app and generator JSDOM matrices. @@ -268,18 +266,19 @@ That path removes the most obvious duplication first while preserving the highes ### Current Status -- The runtime matrix remains unchanged. +- The runtime matrix remains the broad generated safety net. - `app-schema-interaction-matrix.test.js` and `generator-schema-interaction-matrix.test.js` now run a page-wiring smoke subset instead of the full shared `uiScenarios` fixture. -- The retained page-shell smoke scenarios are: - - `custom-literal-base` - - `custom-regex-base` - - `faker-helpers-arrayElement-base` - - `domain-commerce-price-example-1` - - `custom-enum-pairwise` +- The retained page-shell smoke scenarios are selected by semantic coverage lane: + - custom literal + - custom regex + - faker `helpers.arrayElement` + - domain `commerce.price` example with guided params + - enum pairwise - The smoke subset keeps: - simple schema editing and generate wiring - regex/text synchronization coverage - one representative faker helper flow - one representative domain/options flow - pairwise wiring coverage -- The broader semantics matrix still lives in `schema-interaction-runtime-matrix.test.js`, with parity sweeps remaining opt-in. +- The broad app-vs-generator parity sweep, parity fixture writer, parity fixture JSON, and static matrix summary artifact have been removed. +- The retained matrix formatting helper only formats chunk labels and command summaries for the active runtime and smoke suites. diff --git a/docs-src/docs/040-test-data/035-method-picker-ui-spec.md b/docs/method-picker-ui-spec.md similarity index 96% rename from docs-src/docs/040-test-data/035-method-picker-ui-spec.md rename to docs/method-picker-ui-spec.md index 81702018..5279b1c6 100644 --- a/docs-src/docs/040-test-data/035-method-picker-ui-spec.md +++ b/docs/method-picker-ui-spec.md @@ -1,7 +1,3 @@ ---- -title: Method Picker UI Spec ---- - # Method Picker UI Spec ## Purpose @@ -60,7 +56,7 @@ Applies to both schema-editing surfaces: ## Data + Metadata Sources - Command lists: - - domain: visible domain catalog (`getVisibleDomainCommands`) + - domain: visible domain catalog (`getVisibleDomainCommands`), including supported domain commands such as `datatype.enum` - faker: approved `helpers.*` commands - top-level schema types: `enum`, `literal`, `regex` - Help metadata: diff --git a/packages/core-ui/js/gui_components/app/import-export-download-control/import-export-download-control-view.js b/packages/core-ui/js/gui_components/app/import-export-download-control/import-export-download-control-view.js index 506cfdbf..675c16d3 100644 --- a/packages/core-ui/js/gui_components/app/import-export-download-control/import-export-download-control-view.js +++ b/packages/core-ui/js/gui_components/app/import-export-download-control/import-export-download-control-view.js @@ -27,7 +27,7 @@ class ImportExportDownloadControlView { return `
- +
${renderIconHtml('settings', { className: 'app-icon export-encoding-settings__icon' })} diff --git a/packages/core-ui/js/gui_components/app/import-export-workspace/import-export-workspace-view.js b/packages/core-ui/js/gui_components/app/import-export-workspace/import-export-workspace-view.js index ef0a57b7..85f0a189 100644 --- a/packages/core-ui/js/gui_components/app/import-export-workspace/import-export-workspace-view.js +++ b/packages/core-ui/js/gui_components/app/import-export-workspace/import-export-workspace-view.js @@ -1,8 +1,13 @@ import { resolveDocumentObj } from '../../shared/dom/default-objects.js'; +import { + bindDetailsContentVisibility, + setDetailsContentVisibility, +} from '../../shared/dom/details-disclosure-focus.js'; const TOOLBAR_ROOT_ROLE = 'import-export-toolbar-root'; const GRID_PREVIEW_SYNC_ROOT_ROLE = 'grid-preview-sync-root'; const TEXT_PREVIEW_EDITOR_ROOT_ROLE = 'text-preview-editor-root'; +const TOOLBAR_DETAILS_ROLE = 'import-export-toolbar-details'; class ImportExportWorkspaceView { constructor({ root, controller, documentObj, services = {} } = {}) { @@ -15,6 +20,7 @@ class ImportExportWorkspaceView { this.textPreviewEditor = null; this.formatSelector = null; this.formatOptionsPanel = null; + this.unbindToolbarDetailsVisibility = null; } mount() { @@ -23,6 +29,7 @@ class ImportExportWorkspaceView { } this.root.innerHTML = this.template(); + this.bindDisclosureVisibility(); this.createFeatures(); this.render(); this.services.updateHelpHints?.(); @@ -32,7 +39,7 @@ class ImportExportWorkspaceView { return `
-
+
Import / Export
@@ -105,6 +112,21 @@ class ImportExportWorkspaceView { return this.root?.querySelector?.(`[data-role="${role}"]`) || null; } + bindDisclosureVisibility() { + this.unbindToolbarDetailsVisibility?.(); + this.unbindToolbarDetailsVisibility = bindDetailsContentVisibility({ + detailsElement: this.getToolbarDetailsElement(), + contentElement: this.getElementByRole(TOOLBAR_ROOT_ROLE), + }); + } + + syncToolbarDetailsVisibility() { + setDetailsContentVisibility({ + detailsElement: this.getToolbarDetailsElement(), + contentElement: this.getElementByRole(TOOLBAR_ROOT_ROLE), + }); + } + render() { const state = this.controller.getState(); this.gridPreviewSyncControl?.update?.(state); @@ -121,6 +143,8 @@ class ImportExportWorkspaceView { } destroy() { + this.unbindToolbarDetailsVisibility?.(); + this.unbindToolbarDetailsVisibility = null; this.formatOptionsPanel?.destroy?.(); this.formatSelector?.destroy?.(); this.textPreviewEditor?.destroy?.(); @@ -142,13 +166,14 @@ class ImportExportWorkspaceView { } getToolbarDetailsElement() { - return this.getElementByRole('import-export-toolbar-details'); + return this.getElementByRole(TOOLBAR_DETAILS_ROLE); } openToolbarDetails() { const detailsElement = this.getToolbarDetailsElement(); if (detailsElement) { detailsElement.open = true; + this.syncToolbarDetailsVisibility(); } } diff --git a/packages/core-ui/js/gui_components/app/page/app-page-shell-view.js b/packages/core-ui/js/gui_components/app/page/app-page-shell-view.js index 716937cf..27beec35 100644 --- a/packages/core-ui/js/gui_components/app/page/app-page-shell-view.js +++ b/packages/core-ui/js/gui_components/app/page/app-page-shell-view.js @@ -1,7 +1,10 @@ +import { bindDetailsContentVisibility } from '../../shared/dom/details-disclosure-focus.js'; + class AppPageShellView { constructor({ root, controller } = {}) { this.root = root; this.controller = controller; + this.unbindTestDataDetailsVisibility = null; } mount() { @@ -9,7 +12,7 @@ class AppPageShellView { throw new Error('AppPageShellView requires a root element'); } - this.root.innerHTML = this.template(); + this.render(); } template() { @@ -23,9 +26,9 @@ class AppPageShellView {
- +
Test Data -
+
@@ -35,10 +38,22 @@ class AppPageShellView { } render() { + this.unbindTestDataDetailsVisibility?.(); + this.unbindTestDataDetailsVisibility = null; this.root.innerHTML = this.template(); + this.bindDisclosureVisibility(); + } + + bindDisclosureVisibility() { + this.unbindTestDataDetailsVisibility = bindDetailsContentVisibility({ + detailsElement: this.root.querySelector('[data-role="test-data-details"]'), + contentElement: this.root.querySelector('[data-role="test-data-details-content"]'), + }); } destroy() { + this.unbindTestDataDetailsVisibility?.(); + this.unbindTestDataDetailsVisibility = null; this.root.replaceChildren(); } } diff --git a/packages/core-ui/js/gui_components/app/population-actions/population-actions-view.js b/packages/core-ui/js/gui_components/app/population-actions/population-actions-view.js index 44aa9347..a6d565fe 100644 --- a/packages/core-ui/js/gui_components/app/population-actions/population-actions-view.js +++ b/packages/core-ui/js/gui_components/app/population-actions/population-actions-view.js @@ -68,7 +68,9 @@ class PopulationActionsView { helpHtml: state.generateHelpHtml, ariaLabel: state.generateHelpLabel, })} - + ${renderIconHtml('file-plus', { className: 'app-icon shared-file-action-icon generator-file-icon' })} ${escapeHtml(state.generateLabel)} @@ -83,7 +85,11 @@ class PopulationActionsView { helpHtml: state.generatePairwiseHelpHtml, ariaLabel: state.generatePairwiseHelpLabel, })} - + ${renderIconHtml('file-plus', { className: 'app-icon shared-file-action-icon generator-file-icon' })} ${escapeHtml(state.generatePairwiseLabel)} @@ -95,7 +101,9 @@ class PopulationActionsView { helpHtml: state.generateSchemaHelpHtml, ariaLabel: state.generateSchemaHelpLabel, })} - diff --git a/packages/core-ui/js/gui_components/app/test-data-grid/generation/test-data-generation-service.js b/packages/core-ui/js/gui_components/app/test-data-grid/generation/test-data-generation-service.js index 0a11df30..359cdf7d 100644 --- a/packages/core-ui/js/gui_components/app/test-data-grid/generation/test-data-generation-service.js +++ b/packages/core-ui/js/gui_components/app/test-data-grid/generation/test-data-generation-service.js @@ -284,9 +284,16 @@ function createTestDataGenerationService({ try { const algorithm = selection?.algorithm; + const schemaState = getCurrentSchemaRowValidation({ syncFromText: true }); + if (schemaState.errors.length > 0) { + showSchemaError(schemaErrorsToText(schemaState.errors || [])); + setTestDataStatus('Schema validation failed.', { severity: 'error', dismissable: true }); + return; + } + const confirmed = await confirmCartesianProductSelection({ algorithm, - valueCounts: generationEngine.getCombinationInput({ syncFromText: false }).enumValueCounts, + valueCounts: generationEngine.getCombinationInput({ schemaState }).enumValueCounts, requestConfirm, }); if (!confirmed) { @@ -305,7 +312,7 @@ function createTestDataGenerationService({ const result = generationEngine.generateCombinations({ strength, algorithm, - validationOptions: { syncFromText: false }, + validationOptions: { schemaState }, }); if (!result.ok) { surfaceEnginePresentation( diff --git a/packages/core-ui/js/gui_components/data-grid-editor/data-grid-component-view.js b/packages/core-ui/js/gui_components/data-grid-editor/data-grid-component-view.js index a8ac3bb0..a6474bd6 100644 --- a/packages/core-ui/js/gui_components/data-grid-editor/data-grid-component-view.js +++ b/packages/core-ui/js/gui_components/data-grid-editor/data-grid-component-view.js @@ -116,6 +116,7 @@ function createAppGridTabulatorOptions({ editor: 'input', editorParams: { selectContents: true }, headerFilter: 'input', + headerFilterParams: { elementAttributes: { 'aria-label': 'Filter table column' } }, headerFilterFunc: 'like', sorter: 'string', titleFormatter: customHeaderFormatter, diff --git a/packages/core-ui/js/gui_components/generator/generation/data-generator-generation-actions.js b/packages/core-ui/js/gui_components/generator/generation/data-generator-generation-actions.js index a3d62b16..e5d8cf11 100644 --- a/packages/core-ui/js/gui_components/generator/generation/data-generator-generation-actions.js +++ b/packages/core-ui/js/gui_components/generator/generation/data-generator-generation-actions.js @@ -120,7 +120,10 @@ function previewGeneratorData({ return; } - const result = schemaGenerationService?.generateRows?.({ rowCount: rowCount.value }); + const result = schemaGenerationService?.generateRows?.({ + rowCount: rowCount.value, + options: { showErrors: true }, + }); if (typeof result?.then === 'function') { return result.then(applyResult); } @@ -158,7 +161,10 @@ async function generateGeneratorDataFile({ return; } - const result = await schemaGenerationService?.generateRows?.({ rowCount: rowCount.value }); + const result = await schemaGenerationService?.generateRows?.({ + rowCount: rowCount.value, + options: { showErrors: true }, + }); if (!result?.ok) { surfaceGenerationResult({ operationKind: 'generateRows', @@ -223,7 +229,7 @@ async function generateGeneratorAllPairsDataFile({ scheduleClearGenerationStatus, recordLastUsedSchema = () => null, }) { - const result = schemaGenerationService?.generatePairwise?.(); + const result = schemaGenerationService?.generatePairwise?.({ showErrors: true }); if (!result?.ok) { surfaceGenerationResult({ operationKind: 'generatePairwise', @@ -291,10 +297,23 @@ async function generateGeneratorCombinationsDataFile({ }) { const strength = Number.parseInt(selection?.strength, 10); const algorithm = selection?.algorithm; - const combinationInput = schemaGenerationService?.getCombinationInput?.() || { + const combinationInput = schemaGenerationService?.getCombinationInput?.({ showErrors: true }) || { enumColumnCount: 0, enumValueCounts: [], }; + if (Array.isArray(combinationInput.errors) && combinationInput.errors.length > 0) { + surfaceGenerationResult({ + operationKind: 'generateCombinations', + result: { + ok: false, + errors: combinationInput.errors, + rows: combinationInput.rows || [], + }, + surfacePageError, + setGenerationStatus, + }); + return; + } const confirmed = await confirmCartesianProductSelection({ algorithm, @@ -310,7 +329,11 @@ async function generateGeneratorCombinationsDataFile({ return; } - const result = schemaGenerationService?.generateCombinations?.({ strength, algorithm }); + const result = schemaGenerationService?.generateCombinations?.({ + strength, + algorithm, + options: { showErrors: true }, + }); if (!result?.ok) { surfaceGenerationResult({ operationKind: 'generateCombinations', diff --git a/packages/core-ui/js/gui_components/generator/generation/generator-schema-generation-service.js b/packages/core-ui/js/gui_components/generator/generation/generator-schema-generation-service.js index a48abbf6..d64eabf2 100644 --- a/packages/core-ui/js/gui_components/generator/generation/generator-schema-generation-service.js +++ b/packages/core-ui/js/gui_components/generator/generation/generator-schema-generation-service.js @@ -29,8 +29,8 @@ function createGeneratorSchemaGenerationService({ function getValidatedSchemaState(options) { const parsed = options?.schemaState || syncSchemaRowsFromTextMode?.({ - showErrors: false, - applySemanticValidation: false, + showErrors: options?.showErrors === true, + applySemanticValidation: options?.applySemanticValidation === true, }) || { rows: [], errors: [] }; if (parsed.errors?.length > 0) { @@ -96,7 +96,16 @@ function createGeneratorSchemaGenerationService({ }, getCombinationInput(options) { - return generationEngine.getCombinationInput(options); + const schemaState = getValidatedSchemaState(options); + if (schemaState.errors.length > 0) { + return { + enumColumnCount: 0, + enumValueCounts: [], + errors: schemaState.errors, + rows: schemaState.rows || [], + }; + } + return generationEngine.getCombinationInput({ ...options, schemaState }); }, generateRows({ rowCount, options } = {}) { diff --git a/packages/core-ui/js/gui_components/generator/preview/index.js b/packages/core-ui/js/gui_components/generator/preview/index.js index 7dd63178..ff35661b 100644 --- a/packages/core-ui/js/gui_components/generator/preview/index.js +++ b/packages/core-ui/js/gui_components/generator/preview/index.js @@ -26,6 +26,7 @@ function createDefaultPreviewGridFactory({ TabulatorCtor, GridExtensionClass } = columnDefaults: { resizable: true, headerFilter: 'input', + headerFilterParams: { elementAttributes: { 'aria-label': 'Filter preview column' } }, headerFilterFunc: 'like', sorter: 'string', }, diff --git a/packages/core-ui/js/gui_components/generator/runtime/create-generator-page-schema-services.js b/packages/core-ui/js/gui_components/generator/runtime/create-generator-page-schema-services.js index 2d9dde54..3d21d72f 100644 --- a/packages/core-ui/js/gui_components/generator/runtime/create-generator-page-schema-services.js +++ b/packages/core-ui/js/gui_components/generator/runtime/create-generator-page-schema-services.js @@ -1,4 +1,4 @@ -import { getKnownFakerCommandsAlphabetical } from '../../shared/faker-commands.js'; +import { getAllowedFakerCommandsAlphabetical } from '../../shared/faker-commands.js'; import { getKnownDomainCommandsAlphabetical } from '../../shared/domain-commands.js'; import { buildSchemaModeHelpHtml } from '../../shared/test-data/help/schema-mode-help-builder.js'; import { createSchemaEditingSession } from '../../shared/test-data/schema/schema-controller.js'; @@ -22,7 +22,7 @@ function createGeneratorPageSchemaServices({ dataRulesToSchemaText, sampleSchemaText, } = {}) { - const fakerCommands = getKnownFakerCommandsAlphabetical().filter( + const fakerCommands = getAllowedFakerCommandsAlphabetical().filter( (command) => command !== 'RegEx' && command.startsWith('helpers.') ); const domainCommands = getKnownDomainCommandsAlphabetical(); diff --git a/packages/core-ui/js/gui_components/shared/dom/details-disclosure-focus.js b/packages/core-ui/js/gui_components/shared/dom/details-disclosure-focus.js new file mode 100644 index 00000000..03b5d3f3 --- /dev/null +++ b/packages/core-ui/js/gui_components/shared/dom/details-disclosure-focus.js @@ -0,0 +1,33 @@ +function setDetailsContentVisibility({ detailsElement, contentElement } = {}) { + if (!detailsElement || !contentElement) { + return; + } + + if (detailsElement.open) { + contentElement.removeAttribute('aria-hidden'); + contentElement.removeAttribute('inert'); + contentElement.inert = false; + return; + } + + contentElement.setAttribute('aria-hidden', 'true'); + contentElement.setAttribute('inert', ''); + contentElement.inert = true; +} + +function bindDetailsContentVisibility({ detailsElement, contentElement } = {}) { + setDetailsContentVisibility({ detailsElement, contentElement }); + + if (!detailsElement) { + return () => {}; + } + + const handleToggle = () => setDetailsContentVisibility({ detailsElement, contentElement }); + detailsElement.addEventListener('toggle', handleToggle); + + return () => { + detailsElement.removeEventListener('toggle', handleToggle); + }; +} + +export { bindDetailsContentVisibility, setDetailsContentVisibility }; diff --git a/packages/core-ui/js/gui_components/shared/domain-command-help-metadata.js b/packages/core-ui/js/gui_components/shared/domain-command-help-metadata.js index c161f07d..0b1695db 100644 --- a/packages/core-ui/js/gui_components/shared/domain-command-help-metadata.js +++ b/packages/core-ui/js/gui_components/shared/domain-command-help-metadata.js @@ -1,107 +1 @@ -import { getDomainKeywordHelpByAlias } from '@anywaydata/core/domain/domain-keywords.js'; -import { normalizeUsageExamples } from '@anywaydata/core/command-help/command-help-contract.js'; -import { validateEnumMemberValue } from '@anywaydata/core/command-help/command-help-validators.js'; -import { buildDocsUrl } from '@anywaydata/site-config'; - -const ANYWAYDATA_DOMAIN_DOCS_BASE = buildDocsUrl('test-data/domain'); - -const SYNTHETIC_DOMAIN_HELP = Object.freeze({ - 'datatype.enum': { - canonical: 'awd.domain.datatype.enum', - summary: - 'Enum helper accepts a list of values and returns one value at random. Supports enum(value1,value2), enum value1,value2, or datatype.enum(value1,value2).', - docsUrl: `${ANYWAYDATA_DOMAIN_DOCS_BASE}/datatype`, - usageExamples: [ - { - functionCall: 'datatype.enum(active,inactive,pending)', - sampleReturnValue: 'active', - description: 'Shows the canonical datatype enum helper with three discrete values.', - }, - ], - returnType: 'string', - validator: validateEnumMemberValue, - 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', - }, - ], - }, -}); - -function normalizeSyntheticDomainHelp(command, definition) { - const args = Array.isArray(definition?.args) ? definition.args : []; - const usageExamples = normalizeUsageExamples({ - command, - returnType: definition?.returnType, - usageExamples: definition?.usageExamples, - }); - - return { - canonical: definition.canonical, - summary: definition.summary, - docsUrl: definition.docsUrl, - fakerDocsUrl: String(definition.fakerDocsUrl || '').trim(), - usageExamples, - validator: definition?.validator, - returnType: definition.returnType, - args, - }; -} - -function getDomainPageFromCommand(command) { - const value = String(command || '').trim(); - if (!value.includes('.')) { - return ''; - } - const domain = value.split('.')[0]; - if (!domain) { - return ''; - } - return `${ANYWAYDATA_DOMAIN_DOCS_BASE}/${domain}`; -} - -function resolveDomainDocsUrl(command, keywordDocsUrl) { - const explicitDocsUrl = String(keywordDocsUrl || '').trim(); - if ( - explicitDocsUrl.startsWith('https://anywaydata.com/') || - explicitDocsUrl.startsWith('/docs/') || - explicitDocsUrl.startsWith('docs/') - ) { - return explicitDocsUrl; - } - const domainPage = getDomainPageFromCommand(command); - if (domainPage) { - return domainPage; - } - return explicitDocsUrl || `${ANYWAYDATA_DOMAIN_DOCS_BASE}/domain-test-data`; -} - -function getDomainCommandHelp(command) { - const normalizedCommand = String(command || '').trim(); - const synthetic = SYNTHETIC_DOMAIN_HELP[normalizedCommand]; - if (synthetic) { - return normalizeSyntheticDomainHelp(normalizedCommand, synthetic); - } - const commandHelp = getDomainKeywordHelpByAlias(command); - if (!commandHelp) { - return null; - } - - return { - canonical: commandHelp.canonical, - summary: commandHelp.summary || '', - docsUrl: resolveDomainDocsUrl(command, commandHelp.docsUrl || ''), - fakerDocsUrl: String(commandHelp.fakerDocsUrl || '').trim(), - usageExamples: Array.isArray(commandHelp.usageExamples) ? commandHelp.usageExamples : [], - validator: commandHelp.validator, - returnType: commandHelp.returnType || '', - args: Array.isArray(commandHelp.args) ? commandHelp.args : [], - }; -} - -export { getDomainCommandHelp }; +export { getDomainCommandHelp } from '@anywaydata/core/domain/domain-command-metadata.js'; diff --git a/packages/core-ui/js/gui_components/shared/domain-commands.js b/packages/core-ui/js/gui_components/shared/domain-commands.js index 256e1ec6..ed064e6c 100644 --- a/packages/core-ui/js/gui_components/shared/domain-commands.js +++ b/packages/core-ui/js/gui_components/shared/domain-commands.js @@ -1,35 +1,5 @@ -import { DOMAIN_KEYWORD_ALIAS_INDEX } from '@anywaydata/core/domain/domain-keywords.js'; - -const SYNTHETIC_DOMAIN_COMMANDS = Object.freeze(['datatype.enum']); - -function getDomainKeywordEntries() { - const entries = Object.values(DOMAIN_KEYWORD_ALIAS_INDEX.byCanonical || {}); - return entries.sort((a, b) => String(a.keyword || '').localeCompare(String(b.keyword || ''))); -} - -function getKnownDomainCommandsAlphabetical() { - const commands = getDomainKeywordEntries().map((entry) => { - const shortest = String(entry.shortestUniqueAlias || '').trim(); - return shortest || String(entry.keyword || '').trim(); - }); - SYNTHETIC_DOMAIN_COMMANDS.forEach((command) => { - if (!commands.includes(command)) { - commands.push(command); - } - }); - return commands.sort((a, b) => a.localeCompare(b)); -} - -function getKnownDomainCommandsLongestFirst() { - return [...getKnownDomainCommandsAlphabetical()].sort((a, b) => b.length - a.length || a.localeCompare(b)); -} - -function getDomainKeywordByCommand(command) { - const key = String(command || '').trim(); - if (!key) { - return null; - } - return DOMAIN_KEYWORD_ALIAS_INDEX.byAlias[key] || null; -} - -export { getKnownDomainCommandsAlphabetical, getKnownDomainCommandsLongestFirst, getDomainKeywordByCommand }; +export { + getKnownDomainCommandsAlphabetical, + getKnownDomainCommandsLongestFirst, + getDomainKeywordByCommand, +} from '@anywaydata/core/domain/domain-command-metadata.js'; diff --git a/packages/core-ui/js/gui_components/shared/instructions/instructions-view.js b/packages/core-ui/js/gui_components/shared/instructions/instructions-view.js index 8d0f5d6c..8f2e739f 100644 --- a/packages/core-ui/js/gui_components/shared/instructions/instructions-view.js +++ b/packages/core-ui/js/gui_components/shared/instructions/instructions-view.js @@ -59,7 +59,7 @@ class InstructionsView { const className = escapeHtml(action.className || ''); const label = escapeHtml(action.label || ''); const actionId = escapeHtml(action.actionId || ''); - return ``; + return ``; }) .join(''); const helpAttributes = state.helpText ? ` data-help-text="${escapeHtml(state.helpText)}"` : ''; diff --git a/packages/core-ui/js/gui_components/shared/schema-row-rule-mapper.js b/packages/core-ui/js/gui_components/shared/schema-row-rule-mapper.js index 3750acb6..b48406c5 100644 --- a/packages/core-ui/js/gui_components/shared/schema-row-rule-mapper.js +++ b/packages/core-ui/js/gui_components/shared/schema-row-rule-mapper.js @@ -1,3 +1,5 @@ +import { EnumParser } from '@anywaydata/core/data_generation/utils/enumParser.js'; + const SOURCE_TYPE_FAKER = 'faker'; const SOURCE_TYPE_DOMAIN = 'domain'; const SOURCE_TYPE_REGEX = 'regex'; @@ -46,21 +48,8 @@ function normaliseCommandParams(paramsValue, { allowUnwrapped = false } = {}) { return `(${params})`; } -function buildEnumRuleSpec(enumInput) { - const enumValue = String(enumInput ?? '').trim(); - if (enumValue.length === 0) { - return ''; - } - if (/^(enum|datatype\.enum|awd\.datatype\.enum)\s*\(/i.test(enumValue)) { - return `enum(${extractEnumValueFromRuleSpec(enumValue)})`; - } - if (/^enum\s+/i.test(enumValue)) { - return `enum(${enumValue.replace(/^enum\s+/i, '').trim()})`; - } - if (enumValue.startsWith('(') && enumValue.endsWith(')')) { - return `enum${enumValue}`; - } - return `enum(${enumValue})`; +function isDomainEnumCommand(commandValue) { + return /^(?:datatype\.enum|awd\.datatype\.enum)$/i.test(String(commandValue || '').trim()); } function buildRuleSpecFromSchemaRow(row) { @@ -73,10 +62,10 @@ function buildRuleSpecFromSchemaRow(row) { if (sourceType === SOURCE_TYPE_DOMAIN) { const command = normaliseDomainCommand(row?.command); const params = normaliseCommandParams(row?.params, { - allowUnwrapped: command.toLowerCase() === 'datatype.enum', + allowUnwrapped: isDomainEnumCommand(command), }); - if (command.toLowerCase() === 'datatype.enum') { - return buildEnumRuleSpec(params); + if (isDomainEnumCommand(command)) { + return EnumParser.buildSchemaRuleSpecFromInput(params); } return `${command}${params}`; } @@ -103,28 +92,13 @@ function buildRuleSpecFromSchemaRow(row) { return regexValue; } if (sourceType === SOURCE_TYPE_ENUM) { - return buildEnumRuleSpec(row?.value); + return EnumParser.buildSchemaRuleSpecFromInput(row?.value); } return String(row?.value ?? '').trim(); } function extractEnumValueFromRuleSpec(ruleSpec) { - const value = String(ruleSpec ?? '').trim(); - const wrappedMatch = value.match(/^(?:enum|datatype\.enum|awd\.datatype\.enum)\s*\(([\s\S]*)\)$/i); - if (wrappedMatch) { - return wrappedMatch[1].trim(); - } - if (/^enum\s+/i.test(value)) { - const shorthand = value.replace(/^enum\s+/i, '').trim(); - if (shorthand.startsWith('(') && shorthand.endsWith(')') && shorthand.length >= 2) { - return shorthand.slice(1, -1).trim(); - } - return shorthand; - } - if (value.startsWith('(') && value.endsWith(')') && value.length >= 2) { - return value.slice(1, -1).trim(); - } - return value; + return EnumParser.extractEnumDisplayValue(ruleSpec); } function extractLiteralValueFromRuleSpec(ruleSpec) { diff --git a/packages/core-ui/js/gui_components/shared/stored-schemas-manager/stored-schemas-manager-view.js b/packages/core-ui/js/gui_components/shared/stored-schemas-manager/stored-schemas-manager-view.js index b1789c44..305b5a27 100644 --- a/packages/core-ui/js/gui_components/shared/stored-schemas-manager/stored-schemas-manager-view.js +++ b/packages/core-ui/js/gui_components/shared/stored-schemas-manager/stored-schemas-manager-view.js @@ -86,12 +86,12 @@ class StoredSchemasManagerView { Managed Stored Schemas (0)
- + - +
diff --git a/packages/core-ui/js/gui_components/shared/test-data/generation/generation-controller.js b/packages/core-ui/js/gui_components/shared/test-data/generation/generation-controller.js index 22557906..ecbbd915 100644 --- a/packages/core-ui/js/gui_components/shared/test-data/generation/generation-controller.js +++ b/packages/core-ui/js/gui_components/shared/test-data/generation/generation-controller.js @@ -10,6 +10,7 @@ import { createPairwiseTableFromGenerator, createCombinationsTableFromGenerator, } from './generation-runtime.js'; +import { EnumParser } from '@anywaydata/core/data_generation/utils/enumParser.js'; function createGeneratorFromDataRules({ dataRules = [], TestDataGeneratorClass, faker, RandExp }) { const generator = new TestDataGeneratorClass(faker, RandExp); @@ -130,6 +131,9 @@ function createConfiguredGeneratorFromSchemaRows({ .trim() .toLowerCase() === 'datatype.enum'; + const toCanonicalEnumDomainRuleSpec = (row) => + EnumParser.normalizeToCanonicalDomainRuleSpec(buildRuleSpecFromSchemaRow(row)); + const { errors, rows } = validateSchemaRows(schemaRows); if (errors.length > 0) { return { generator: null, errors, rows: [] }; @@ -157,8 +161,10 @@ function createConfiguredGeneratorFromSchemaRows({ return; } if (row.sourceType === SOURCE_TYPE_FAKER || row.sourceType === SOURCE_TYPE_DOMAIN) { - rule.type = isDatatypeEnumDomainRow(row) ? SOURCE_TYPE_ENUM : row.sourceType; - rule.ruleSpec = buildRuleSpecFromSchemaRow(row); + rule.type = isDatatypeEnumDomainRow(row) ? SOURCE_TYPE_DOMAIN : row.sourceType; + rule.ruleSpec = isDatatypeEnumDomainRow(row) + ? toCanonicalEnumDomainRuleSpec(row) + : buildRuleSpecFromSchemaRow(row); return; } if (row.sourceType === SOURCE_TYPE_LITERAL) { @@ -167,8 +173,8 @@ function createConfiguredGeneratorFromSchemaRows({ return; } if (row.sourceType === SOURCE_TYPE_ENUM) { - rule.type = SOURCE_TYPE_ENUM; - rule.ruleSpec = buildRuleSpecFromSchemaRow(row); + rule.type = SOURCE_TYPE_DOMAIN; + rule.ruleSpec = toCanonicalEnumDomainRuleSpec(row); return; } rule.type = SOURCE_TYPE_REGEX; diff --git a/packages/core-ui/js/gui_components/shared/test-data/help/schema-mode-help-builder.js b/packages/core-ui/js/gui_components/shared/test-data/help/schema-mode-help-builder.js index 891156a7..aa64438f 100644 --- a/packages/core-ui/js/gui_components/shared/test-data/help/schema-mode-help-builder.js +++ b/packages/core-ui/js/gui_components/shared/test-data/help/schema-mode-help-builder.js @@ -11,7 +11,7 @@ function buildSchemaModeHelpHtml({ inTextMode, supplementalLinkUrl = '', supplem person.firstName Status -enum(active,inactive,pending) +active,inactive,pending `; } diff --git a/packages/core-ui/js/gui_components/shared/test-data/schema/schema-controller.js b/packages/core-ui/js/gui_components/shared/test-data/schema/schema-controller.js index 28f07e2d..9ede7a3b 100644 --- a/packages/core-ui/js/gui_components/shared/test-data/schema/schema-controller.js +++ b/packages/core-ui/js/gui_components/shared/test-data/schema/schema-controller.js @@ -25,7 +25,7 @@ function parseSchemaTextToRows({ schemaTextToDataRules, schemaText, faker, RandE const parsedConstraints = Array.isArray(parseResult.constraints) ? parseResult.constraints : []; const blockingErrors = parseErrors.filter((error) => !isRecoverableSchemaParseError(error)); if (blockingErrors.length > 0) { - return { rows: [], errors: blockingErrors, tokens, constraints: parsedConstraints }; + return { rows: [], errors: parseErrors, tokens, constraints: parsedConstraints }; } const parsedRows = mapParsedRulesToRows({ @@ -43,7 +43,7 @@ function parseSchemaTextToRows({ schemaTextToDataRules, schemaText, faker, RandE rawRuleSpec: ruleTokens[index]?.rule ?? parseResult.dataRules?.[index]?.ruleSpec ?? '', }) ), - errors: [], + errors: parseErrors, tokens, constraints: parsedConstraints, }; @@ -260,7 +260,7 @@ function createSchemaEditingSession({ } const parsed = parseTextToRows(schemaText); - if (parsed.errors.length > 0 && parsed.rows.length === 0) { + if (parsed.errors.length > 0) { return parsed; } @@ -278,7 +278,7 @@ function createSchemaEditingSession({ function toggleMode({ schemaText, preserveEmptyRows = true } = {}) { if (state.isTextMode) { const parsed = parseTextToRows(schemaText); - if (parsed.errors.length > 0 && parsed.rows.length === 0) { + if (parsed.errors.length > 0) { return { ok: false, errors: parsed.errors, rows: parsed.rows, tokens: parsed.tokens || [] }; } state.rows = parsed.rows.length > 0 || !preserveEmptyRows ? parsed.rows : [createBlankSchemaRow()]; diff --git a/packages/core-ui/js/gui_components/shared/test-data/schema/schema-examples.js b/packages/core-ui/js/gui_components/shared/test-data/schema/schema-examples.js index 062da9a2..60bed2b7 100644 --- a/packages/core-ui/js/gui_components/shared/test-data/schema/schema-examples.js +++ b/packages/core-ui/js/gui_components/shared/test-data/schema/schema-examples.js @@ -26,10 +26,10 @@ Email internet.email Status -enum(active,inactive,pending) +active,inactive,pending Priority -enum(high,medium,low) +high,medium,low Created Date date.recent`; diff --git a/packages/core-ui/js/gui_components/shared/test-data/schema/schema-row-mapper.js b/packages/core-ui/js/gui_components/shared/test-data/schema/schema-row-mapper.js index d597bee6..c5058921 100644 --- a/packages/core-ui/js/gui_components/shared/test-data/schema/schema-row-mapper.js +++ b/packages/core-ui/js/gui_components/shared/test-data/schema/schema-row-mapper.js @@ -17,6 +17,7 @@ import { normaliseDomainCommand, normaliseFakerCommand, } from '../../schema-row-rule-mapper.js'; +import { EnumParser } from '@anywaydata/core/data_generation/utils/enumParser.js'; import { getKnownFakerCommandsLongestFirst } from '../../faker-commands.js'; import { getKnownDomainCommandsLongestFirst, getDomainKeywordByCommand } from '../../domain-commands.js'; import { extractFakerCommandAndParams, extractDomainCommandAndParams } from './command-spec-parser.js'; @@ -38,6 +39,14 @@ function createDefaultSchemaRow() { }; } +function clearRowValidationState(row) { + return { + ...row, + semanticValidationIssues: [], + validation: createSchemaRowValidation(), + }; +} + function looksLikeMethodRuleSpec(ruleSpec) { const spec = String(ruleSpec ?? '').trim(); if (!spec) { @@ -103,7 +112,7 @@ function preservePreviousMethodLikeSourceType({ row, previousRow, rawRuleSpec }) } function applySchemaSourceTypeChange(currentRow, nextSourceType) { - const current = currentRow || {}; + const current = clearRowValidationState(currentRow || {}); const resolvedNextSourceType = String(nextSourceType || '') .trim() .toLowerCase(); @@ -171,6 +180,7 @@ function applySchemaCommandSelection(currentRow, { sourceType, command } = {}) { function mapDataRuleToSchemaRow(rule, { createBlankSchemaRow = createDefaultSchemaRow } = {}) { const row = createBlankSchemaRow(); + const normalizedRuleSpec = String(rule?.ruleSpec || '').trim(); row.name = String(rule?.name ?? ''); row.comments = String(rule?.comments ?? ''); row.leadingTextLines = Array.isArray(rule?.leadingTextLines) @@ -181,6 +191,17 @@ function mapDataRuleToSchemaRow(rule, { createBlankSchemaRow = createDefaultSche .trim() .toLowerCase() || SOURCE_TYPE_REGEX; + if ( + (row.sourceType === SOURCE_TYPE_ENUM || row.sourceType === SOURCE_TYPE_DOMAIN) && + EnumParser.hasEnumInvocationShape(normalizedRuleSpec) + ) { + row.sourceType = SOURCE_TYPE_ENUM; + row.command = ''; + row.params = ''; + row.value = extractEnumValueFromRuleSpec(normalizedRuleSpec); + return row; + } + if (row.sourceType === SOURCE_TYPE_FAKER) { const parts = extractFakerCommandAndParams(rule?.ruleSpec, { normaliseFakerCommand, diff --git a/packages/core-ui/js/gui_components/shared/test-data/schema/schema-row-validation.js b/packages/core-ui/js/gui_components/shared/test-data/schema/schema-row-validation.js index f0ac0198..939c8228 100644 --- a/packages/core-ui/js/gui_components/shared/test-data/schema/schema-row-validation.js +++ b/packages/core-ui/js/gui_components/shared/test-data/schema/schema-row-validation.js @@ -1,6 +1,7 @@ import { SOURCE_TYPE_FAKER, SOURCE_TYPE_DOMAIN, + SOURCE_TYPE_ENUM, SOURCE_TYPE_REGEX, normaliseFakerCommand, normaliseDomainCommand, @@ -110,6 +111,10 @@ function buildSemanticValidationRuleSpec(row, ruleSpec) { const spec = String(ruleSpec ?? ''); const trimmedSpec = spec.trim(); + if (sourceType === SOURCE_TYPE_DOMAIN && normaliseDomainCommand(row?.command).toLowerCase() === 'datatype.enum') { + return trimmedSpec.length > 0 ? trimmedSpec : 'datatype.enum()'; + } + if (sourceType === SOURCE_TYPE_REGEX && trimmedSpec.length > 0) { if (/^(regex|datatype\.regex|awd\.datatype\.regex)\s*\(/i.test(trimmedSpec)) { return trimmedSpec; @@ -252,6 +257,20 @@ function getStaticSchemaRowValidationIssues(row, rowIndex) { } } + if (sourceType === SOURCE_TYPE_ENUM) { + const rawValue = String(row?.value ?? '').trim(); + if (!rawValue) { + issues.push( + createRowValidationIssue({ + rowIndex, + code: 'missing_enum_value', + field: 'value', + message: `Row ${rowIndex + 1}: enum value is required.`, + }) + ); + } + } + return issues; } diff --git a/packages/core-ui/js/gui_components/shared/test-data/schema/schema-runtime.js b/packages/core-ui/js/gui_components/shared/test-data/schema/schema-runtime.js index ba9579d0..90da1c93 100644 --- a/packages/core-ui/js/gui_components/shared/test-data/schema/schema-runtime.js +++ b/packages/core-ui/js/gui_components/shared/test-data/schema/schema-runtime.js @@ -4,6 +4,8 @@ * - Shared counting helpers for generated rule collections. */ +import { EnumParser } from '@anywaydata/core/data_generation/utils/enumParser.js'; + function parseSchemaText({ schemaTextToDataRules, schemaText, faker, RandExp }) { return schemaTextToDataRules({ schemaText: String(schemaText ?? ''), @@ -14,7 +16,7 @@ function parseSchemaText({ schemaTextToDataRules, schemaText, faker, RandExp }) } function countEnumRules(rules = []) { - return (Array.isArray(rules) ? rules : []).filter((rule) => rule?.type === 'enum').length; + return (Array.isArray(rules) ? rules : []).filter((rule) => EnumParser.isEnumLikeRule(rule)).length; } export { parseSchemaText, countEnumRules }; diff --git a/packages/core-ui/js/gui_components/shared/test-data/schema/shared-schema-editor-controller.js b/packages/core-ui/js/gui_components/shared/test-data/schema/shared-schema-editor-controller.js index 4a0e42b1..3661fb0a 100644 --- a/packages/core-ui/js/gui_components/shared/test-data/schema/shared-schema-editor-controller.js +++ b/packages/core-ui/js/gui_components/shared/test-data/schema/shared-schema-editor-controller.js @@ -415,7 +415,7 @@ function createSharedSchemaEditorController({ return { ...createBlankRow(), ...mapped }; }, }); - if (parsed.errors.length > 0 && parsed.rows.length === 0) { + if (parsed.errors.length > 0) { if (showErrors) { setSchemaError(schemaErrorsToText(parsed.errors)); } diff --git a/packages/core-ui/js/gui_components/shared/test-data/schema/shared-schema-editor-ui.js b/packages/core-ui/js/gui_components/shared/test-data/schema/shared-schema-editor-ui.js index 9013dfda..054df237 100644 --- a/packages/core-ui/js/gui_components/shared/test-data/schema/shared-schema-editor-ui.js +++ b/packages/core-ui/js/gui_components/shared/test-data/schema/shared-schema-editor-ui.js @@ -95,8 +95,8 @@ function renderSharedSchemaRows({
- - + + +
` - : `` + : `` } ${ validationMessage diff --git a/packages/core-ui/js/gui_components/shared/test-data/ui/method-picker-modal.js b/packages/core-ui/js/gui_components/shared/test-data/ui/method-picker-modal.js index c5e4721a..98c136f7 100644 --- a/packages/core-ui/js/gui_components/shared/test-data/ui/method-picker-modal.js +++ b/packages/core-ui/js/gui_components/shared/test-data/ui/method-picker-modal.js @@ -298,6 +298,7 @@ function openMethodPickerModal({ windowObj = resolveWindowObj(windowObj, documentObj); ensureCriticalStyles(documentObj); ensureStyles(documentObj); + const previouslyFocusedElement = documentObj.activeElement; const overlay = documentObj.createElement('div'); overlay.className = 'method-picker-overlay'; overlay.setAttribute('data-role', 'method-picker-overlay'); @@ -475,8 +476,19 @@ function openMethodPickerModal({ } return new Promise((resolve) => { + function restorePreviousFocus() { + if ( + previouslyFocusedElement && + previouslyFocusedElement !== documentObj.body && + documentObj.contains?.(previouslyFocusedElement) + ) { + previouslyFocusedElement.focus?.(); + } + } + function close(result) { overlay.remove(); + restorePreviousFocus(); resolve(result || null); } diff --git a/packages/core-ui/js/gui_components/shared/test-data/ui/params-editor-modal.js b/packages/core-ui/js/gui_components/shared/test-data/ui/params-editor-modal.js index 6f3a64e7..9ffe4b74 100644 --- a/packages/core-ui/js/gui_components/shared/test-data/ui/params-editor-modal.js +++ b/packages/core-ui/js/gui_components/shared/test-data/ui/params-editor-modal.js @@ -2,6 +2,14 @@ import { createHelpTooltipService } from '../../../../help/help-tooltips.js'; import { getDefaultDocumentObj, getDefaultWindowObj, resolveWindowObj } from '../../dom/default-objects.js'; const STYLE_ID = 'params-editor-modal-styles-link'; +const FOCUSABLE_SELECTOR = [ + 'a[href]', + 'button:not([disabled])', + 'input:not([disabled])', + 'select:not([disabled])', + 'textarea:not([disabled])', + '[tabindex]:not([tabindex="-1"])', +].join(','); function escapeHtml(text) { return String(text ?? '') @@ -364,7 +372,8 @@ function buildParamsTextFromEditorEntries({ entries = [], validateParams = null } } const formattedValue = formatEditorValue(rawValue, entry.mode || 'auto', entry.type || ''); - formattedValues.push(entry.name && entry.variadic !== true ? `${entry.name}=${formattedValue}` : formattedValue); + const useNamedParam = entry.name && entry.variadic !== true && entry.positionalOnly !== true; + formattedValues.push(useNamedParam ? `${entry.name}=${formattedValue}` : formattedValue); } const paramsText = formattedValues.length > 0 ? `(${formattedValues.join(',')})` : ''; @@ -592,6 +601,57 @@ function renderEntryRows(entries = []) { .join(''); } +function isFocusableElement(element) { + if (!element || typeof element.focus !== 'function') { + return false; + } + if (element.hidden || element.getAttribute?.('aria-hidden') === 'true') { + return false; + } + return true; +} + +function getFocusableElements(rootElement) { + return Array.from(rootElement?.querySelectorAll?.(FOCUSABLE_SELECTOR) || []).filter(isFocusableElement); +} + +function trapTabFocus(event, dialogElement, documentObj) { + if (event.key !== 'Tab') { + return false; + } + + const focusableElements = getFocusableElements(dialogElement); + if (focusableElements.length === 0) { + event.preventDefault(); + dialogElement?.focus?.(); + return true; + } + + const firstElement = focusableElements[0]; + const lastElement = focusableElements[focusableElements.length - 1]; + const activeElement = documentObj?.activeElement; + + if (!dialogElement.contains(activeElement)) { + event.preventDefault(); + (event.shiftKey ? lastElement : firstElement).focus(); + return true; + } + + if (event.shiftKey && activeElement === firstElement) { + event.preventDefault(); + lastElement.focus(); + return true; + } + + if (!event.shiftKey && activeElement === lastElement) { + event.preventDefault(); + firstElement.focus(); + return true; + } + + return false; +} + function openParamsEditorModal({ documentObj = getDefaultDocumentObj(), windowObj = getDefaultWindowObj(), @@ -605,6 +665,7 @@ function openParamsEditorModal({ } windowObj = resolveWindowObj(windowObj, documentObj); ensureStyles(documentObj); + const previouslyFocusedElement = documentObj.activeElement; const parsed = parseInitialParamEntries({ params: helpModel?.params || [], @@ -615,7 +676,7 @@ function openParamsEditorModal({ overlay.setAttribute('data-role', 'params-editor-overlay'); const commandHelpHtml = buildCommandHelpHtml(helpModel, commandLabel); overlay.innerHTML = ` -