diff --git a/apps/mcp/src/mcp.test.js b/apps/mcp/src/mcp.test.js index 3a26e4ac..7ae75276 100644 --- a/apps/mcp/src/mcp.test.js +++ b/apps/mcp/src/mcp.test.js @@ -54,6 +54,26 @@ test('MCP server handles generate_data_from_spec tool call', () => { expect(response?.result?.structuredContent?.ok).toBe(true); }); +test('MCP server allows registered domain commands in safe mode', () => { + const response = requestServer({ + jsonrpc: '2.0', + id: 202, + method: 'tools/call', + params: { + name: 'generate_data_from_spec', + arguments: { + textSpec: 't2\nautoIncrement.timestamp(start="12th June 2026 at 4pm", step=60, type="minutes")', + rowCount: 3, + outputFormat: 'json', + }, + }, + }); + const payload = JSON.parse(response?.result?.content?.[0]?.text || '{}'); + expect(payload.ok).toBe(true); + expect(payload.rows).toEqual([['2026-06-12T16:00:00Z'], ['2026-06-12T17:00:00Z'], ['2026-06-12T18:00:00Z']]); + expect(response?.result?.isError).toBe(false); +}); + test('MCP server accepts comments and blank lines in textSpec', () => { const response = requestServer({ jsonrpc: '2.0', diff --git a/docs-src/blog/2026-06-12-release-prep-combinatorial-grid-workflows.md b/docs-src/blog/2026-06-12-release-prep-combinatorial-grid-workflows.md index b72899b1..2efdee86 100644 --- a/docs-src/blog/2026-06-12-release-prep-combinatorial-grid-workflows.md +++ b/docs-src/blog/2026-06-12-release-prep-combinatorial-grid-workflows.md @@ -1,6 +1,6 @@ --- slug: release-prep-combinatorial-grid-workflows -title: "Release Prep: Stronger Combinatorial Generation and Faster Grid Workflows" +title: 'Release Prep: Stronger Combinatorial Generation and Faster Grid Workflows' authors: [alan] tags: [release, feature, combinatorial, schema, import, export, ux] date: 2026-06-12T10:00 @@ -12,7 +12,30 @@ This release adds broader combinatorial generation, schema authoring improvement -## 1. N-wise combinatorial generation, not just pairwise +## 1. Auto-increment timestamps for deterministic event streams + +You can now generate timestamps that move forward one row at a time instead of relying on purely random dates. + +Example: + +```text +CreatedAt +autoIncrement.timestamp(start="2026-06-12T12:39:23Z", step=1, type="seconds") +``` + +That produces: + +- row 1: `2026-06-12T12:39:23Z` +- row 2: `2026-06-12T12:39:24Z` +- row 3: `2026-06-12T12:39:25Z` + +This is useful for audit logs, event streams, ordered API records, and any test data where time should progress predictably across generated rows. + +Docs: + +- [autoIncrement Domain](/docs/test-data/domain/autoIncrement) + +## 2. N-wise combinatorial generation, not just pairwise The biggest addition is that combinatorial generation now goes beyond pairwise. @@ -42,7 +65,7 @@ Docs: ![N-wise combinations dialog](/img/release-198/n-wise-generation.png) -## 2. Schema constraints with PICT-style `IF ... THEN ...` +## 3. Schema constraints with PICT-style `IF ... THEN ...` Schema constraints make generated combinations more realistic by filtering out invalid rows. @@ -71,7 +94,7 @@ Docs: - [Schema Definition](/docs/test-data/Schema-Definition) -## 3. Grid to Enum Schema for turning existing tables into generators +## 4. Grid to Enum Schema for turning existing tables into generators If you already have representative data in the main grid, you can now turn that grid into an enum schema automatically. @@ -103,7 +126,7 @@ Docs: ![Grid to enum schema in the app](/img/release-198/grid-to-enum-schema.png) -## 4. Constraint-aware auto-increment sequences for generated identifiers +## 5. Constraint-aware auto-increment sequences for generated identifiers Schemas can now generate sequential IDs through the domain model with `autoIncrement.sequence`. @@ -128,7 +151,7 @@ Docs: - [Auto Increment Sequences](/docs/test-data/auto-increment-sequences) -## 5. PICT-style inline enum definitions such as `Name: values` +## 6. PICT-style inline enum definitions such as `Name: values` Schema text now fits more naturally with compact PICT-style authoring. @@ -153,7 +176,7 @@ Docs: - [Schema Definition](/docs/test-data/Schema-Definition) -## 6. Import trimming controls for cleaner amend and import workflows +## 7. Import trimming controls for cleaner amend and import workflows Imported files and clipboard data can now be normalized during import. @@ -176,7 +199,7 @@ Docs: ![Import trim settings](/img/release-198/import-trim-settings.png) -## 7. File export settings for line endings and BOM +## 8. File export settings for line endings and BOM Downloads now support file transport settings without changing the preview text shown in the browser. @@ -193,7 +216,7 @@ Docs: ![Download encoding settings](/img/release-198/export-encoding-settings.png) -## 8. Right-click context menu in the main data grid +## 9. Right-click context menu in the main data grid The editable grid now has a right-click context menu for common grid actions. @@ -203,7 +226,7 @@ Docs: - [Data Grid Editable](/docs/test-data/data-grid-editable) -## 9. Always-visible total row counts in the data grid +## 10. Always-visible total row counts in the data grid The main grid now shows total row counts, and filtered views also show how many rows remain visible. 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 96accc52..1ee8296b 100644 --- a/docs-src/docs/040-test-data/018-Schema-Definition.md +++ b/docs-src/docs/040-test-data/018-Schema-Definition.md @@ -91,7 +91,12 @@ Customer Name person.fullName ``` -This generates names such as `Alice Smith`. +```text +CreatedAt +autoIncrement.timestamp(start="2026-06-12T12:39:23Z", step=1, type="seconds") +``` + +The `Customer Name` example generates names such as `Alice Smith`. The `CreatedAt` example generates deterministic timestamps for time-ordered rows. ## Comments and blank lines diff --git a/docs-src/docs/040-test-data/domain/000-domain-test-data.md b/docs-src/docs/040-test-data/domain/000-domain-test-data.md index 03d4b53c..3d4b7c59 100644 --- a/docs-src/docs/040-test-data/domain/000-domain-test-data.md +++ b/docs-src/docs/040-test-data/domain/000-domain-test-data.md @@ -39,6 +39,11 @@ Date date.between(from=1577836800000, to=1659312000000) ``` +```txt +CreatedAt +autoIncrement.timestamp(start="2026-06-12T12:39:23Z", step=1, type="seconds") +``` + ```txt IBAN finance.iban(formatted=true, countryCode="GB") @@ -62,9 +67,9 @@ For faker helper templates and utility functions, use faker helpers: ## Domains +- [autoIncrement](/docs/test-data/domain/autoIncrement) - [airline](/docs/test-data/domain/airline) - [animal](/docs/test-data/domain/animal) -- [autoIncrement](/docs/test-data/domain/autoIncrement) - [book](/docs/test-data/domain/book) - [color](/docs/test-data/domain/color) - [commerce](/docs/test-data/domain/commerce) @@ -89,4 +94,4 @@ For faker helper templates and utility functions, use faker helpers: - [string](/docs/test-data/domain/string) - [system](/docs/test-data/domain/system) - [vehicle](/docs/test-data/domain/vehicle) -- [word](/docs/test-data/domain/word) \ No newline at end of file +- [word](/docs/test-data/domain/word) diff --git a/docs-src/docs/040-test-data/domain/040-autoIncrement.md b/docs-src/docs/040-test-data/domain/040-autoIncrement.md index ece5e8bd..e36a3c0e 100644 --- a/docs-src/docs/040-test-data/domain/040-autoIncrement.md +++ b/docs-src/docs/040-test-data/domain/040-autoIncrement.md @@ -6,7 +6,7 @@ description: "Domain keyword reference for autoIncrement." # autoIncrement Domain -The `autoIncrement` domain provides stateful sequence helpers for accepted generated rows. +The `autoIncrement` domain provides deterministic values that move forward for each generated row. ## Methods @@ -43,3 +43,40 @@ Example return values: - `1` - `15` - `filename001.txt` + +### `autoIncrement.timestamp` + +Generates a timestamp that starts from a fixed point and increments by the configured amount for each generated row. + +- Canonical: `awd.domain.autoIncrement.timestamp` + +| Arg | Type | Required | Description | +| --- | --- | --- | --- | +| `start` | `string\|number` | no | Starting timestamp. Defaults to the generation run start time. Valid examples include `2026-06-12T12:39:23Z`, `20/03/1969`, `12-06-2026 12:39:23`, or a Unix timestamp such as `1718195963000`. | +| `step` | `number` | no | Amount added for each generated row. Defaults to `1`. | +| `type` | `string` | no | Unit applied to `step` for each row. Supports `milliseconds`, `seconds`, `minutes`, `hours`, `days`, `weeks`, `months`, or `years`. Defaults to `seconds`. | +| `outputFormat` | `string` | no | Output format. Defaults to ISO-8601 without milliseconds. Use `iso8601` for the default behaviour or a custom pattern such as `yyyy-MM-dd HH:mm:ss`. | +| `inputFormat` | `string` | no | Optional parse pattern used only for `start` when you want to match a specific text shape such as `dd/MM/yyyy` or `dd-MM-yyyy HH:mm:ss`. | + +Examples: + +```txt +autoIncrement.timestamp() +``` + +```txt +autoIncrement.timestamp(start="2026-06-12T12:39:23Z", step=1, type="seconds") +``` + +```txt +autoIncrement.timestamp(start="20/03/1969", step=1, type="days", outputFormat="yyyy-MM-dd") +``` + +```txt +autoIncrement.timestamp(start="12-06-2026 12:39:23", step=15, type="minutes", outputFormat="yyyy-MM-dd HH:mm:ss", inputFormat="dd-MM-yyyy HH:mm:ss") +``` + +Example return values: +- `2026-06-12T12:39:23Z` +- `2026-06-12T12:39:24Z` +- `2026-06-12T12:39:25Z` diff --git a/package.json b/package.json index a628440e..5ba7fcc4 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "test:browser": "node ./node_modules/@playwright/test/cli.js test", "test:browser:headed": "node ./node_modules/@playwright/test/cli.js test --headed", "test:browser:install": "node ./node_modules/@playwright/test/cli.js install chromium", - "test:api": "node ./node_modules/@playwright/test/cli.js test --config=playwright-api.config.js", + "test:api": "node ./scripts/run-playwright-api-tests.mjs", "test:api:headed": "node ./node_modules/@playwright/test/cli.js test --config=playwright-api.config.js --headed", "test:api:verbose": "node ./node_modules/@playwright/test/cli.js test --config=playwright-api.config.js --reporter=verbose", "lint": "eslint \"apps/**/*.js\" \"packages/**/*.js\"", @@ -103,6 +103,9 @@ "@anywaydata/core": "workspace:^", "@mcp-b/webmcp-polyfill": "3.0.0", "@popperjs/core": "2.11.8", + "chrono-node": "^2.9.1", + "date-fns": "^4.4.0", + "date-fns-tz": "^3.2.0", "papaparse": "5.5.3", "randexp": "0.5.3", "tippy.js": "6.3.7" diff --git a/packages/core-ui/src/tests/grid/schema/test-data-command-catalog.test.js b/packages/core-ui/src/tests/grid/schema/test-data-command-catalog.test.js index a850eb69..88e2b697 100644 --- a/packages/core-ui/src/tests/grid/schema/test-data-command-catalog.test.js +++ b/packages/core-ui/src/tests/grid/schema/test-data-command-catalog.test.js @@ -71,6 +71,7 @@ describe('Test Data Command Catalog', () => { it('should also populate domain commands for the domain dropdown section', () => { const domainCommands = getDomainCommands(); expect(domainCommands.length).toBeGreaterThan(0); + expect(domainCommands).toContain('autoIncrement.timestamp'); expect(domainCommands).toContain('number.int'); expect(domainCommands).toContain('string.counterString'); expect(domainCommands.some((command) => command.startsWith('helpers.'))).toBe(false); @@ -78,8 +79,10 @@ describe('Test Data Command Catalog', () => { it('should provide picker options with schema help metadata', () => { const values = getMethodPickerOptions(''); + const autoIncrementEntry = values.find((entry) => entry.command === 'autoIncrement.timestamp'); const domainEntry = values.find((entry) => entry.command === 'number.int'); const fakerEntry = values.find((entry) => entry.command === 'helpers.arrayElement'); + expect(autoIncrementEntry?.sourceType).toBe('domain'); expect(domainEntry?.sourceType).toBe('domain'); expect(fakerEntry?.sourceType).toBe('faker'); expect(Array.isArray(domainEntry?.helpModel?.params)).toBe(true); diff --git a/packages/core-ui/src/tests/shared/help-model-builder.test.js b/packages/core-ui/src/tests/shared/help-model-builder.test.js index 5bb55f8d..d2d16599 100644 --- a/packages/core-ui/src/tests/shared/help-model-builder.test.js +++ b/packages/core-ui/src/tests/shared/help-model-builder.test.js @@ -66,6 +66,15 @@ describe('help-model-builder', () => { expect(renderSchemaHelpHtml(model)).toContain('delimiter'); }); + test('builds domain help for auto-increment timestamps with step metadata', () => { + const model = buildSchemaHelpModel('domain', 'autoIncrement.timestamp'); + + expect(model.show).toBe(true); + expect(model.heading).toContain('autoIncrement.timestamp'); + expect(renderSchemaHelpHtml(model)).toContain('run start time'); + expect(renderSchemaHelpHtml(model)).toContain('outputFormat'); + }); + test('preserves custom domain docs links and parameters for auto increment help', () => { const model = buildSchemaHelpModel('domain', 'autoIncrement.sequence'); diff --git a/packages/core-ui/src/tests/utils/domain-command-help-metadata.test.js b/packages/core-ui/src/tests/utils/domain-command-help-metadata.test.js index df1ef8a5..b49eef2e 100644 --- a/packages/core-ui/src/tests/utils/domain-command-help-metadata.test.js +++ b/packages/core-ui/src/tests/utils/domain-command-help-metadata.test.js @@ -13,6 +13,12 @@ describe('domain command help metadata docs links', () => { expect(enumHelp.docsUrl).toBe('https://anywaydata.com/docs/test-data/domain/datatype'); }); + test('maps autoIncrement timestamp help to the autoIncrement domain page', () => { + const autoIncrementHelp = getDomainCommandHelp('autoIncrement.timestamp'); + expect(autoIncrementHelp).toBeTruthy(); + expect(autoIncrementHelp.docsUrl).toBe('https://anywaydata.com/docs/test-data/domain/autoIncrement'); + }); + test('keeps explicit anywaydata docs pages for custom domain commands', () => { const sequenceHelp = getDomainCommandHelp('autoIncrement.sequence'); expect(sequenceHelp).toBeTruthy(); diff --git a/packages/core/js/data_generation/domain/domainTestDataGenerator.js b/packages/core/js/data_generation/domain/domainTestDataGenerator.js index df166033..cf63ddbf 100644 --- a/packages/core/js/data_generation/domain/domainTestDataGenerator.js +++ b/packages/core/js/data_generation/domain/domainTestDataGenerator.js @@ -10,7 +10,7 @@ class DomainTestDataGenerator { this.nextAutoIncrementRuleId = 1; } - generateFrom(aRule) { + generateFrom(aRule, executionContext = {}) { const ruleSpec = String(aRule?.ruleSpec || '').trim(); const parsed = parseKeywordInvocation(ruleSpec); if (Array.isArray(parsed?.errors) && parsed.errors.length > 0) { @@ -19,6 +19,7 @@ class DomainTestDataGenerator { try { const result = executeDomainKeyword(parsed.keyword, { + ...executionContext, faker: this.faker, args: Array.isArray(parsed.args) ? parsed.args : [], autoIncrementState: this.#getAutoIncrementStateForRule(aRule), diff --git a/packages/core/js/data_generation/n-wise/pairwiseTestDataGenerator.js b/packages/core/js/data_generation/n-wise/pairwiseTestDataGenerator.js index b33d1787..d78cf7d7 100644 --- a/packages/core/js/data_generation/n-wise/pairwiseTestDataGenerator.js +++ b/packages/core/js/data_generation/n-wise/pairwiseTestDataGenerator.js @@ -202,7 +202,8 @@ export class PairwiseTestDataGenerator { * for non-enum rules */ generateCompleteDataRecords(enumCombinations) { - return enumCombinations.map((enumRecord) => { + const runStartedAt = new Date(); + return enumCombinations.map((enumRecord, rowIndex) => { const completeRecord = {}; // Preserve original schema order across enum and non-enum columns. @@ -210,7 +211,7 @@ export class PairwiseTestDataGenerator { if (this.isRuleType(rule, 'enum')) { completeRecord[rule.name] = enumRecord.get(rule.name); } else { - completeRecord[rule.name] = this.generateRandomValueForRule(rule); + completeRecord[rule.name] = this.generateRandomValueForRule(rule, { rowIndex, runStartedAt }); } } @@ -221,7 +222,7 @@ export class PairwiseTestDataGenerator { /** * Generate a random value for a non-enum rule */ - generateRandomValueForRule(rule) { + generateRandomValueForRule(rule, generationContext = {}) { switch (rule.type) { case 'literal': return rule.ruleSpec; @@ -236,7 +237,7 @@ export class PairwiseTestDataGenerator { return this.generateRegexValue(rule); case 'domain': - return this.generateDomainValue(rule); + return this.generateDomainValue(rule, generationContext); default: return `random_${rule.name}_${Math.floor(Math.random() * 1000)}`; @@ -302,10 +303,10 @@ export class PairwiseTestDataGenerator { /** * Generate a value using domain keywords */ - generateDomainValue(rule) { + generateDomainValue(rule, generationContext = {}) { try { if (this.domainGenerator) { - const result = this.domainGenerator.generateFrom(rule); + const result = this.domainGenerator.generateFrom(rule, generationContext); return result.data ?? `domain_${rule.ruleSpec}_${Math.floor(Math.random() * 1000)}`; } return `domain_${rule.ruleSpec}_${Math.floor(Math.random() * 1000)}`; diff --git a/packages/core/js/data_generation/rulesBasedDataGenerator.js b/packages/core/js/data_generation/rulesBasedDataGenerator.js index 6e635fa8..9b50931d 100644 --- a/packages/core/js/data_generation/rulesBasedDataGenerator.js +++ b/packages/core/js/data_generation/rulesBasedDataGenerator.js @@ -29,15 +29,16 @@ export class RulesBasedDataGenerator { const headers = fromRules.map((rule) => rule.name); data.push(headers); + const runStartedAt = new Date(); for (var row = 0; row < thisMany; row++) { - const aRow = this.generateRandomRow(fromRules, options); + const aRow = this.generateRandomRow(fromRules, { ...options, rowIndex: row, runStartedAt }); data.push(aRow); } return data; } - generateRandomRow(fromRules, { constraints = [], maxAttempts = 1 } = {}) { + generateRandomRow(fromRules, { constraints = [], maxAttempts = 1, rowIndex = 0, runStartedAt = new Date() } = {}) { const rules = Array.isArray(fromRules) ? fromRules : []; const safeConstraints = Array.isArray(constraints) ? constraints : []; const committedGeneratorState = this.captureGeneratorStates(); @@ -73,7 +74,7 @@ export class RulesBasedDataGenerator { generator = this.defaultGenerator; } - value = generator.generateFrom(rule); + value = generator.generateFrom(rule, { rowIndex, runStartedAt }); if (value.isError) { dataGen = '**ERROR**'; diff --git a/packages/core/js/data_generation/testDataGenerator.js b/packages/core/js/data_generation/testDataGenerator.js index 72f9a3da..754d189b 100644 --- a/packages/core/js/data_generation/testDataGenerator.js +++ b/packages/core/js/data_generation/testDataGenerator.js @@ -79,9 +79,12 @@ export class TestDataGenerator { generateRow() { this.runtimeErrors = []; + const runStartedAt = new Date(); const generatedRow = this.generator.generateRandomRow(this.rulesParser.testDataRules.rules, { constraints: this.rulesParser.testDataRules.constraints, maxAttempts: 1000, + rowIndex: 0, + runStartedAt, }); if (!generatedRow) { this.runtimeErrors = [SchemaParsingErrors.constraintGenerationFailed()]; diff --git a/packages/core/js/domain/auto-increment-timestamp.js b/packages/core/js/domain/auto-increment-timestamp.js new file mode 100644 index 00000000..c62b9ca2 --- /dev/null +++ b/packages/core/js/domain/auto-increment-timestamp.js @@ -0,0 +1,195 @@ +import { parseDate as parseNaturalDate } from 'chrono-node'; +import { + addDays, + addHours, + addMilliseconds, + addMinutes, + addMonths, + addSeconds, + addWeeks, + addYears, + isValid, + parse, + parseISO, +} from 'date-fns'; +import { formatInTimeZone } from 'date-fns-tz'; + +const DEFAULT_OUTPUT_FORMAT = 'iso8601'; +const DEFAULT_STEP_TYPE = 'seconds'; +const DEFAULT_STEP_VALUE = 1; +const ISO_OUTPUT_ALIASES = new Set(['iso', 'iso8601', 'iso-8601']); +const STEP_TYPE_NORMALISERS = Object.freeze({ + millisecond: 'milliseconds', + milliseconds: 'milliseconds', + second: 'seconds', + seconds: 'seconds', + minute: 'minutes', + minutes: 'minutes', + hour: 'hours', + hours: 'hours', + day: 'days', + days: 'days', + week: 'weeks', + weeks: 'weeks', + month: 'months', + months: 'months', + year: 'years', + years: 'years', +}); + +function cloneDate(value) { + return new Date(value.getTime()); +} + +function toUtcDatePreservingClock(dateValue) { + return new Date( + Date.UTC( + dateValue.getFullYear(), + dateValue.getMonth(), + dateValue.getDate(), + dateValue.getHours(), + dateValue.getMinutes(), + dateValue.getSeconds(), + dateValue.getMilliseconds() + ) + ); +} + +function isIsoLikeDateString(value) { + return /^\d{4}-\d{2}-\d{2}(?:[T\s].*)?$/.test(String(value || '').trim()); +} + +function ensureValidDate(dateValue, description) { + if (!isValid(dateValue)) { + throw new Error(`Invalid ${description}.`); + } + return dateValue; +} + +function normaliseStepType(stepType) { + const raw = String(stepType || DEFAULT_STEP_TYPE) + .trim() + .toLowerCase(); + const normalised = STEP_TYPE_NORMALISERS[raw]; + if (!normalised) { + throw new Error( + 'Invalid argument for type: expected milliseconds, seconds, minutes, hours, days, weeks, months, or years.' + ); + } + return normalised; +} + +function getNumericArg(value, argName) { + if (typeof value !== 'number' || !Number.isFinite(value)) { + throw new Error(`Invalid argument for ${argName}: expected a number.`); + } + return value; +} + +function resolveNow(executionContext = {}) { + if (executionContext.runStartedAt instanceof Date) { + return cloneDate(executionContext.runStartedAt); + } + if (typeof executionContext.nowProvider === 'function') { + const provided = executionContext.nowProvider(); + if (provided instanceof Date) { + return cloneDate(provided); + } + } + return new Date(); +} + +function resolveStartDate(startValue, inputFormat, fallbackNow) { + if (startValue instanceof Date) { + return ensureValidDate(cloneDate(startValue), 'start date'); + } + + if (typeof startValue === 'number') { + return ensureValidDate(new Date(startValue), 'start date'); + } + + if (typeof startValue === 'undefined') { + return cloneDate(fallbackNow); + } + + const startText = String(startValue || '').trim(); + if (!startText) { + return cloneDate(fallbackNow); + } + + if (typeof inputFormat === 'string' && inputFormat.trim().length > 0) { + const parsedWithFormat = ensureValidDate(parse(startText, inputFormat, fallbackNow), 'start date'); + return toUtcDatePreservingClock(parsedWithFormat); + } + + if (isIsoLikeDateString(startText)) { + return ensureValidDate(parseISO(startText), 'start date'); + } + + const naturalDate = parseNaturalDate(startText, fallbackNow, { forwardDate: false }); + if (naturalDate instanceof Date) { + return toUtcDatePreservingClock(ensureValidDate(naturalDate, 'start date')); + } + + return ensureValidDate(new Date(startText), 'start date'); +} + +function addStep(dateValue, amount, stepType) { + switch (stepType) { + case 'milliseconds': + return addMilliseconds(dateValue, amount); + case 'seconds': + return addSeconds(dateValue, amount); + case 'minutes': + return addMinutes(dateValue, amount); + case 'hours': + return addHours(dateValue, amount); + case 'days': + return addDays(dateValue, amount); + case 'weeks': + return addWeeks(dateValue, amount); + case 'months': + return addMonths(dateValue, amount); + case 'years': + return addYears(dateValue, amount); + default: + throw new Error(`Unsupported step type: ${stepType}`); + } +} + +function formatTimestamp(dateValue, outputFormat) { + const formatValue = String(outputFormat || DEFAULT_OUTPUT_FORMAT) + .trim() + .toLowerCase(); + if (!formatValue || ISO_OUTPUT_ALIASES.has(formatValue)) { + return dateValue.toISOString().replace(/\.\d{3}Z$/, 'Z'); + } + return formatInTimeZone(dateValue, 'UTC', outputFormat); +} + +function executeCustomAutoIncrementTimestamp(executionContext = {}) { + const args = Array.isArray(executionContext.args) ? executionContext.args : []; + const startArg = args[0]; + const stepArg = args[1]; + const typeArg = args[2]; + const outputFormatArg = args[3]; + const inputFormatArg = args[4]; + const state = executionContext.autoIncrementState || null; + const rowIndex = + Number.isInteger(executionContext.rowIndex) && executionContext.rowIndex >= 0 ? executionContext.rowIndex : 0; + const now = resolveNow(executionContext); + const startDate = resolveStartDate(startArg, inputFormatArg, now); + const stepValue = typeof stepArg === 'undefined' ? DEFAULT_STEP_VALUE : getNumericArg(stepArg, 'step'); + const stepType = normaliseStepType(typeArg); + + if (state && typeof state === 'object') { + const currentDate = state.nextDate instanceof Date ? cloneDate(state.nextDate) : startDate; + state.nextDate = addStep(cloneDate(currentDate), stepValue, stepType); + return formatTimestamp(currentDate, outputFormatArg); + } + + const offsetDate = rowIndex === 0 ? startDate : addStep(startDate, stepValue * rowIndex, stepType); + return formatTimestamp(offsetDate, outputFormatArg); +} + +export { executeCustomAutoIncrementTimestamp }; diff --git a/packages/core/js/domain/domain-custom-autoincrement-keyword-definitions.js b/packages/core/js/domain/domain-custom-autoincrement-keyword-definitions.js index 1aa3df5d..709347ac 100644 --- a/packages/core/js/domain/domain-custom-autoincrement-keyword-definitions.js +++ b/packages/core/js/domain/domain-custom-autoincrement-keyword-definitions.js @@ -1,4 +1,65 @@ const DOMAIN_CUSTOM_AUTOINCREMENT_KEYWORD_DEFINITIONS = [ + { + keyword: 'autoIncrement.timestamp', + delegate: { + type: 'custom', + target: 'autoIncrement.timestamp', + }, + help: { + summary: + 'Generates a timestamp that starts from a fixed point and increments by the configured amount for each generated row.', + docsUrl: 'https://anywaydata.com/docs/test-data/domain/autoIncrement', + example: 'autoIncrement.timestamp(start="2026-06-12T12:39:23Z", step=1, type="seconds")', + examples: [ + 'autoIncrement.timestamp()', + 'autoIncrement.timestamp(start="20/03/1969", step=1, type="days")', + 'autoIncrement.timestamp(start="2026-06-12 12:39:23", step=15, type="minutes", outputFormat="yyyy-MM-dd HH:mm:ss")', + ], + exampleReturnValues: ['2026-06-12T12:39:23Z', '2026-06-12T12:39:24Z', '2026-06-12T12:39:25Z'], + returnType: 'string', + args: [ + { + name: 'start', + type: 'string|number', + required: false, + description: + 'Starting timestamp. Defaults to the generation run start time. Valid examples include "2026-06-12T12:39:23Z", "20/03/1969", "12-06-2026 12:39:23", or a Unix timestamp like 1718195963000.', + examples: ['2026-06-12T12:39:23Z', '20/03/1969', 1718195963000], + }, + { + name: 'step', + type: 'number', + required: false, + description: 'Amount added for each generated row. Defaults to 1.', + examples: [1, 15], + }, + { + name: 'type', + type: 'string', + required: false, + description: + 'Unit applied to step for each row. Supports milliseconds, seconds, minutes, hours, days, weeks, months, or years. Defaults to seconds.', + examples: ['seconds', 'days'], + }, + { + name: 'outputFormat', + type: 'string', + required: false, + description: + 'Output format. Defaults to ISO-8601 without milliseconds. Use "iso8601" for the default behaviour or a custom pattern such as "yyyy-MM-dd HH:mm:ss".', + examples: ['iso8601', "yyyy-MM-dd'T'HH:mm:ssXXX"], + }, + { + name: 'inputFormat', + type: 'string', + required: false, + description: + 'Optional parse pattern used only for the start argument when you want to match a specific text shape such as "dd/MM/yyyy" or "dd-MM-yyyy HH:mm:ss".', + examples: ['dd/MM/yyyy', 'yyyy-MM-dd HH:mm:ss'], + }, + ], + }, + }, { keyword: 'autoIncrement.sequence', delegate: { diff --git a/packages/core/js/domain/domain-keywords.js b/packages/core/js/domain/domain-keywords.js index 47c9eeb1..c4ef90c9 100644 --- a/packages/core/js/domain/domain-keywords.js +++ b/packages/core/js/domain/domain-keywords.js @@ -1,4 +1,5 @@ import { DOMAIN_KEYWORD_DEFINITIONS } from './domain-keyword-definitions.js'; +import { executeCustomAutoIncrementTimestamp } from './auto-increment-timestamp.js'; import { executeCustomAutoIncrementSequence } from './auto-increment-sequence.js'; import { executeCustomCounterString } from './counterstring.js'; @@ -224,6 +225,7 @@ function applyFakerArgTransform(keyword, args = []) { } const BUILT_IN_CUSTOM_DELEGATES = { + 'autoIncrement.timestamp': executeCustomAutoIncrementTimestamp, 'autoIncrement.sequence': executeCustomAutoIncrementSequence, 'literal.value': (executionContext = {}) => { const args = Array.isArray(executionContext.args) ? executionContext.args : []; diff --git a/packages/core/package.json b/packages/core/package.json index 2426aa97..75949d95 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -28,6 +28,9 @@ }, "dependencies": { "@faker-js/faker": "9.7.0", + "chrono-node": "^2.9.1", + "date-fns": "^4.4.0", + "date-fns-tz": "^3.2.0", "papaparse": "5.5.3", "randexp": "0.5.3" } diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7547f98d..68b27888 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -14,6 +14,7 @@ import { import { looksLikeInlineRuleSpec, startsConstraint } from '../js/data_generation/inline-schema-rule.js'; import { parseSchemaText } from '../js/data_generation/schema-conversion.js'; import { hasSafeFakerLiteralArguments } from '../js/data_generation/faker/safeLiteralArgumentParser.js'; +import { getDomainKeywordByAlias } from '../js/domain/domain-keywords.js'; import { OPTION_KEYS_BY_FORMAT, OPTION_TIPS_BY_FORMAT, @@ -153,6 +154,10 @@ export function validateSafeFakerRules(textSpec) { } const fakerCommand = getFakerCommandFromRule(ruleLine); + if (getDomainKeywordByAlias(fakerCommand)) { + continue; + } + if (!knownCommandSet.has(fakerCommand)) { return { ok: false, diff --git a/packages/core/src/tests/core-api/generateFromTextSpec.test.js b/packages/core/src/tests/core-api/generateFromTextSpec.test.js index 6135f5f6..d5733fd4 100644 --- a/packages/core/src/tests/core-api/generateFromTextSpec.test.js +++ b/packages/core/src/tests/core-api/generateFromTextSpec.test.js @@ -278,6 +278,13 @@ test('validateSafeFakerRules accepts pict-style inline faker commands', () => { expect(result.ok).toBe(true); }); +test('validateSafeFakerRules accepts registered domain keywords', () => { + const result = validateSafeFakerRules( + 'CreatedAt\nautoIncrement.timestamp(start="12th June 2026 at 4pm", step=60, type="minutes")' + ); + expect(result.ok).toBe(true); +}); + test('validateSafeFakerRules accepts js-style object literal faker args', () => { const result = validateSafeFakerRules('Template\nhelpers.mustache("{{name}}", { name: "Ada" })'); expect(result.ok).toBe(true); diff --git a/packages/core/src/tests/data_generation/autoincrement-timestamp-integration.test.js b/packages/core/src/tests/data_generation/autoincrement-timestamp-integration.test.js new file mode 100644 index 00000000..1fb13491 --- /dev/null +++ b/packages/core/src/tests/data_generation/autoincrement-timestamp-integration.test.js @@ -0,0 +1,49 @@ +import { faker } from '@faker-js/faker'; +import RandExp from 'randexp'; +import { TestDataGenerator } from '../../../js/data_generation/testDataGenerator.js'; +import { PairwiseTestDataGenerator } from '../../../js/data_generation/n-wise/pairwiseTestDataGenerator.js'; +import { TestDataRule } from '../../../js/data_generation/testDataRule.js'; + +describe('autoIncrement.timestamp integration', () => { + test('increments across generated rows in normal rules-based generation', () => { + const generator = new TestDataGenerator(faker, RandExp); + generator.importSpec('CreatedAt\nautoIncrement.timestamp(start="2026-06-12T12:39:23Z", step=1, type="seconds")'); + generator.compile(); + + expect(generator.isValid()).toBe(true); + + const rows = generator.generate(3); + expect(rows).toEqual([['CreatedAt'], ['2026-06-12T12:39:23Z'], ['2026-06-12T12:39:24Z'], ['2026-06-12T12:39:25Z']]); + }); + + test('increments across generated rows in pairwise output alongside enum columns', () => { + const rules = [ + new TestDataRule('Browser', 'enum("Chrome","Firefox")'), + new TestDataRule('Theme', 'enum("Light","Dark")'), + new TestDataRule( + 'CreatedAt', + 'autoIncrement.timestamp(start="2026-06-12T12:39:23Z", step=5, type="minutes", outputFormat="yyyy-MM-dd HH:mm:ss")' + ), + ]; + + rules[0].type = 'enum'; + rules[1].type = 'enum'; + rules[2].type = 'domain'; + + const generator = new PairwiseTestDataGenerator(faker); + const initResult = generator.initializeFromRules(rules); + expect(initResult.isError).toBe(false); + + const result = generator.generateAllDataRecordsAsRows(); + expect(result.isError).toBe(false); + + const rows = result.data.data; + expect(rows[0]).toEqual(['Browser', 'Theme', 'CreatedAt']); + expect(rows.slice(1).map((row) => row[2])).toEqual([ + '2026-06-12 12:39:23', + '2026-06-12 12:44:23', + '2026-06-12 12:49:23', + '2026-06-12 12:54:23', + ]); + }); +}); diff --git a/packages/core/src/tests/data_generation/generateDataFromMultiLineSpec.test.js b/packages/core/src/tests/data_generation/generateDataFromMultiLineSpec.test.js index 382b3c7c..03c939dd 100644 --- a/packages/core/src/tests/data_generation/generateDataFromMultiLineSpec.test.js +++ b/packages/core/src/tests/data_generation/generateDataFromMultiLineSpec.test.js @@ -176,6 +176,17 @@ describe('TestDataGenerator meets Acceptance Criteria', () => { expect(generator.generateRow()).toEqual([10]); }); + + test('increments auto increment timestamp when rows are generated one at a time', () => { + const generator = new TestDataGenerator(faker, RandExp); + + generator.importSpec('CreatedAt\nautoIncrement.timestamp(start="2026-06-12T16:00:00Z",step=1,type="hours")'); + generator.compile(); + + expect(generator.generateRow()).toEqual(['2026-06-12T16:00:00Z']); + expect(generator.generateRow()).toEqual(['2026-06-12T17:00:00Z']); + expect(generator.generateRow()).toEqual(['2026-06-12T18:00:00Z']); + }); }); describe('TestDataGenerator parsing detects errors in spec', () => { diff --git a/packages/core/src/tests/data_generation/unit/domain/domain-autoincrement-timestamp-exec.test.js b/packages/core/src/tests/data_generation/unit/domain/domain-autoincrement-timestamp-exec.test.js new file mode 100644 index 00000000..ad0e6dbf --- /dev/null +++ b/packages/core/src/tests/data_generation/unit/domain/domain-autoincrement-timestamp-exec.test.js @@ -0,0 +1,82 @@ +import { executeDomainKeyword } from '../../../../../js/domain/domain-keywords.js'; +import { parseKeywordInvocation } from '../../../../../js/domain/domain-keyword-parser.js'; + +describe('autoIncrement.timestamp domain keyword execution', () => { + test('uses the generation run start time by default and increments by seconds per row', () => { + const runStartedAt = new Date('2026-06-12T12:39:23.000Z'); + + expect(executeDomainKeyword('autoIncrement.timestamp', { args: [], rowIndex: 0, runStartedAt })).toBe( + '2026-06-12T12:39:23Z' + ); + expect(executeDomainKeyword('autoIncrement.timestamp', { args: [], rowIndex: 1, runStartedAt })).toBe( + '2026-06-12T12:39:24Z' + ); + expect(executeDomainKeyword('autoIncrement.timestamp', { args: [], rowIndex: 2, runStartedAt })).toBe( + '2026-06-12T12:39:25Z' + ); + }); + + test('supports explicit ISO timestamps and day increments', () => { + const args = ['2026-06-12T12:39:23Z', 2, 'days']; + + expect(executeDomainKeyword('autoIncrement.timestamp', { args, rowIndex: 0 })).toBe('2026-06-12T12:39:23Z'); + expect(executeDomainKeyword('autoIncrement.timestamp', { args, rowIndex: 1 })).toBe('2026-06-14T12:39:23Z'); + expect(executeDomainKeyword('autoIncrement.timestamp', { args, rowIndex: 2 })).toBe('2026-06-16T12:39:23Z'); + }); + + test('supports permissive non-ISO parsing with custom output formatting', () => { + const parsed = parseKeywordInvocation( + 'autoIncrement.timestamp(start="20/03/1969", step=1, type="days", outputFormat="yyyy-MM-dd")' + ); + + expect(parsed.errors).toEqual([]); + expect(executeDomainKeyword(parsed.keyword, { args: parsed.args, rowIndex: 0 })).toBe('1969-03-20'); + expect(executeDomainKeyword(parsed.keyword, { args: parsed.args, rowIndex: 3 })).toBe('1969-03-23'); + }); + + test('supports inputFormat when the start value is not ISO-8601', () => { + const parsed = parseKeywordInvocation( + 'autoIncrement.timestamp(start="12-06-2026 12:39:23", step=15, type="minutes", outputFormat="yyyy-MM-dd HH:mm:ss", inputFormat="dd-MM-yyyy HH:mm:ss")' + ); + + expect(parsed.errors).toEqual([]); + expect(executeDomainKeyword(parsed.keyword, { args: parsed.args, rowIndex: 0 })).toBe('2026-06-12 12:39:23'); + expect(executeDomainKeyword(parsed.keyword, { args: parsed.args, rowIndex: 2 })).toBe('2026-06-12 13:09:23'); + }); + + test('rejects unsupported increment units', () => { + expect(() => + executeDomainKeyword('autoIncrement.timestamp', { + args: ['2026-06-12T12:39:23Z', 1, 'fortnights'], + rowIndex: 0, + }) + ).toThrow( + 'Invalid argument for type: expected milliseconds, seconds, minutes, hours, days, weeks, months, or years.' + ); + }); + + test('advances via auto increment state when rows are generated one at a time', () => { + const state = {}; + + expect( + executeDomainKeyword('autoIncrement.timestamp', { + args: ['2026-06-12T16:00:00Z', 1, 'hours'], + autoIncrementState: state, + }) + ).toBe('2026-06-12T16:00:00Z'); + + expect( + executeDomainKeyword('autoIncrement.timestamp', { + args: ['2026-06-12T16:00:00Z', 1, 'hours'], + autoIncrementState: state, + }) + ).toBe('2026-06-12T17:00:00Z'); + + expect( + executeDomainKeyword('autoIncrement.timestamp', { + args: ['2026-06-12T16:00:00Z', 1, 'hours'], + autoIncrementState: state, + }) + ).toBe('2026-06-12T18:00:00Z'); + }); +}); diff --git a/packages/core/src/tests/data_generation/unit/domain/domainKeywords.test.js b/packages/core/src/tests/data_generation/unit/domain/domainKeywords.test.js index b4e7357b..86fc7c09 100644 --- a/packages/core/src/tests/data_generation/unit/domain/domainKeywords.test.js +++ b/packages/core/src/tests/data_generation/unit/domain/domainKeywords.test.js @@ -155,6 +155,8 @@ describe('domain keyword catalog', () => { expect(byKeyword.get('finance.accountNumber')?.help?.returnType).toBe('string'); expect(byKeyword.get('date.month')?.help?.returnType).toBe('string'); expect(byKeyword.get('date.weekday')?.help?.returnType).toBe('string'); + expect(byKeyword.get('autoIncrement.timestamp')?.delegate?.type).toBe('custom'); + expect(byKeyword.get('autoIncrement.timestamp')?.help?.returnType).toBe('string'); expect(byKeyword.get('internet.email')?.delegate?.argTransform).toBe('optionsFromHelpArgs'); expect(byKeyword.get('internet.httpStatusCode')?.help?.returnType).toBe('number'); expect(byKeyword.get('internet.port')?.help?.returnType).toBe('number'); diff --git a/packages/core/src/tests/data_generation/unit/domain/domainTestDataGenerator-state.test.js b/packages/core/src/tests/data_generation/unit/domain/domainTestDataGenerator-state.test.js index 6216a4d5..32398e8b 100644 --- a/packages/core/src/tests/data_generation/unit/domain/domainTestDataGenerator-state.test.js +++ b/packages/core/src/tests/data_generation/unit/domain/domainTestDataGenerator-state.test.js @@ -15,4 +15,40 @@ describe('DomainTestDataGenerator state restore', () => { expect(generator.generateFrom(secondRule).data).toBe(100); }); + + test('uses the generator faker instance instead of an execution-context override', () => { + const generatorFaker = { + person: { + firstName: () => 'generator-faker', + }, + }; + const overrideFaker = { + person: { + firstName: () => 'override-faker', + }, + }; + const generator = new DomainTestDataGenerator(generatorFaker); + + const result = generator.generateFrom( + { ruleSpec: 'person.firstName' }, + { + faker: overrideFaker, + } + ); + + expect(result.data).toBe('generator-faker'); + }); + + test('uses parsed rule args instead of execution-context args overrides', () => { + const generator = new DomainTestDataGenerator(faker); + + const result = generator.generateFrom( + { ruleSpec: 'literal.value("from-rule")' }, + { + args: ['from-context'], + } + ); + + expect(result.data).toBe('from-rule'); + }); }); diff --git a/packages/core/src/tests/mcp/anywaydata-mcp-contract.test.js b/packages/core/src/tests/mcp/anywaydata-mcp-contract.test.js index 50684cf0..f957acac 100644 --- a/packages/core/src/tests/mcp/anywaydata-mcp-contract.test.js +++ b/packages/core/src/tests/mcp/anywaydata-mcp-contract.test.js @@ -27,6 +27,17 @@ describe('AnyWayData MCP contract', () => { expect(result.format).toBe('json'); }); + test('executes domain keyword generation through the shared contract', () => { + const result = executeAnyWayDataMcpTool('generate_data_from_spec', { + textSpec: 't2\nautoIncrement.timestamp(start="12th June 2026 at 4pm", step=60, type="minutes")', + rowCount: 3, + outputFormat: 'json', + }); + + expect(result.ok).toBe(true); + expect(result.rows).toEqual([['2026-06-12T16:00:00Z'], ['2026-06-12T17:00:00Z'], ['2026-06-12T18:00:00Z']]); + }); + test('describes flat formatter options on generation tools', () => { const generateTool = listAnyWayDataMcpTools().find((tool) => tool.name === 'generate_data_from_spec'); const amendTool = listAnyWayDataMcpTools().find((tool) => tool.name === 'amend_data_from_spec'); diff --git a/playwright-api.config.js b/playwright-api.config.js index 8872f435..4537ccc1 100644 --- a/playwright-api.config.js +++ b/playwright-api.config.js @@ -8,7 +8,7 @@ module.exports = defineConfig({ testDir: './apps/api/src/tests', testMatch: '**/*.spec.js', timeout: 30000, - workers: process.env.CI ? 6 : 4, + workers: process.env.CI ? 6 : 1, expect: { timeout: 15000, }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c21618db..4a2ff2b5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,15 @@ importers: '@popperjs/core': specifier: 2.11.8 version: 2.11.8 + chrono-node: + specifier: ^2.9.1 + version: 2.9.1 + date-fns: + specifier: ^4.4.0 + version: 4.4.0 + date-fns-tz: + specifier: ^3.2.0 + version: 3.2.0(date-fns@4.4.0) papaparse: specifier: 5.5.3 version: 5.5.3 @@ -185,6 +194,15 @@ importers: '@faker-js/faker': specifier: 9.7.0 version: 9.7.0 + chrono-node: + specifier: ^2.9.1 + version: 2.9.1 + date-fns: + specifier: ^4.4.0 + version: 4.4.0 + date-fns-tz: + specifier: ^3.2.0 + version: 3.2.0(date-fns@4.4.0) papaparse: specifier: 5.5.3 version: 5.5.3 @@ -3807,6 +3825,10 @@ packages: resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} engines: {node: '>=6.0'} + chrono-node@2.9.1: + resolution: {integrity: sha512-nqP8Zp11efCYQIESXPxeDM8ikzN5BDb3Zzou+a66fZq+X2hzKFdsNLQE2/uBAh//BZEMbaMo1eTnagK7hOenAg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} @@ -4142,6 +4164,14 @@ packages: resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + date-fns-tz@3.2.0: + resolution: {integrity: sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==} + peerDependencies: + date-fns: ^3.0.0 || ^4.0.0 + + date-fns@4.4.0: + resolution: {integrity: sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==} + debounce@1.2.1: resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} @@ -13357,6 +13387,8 @@ snapshots: chrome-trace-event@1.0.4: {} + chrono-node@2.9.1: {} + ci-info@3.9.0: {} ci-info@4.4.0: {} @@ -13717,6 +13749,12 @@ snapshots: transitivePeerDependencies: - '@noble/hashes' + date-fns-tz@3.2.0(date-fns@4.4.0): + dependencies: + date-fns: 4.4.0 + + date-fns@4.4.0: {} + debounce@1.2.1: {} debug@2.6.9: diff --git a/scripts/run-playwright-api-tests.mjs b/scripts/run-playwright-api-tests.mjs new file mode 100644 index 00000000..2ccd65cb --- /dev/null +++ b/scripts/run-playwright-api-tests.mjs @@ -0,0 +1,32 @@ +import { mkdir, rm } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { spawn } from 'node:child_process'; + +const scriptDir = path.dirname(fileURLToPath(import.meta.url)); +const repoRoot = path.resolve(scriptDir, '..'); +const cacheDir = path.join(repoRoot, 'test-results', 'playwright-api-transform-cache'); + +await rm(cacheDir, { recursive: true, force: true }); +await mkdir(cacheDir, { recursive: true }); + +const child = spawn( + process.execPath, + ['./node_modules/@playwright/test/cli.js', 'test', '--config=playwright-api.config.js'], + { + cwd: repoRoot, + stdio: 'inherit', + env: { + ...process.env, + PWTEST_CACHE_DIR: cacheDir, + }, + } +); + +child.on('exit', (code, signal) => { + if (signal) { + process.kill(process.pid, signal); + return; + } + process.exit(code ?? 1); +}); diff --git a/scripts/run-storybook-tests.mjs b/scripts/run-storybook-tests.mjs index 76a03cb1..27a078b9 100644 --- a/scripts/run-storybook-tests.mjs +++ b/scripts/run-storybook-tests.mjs @@ -38,7 +38,7 @@ function ensurePortFree(port) { ensurePortFree(6006); -const child = spawn('pnpm', ['exec', 'vitest', '--project=storybook', '--run'], { +const child = spawn('pnpm', ['exec', 'vitest', '--project=storybook', '--run', '--api.host=127.0.0.1'], { stdio: 'inherit', shell: process.platform === 'win32', }); diff --git a/vitest.config.mjs b/vitest.config.mjs index caffd408..8621cddd 100644 --- a/vitest.config.mjs +++ b/vitest.config.mjs @@ -16,7 +16,7 @@ export default defineConfig({ storybookTest({ configDir: path.join(dirname, '.storybook'), storybookUrl: 'http://127.0.0.1:6006', - storybookScript: 'pnpm exec storybook dev -p 6006 --exact-port --ci', + storybookScript: 'pnpm exec storybook dev -p 6006 --host 127.0.0.1 --exact-port --ci', }), ], test: { @@ -27,6 +27,10 @@ export default defineConfig({ enabled: true, headless: true, provider: playwright({}), + api: { + host: '127.0.0.1', + port: 51315, + }, instances: [{ browser: 'chromium' }], }, },