Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions .codex-dev-web.pid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
125008
1 change: 1 addition & 0 deletions .codex-dev-web.stderr.log
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
$ vite --config ./apps/web/vite.config.mjs
5 changes: 5 additions & 0 deletions .codex-dev-web.stdout.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

VITE v8.0.16 ready in 284 ms

➜ Local: http://127.0.0.1:4173/
 ➜ press h + enter to show help
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ testenv/
experiments
experiments/*
.codex-logs/
.codex-dev-web.log
1 change: 1 addition & 0 deletions .storybook/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export default {
'@anywaydata/core/utils': path.resolve(__dirname, '../packages/core/js/utils'),
'@anywaydata/core/faker': path.resolve(__dirname, '../packages/core/js/faker'),
'@anywaydata/core/domain': path.resolve(__dirname, '../packages/core/js/domain'),
'@anywaydata/core/command-help': path.resolve(__dirname, '../packages/core/js/command-help'),
'@anywaydata/core/libs': path.resolve(__dirname, '../packages/core/js/libs'),
'@anywaydata/core': path.resolve(__dirname, '../packages/core/src/index.js'),
},
Expand Down
13 changes: 13 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,19 @@ If verification fails:
- `verify:ci` is the full aggregate gate, including coverage, used to mirror the main-branch CI path locally.
- Pull request CI runs parallel independent gates and intentionally skips coverage; coverage runs on `master` push CI.

## Command Help Example Conventions

When changing command help metadata, usage examples, or command-help tests:

- treat `usageExamples` as the source of truth for example maintenance and validation
- do not add or preserve legacy `example`, `examples`, or `exampleReturnValues` fields on keyword definitions or runtime command-help objects
- use domain named-parameter invocation form for all non-`helpers.*` command help examples, e.g. `internet.password(length=12, memorable=true)`
- allow faker-style invocation examples only for `helpers.*` commands
- do not convert domain-backed command help examples into faker object or positional syntax when exposing help metadata
- for domain commands, `docsUrl` must point to the AnyWayData docs page and never to `fakerjs.dev`
- for faker-backed domain commands, expose the upstream Faker reference separately as `fakerDocsUrl`
- validators are called as `(value, context)` and must use `context.fieldDefinition` for rule-specific checks such as enum membership or counterstring length bounds

## Browser Test Interaction Rules

When changing UI code, UI test abstractions, or browser tests:
Expand Down
115 changes: 38 additions & 77 deletions apps/web/src/stories/method-picker-dialog.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,75 +2,28 @@ import React from 'react';
import { Canvas, Controls, Description, Title } from '@storybook/addon-docs/blocks';
import { expect, fn, userEvent, within } from 'storybook/test';
import { openMethodPickerModal } from '../../../../packages/core-ui/js/gui_components/shared/test-data/ui/method-picker-modal.js';
import { buildSchemaHelpModel } from '../../../../packages/core-ui/js/gui_components/shared/test-data/help/help-model-builder.js';

const METHOD_PICKER_RECENT_STORAGE_KEY = 'anywaydata.method-picker.recent';
const METHOD_PICKER_STYLE_ID = 'storybook-method-picker-modal-styles-link';
const CORE_COMMANDS = new Set(['enum', 'literal', 'regex']);

const METHOD_OPTIONS = [
{
sourceType: 'regex',
command: 'regex',
helpModel: {
summary: 'Generate values from a regular expression.',
heading: 'regex',
examples: ['regex("[A-Z]{3}")'],
params: [
{
name: 'pattern',
type: 'string',
description: 'Regular expression source used to build generated values.',
example: '"[A-Z]{3}"',
},
],
exampleReturnValues: ['ABC', 'QWE'],
docsUrl: 'https://anywaydata.com/docs/test-data/regex',
},
},
{
sourceType: 'faker',
command: 'helpers.arrayElement',
helpModel: {
summary: 'Choose one item from the provided array.',
heading: 'helpers.arrayElement',
examples: ['helpers.arrayElement(["active", "paused", "deleted"])'],
params: [
{
name: 'array',
type: 'array',
description: 'List of values to choose from.',
example: '["active", "paused", "deleted"]',
},
],
exampleReturnValues: ['active', 'paused'],
docsUrl: 'https://fakerjs.dev/api/helpers.html#arrayelement',
},
},
{
sourceType: 'domain',
command: 'internet.password',
helpModel: {
summary: 'Generate a password-like string.',
heading: 'internet.password',
examples: ['internet.password()'],
params: [],
exampleReturnValues: ['hS9!wQ2'],
docsUrl: 'https://fakerjs.dev/api/internet.html#password',
},
},
{
sourceType: 'domain',
command: 'commerce.price',
helpModel: {
summary: 'Generate a commerce-style price.',
heading: 'commerce.price',
examples: ['commerce.price()'],
params: [],
exampleReturnValues: ['19.99'],
docsUrl: 'https://fakerjs.dev/api/commerce.html#price',
},
},
];
const METHOD_OPTION_SPECS = Object.freeze([
{ sourceType: 'regex', command: 'regex', helpCommand: '' },
{ sourceType: 'faker', command: 'helpers.arrayElement', helpCommand: 'helpers.arrayElement' },
{ sourceType: 'domain', command: 'internet.password', helpCommand: 'internet.password' },
{ sourceType: 'domain', command: 'commerce.price', helpCommand: 'commerce.price' },
]);

function buildMethodOptions() {
return METHOD_OPTION_SPECS.map(({ sourceType, command, helpCommand }) => ({
sourceType,
command,
helpModel: buildSchemaHelpModel(sourceType, helpCommand),
}));
}

const METHOD_OPTIONS = buildMethodOptions();

function escapeHtml(text) {
return String(text ?? '')
Expand All @@ -91,23 +44,29 @@ function toExampleList(value) {

function getReturnExamples(model) {
const unique = new Set();
const add = (value) => {
toExampleList(value).forEach((entry) => unique.add(entry));
};
add(model?.example);
add(model?.exampleReturnValues);
add(model?.returnExamples);
return [...unique];
const usageExamples = Array.isArray(model?.usageExamples) ? model.usageExamples : [];
usageExamples.forEach((usageExample) => {
if (Object.prototype.hasOwnProperty.call(usageExample || {}, 'sampleReturnValue')) {
unique.add(String(usageExample.sampleReturnValue ?? '').trim());
}
});
toExampleList(model?.returnExamples).forEach((entry) => unique.add(entry));
return [...unique].filter(Boolean);
}

function getUsageFunctionCalls(model) {
return (Array.isArray(model?.usageExamples) ? model.usageExamples : [])
.map((usageExample) => String(usageExample?.functionCall || '').trim())
.filter(Boolean);
}

function buildSearchText(option) {
const params = Array.isArray(option?.helpModel?.params) ? option.helpModel.params.map((p) => p?.name || '') : [];
const usageExamples = toExampleList(option?.helpModel?.examples);
const usageExamples = getUsageFunctionCalls(option?.helpModel);
const returnExamples = getReturnExamples(option?.helpModel);
return [
option.command,
option.helpModel?.summary || '',
option.helpModel?.example || '',
usageExamples.join(' '),
returnExamples.join(' '),
params.join(' '),
Expand Down Expand Up @@ -293,7 +252,7 @@ function createVisualMethodPickerStory(root, args) {
return;
}
const model = selected.helpModel || {};
const usageExamples = toExampleList(model.examples);
const usageExamples = getUsageFunctionCalls(model);
const returnExamples = getReturnExamples(model);
const docsUrl = String(model.docsUrl || '').trim();
const hasParams = Array.isArray(model.params) && model.params.length > 0;
Expand Down Expand Up @@ -536,7 +495,7 @@ export const VisualAlwaysOpen = {
await expect(canvas.getByRole('dialog', { name: 'Choose Method' })).toBeVisible();
await expect(
canvas.getByRole('button', {
name: 'helpers.arrayElement Choose one item from the provided array. faker',
name: 'helpers.arrayElement Returns one random element from the supplied array. faker',
})
).toHaveClass('is-selected');

Expand Down Expand Up @@ -573,7 +532,9 @@ export const ChooseFakerMethod = {
await userEvent.click(canvas.getByRole('button', { name: 'Open method picker' }));
const dialog = within(document.body);
await userEvent.click(
dialog.getByRole('button', { name: 'helpers.arrayElement Choose one item from the provided array. faker' })
dialog.getByRole('button', {
name: 'helpers.arrayElement Returns one random element from the supplied array. faker',
})
);
await userEvent.click(dialog.getByRole('button', { name: 'Apply' }));
await expect(canvas.getByText('faker:helpers.arrayElement')).toBeVisible();
Expand All @@ -600,7 +561,7 @@ export const FilterAndChooseDomainMethod = {
const dialog = within(document.body);
await userEvent.type(dialog.getByRole('searchbox', { name: 'Filter methods' }), 'commerce');
await userEvent.click(
dialog.getByRole('button', { name: 'commerce.price Generate a commerce-style price. domain' })
dialog.getByRole('button', { name: 'commerce.price Generates a price between min and max (inclusive). domain' })
);
await userEvent.click(dialog.getByRole('button', { name: 'Apply' }));
await expect(canvas.getByText('domain:commerce.price')).toBeVisible();
Expand Down
6 changes: 1 addition & 5 deletions apps/web/src/stories/shared-schema-definition.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -523,12 +523,8 @@ export const ParamsDialog = {
await expect(firstHelpIcon).toHaveAttribute('data-help-text', expect.stringContaining('Default: 1'));
await expect(dialogScope.getByRole('textbox', { name: /start value/i }).value).toBe('1');
await expect(dialogScope.getByRole('textbox', { name: /step value/i }).value).toBe('1');
let prefixInput = dialogScope.getByRole('textbox', { name: /prefix value/i });
const prefixInput = dialogScope.getByRole('textbox', { name: /prefix value/i });
await userEvent.click(prefixInput);
await waitFor(() => {
prefixInput = dialogScope.getByRole('textbox', { name: /prefix value/i });
expect(document.activeElement).toBe(prefixInput);
});
await userEvent.clear(prefixInput);
await userEvent.type(prefixInput, 'filename');
await waitFor(() =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ class SchemaEditorComponent {
await this.methodPicker.chooseCommand(requested, { tab: pickerTab });
}

if (assertSchemaTextIncludesType) {
if (assertSchemaTextIncludesType && lower !== 'regex') {
await expect.poll(async () => (await this.getSchemaText()).toLowerCase(), { timeout: 3000 }).toContain(lower);
}
}
Expand Down
4 changes: 3 additions & 1 deletion apps/web/src/writer-schema-page.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,9 @@ function buildWriterDomainCommandGuide(domainCommands = []) {
.slice(0, 6)
.map((command) => {
const help = getDomainCommandHelp(command);
const example = String(help?.example || '').trim();
const example = Array.isArray(help?.usageExamples)
? String(help.usageExamples.find((usageExample) => usageExample?.functionCall)?.functionCall || '').trim()
: '';
const summary = String(help?.summary || '')
.trim()
.replace(/\s+/g, ' ');
Expand Down
51 changes: 51 additions & 0 deletions apps/web/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,34 @@ body {
font-family: Arial, Helvetica, sans-serif;
}

.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}

.skip-link {
position: absolute;
left: 0.75rem;
top: -3rem;
z-index: 5000;
padding: 0.6rem 0.9rem;
border-radius: 0.45rem;
background: #0b6aa2;
color: #ffffff;
text-decoration: none;
}

.skip-link:focus {
top: 0.75rem;
}

a {
color: #0b63ce;
}
Expand Down Expand Up @@ -1865,6 +1893,22 @@ body.theme-dark .generator-status-text[data-severity='info'] {
justify-self: start;
}

.method-picker-usage-example {
padding: 0.55rem 0.7rem;
margin-bottom: 0.65rem;
border: 1px solid var(--border-color);
border-radius: 0.55rem;
background: color-mix(in srgb, var(--panel-bg) 90%, #ebf9f2 10%);
}

.method-picker-usage-example p {
margin: 0 0 0.4rem;
}

.method-picker-usage-example p:last-child {
margin-bottom: 0;
}

.option-help-icon {
margin-right: 0.35rem;
cursor: help;
Expand Down Expand Up @@ -2092,6 +2136,13 @@ body.theme-dark .shared-schema-row-validation {
}
}

.theme-doc-markdown table {
display: block;
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}

.shared-schema-command-picker-button,
.test-data-grid-command-picker-trigger {
width: 100%;
Expand Down
4 changes: 4 additions & 0 deletions apps/web/vite.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ export default defineConfig({
find: /^@anywaydata\/core\/domain\/(.*)$/,
replacement: path.resolve(__dirname, '../../packages/core/js/domain/$1'),
},
{
find: /^@anywaydata\/core\/command-help\/(.*)$/,
replacement: path.resolve(__dirname, '../../packages/core/js/command-help/$1'),
},
{
find: /^@anywaydata\/core\/data_formats\/(.*)$/,
replacement: path.resolve(__dirname, '../../packages/core/js/data_formats/$1'),
Expand Down
Loading
Loading