关于 web-test
web-test 是面向 1C:Enterprise Web 客户端的浏览器自动化技能,适用于测试和操作业务界面。它基于 Playwright,支持一次性运行脚本或连接交互会话,并可读取表格与表单状态。
主要功能
- 01Playwright 自动化浏览器交互
- 02导航分区并打开命令
- 03读取表单、报表和表格
- 04填写字段并筛选列表
- 05支持一次性及交互执行
适用场景
- 01自动执行 1C Web 客户端测试场景
- 02读取业务表格和报表数据
- 03逐步调试表单操作脚本
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ nikolay-shirokov/ cc-1c-skills --skill web-test安装后在 Claude Code 中输入/web-test确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
.gitignorerecording.mdregress.mdscripts/browser.mjsdom.mjsscripts/cli/exec-context.mjsserver.mjssession.mjsutil.mjsscripts/cli/commands/exec.mjsrun.mjsshot.mjsstart.mjsstatus.mjsstop.mjstest.mjsscripts/cli/test-runner/assertions.mjscontext-pool.mjsdiscover.mjsreporters.mjsseverity.mjssuite-root.mjsscripts/dom/_shared.mjsedd.mjsedit-state.mjserrors-stack.mjserrors.mjsfilter.mjsform-state.mjsforms.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 }。
| 选项 | 默认值 | 说明 |
|---|---|---|
maxRows | 20 | 每次调用最多返回的行数 |
offset | 0 | 跳过前 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,数字 = 限制次数) |
table | tables[] 中的网格名称(用于多网格表单) |
// 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 是用于项目级覆盖的专用模式。