Skip to main content
Glama
dills122

Formly Agent Contracts

by dills122

Formly Contract

Formly Contract 将 Angular Formly 字段配置转换为稳定、带版本号的 JSON,使 E2E 测试编写者或编码代理无需猜测表单结构即可理解。

给定一个 FormlyFieldConfig[],该适配器描述:

  • 表单中的控件、展示内容、分组和可重复模板;

  • 每个字段的模型路径、Formly 类型、标签、约束和选项;

  • 已知的可见性、必填、只读、禁用和动态选项行为;

  • 精确的或由应用推导出的测试定位器,如 data-testiddata-test-iddata-cy

  • 哪些内容直接来自配置,哪些由受控的 Formly 构建解析得到,哪些仍然未知;以及

  • 对无法安全表示的行为的稳定诊断信息。

结果是确定性的 Form Contract,具有严格的运行时验证、规范化序列化和内容哈希。该契约旨在作为 Cypress/Playwright 测试规划和未来代理工具链的可靠输入。它不是 Formly 运行时对象的转储。

当前已有内容

本仓库目前提供 schema v0.3 和两个工作区包:

用途

@formly-contract/contract-schema

契约 DTO、运行时验证、规范化 JSON 和 SHA-256 内容哈希

@formly-contract/formly-adapter

针对 Formly 6.1 的安全声明式提取和可信场景编译

此外还包括:

  • 使用合成黄金表单的确定性 CLI 演示;

  • 一个浏览器渲染的 Angular 测试应用,包含十二个合成 Formly 夹具;以及

  • 针对固定的 Angular 20.3.29 和 Formly 6.1.8 组合的兼容性覆盖。

解析器和契约是当前的产品。生产级 MCP 服务器、自动 Playwright 生成、浏览器观察和应用源码发现是未来的层次,不包含在此 MVP 中。

Related MCP server: SpecBridge MCP

在你自己的 Angular/Formly 代码库中使用

该包作为构建/测试工具运行在你的 Angular 应用旁边。它不需要被添加到应用的浏览器 bundle 中。典型的采用流程是:

application-owned Formly factories
              |
     generation script or CI job
              |
       versioned contract JSON
              |
 Playwright / Cypress / agent tooling

1. 添加包

这些包尚未发布到 npm。在首次发布之前,请将此仓库克隆到消费应用旁边,并构建这两个包:

git clone https://github.com/dills122/formly-contract.git
cd formly-contract
pnpm install --frozen-lockfile
pnpm --filter @formly-contract/contract-schema build
pnpm --filter @formly-contract/formly-adapter build

然后从消费应用的 package.json 中链接它们(根据你的检出位置调整相对路径):

{
  "devDependencies": {
    "@formly-contract/contract-schema": "link:../formly-contract/packages/contract-schema",
    "@formly-contract/formly-adapter": "link:../formly-contract/packages/formly-adapter"
  }
}

在消费应用中运行 pnpm install。该应用必须已经提供兼容的 Angular 和 Formly 对等依赖;当前测试的组合是 Angular 20.3.29 搭配 Formly 6.1.8。包发布后,常规的带版本号 pnpm add --save-dev 依赖将取代这些本地链接。

2. 选择要暴露的表单

应用源码发现刻意不设为自动。创建一个小的、应用自有的注册表,只导入你希望契约生成器检查的表单工厂:

// tools/contract-forms.ts
import type { FormlyFieldConfig } from '@ngx-formly/core';
import { createClaimFields } from '../src/app/claims/claim.fields';
import { createCustomerFields } from '../src/app/customers/customer.fields';

export interface ContractFormTarget {
  id: string;
  createFields: () => FormlyFieldConfig[];
}

export const contractForms: ContractFormTarget[] = [
  { id: 'claims.create', createFields: () => createClaimFields() },
  { id: 'customers.edit', createFields: () => createCustomerFields() },
];

每个工厂应返回一棵全新的字段树。如果工厂需要应用输入,请将其包装在带有合成值的闭包中,这些值在本地开发和 CI 中使用是安全的。

3. 生成契约产物

在应用仓库中添加一个构建时脚本:

// tools/generate-form-contracts.ts
import { mkdir, writeFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { canonicalStringify } from '@formly-contract/contract-schema';
import { extractFormContract } from '@formly-contract/formly-adapter';
import { contractForms } from './contract-forms';

const outputDirectory = resolve('artifacts/form-contracts');
await mkdir(outputDirectory, { recursive: true });

for (const target of contractForms) {
  const { contract, diagnostics } = extractFormContract({
    formId: target.id,
    fields: target.createFields(),
  });

  await writeFile(
    resolve(outputDirectory, `${target.id}.json`),
    `${canonicalStringify(contract)}\n`,
  );

  console.log(
    `${target.id}: ${contract.nodes.length} root nodes, ${diagnostics.length} diagnostics`,
  );
}

使用消费仓库已使用的 TypeScript 运行器运行此文件,或将其编译为面向 Node 的工具项目的一部分。生成的 JSON 可以提交以供审查、作为 CI 产物上传,或由下游测试编写工具读取。由于它是规范化的且带内容哈希,意外的表单契约变更会在源码控制或 CI 中可见。

这条声明式路径是最好的起点。它捕获静态结构,并将表达式回调记录为动态元数据,而无需执行任意应用代码。

4. 在 Playwright 中使用契约

在信任存储的 JSON 之前先验证它,找到你需要的语义节点,并使用其精确的定位器候选之一。对于标准的 data-testid 定位器:

import { readFile } from 'node:fs/promises';
import {
  parseFormContract,
  type ContractNode,
  type ModelPathSegment,
} from '@formly-contract/contract-schema';

function findNodeByPath(
  nodes: readonly ContractNode[],
  modelPath: readonly ModelPathSegment[],
): ContractNode | undefined {
  for (const node of nodes) {
    if (
      node.modelPath.length === modelPath.length &&
      node.modelPath.every((segment, index) => segment === modelPath[index])
    ) {
      return node;
    }

    const nested = findNodeByPath(
      node.arrayTemplate
        ? [...node.children, node.arrayTemplate]
        : node.children,
      modelPath,
    );
    if (nested) return nested;
  }
}

const contract = parseFormContract(
  JSON.parse(
    await readFile('artifacts/form-contracts/claims.create.json', 'utf8'),
  ),
);

const claimantName = findNodeByPath(contract.nodes, ['claimant', 'name']);

const testId = claimantName?.locators.find(
  (locator) =>
    locator.strategy === 'testId' && locator.attribute === 'data-testid',
);

if (!claimantName || !testId) {
  throw new Error('claimant.name has no exact data-testid locator');
}

await page.getByTestId(testId.value).fill('Ada Lovelace');

实际消费者通常会将递归节点查找和定位器选择放在共享的 Playwright 或 Cypress 辅助函数中。复合控件可以暴露多个定位器目标,因此辅助函数应按 target 选择,而不是假设一个 Formly 节点总是映射到一个 DOM 元素。空定位器数组和诊断信息必须作为证据缺失来处理,而不是用发明的选择器来替代。

5. 在需要时解析动态行为

如果表达式决定可见性、必填/只读状态或选项列表,请添加合成场景并调用 compileFormContractScenario。在配置了应用真实 Formly 模块和自定义类型的可信 Angular 测试/构建环境中运行该 API。为每个有意义的场景生成一个产物,仅使用合成模型和表单状态数据。

合成兼容性测试工具 展示了获取 FormlyFormBuilder 的完整 Angular TestBed 设置。下面的详细 API 示例展示了场景调用。

为什么这很有用

大型 Formly 表单通常由嵌套分组、共享片段、自定义字段类型、表达式、动态选项和应用约定组装而成。反复阅读这些源码很慢,而从渲染页面猜测会导致脆弱的测试。

本项目创建了一个小而明确的边界:

Formly fields + synthetic scenario
                |
       safe contract projection
                |
   deterministic versioned JSON
                |
 E2E planning / agent inspection

消费者可以检查一个契约来回答以下问题:

  • 存在哪些控件,顺序如何?

  • 每个控件编辑哪个模型值?

  • 哪些值和验证边界是已知的?

  • 选项列表是空的、静态的、动态的还是异步的?

  • 哪些字段可能被隐藏、必填、只读或禁用?

  • 哪些 data-*、role、label、placeholder 或 DOM-ID 定位器候选可用?

  • 哪些事实是精确的、推导的、为某个场景解析的,或仍然未知的?

试用本仓库

前置要求:

  • Node.js 22.22.1

  • pnpm 10.23.0

pnpm install --frozen-lockfile
pnpm demo

pnpm demo 构建包切片并打印一个规范化 JSON 契约。使用以下命令运行完整的仓库门禁:

pnpm check

该命令运行 lint、所有测试、包和 Angular 生产构建、演示冒烟测试以及文档检查。

提取声明的表单结构

当你拥有 Formly 配置并希望在不运行回调的情况下检查它时,使用 extractFormContract

import { extractFormContract } from '@formly-contract/formly-adapter';
import type { FormlyFieldConfig } from '@ngx-formly/core';

const fields: FormlyFieldConfig[] = [
  {
    key: 'profile.name',
    type: 'input',
    props: {
      label: 'Name',
      required: true,
      attributes: { 'data-testid': 'profile-name' },
    },
  },
];

const { contract, diagnostics } = extractFormContract({
  formId: 'example.profile',
  fields,
});

此路径是纯函数且非变异的。它不调用表达式函数、不订阅 Observables、不运行验证器,也不渲染 Angular 组件。可识别的回调成为动态规则元数据;不支持的行为成为显式诊断信息。返回的节点具有稳定 ID example.profile::path:s_profile.s_name、模型路径 ['profile', 'name']、其必填约束以及精确的 data-testid 定位器。

解析合成场景

当必填、只读、禁用、隐藏、选项或定位器属性依赖于 Formly 表达式回调时,使用 compileFormContractScenario

import { inject } from '@angular/core';
import { FormlyFormBuilder } from '@ngx-formly/core';
import { compileFormContractScenario } from '@formly-contract/formly-adapter';

const builder = inject(FormlyFormBuilder);
const { contract, diagnostics } = compileFormContractScenario({
  formId: 'example.profile',
  builder,
  createFields: () => createProfileFields(),
  model: { contactMethod: 'email' },
  formState: { readonly: false },
});

这是一个可信的构建/CI API。它使用应用配置的 FormlyFormBuilder,因此应用和 Formly 回调可能会运行。模型和表单状态必须是可结构化克隆的;两者在字段工厂或构建器运行之前都会被克隆。

构建后的字段树仍然经过与声明式提取相同的白名单。例如,动态选项被缩减为公开的 label/value/disabled 记录,而不是从应用对象复制任意属性。

不要直接从 MCP 或其他不受信任的请求处理器中暴露此编译器。查询层应读取先前生成的契约产物。

测试定位器

每个节点都有一个有序的 locators 数组。适配器自动从 props.attributes 中读取这些常见属性:

  • data-testid

  • data-test-id

  • data-test

  • data-cy

  • data-pw

它还可以保留显式的 role、可访问名称、placeholder 和 Formly 字段 ID 候选。空数组意味着未找到可靠的定位器;适配器从不发明 CSS 或 XPath。

具有自己命名约定的应用可以设置 testIdAttributes 并提供确定性的 deriveLocators 回调。该回调只接收冻结的身份数据,而不是实时的 Formly 字段。它可以为复合小部件(如日期范围)返回多个命名目标;其输出标记为 confidence: "derived"。完整的契约和示例请参阅 v0.3 定位器规范

证据模型

契约将三个证据级别分开:

证据

含义

当前可用?

declared

从提供的 Formly 配置中安全读取

resolved

从针对一个合成场景的受控 Formly 构建中读取

observed

在真实渲染的浏览器 DOM 中看到

Schema 已就绪;捕获层尚未实现

解析的定位器不会静默地呈现为浏览器观察到的。同样,不透明或异步行为会被报告而不是猜测。

支持的契约信息

Schema v0.3 可以表示:

  • 有序的控件、分组、仅展示节点和数组模板;

  • 稳定的语义节点 ID 和累积模型路径;

  • Formly 和常见语义控件类型;

  • 标签、描述、placeholder、JSON 安全的默认值和包装器;

  • 必填、最小/最大、长度、字符串模式和命名约束;

  • 静态和解析的公开选项,以及动态/异步选项源元数据;

  • 字符串/布尔条件和回调/异步动态规则元数据;

  • 解析的隐藏、只读和禁用状态;

  • 精确和推导的定位器候选,包括多个命名目标;以及

  • 确定性诊断、规范化 JSON 和内容哈希。

有意的限制

  • 表单必须显式提供;适配器不发现任意的 TypeScript 导出或应用路由。

  • 声明式提取从不评估函数或函数源码。

  • 场景编译器执行初始的受控 Formly 构建,但不等待远程选项或生命周期驱动的浏览器行为。

  • Formly RegExp 模式会被诊断;v0.3 仅表示字符串模式。

  • 自定义小部件操作和值编解码器尚未建模。

  • 项目当前不生成或执行 Cypress/Playwright 测试。

  • 不包含生产级 MCP 服务器或浏览器观察层。

  • 兼容性已针对 Angular 20.3.29 搭配 Formly 6.1.8 验证,而非所有 Angular/Formly 组合。

  • npm 发布和发布自动化尚未包含。

合成测试应用

Angular 测试应用包含十二个虚构表单,涵盖原生和自定义字段、包装器、验证器、扩展、预设、表达式、验证、重复器、不透明行为以及遗留的 Formly v6 别名。

pnpm app:serve

打开 http://127.0.0.1:4200/ 并从目录中选择一个夹具。

工作场所的表单和数据应保留在私有工作仓库中。私有夹具模块可以实现 TestFormDefinition 并通过 TEST_FORM_GROUPS 注册一个分组,而无需将工作场所的标签、标识符、选项或规则复制到本公开项目中。

仓库布局

packages/
  contract-schema/   Versioned DTOs, validation, canonical JSON, and hashing
  formly-adapter/     Declared extraction and trusted Formly scenario builds
fixtures/
  synthetic-form/    Public golden form and real-builder compatibility fixture
apps/
  demo-cli/          Prints the deterministic golden contract
  formly-test-app/   Browser-rendered Angular/Formly fixture catalog
docs/                Specifications, ADRs, delivery plans, and evidence

路线图

预期的交付路径是:

Form Contract packages (current)
              |
      read-only MCP queries
              |
        typed E2E intent
              |
 deterministic Playwright/Cypress drivers
              |
 browser observation and parity checks

未来的层应消费不可变契约。它们不应将 Angular 执行、任意回调评估或选择器发明移入常规代理请求中。

文档

贡献与安全

欢迎贡献。参与前请阅读 CONTRIBUTING.md行为准则。请通过 SECURITY.md 中描述的私有流程报告安全问题。

本项目基于 MIT 许可证 提供。

Maintenance

ActivityMaintained
ResponsivenessResponsive

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/dills122/formly-contract'

If you have feedback or need assistance with the MCP directory API, please join our Discord server