mcpField
W
web-test

web-test

github.com/nikolay-shirokov测试与质量74158 次安装今天更新
代码仓库

关于 web-test

web-test 是面向 1C:Enterprise Web 客户端的浏览器自动化技能,适用于测试和操作业务界面。它基于 Playwright,支持一次性运行脚本或连接交互会话,并可读取表格与表单状态。

主要功能

  1. 01Playwright 自动化浏览器交互
  2. 02导航分区并打开命令
  3. 03读取表单、报表和表格
  4. 04填写字段并筛选列表
  5. 05支持一次性及交互执行

适用场景

  1. 01自动执行 1C Web 客户端测试场景
  2. 02读取业务表格和报表数据
  3. 03逐步调试表单操作脚本

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/nikolay-shirokov/cc-1c-skills --skill web-test

安装后在 Claude Code 中输入/web-test确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

.gitignore
recording.md
regress.md
scripts/
browser.mjs
dom.mjs
scripts/cli/
exec-context.mjs
server.mjs
session.mjs
util.mjs
scripts/cli/commands/
exec.mjs
run.mjs
shot.mjs
start.mjs
status.mjs
stop.mjs
test.mjs
scripts/cli/test-runner/
assertions.mjs
context-pool.mjs
discover.mjs
reporters.mjs
severity.mjs
suite-root.mjs
scripts/dom/
_shared.mjs
edd.mjs
edit-state.mjs
errors-stack.mjs
errors.mjs
filter.mjs
form-state.mjs
forms.mjs

详细介绍

/web-test — 1C Web 客户端浏览器自动化

通过 Playwright 自动执行与 1C:Enterprise Web 客户端的用户交互,包括导航到各个分区、填写表单、读取表格和报表,以及筛选列表。

快速开始

RUN="${CLAUDE_SKILL_DIR}/scripts/run.mjs"

# One-shot: opens browser → runs script → closes browser → exits
node $RUN run http://localhost:8081/bpdemo test-scenario.js

# Or pipe inline:
cat <<'SCRIPT' | node $RUN run http://localhost:8081/bpdemo -
await navigateSection('Продажи');
await openCommand('Заказы клиентов');
await clickElement('Создать');
await fillFields({ 'Клиент': 'Альфа' });
await clickElement('Провести и закрыть');
SCRIPT

设置(首次使用)

cd "${CLAUDE_SKILL_DIR}/scripts" && npm install && npx playwright install chromium

需要 Node.js 18+。npm install 会安装 Playwright;npx playwright install chromium 会下载浏览器。

URL 解析

读取项目根目录中的 .v8-project.json。每个数据库都有 id,并可选择设置 webUrl。 构造 URL:http://localhost:8081/<id>,或在设置了 webUrl 时使用该值。 如果数据库尚未发布,请先使用 /web-publish。

执行模式

自主模式(完整场景首选)
node $RUN run <url> script.js   # exits when done, no session
node $RUN run <url> script.js --timeout-min=10   # bound the script (default 30 min)

限制适用于脚本,不适用于 1C 启动:--timeout=<ms>、--timeout-min=<min> 或 WEB_TEST_EXEC_TIMEOUT_MS。run 的退出代码:0 — 成功,1 — 脚本错误,2 — 超时。

交互模式(逐步开发)
# 1. Start session in the background — `start` stays running as the server, so don't wait on
#    its stdout. Poll `status` instead: it exits 0 only once the session is loaded and live.
node $RUN start <url>            # run_in_background=true
until node $RUN status >/dev/null 2>&1; do sleep 2; done   # exit 0 = ready

# 2. Execute scripts against running session
cat <<'SCRIPT' | node $RUN exec -
const form = await getFormState();
console.log(JSON.stringify(form, null, 2));
SCRIPT

# 2b. Execute without video recording (for debugging/testing)
cat script.js | node $RUN exec - --no-record

# 2c. Override exec HTTP timeout (default 30 min). Use for long scripts
#     such as multi-block recordings + addNarration.
cat script.js | node $RUN exec - --timeout-min=120
cat script.js | node $RUN exec - --timeout=7200000
WEB_TEST_EXEC_TIMEOUT_MS=7200000 node $RUN exec script.js

# 3. Screenshot
node $RUN shot result.png

# 4. Stop (logout + close)
node $RUN stop

start 会在后台运行 HTTP 服务。在其他 shell 中使用 exec/shot/stop。

编写 exec 脚本

所有 browser.mjs 导出项都是全局变量,无需 import。 console.log() 的输出会包含在 JSON 响应中。 也可以使用 writeFileSync / readFileSync。

API 参考

导航
navigateSection(name) → { navigated, sections, commands }

转到顶级分区(模糊匹配)。返回该分区中的命令列表。

await navigateSection('Продажи');
// { navigated: 'Продажи', sections: [...], commands: ['Заказы клиентов', ...] }
openCommand(name) → 表单状态

从功能面板打开命令(模糊匹配)。返回打开表单的状态。

const form = await openCommand('Заказы клиентов');
navigateLink(url) → 表单状态

通过元数据路径打开任意 1C 对象(Shift+F11 对话框)。绕过分区/命令导航。

await navigateLink('Документ.ЗаказКлиента');
await navigateLink('РегистрНакопления.ЗаказыКлиентов');
await navigateLink('Справочник.Контрагенты');
openFile(path) → 表单状态

通过“文件” → “打开”来打开外部数据处理器或报表(EPF/ERF)。会自动处理安全确认对话框。

const form = await openFile('C:\\WS\\build\\МояОбработка.epf');
const form = await openFile('build/МояОбработка.epf'); // relative paths work too
switchTab(name) → 表单状态

切换到已打开的选项卡/窗口(模糊匹配)。

读取表单状态
getFormState() → { form, formCount, openForms, title, fields, buttons, tabs, navigation?, table, tables, filters, reportSettings? }

返回当前表单结构。这是了解屏幕内容的主要方式。

form — 活动表单编号;桌面上没有打开表单时为 null。

formCount — 已打开表单的数量。可用于了解堆叠了多少个窗口。0 表示桌面。

openForms — 所有已打开表单编号的数组(例如 [0, 1])。即使 1C 设置中隐藏了打开窗口选项卡栏,也能正常使用。

modal — 活动表单为阻止 UI 操作的模态对话框时为 true。仅在模态对话框活动时出现。

openTabs — 来自打开窗口选项卡栏的 { name, active? } 数组。仅在 1C 设置中启用了选项卡栏时出现。不要依赖此项,应改用 formCount/openForms。

title — 活动表单的标题("Контрагенты"、"Заказ поставщику ТД00-000052 от 05.07.2022")。读取自表单自身的标题栏,不受打开窗口选项卡栏影响;表单没有标题栏时,回退到活动选项卡的标题;两者都不可用时为 null。

fields — 每个字段包含:name、value、label?、actions?(select、clear、open)、required?(未填写的必填字段为 true)、disabled?(控件不可用)。buttons[] 也包含 disabled?。

navigation — 表单导航面板链接(适用于带有从属目录的对象):[{ name, active? }]。可通过 clickElement() 点击。仅当表单有导航面板时出现(例如“Основное”、“Объекты метаданных”、“Подсистемы”)。 groups — 可折叠和弹出式表单组:[{ name, title, collapsed, behavior? }]。collapsed: true 表示组的内容处于隐藏状态——展开前不会显示表单的这部分内容(常见于“Администрирование → Интернет-поддержка и сервисы”这类设置页面)。behavior: 'popup' 表示这是弹出式组(内容会显示在浮动面板中);普通可折叠组不包含此属性。使用 clickElement 点击组标题即可展开/折叠(或打开/关闭弹出面板),用法与树节点相同:{ expand: true } 显示内容(可重复执行),{ expand: false } 隐藏内容,{ toggle: true } 切换状态。展开后,组的内容会在下一次 getFormState() 中变为可读取状态(其字段/超链接/文本会显示)。普通(不可折叠)组不会列出。 组标题不是稳定的键:相同标题可能在表单的不同区域重复出现,组也可能在展开时更改标题(“Показать детализацию” ↔ “Скрыть детализацию”),此后使用旧标题点击会返回“not found”。因此,点击结果会报告 group(组的技术名称,不会变化且可供 clickElement 使用)和 title(当前标题)——如果计划再次点击同一组,请使用 group。

const form = await getFormState();
// form.groups = [{ name: "ГруппаНовости", title: "Новости", collapsed: true }, ...]
const r = await clickElement('Новости', { expand: true });   // reveal the group's content
// r.clicked = { kind: 'formGroup', name: 'Новости', group: 'ГруппаНовости', title: 'Новости', toggled: true }
await clickElement(r.clicked.group, { expand: false });      // stable key — safe to reuse

tables — 所有可见网格的数组:[{ name, columns, rowCount, label? }]。label 是屏幕上显示的视觉分组标题(例如“Входящие”);网格没有可见标题时则不包含此属性。使用 readTable() 读取实际数据。

table — 向后兼容的首个网格别名:{ present, columns, rowCount }。

htmlFields — 屏幕上的 HTML 文档字段:[{ name, label? }]。使用 getHtmlFrame() 进入字段。非活动页面、折叠组中或被表单隐藏的字段,在显示之前不会列出。

reportSettings — 对于 DCS 报表,提供易读的筛选设置,而非原始技术名称:

const form = await getFormState();
// form.reportSettings = [
//   { name: "Склад", enabled: true, value: "Склад бытовой техники", actions: ["select"] },
//   { name: "Номенклатура", enabled: false, value: "" }
// ]

errorModal — 如果存在,表示 1C 显示了错误对话框。读取消息并决定如何继续。

confirmation — 如果存在,表示显示了“是/否”对话框。调用 clickElement('Да') 或 clickElement('Нет')。

errors.stateText — SpreadsheetDocument 状态消息数组(例如 "Не установлено значение параметра \"X\"", "Отчет не сформирован...", "Изменились настройки...")。当报表区域显示信息栏而非数据时,此属性会出现。列表仍在搜索时,同一信息栏会显示 "Поиск..."——信息栏显示期间操作不会返回,因此经过筛选的列表不会返回之前的行。

getHtmlFrame(name?) → Playwright FrameLocator

进入活动表单中 HTML 文档字段(其 HTML 或 React 应用),可按 htmlFields 中的字段名称或标题指定。表单仅显示一个 HTML 字段时,可以省略 name。在框架内,页面属于应用本身——直接使用 Playwright 操作:

const app = await getHtmlFrame('Чат');
await app.getByTestId('chat-message').filter({ visible: true }).fill('text');  // hidden duplicates
await app.getByRole('button', { name: 'Отправить' }).click();

如果名称未知、显示了多个字段但未提供名称,或字段尚不可见,则会抛出异常(并附上字段列表)——请先打开其页面或展开其组。原始 Playwright 操作不会自动检测 1C 错误:调用 1C 服务的操作执行后,请检查 getFormState().errors。表单上方打开模态窗口后,不要继续使用该框架——点击会落在模态窗口上。

读取数据
readTable({ maxRows?, offset?, table? }) → { columns, rows, total, shown, offset, hasMore }

分页读取实际网格数据。每行的格式为 { columnName: value }。

选项默认值说明
maxRows20每次调用最多返回的行数
offset0跳过前 N 行
table—来自 tables[] 的网格名称(用于多网格表单)

图片列。 显示图标的单元格(状态/阶段标记、“ЭДО”标记、附件的回形针)读取时会显示为 'pic:<N>'(N = 图标帧/状态),图标不存在时则显示为 ''——因此可以通过真值判断是否存在,且图标会因索引不同而有所区别。仅含图标的列(无标题文本)仍会显示,列名为其工具提示或 '(picture)'。这些值为只读——请按文本列筛选/选择行,不要使用 'pic:N'。

if (t.rows[0]['Присоединенные файлы']) { /* has an attached file */ }
t.rows[0]['ЭДО'] === 'pic:1';   // connected to 1С-ЭДО ('pic:0' = not)

分组表头。 合并在分组标题下的列会带上该标题:'Цена / План'、'Цена / Факт'——标题本身不是数据列。这类名称也适用于 clickElement({row, column}) 和 fillTableRow;短名称('Факт')也能解析,此时会选取最左侧的匹配项。

特殊行字段:

  • _kind: 'group' — 分层分组行
  • _kind: 'parent' — 分层结构中的父行
  • _tree: 'expanded'|'collapsed' — 树节点状态
  • _level: N — 树视图中的嵌套深度
  • _selected: true — 行已选中(高亮显示)。使用 clickElement({ modifier: 'ctrl'|'shift' }) 验证多选
  • hierarchical: true — 列表包含分组(在结果对象上)
  • viewMode: 'tree' — 树视图已启用(在结果对象上)

行状态 — 在对象列表中,根据行的状态图标解码(无需向列表添加列):

  • _deleted: true|false — 标记为删除(目录、单据、任务、业务流程、会计科目表/计算类型)
  • _posted: true|false — 单据
  • _predefined: true|false — 目录、会计科目表/计算类型
  • _completed: true|false — 任务
  • _started、_finished — 业务流程
  • _rowPic: '<icon>:<N>' — 原始图标 ID,用于诊断
const t = await readTable();
const doc = t.rows.find(r => r['Номер'] === 'ТД00-000005');
if (doc._deleted === true) { /* marked for deletion */ }

缺少状态字段表示“未知”,绝不表示 false——该属性可能不适用(单据没有 _predefined),也可能无法识别图标。因此,if (!row._deleted) 并不安全:它会把“未知”当作“未删除”。请明确比较(=== true/=== false),并将 undefined 视为第三种结果。对象列表之外的行(表单表格部分、值列表)完全没有状态字段。如果存在 _rowPic,但没有布尔字段,请报告其值——该图标需要解码支持。

total 对于长列表容易引起误解。 1С 会对动态列表和表单表格部分进行虚拟化——DOM 仅保留一个可见行窗口。total/shown 统计的是当前已加载的数据,而非底层集合的大小。请使用 hasMore 判断窗口之外是否还有数据:

const t = await readTable();
// t.hasMore = { above: false, below: true }   ← form tabular section, scrollbar visible
// t.hasMore = { below: true }                 ← dynamic list (catalog/journal/register)
// t.hasMore = { below: false }                ← everything visible / end of list reached
  • hasMore.below — 始终存在。true ⇒ 向下滚动(PageDown/使用 scroll:true 调用 clickElement)会显示更多行。
  • hasMore.above — 通常也存在。通过动态列表翻页按钮(#vertButtonScroll)或表格部分滚动条检测。仅少数既没有此类控件的网格会缺少此字段——缺少时请视为未知。
const t = await readTable({ maxRows: 50 });
console.log('Columns:', t.columns);
console.log('Loaded:', t.shown, 'rows; more below:', t.hasMore.below);
// Pagination:
const page2 = await readTable({ maxRows: 50, offset: 50 });
readSpreadsheet() → { title?, headers?, data?, totals?, rows?, total }

点击“Сформировать”后读取报表输出(SpreadsheetDocument)。

检测到表头行时会返回结构化数据:

await clickElement('Сформировать');
await wait(5);
const report = await readSpreadsheet();
// { title: "Остатки товаров", headers: ["Номенклатура", "Склад", "Количество"],
//   data: [{ "Номенклатура": "Бумага", "Склад": "Основной", "Количество": "150" }, ...],
//   totals: { "Количество": "1250" }, total: 42 }

无法检测到表头时,则返回 { rows: string[][], total }。

getSections() → { activeSection, sections, commands }

读取分区面板和命令,不进行导航。

getCommands() → string[]

当前分区的命令。

getPageState() → { activeSection, activeTab, sections, tabs }

分区及所有打开的标签页。

操作

返回值形状约定。 所有操作函数都会返回一个扁平表单状态(形状与 getFormState() 相同),并附带操作专属的额外字段:clicked、focused、selected、filled、notFilled、closed、opened、navigated、deleted、filtered、unfiltered。错误始终位于顶层的 .errors 下(如果存在)——执行包装器会在 .errors.modal / .errors.balloon 存在时自动抛出异常。

clickElement(text, { dblclick?, table?, expand?, modifier?, scroll?, file? }) → 表单状态

单击按钮、超链接、标签页、导航面板链接或网格行(模糊匹配)。

禁用的控件会抛出异常。 如果控件不可用,clickElement、fillFields 和 selectValue 会抛出 "X" is disabled,而不是报告虚假的成功——请先检查 getFormState().buttons[].disabled / fields[].disabled。

  • table — 将按钮搜索范围限定到特定网格的命令面板(按 tables[] 中的名称):

    await clickElement('Добавить', { table: 'Исходящие' }); // clicks "Добавить" near "Исходящие" grid
    
  • 在列表中单击可选中一行。双击可打开该项:

    await clickElement('0000-000227', { dblclick: true }); // opens document
    
  • 打开菜单时会返回 submenu[] — 使用菜单项名称再次单击:

    const r = await clickElement('Ещё');
    // r.submenu = ['Расширенный поиск', 'Настройки', ...]
    await clickElement('Расширенный поиск');
    
  • 树节点:默认单击 = 选择(突出显示行)。使用 { expand: true } 可展开/折叠:

    await clickElement('ИСУ ФХД');                      // select row
    await clickElement('ИСУ ФХД', { expand: true });    // expand/collapse
    
  • 使用 file 选择文件或文件夹:对于会打开文件选择对话框(«Добавить из файла...»、«Загрузить»)的按钮,单击该按钮,并在出现的任意对话框中选择路径——可能是 1C 的「Выбор файлов」窗口,也可能是(Windows)1C 文件系统扩展的原生文件/文件夹对话框。只能在原生对话框中选择文件夹。返回 file: { path, via: 'html' | 'native' };如果路径不存在,或在 timeout 时间内(默认 10 s)没有出现对话框,则抛出异常:

    await clickElement('Добавить из файла...', { file: 'build/МоёРасширение.cfe' });
    await clickElement('Выбрать каталог', { file: 'C:/Exchange/out' });
    
  • 聚焦字段(最后手段,未提供 table 时):如果 text 与任何可单击控件都不匹配,但与某个表单字段的名称/标签匹配,则单击输入框以聚焦,不更改其值。返回 focused: { field, id, ok }(如果字段无法获得焦点,则 ok: false)。可用于驱动依赖焦点的按键操作:

    await clickElement('Контрагент');          // focus the reference field
    await getPage().keyboard.press('F4');      // open its selection form
    
  • 使用 modifier: 'ctrl'(添加到选中项)或 modifier: 'shift'(选择范围)多选行:

    await clickElement('Номенклатура 1');                          // select first row
    await clickElement('Номенклатура 2', { modifier: 'ctrl' });   // add to selection
    await clickElement('Номенклатура 5', { modifier: 'shift' });  // select range 2..5
    // Verify selection:
    const t = await readTable();
    t.rows.filter(r => r._selected);  // rows with _selected: true
    
  • 按(行,列)单击单元格 — 第一个参数使用 { row, column }。路由规则:表单上的电子表格 → 电子表格钻取;否则 → 网格单元格。如果两者同时存在,可传入 table: 'GridName' 强制指定网格。

    电子表格报表钻取:

    const report = await readSpreadsheet();
    // report.data[0] = { 'К1': 'Материалы строительные', 'К6': '150 000', ... }
    await clickElement({ row: 0, column: 'К6' }, { dblclick: true });                      // by index
    await clickElement({ row: { 'К1': 'Материалы' }, column: 'К6' }, { dblclick: true });  // by filter
    await clickElement({ row: 'totals', column: 'К6' }, { dblclick: true });               // totals row
    await clickElement('150 000', { dblclick: true });                                     // fallback: by text
    

    表单网格单元格(目录列表、日志、表格部分)。视口外的列会自动水平滚动(可绕过冻结列)。对于当前 DOM 窗口之外、基于筛选条件的行,使用 scroll: true | number:

    await clickElement({ row: 0, column: 'Количество' }, { table: 'Товары', dblclick: true });
    await clickElement({ row: { 'Номенклатура': 'Бумага' }, column: 'Цена' }, { table: 'Товары' });
    await clickElement({ row: { 'Номер': '0000-000601' }, column: 'Сумма' },
                       { table: 'Реализации', scroll: true });  // PageDown loop, max 50
    

    注意事项:

    • row: <number> 是当前 DOM 窗口中的索引,不是绝对索引——1С 会对长列表进行虚拟化。row: 0 是任何先前滚动后加载的最顶端行。要访问长列表中的任意行,请使用 row: { col: val } + scroll: true。
    • scroll: true 只会向下移动(PageDown)。若要向上移动,请先通过 getPage().keyboard 按 Home,或使用 filterList 缩小范围。
    • 如果筛选条件匹配多行,将采用第一个匹配行——请细化筛选条件以消除歧义。
fillFields({ name: value }) → 带 filled 的表单状态

按标签填充表单字段(模糊匹配)。自动检测字段类型。

值字段类型方法
'Конфетпром'引用粘贴板粘贴 + 输入提示
'5000'纯文本粘贴板粘贴
'true' / 'да'复选框切换
'Оплата поставщику'单选按钮标签模糊匹配
'' / null任意(复选框/单选按钮除外)通过 Shift+F4 清除

DCS 报表筛选器:使用人类可读的标签名称。复选框会自动启用:

await fillFields({
  'Склад': 'Склад бытовой техники',   // auto-enables "Склад" checkbox + fills value
  'Номенклатура': 'Вентилятор'          // same: enables checkbox + fills
});

返回带有 filled: [{ field, ok: true, value, method }] 的表单状态(method:clear|toggle|radio|paste|dropdown|form|typeahead)。任意字段失败时都会抛出异常,并在详细消息中列出出错字段和可用选项——如果调用已返回,则所有字段都已填充,无需逐项检查。

selectValue(field, search, opts?) → 带 selected 的表单状态

通过下拉列表或选择表单,从引用字段中选择一个值。对于需要从目录中精确选择的引用字段,这比 fillFields 更可靠。传入空 search('' 或 null)可清除字段(Shift+F4)。

search — 简单搜索使用字符串,按字段进行高级搜索使用 { field: value } 对象:

await selectValue('Организация', 'Конфетпром');
// result.selected = { field: 'Организация', search: 'Конфетпром', method: 'dropdown'|'form' }

// Per-field search (disambiguate by multiple columns):
await selectValue('Документ', { 'Номер': '0000-000601', 'Дата': '29.12.2016' }, { type: 'Реализация (акт' });

对于复合类型字段(可接受多种类型),指定 type 以先选择类型,再选择值:

await selectValue('Документ', '0000-000601', { type: 'Реализация (акт' });
// Clears field → opens type dialog → picks type via Ctrl+F → picks value from selection form
// result.selected = { field: 'Документ', search: '0000-000601', type: 'Реализация (акт', method: 'form' }

也支持 DCS 标签——会自动启用配对的复选框。

多选 — 传入一个数组即可一次选择多个值;字段将被设置为恰好包含这些值 (替换原有值)。返回 selected: { field, values, notSelected? }——values 是 实际选中的值;字段未提供的值会放入 notSelected: [{ value, reason }] (调用不会抛出异常)。元素格式与 search 相同(字符串或 { col: value })。

await selectValue('Наименование компании', ['Альфа ООО', 'Бета АО']);
// selected: { field: 'Наименование компании', values: ['Альфа ООО', 'Бета АО'] }
fillTableRow(fields, opts) → 带 filled(以及可选的 notFilled)的表单状态

通过 Tab 导航填充表格行单元格。值可以是纯字符串、用于复合类型单元格的 { value, type },或用于清除的 ''/null(Shift+F4)。

返回带有 filled: [{ field, ok, ...}] 的表单状态。成功时,条目格式为 { field, ok: true, method, value }(method:direct|paste|dropdown|form|type-direct|skip|clear|toggle);单字段失败时,格式为 { field, ok: false, error, message }。未匹配的字段会放入 notFilled: [...]。

与 fillFields 不同,fillTableRow 遇到单字段失败时不会抛出异常——错误会作为 filled[] 中的 ok: false 条目显示,以便调用方有针对性地处理(例如重试一个单元格,同时保留行中其他已填充的内容)。使用 r.filled.filter(f => !f.ok) 检查。错误代码:composite_type/type_required/type_dialog_failed(使用 {value, type} 重试);column_not_found(通过 readTable 检查列名);no_selection_form/no_selection_after_type(重试或回退到 selectValue);not_found/no_match/ambiguous(细化搜索文本);still_open(选中了分组——请选择叶级行)。来自 1C 的软验证错误(balloon、modal)仍会通过 exec-wrapper 抛出。

选项说明
tab填充前切换到相应标签页
add填充前添加新行
row编辑现有行:从 0 开始的索引,或用于按单元格值定位行的 { col: value } 筛选条件(一个或多个列)
scroll配合 row 筛选条件使用——扫描当前 DOM 窗口之外的内容(true = 最多 50 次 PageDown,数字 = 限制次数)
tabletables[] 中的网格名称(用于多网格表单)
// Add new row:
await fillTableRow(
  { 'Номенклатура': 'Бумага', 'Количество': '10', 'Цена': '100' },
  { tab: 'Товары', add: true }
);
// Edit existing row by index:
await fillTableRow(
  { 'Количество': '20' },
  { tab: 'Товары', row: 0 }
);
// Edit existing row located by cell values (одна или несколько колонок):
await fillTableRow({ 'Цена': '120' }, { table: 'Товары', row: { 'Номенклатура': 'Бумага' } });
await fillTableRow({ 'Сумма': '500' }, { row: { 'Номер': '0000-000601', 'Дата': '29.12.2016' }, scroll: true });
// Multi-grid form — add row to specific table:
await fillTableRow(
  { 'Объект': 'БДДС' },
  { table: 'Исходящие', add: true }
);
// Composite-type cell (e.g. SubConto accepting multiple types):
await fillTableRow(
  { 'СубконтоКт1': { value: 'Голованов', type: 'Физическое лицо' } },
  { tab: 'Проводки' }
);
  • 基于 Tab 的顺序导航——字段顺序由 1C 表单配置设置
  • 单元格模糊匹配:“Количество”可匹配“ТоварыКоличество”
  • 自动通过自动完成弹窗检测引用单元格
deleteTableRow(row, { tab?, table? }) → 表单状态

按从 0 开始的索引删除行。对于多网格表单,table 用于指定目标网格。

closeForm({ save? }) → 带有 closed 的表单状态

通过 Escape 关闭当前表单。返回带有 closed: true/false 的表单状态,用于指示表单是否确实已关闭。

参数行为
{ save: false }在确认对话框中自动点击“Нет”
{ save: true }在确认对话框中自动点击“Да”
{}(省略)如果出现对话框,则返回 confirmation 字段

closed — 如果表单已关闭(表单编号发生变化),则为 true;如果仍处于打开状态(例如,Escape 被忽略),则为 false。务必检查此值,以确认表单确实已关闭。关闭后,检查 formCount,查看还剩多少个表单。

优先使用此方法,而不是 clickElement('×')——标签页上的关闭按钮含义不明确。

filterList(text, opts?) → 表单状态

筛选列表。简单模式会搜索所有列;高级模式会针对特定字段进行筛选。

await filterList('КП00-000018');                          // simple — all columns
await filterList('Мишка', { field: 'Наименование' });     // advanced — specific column
await filterList('Мишка', { field: 'Наименование', exact: true }); // exact match

也适用于分层目录(会将视图扁平化)。

unfilterList({ field? }) → 表单状态

清除筛选条件。不带参数时清除全部筛选条件;使用 { field } 时清除指定标记。

实用工具
screenshot() → PNG Buffer
wait(seconds) → 表单状态
getPage() → Playwright Page(原始对象,供高级脚本使用)
startRecording(path, opts?) / stopRecording() → MP4 视频录制(如果已经在录制,可使用 { force: true } 重新开始)
showCaption(text, opts?) / hideCaption() → 页面上的文字叠加层
showTitleSlide(text, opts?) / hideTitleSlide() → 全屏标题卡片(片头/片尾)
isRecording() → boolean
setHighlight(on) / isHighlightMode() → 视频自动高亮模式
highlight(text) / unhighlight() → 手动高亮元素(错误时会列出可用元素)
addNarration(videoPath, opts?) → 添加 TTS 语音旁白的 MP4
getCaptions() → 上次录制的字幕时间戳

有关设置(ffmpeg)、高亮模式、TTS 旁白、API 详情和示例,请参阅 recording.md。 如果 .v8-project.json 中有 ffmpegPath,请将其传递给 startRecording({ ffmpegPath })。 如果 .v8-project.json 中有 tts 配置,请将整个对象作为选项传递给 addNarration()。

常见模式

创建并保存文档
await navigateSection('Продажи');
await openCommand('Заказы клиентов');
await clickElement('Создать');
await fillFields({ 'Организация': 'Конфетпром', 'Контрагент': 'Альфа' });
await fillTableRow({ 'Номенклатура': 'Бумага', 'Количество': '10' }, { tab: 'Товары', add: true });
await clickElement('Провести и закрыть');
从列表中打开项目
await clickElement('КП00-000227', { dblclick: true });
// Always use { dblclick: true } — single click only selects the row
使用分层列表
await filterList('Конфетпром');                               // flatten + search
await clickElement('Конфетпром ООО', { dblclick: true });     // open
await closeForm();
await unfilterList();                                          // restore hierarchy
生成并读取报表
// Fill report filters using readable labels
await fillFields({ 'Склад': 'Основной склад' });
await clickElement('Сформировать');
await wait(5);
const report = await readSpreadsheet();
console.log('Title:', report.title);
console.log('Data rows:', report.data?.length);
下钻查看报表单元格
// Generate report
await clickElement('Сформировать');
await wait(5);
const report = await readSpreadsheet();

// Double-click cell to open drill-down (uses coordinates from readSpreadsheet)
await clickElement({ row: 0, column: 'К6' }, { dblclick: true });
// Modal dialog "Выбор поля" opens
await clickElement('Регистратор');
await clickElement('Выбрать');
await wait(10);
const drilldown = await readSpreadsheet();
使用多网格表单

某些表单包含多个网格(例如,同一表单上的“Входящие”和“Исходящие”表格)。如果不指定 table,像“Добавить”这样的按钮会匹配到第一个结果,而 readTable 也会读取第一个网格——这可能不是你需要的网格。

步骤 1 — 通过 getFormState() 查找表格:

const form = await getFormState();
// form.tables = [
//   { name: "ДеревоБизнесПроцессов", columns: ["Полный код", "Бизнес-процесс"], rowCount: 21 },
//   { name: "Входящие", label: "Входящие", columns: ["Объект", "Бизнес-процесс источник", ...], rowCount: 1 },
//   { name: "Исходящие", label: "Исходящие", columns: ["Объект", "Бизнес-процесс приемник", ...], rowCount: 1 }
// ]

步骤 2 — 在任何网格操作中使用 table 名称:

// Read specific table
const t = await readTable({ table: 'Исходящие' });

// Add row — fillTableRow with add:true already clicks the right "Добавить" button
await fillTableRow({ 'Объект': 'БДДС' }, { table: 'Исходящие', add: true });

// Or click buttons separately
await clickElement('Добавить', { table: 'Входящие' });

// Delete from specific table
await deleteTableRow(0, { table: 'Исходящие' });

表格匹配支持技术名称(tables[].name)和可视标签(tables[].label)。标签是屏幕上显示的组标题,适用于根据截图进行操作。名称匹配的优先级高于标签匹配。

键盘快捷键
const page = await getPage();
await page.keyboard.press('F8');  // example: create new item in focused reference field
按键上下文操作
F8引用字段获得焦点时新建目录项
Shift+F4任意输入字段获得焦点时清除字段值(在 fillFields/selectValue/fillTableRow 中通过 ''/null 自动执行)
F4引用字段获得焦点时打开选择表单
Alt+F列表/表格表单打开高级搜索对话框
关闭表单——使用哪种方法
目标方法
过账并关闭文档clickElement('Провести и закрыть')
保存并关闭目录clickElement('Записать и закрыть')
不保存而关闭closeForm({ save: false })
保存并关闭closeForm({ save: true })
关闭(手动确认)closeForm() — 如果出现对话框,则返回 confirmation

Exec 响应格式

{ "ok": true, "output": "...console.log output...", "elapsed": 3.2 }

出错时(会自动截取屏幕截图):

{ "ok": false, "error": "Element not found", "output": "...", "screenshot": "error-shot.png", "elapsed": 1.5 }

run 超时时:

{ "ok": false, "error": "Script timed out after 600000ms (--timeout)", "output": "...", "timedOut": true, "elapsed": 600.1 }

避免循环

  • 每项操作最多尝试 2 次。 如果同一方法连续失败两次,请停止并向用户报告
  • 未找到就是未找到。 如果 filterList 返回 0 行,或筛选后 readTable 为空,该项可能不存在于此数据库中。不要用略有变化的条件重复搜索 5 次
  • 尝试不同的方法,而不是重复同一种方法。 无法通过分区导航找到?试试 navigateLink。无法通过简单搜索找到?试试使用特定字段进行高级搜索。但不要重复使用同一种方法
  • 报告部分结果。 如果找到了列表,却没找到特定项,请说明情况。展示当前已有内容,不要默默重试

重要说明

  • 有头模式 — 1C 需要显示浏览器,不能使用无头模式
  • 启动时间 — 1C 初次连接时需要加载 30-60s(已内置于 start)
  • 模糊匹配 — 所有名称查找均按以下顺序匹配:完全匹配 > 开头匹配 > 包含匹配
  • 剪贴板粘贴 — 所有文本字段均通过 Ctrl+V 填写(可正确触发 1C 事件)。每次操作前都会自动保存 OS 剪贴板,并在操作后恢复,因此本地用户的剪贴板内容在测试运行后仍会保留。可通过 --no-preserve-clipboard(任何命令)、WEB_TEST_PRESERVE_CLIPBOARD=0 环境变量,或在 webtest.config.mjs 中设置 preserveClipboard: false 关闭此功能
  • bash 中的西里尔字母 — 使用 cat <<'SCRIPT' | node $RUN exec - 以避免转义问题
  • 不换行空格 — 1C 使用 \u00a0 替代普通空格。所有匹配都会在内部进行规范化处理
  • 分区面板显示 — navigateSection() 适用于任何面板位置(侧边或顶部),但要求显示模式为“图片和文本”或“文本”。不支持仅显示图标的模式 — API 无法仅从图标读取分区名称

回归测试套件

当用户要求为 1C 解决方案编写自动化回归测试,涉及包含断言、钩子、标签、重试、Allure/JUnit 报告和多用户流程测试的多文件测试套件时,请切换到 test 模式。有关编写规范、侦察流程(元数据 + 通过 exec 进行实时演练)、各应用的文件夹布局、可直接粘贴的模板以及失败排查,请参阅 regress.md。单脚本自动化默认使用临时的 run/exec;test 是用于项目级覆盖的专用模式。

评论

支持 Markdown