Compare commits

..
Author SHA1 Message Date
Nick ShirokovandClaude Opus 4.8 6d119eb473 feat(skd-edit): значение-список параметра в шортхенде (+skd-compile)
Значение по умолчанию у параметра СКД может быть списком (несколько <value>
подряд при valueListAllowed=true). Раньше задать список можно было только через
объектную модель skd-compile; шортхенд (add/modify-parameter, parameters) парсил
value= как скаляр.

Теперь в шортхенде: value=v1, v2, v3 задаёт список (кавычки '...' для запятой
внутри значения). Если задан список (>=2 элементов), valueListAllowed выводится
автоматически. Авто-вывод только в шортхенде — объектная модель остаётся
буквальной (bit-perfect round-trip сохранён).

skd-edit (ps1+py v1.25):
- Split-QuotedCsv/Parse-ValueList — токенайзер по запятым с учётом кавычек, БЕЗ
  разреза по ':' (важно для дат вида 2024-01-01T12:30:45)
- add-parameter: эмит N <value>
- modify-parameter: пред-выемка value=-списка, удаление ВСЕХ старых <value>,
  авто valueListAllowed; scalar value= теперь тоже схлопывает список в один <value>

skd-compile (ps1+py v1.105): тот же разбор списка в Parse-ParamShorthand;
объектная модель не тронута.

Документация: skd-edit/skd-compile SKILL.md (поведение), docs/1c-dcs-spec.md и
docs/skd-dsl-spec.md (формат).

Тесты: add-list, modify list<->scalar, список дат (двоеточия целы), compile-
шортхенд. Полный регресс 413/413 на ps1 и py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 12:26:57 +03:00
Nick ShirokovandClaude Opus 4.8 9877fe403a feat(skd-info): флаг -Raw для lossless round-trip извлечения запроса
skd-info -Mode query был просмотрщиком (заголовки, оглавление батчей,
разделители --- Batch ---) и терял разделители пакетов при split, поэтому
не годился как источник для skd-edit set-query @file.

Флаг -Raw отдаёт текст запроса целиком, verbatim, без декораций и без
дробления на пакеты — все ; и //// на месте. С -OutFile пишет чистый .sql,
который без потерь возвращается через set-query @file. Stdout не усекается
по -Limit. Версия v1.6 в обоих скриптах (ps1 + py).

Документация: таблица параметров/режимов и round-trip workflow в skd-info,
указатель + разводка patch-query vs set-query+-Raw в skd-edit.

Тесты: query-raw (raw без декораций, разделитель //// сохранён) и query-view
(просмотр не задет). Зелёные на ps1 и py.

Чистка: удалён modes-reference.md — галерея примеров вывода избыточна для
модели (инструмент самодемонстрирующийся), а человек покрыт docs/skd-guide.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 22:11:48 +03:00
Nick ShirokovandClaude Opus 4.8 46ee078343 docs(web-test): актуализация контракта test CLI (несколько путей, --url)
Спека §1, regress.md, README приведены в соответствие новому контракту:
сигнатура `test <dir|file>...`, несколько путей (дедуп + сортировка), флаг
--url=, заметка про резолв webtest.config.mjs/_hooks.mjs от каталога первого пути.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 20:07:09 +03:00
Nick ShirokovandClaude Opus 4.8 7f2bf9d2d3 feat(web-test): test CLI принимает несколько путей + флаг --url, fail-fast валидация
Привод контракта `run.mjs test` к общей практике (pytest/jest/playwright):
- позиционные аргументы = пути к тестам, можно несколько
  (`test a.test.mjs b.test.mjs dir/`); discoverTests объединяет, дедуплицирует,
  сортирует (порядок по числовым префиксам сохраняется);
- URL переехал из первого позиционного во флаг --url= (по умолчанию из
  webtest.config.mjs) — раньше url-первым путал: второй путь уходил в page.goto()
  и падал с криптовым 'Cannot navigate to invalid URL';
- fail-fast: несуществующий путь → понятная ошибка вместо падения goto;
  аргумент, похожий на URL, подсказывает про --url=.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 20:07:03 +03:00
Nick ShirokovandClaude Opus 4.8 31fa66d8fe test(web-test): регресс на readSpreadsheet до Сформировать + object-search selectValue
- 11-report: чтение несформированного отчёта бросает осмысленную ошибку,
  не ReferenceError (покрывает import checkForErrors в readSpreadsheet);
- 04-selectvalue: объектный поиск selectValue({Наименование}) выбирает через
  форму выбора (покрывает 3A guard + import filterList).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 17:41:06 +03:00
Nick ShirokovandClaude Opus 4.8 a8e61d02a2 fix(web-test): починка объектного поиска selectValue({field: value})
Три проблемы объектного поиска (следствие рефакторинга на модули):
- импорт filterList отсутствовал — pickFromSelectionForm (Шаг 2) бросал
  ReferenceError, который молча глотался catch'ем, поиск по полю не работал;
- dropdown-путь 3A падал на searchText.toLowerCase() (объект, не строка) —
  теперь объектный search уходит в форму выбора, где обрабатывается per-field;
- сужен catch вокруг filterList: ReferenceError/TypeError пробрасываются,
  чтобы будущие missing-import не маскировались как 'поле не найдено'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 17:41:00 +03:00
Nick ShirokovandClaude Opus 4.8 c8c0c48ead fix(web-test): импорт checkForErrors в readSpreadsheet
readSpreadsheet() в ветке allCells.size===0 (отчёт не сформирован) вызывал
checkForErrors() без импорта — падало с 'checkForErrors is not defined'
вместо осмысленного сообщения. Следствие рефакторинга на модули.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 17:40:52 +03:00
Nick ShirokovandClaude Opus 4.8 f1b61b9e9e test(web-test): фокус-клик по полю вместо fillFields для сброса viewport в 18-cell-click
Шаг focus-click пропуска чекбоксов выводил фокус из ТЧ через fillFields({Комментарий}),
что лишний раз перезаписывало значение. clickElement по полю «Комментарий» фокусирует
его без перезаполнения и так же сбрасывает горизонтальный viewport грида. Поведение
шага не меняется (читаются только булевы Товаров), тест зелёный.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 13:47:18 +03:00
Nick ShirokovandClaude Opus 4.8 9774b8f1c3 fix(web-test): fillTableRow распознаёт переформатированные число/дату в choice-ячейке
fillChoiceCell определял «прижился ли paste» через normYo(after).includes(text),
что ломалось на маск-инпутах: число/дата переформатируются (1234.56 → «1 234,56»,
группировка неразрывным пробелом, запятая) → includes давал false → ложный уход
в F4, где у числа открывался калькулятор и залипал (no_selection_form).

Заменил на поведенческий дискриминатор: появился EDD → ссылка (dropdown);
инпут изменился на непустое без EDD → редактируемая ячейка (direct); инпут
не изменился → НачалоВыбора → F4-форма. + страховка: если F4 открыл не форму
выбора (калькулятор/календарь) — Escape и спасение значения.

Также в EDD-ветке основного Tab-цикла убран слепой fallback items[0]: при
отсутствии exact/includes-совпадения возвращается not_found с очисткой поля,
а не подставляется произвольная первая запись автокомплита.

Регресс: в стенд (дерево) добавлены choice-колонки Число/Дата и булево-поле-ввода;
в 16-tree-form — шаги choice-number/choice-date/bool-input. Полный регресс: 22 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 13:42:34 +03:00
37 changed files with 1035 additions and 354 deletions
+9 -1
View File
@@ -152,11 +152,19 @@ Shorthand: `"Имя [Заголовок]: тип = значение @флаги"
Флаги shorthand:
- `@autoDates` — добавляет к параметру StandardPeriod пару дат `НачалоПериода`/`КонецПериода`, вычисляемых из него. Используй их в тексте запроса как `&НачалоПериода`/`&КонецПериода`; пользователь выбирает только сам период. По умолчанию сам параметр получает `use=Always` и `denyIncompleteValues=true` (чтобы производные даты всегда были заполнены); в объектной форме можно явно переопределить.
- `@valueList``<valueListAllowed>true</valueListAllowed>` — разрешает передавать список значений
- `@valueList``<valueListAllowed>true</valueListAllowed>` — разрешает передавать список значений (при значении-списке ниже подразумевается автоматически)
- `@hidden` — скрытый параметр: `availableAsField=false` + исключается из `"dataParameters": "auto"`
Объектная форма: `title`, `hidden: true`, `valueListAllowed: true`, `availableAsField: false`, `denyIncompleteValues: true`, `use: "Always"`.
Значение-список: несколько значений по умолчанию через запятую в `значение` (для запятой внутри значения — кавычки `'...'`). В объектной форме — массив в `value`.
```json
"parameters": [
"Виды: ChartOfCharacteristicTypesRef.ВидыСубконтоХозрасчетные = ПланВидовХарактеристик.ВидыСубконтоХозрасчетные.Контрагенты, ПланВидовХарактеристик.ВидыСубконтоХозрасчетные.Договоры"
]
```
Если значения по умолчанию нет — пропусти `=` в shorthand или укажи `"value": null` в объектной форме.
Список допустимых значений (availableValues):
@@ -1,4 +1,4 @@
# skd-compile v1.104 — Compile 1C DCS from JSON
# skd-compile v1.105 — Compile 1C DCS from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[string]$DefinitionFile,
@@ -475,6 +475,31 @@ function Parse-TotalShorthand {
# --- 7. Parameter shorthand parser ---
function Split-ValueListCsv {
# Split on top-level commas (respecting 'single'/"double" quotes), strip quotes,
# drop empties. No ':' handling — values may contain colons (dateTime).
param([string]$s)
$result = @()
if ($null -eq $s) { return ,$result }
$items = @()
$buf = New-Object System.Text.StringBuilder
$inQuote = $null
for ($i = 0; $i -lt $s.Length; $i++) {
$ch = $s[$i]
if ($inQuote) { [void]$buf.Append($ch); if ($ch -eq $inQuote) { $inQuote = $null } }
elseif ($ch -eq "'" -or $ch -eq '"') { $inQuote = $ch; [void]$buf.Append($ch) }
elseif ($ch -eq ',') { $items += $buf.ToString(); [void]$buf.Clear() }
else { [void]$buf.Append($ch) }
}
if ($buf.Length -gt 0) { $items += $buf.ToString() }
foreach ($raw in $items) {
$t = $raw.Trim()
if ($t.Length -ge 2 -and (($t[0] -eq "'" -and $t[-1] -eq "'") -or ($t[0] -eq '"' -and $t[-1] -eq '"'))) { $t = $t.Substring(1, $t.Length - 2) }
if ($t -ne "") { $result += $t }
}
return ,$result
}
function Parse-ParamShorthand {
param([string]$s)
@@ -509,7 +534,17 @@ function Parse-ParamShorthand {
$result.name = $Matches[1].Trim()
$result.type = Resolve-TypeStr ($Matches[2].Trim())
if ($Matches[4]) {
$result.value = $Matches[4].Trim()
$rhs = $Matches[4].Trim()
$items = Split-ValueListCsv $rhs
if ($items.Count -ge 2) {
# Multi-value default → list; valueListAllowed implied
$result.value = $items
$result.valueListAllowed = $true
} elseif ($items.Count -eq 1) {
$result.value = $items[0]
} else {
$result.value = $rhs
}
}
} else {
$result.name = $s.Trim()
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# skd-compile v1.104 — Compile 1C DCS from JSON
# skd-compile v1.105 — Compile 1C DCS from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
@@ -325,6 +325,39 @@ def parse_total_shorthand(s):
# --- Parameter shorthand parser ---
def split_value_list_csv(s):
"""Split on top-level commas (respecting single/double quotes), strip quotes,
drop empties. No ':' handling — values may contain colons (dateTime)."""
result = []
if s is None:
return result
items = []
buf = []
in_quote = None
for ch in s:
if in_quote:
buf.append(ch)
if ch == in_quote:
in_quote = None
elif ch in ("'", '"'):
in_quote = ch
buf.append(ch)
elif ch == ',':
items.append("".join(buf))
buf = []
else:
buf.append(ch)
if buf:
items.append("".join(buf))
for raw in items:
t = raw.strip()
if len(t) >= 2 and ((t[0] == "'" and t[-1] == "'") or (t[0] == '"' and t[-1] == '"')):
t = t[1:-1]
if t != "":
result.append(t)
return result
def parse_param_shorthand(s):
result = {'name': '', 'type': '', 'value': None, 'autoDates': False, 'title': None}
@@ -355,7 +388,16 @@ def parse_param_shorthand(s):
result['name'] = m.group(1).strip()
result['type'] = resolve_type_str(m.group(2).strip())
if m.group(4):
result['value'] = m.group(4).strip()
rhs = m.group(4).strip()
items = split_value_list_csv(rhs)
if len(items) >= 2:
# Multi-value default → list; valueListAllowed implied
result['value'] = items
result['valueListAllowed'] = True
elif len(items) == 1:
result['value'] = items[0]
else:
result['value'] = rhs
else:
result['name'] = s.strip()
+11 -1
View File
@@ -91,6 +91,13 @@ Shorthand: `"Имя [Заголовок]: тип = значение [availableVa
- `@autoDates` — генерирует пару скрытых параметров `ДатаНачала`/`ДатаОкончания` для StandardPeriod-параметра.
- `@hidden` — скрывает параметр от пользовательских настроек (для параметров-констант, используемых в запросе).
- `@always` — параметр всегда подставляется в запрос. Часто вместе с `@hidden`, но используется и отдельно (для видимых обязательных параметров типа отчётного периода).
- `@valueList` — разрешает передавать в параметр список значений (при значении-списке ниже подразумевается автоматически, отдельно указывать не обязательно).
Значение-список: несколько значений по умолчанию задаются через запятую в `значение`. Для запятой внутри одного значения — кавычки `'...'`.
```
"Виды [Виды субконто]: ChartOfCharacteristicTypesRef.ВидыСубконтоХозрасчетные = ПланВидовХарактеристик.ВидыСубконтоХозрасчетные.Контрагенты, ПланВидовХарактеристик.ВидыСубконтоХозрасчетные.Договоры"
```
```
"ПС: CatalogRef.Контрагенты = Справочник.Контрагенты.ПустаяСсылка @hidden"
@@ -115,6 +122,7 @@ Shorthand: `"ИмяПараметра [Заголовок] [ключ=значе
"ПериодОтчета [Отчетный период]" # только title
"ПорядокОкругления availableValue=Перечисление.Округления.Окр1: руб., Перечисление.Округления.Окр1000: тыс."
"СчетПС value=ПланСчетов.Хозрасчетный.КассаПредприятия"
"Виды value=ПланВидовХарактеристик.ВидыСубконтоХозрасчетные.Контрагенты, ПланВидовХарактеристик.ВидыСубконтоХозрасчетные.Договоры"
"Контрагент @hidden @always"
```
@@ -122,7 +130,7 @@ Shorthand: `"ИмяПараметра [Заголовок] [ключ=значе
`availableValue=` **заменяет весь список** допустимых значений (старые удаляются). Формат и кавычки — те же, что в `add-parameter`.
`value=` заменяет значение параметра (тип значения подбирается автоматически по объявленному типу параметра).
`value=` заменяет значение параметра. Несколько значений через запятую → **список значений** (заменяет все прежние); для запятой внутри значения — кавычки `'...'`.
Флаги `@hidden` / `@always` — те же, что и в `add-parameter`. Идемпотентны.
@@ -256,6 +264,8 @@ Value — имена ресурсов (как в полях/вычисляемы
Не поддерживает пакетный режим. Value — полный текст запроса или `@path/to/file.sql` (ссылка на внешний файл). Путь разрешается относительно Template.xml, затем CWD.
Когда что: **существенная переработка** (добавить поля, соединения, переписать пакет) → выгрузи запрос через `/skd-info <tpl> -Mode query -Name <набор> -Raw -OutFile file.sql`, отредактируй файл, верни `set-query @file`. `-Raw` отдаёт запрос целиком без декораций, поэтому выгрузка ↔ возврат точны (включая многопакетные запросы). **Точечная замена** (переименовать идентификатор, заменить подстроку) → выгрузка не нужна, используй `patch-query` ниже.
### patch-query — точечная замена в тексте запроса
Shorthand: `"старое => новое [@once]"`. По умолчанию заменяет все вхождения подстроки. Поддерживает пакетный режим и `-DataSet`.
+140 -24
View File
@@ -1,4 +1,4 @@
# skd-edit v1.24 — Atomic 1C DCS editor
# skd-edit v1.25 — Atomic 1C DCS editor
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -378,7 +378,19 @@ function Parse-ParamShorthand {
$hasEq = $null -ne $Matches[3]
$rhs = $Matches[4]
if ($hasEq) {
$result.value = if ($rhs) { $rhs.Trim() } else { "" }
if ($rhs -and $rhs.Trim()) {
$items = Parse-ValueList $rhs.Trim()
if ($items.Count -ge 2) {
# Multi-value default → list; valueListAllowed implied
$result.value = $items
$result.valueListAllowed = $true
} else {
# Scalar (single item, quotes stripped) or empty sentinel
$result.value = if ($items.Count -eq 1) { $items[0] } else { "" }
}
} else {
$result.value = ""
}
}
} else {
$result.name = $s.Trim()
@@ -679,16 +691,13 @@ function Parse-OutputParamShorthand {
return @{ key = $s.Trim(); value = "" }
}
function Parse-AvailableValueList {
# Returns array of @{ value=...; presentation=... } from comma-separated list.
# Items can use 'single' or "double" quotes (stripped). Quoted spans preserve commas/colons.
function Split-QuotedCsv {
# Splits on top-level commas, respecting 'single' and "double" quoted spans.
# Returns raw (un-stripped, un-trimmed) item spans. Used by both availableValue
# (value:presentation) and value-list (values only) parsing.
param([string]$s)
$result = @()
if (-not $s) { return ,$result }
# Tokenize by ',' respecting quoted spans
$items = @()
if ($null -eq $s) { return ,$items }
$buf = New-Object System.Text.StringBuilder
$inQuote = $null
for ($i = 0; $i -lt $s.Length; $i++) {
@@ -707,16 +716,42 @@ function Parse-AvailableValueList {
}
}
if ($buf.Length -gt 0) { $items += $buf.ToString() }
return ,$items
}
# For each item: split into value[:presentation], strip quotes
$stripQuotes = {
param($t)
$t = $t.Trim()
if ($t.Length -ge 2 -and (($t[0] -eq "'" -and $t[-1] -eq "'") -or ($t[0] -eq '"' -and $t[-1] -eq '"'))) {
return $t.Substring(1, $t.Length - 2)
}
return $t
function Strip-Quotes {
# Strips a single surrounding pair of matching quotes; trims first.
param([string]$t)
$t = $t.Trim()
if ($t.Length -ge 2 -and (($t[0] -eq "'" -and $t[-1] -eq "'") -or ($t[0] -eq '"' -and $t[-1] -eq '"'))) {
return $t.Substring(1, $t.Length - 2)
}
return $t
}
function Parse-ValueList {
# Returns array of value strings (quotes stripped) split by top-level commas.
# No ':' handling — values may contain colons (e.g. dateTime 2024-01-01T12:30:00).
param([string]$s)
$result = @()
if ($null -eq $s) { return ,$result }
foreach ($raw in (Split-QuotedCsv $s)) {
$v = Strip-Quotes $raw
if ($v -ne "") { $result += $v }
}
return ,$result
}
function Parse-AvailableValueList {
# Returns array of @{ value=...; presentation=... } from comma-separated list.
# Items can use 'single' or "double" quotes (stripped). Quoted spans preserve commas/colons.
param([string]$s)
$result = @()
if (-not $s) { return ,$result }
$items = Split-QuotedCsv $s
$stripQuotes = { param($t) Strip-Quotes $t }
foreach ($raw in $items) {
$item = $raw.Trim()
@@ -1135,7 +1170,14 @@ function Build-ParamFragment {
}
$vla = [bool]$parsed.valueListAllowed
if ($null -ne $parsed.value) {
$valIsArray = ($parsed.value -is [array]) -or ($parsed.value -is [System.Collections.IList] -and $parsed.value -isnot [string])
if ($valIsArray) {
# Multi-value default (value-list): one <value> per item
foreach ($v in $parsed.value) {
$valueLines = Build-ParamValueXml -type $parsed.type -value $v -indent "$i`t"
foreach ($vl in $valueLines) { $lines += $vl }
}
} elseif ($null -ne $parsed.value) {
if (Test-EmptyValue $parsed.value) {
$emptyXml = Build-EmptyValueXml -type $parsed.type -indent "$i`t" -tagPrefix "" -tagName "value" -valueListAllowed $vla
if ($emptyXml) { $lines += $emptyXml }
@@ -2292,6 +2334,20 @@ switch ($Operation) {
$avPart = $rest.Substring($avIdx)
}
# Separate a multi-value value=... (list) — kv-regex below grabs only a single
# \S+ token, so a comma-separated list (with spaces) wouldn't be captured.
# availableValue already peeled, so 'value=' here is the real value key.
$valueListItems = $null
$vlIdx = $simpleRest.IndexOf('value=')
if ($vlIdx -ge 0) {
$vlRhs = $simpleRest.Substring($vlIdx + 'value='.Length)
$cand = Parse-ValueList $vlRhs
if ($cand.Count -ge 2) {
$valueListItems = $cand
$simpleRest = $simpleRest.Substring(0, $vlIdx).Trim()
}
}
# Process simple key=value pairs (use, denyIncompleteValues, value, etc.)
if ($simpleRest) {
$kvPairs = [regex]::Matches($simpleRest, '(\w+)=(\S+)')
@@ -2337,14 +2393,20 @@ switch ($Operation) {
$fragXml = $valueLines -join "`n"
}
$wasExisting = ($null -ne $existing)
if ($existing) {
# Capture position by next-element sibling, then remove existing
$refNode = $existing.NextSibling
# Collect ALL existing <value> (a param may carry a value-list) — scalar
# value= collapses them to one, so remove every <value>, not just the first.
$allValueEls = @()
foreach ($ch in $paramEl.ChildNodes) {
if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'value' -and $ch.NamespaceURI -eq $schNs) { $allValueEls += $ch }
}
$wasExisting = ($allValueEls.Count -gt 0)
if ($wasExisting) {
# Capture position after the last existing value, then remove all
$refNode = $allValueEls[$allValueEls.Count - 1].NextSibling
while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) {
$refNode = $refNode.NextSibling
}
Remove-NodeWithWhitespace $existing
foreach ($ve in $allValueEls) { Remove-NodeWithWhitespace $ve }
} else {
# Insert before useRestriction/availableValue/denyIncompleteValues/use
$refNode = $null
@@ -2385,6 +2447,60 @@ switch ($Operation) {
}
}
# Process multi-value list (value=v1, v2, ...) — replace ALL <value>, ensure valueListAllowed=true
if ($valueListItems) {
# Declared type from <valueType>
$declaredType = ""
$vtEl = $null
foreach ($ch in $paramEl.ChildNodes) {
if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'valueType' -and $ch.NamespaceURI -eq $schNs) { $vtEl = $ch; break }
}
if ($vtEl) {
foreach ($tnode in $vtEl.ChildNodes) {
if ($tnode.NodeType -eq 'Element' -and $tnode.LocalName -eq 'Type') {
$declaredType = $tnode.InnerText.Trim() -replace '^d\d+p\d+:', ''
break
}
}
}
# Remove ALL existing <value>; capture insertion ref after the last one
$valueEls = @()
foreach ($child in $paramEl.ChildNodes) {
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq 'value' -and $child.NamespaceURI -eq $schNs) { $valueEls += $child }
}
$refNode = $null
if ($valueEls.Count -gt 0) {
$refNode = $valueEls[$valueEls.Count - 1].NextSibling
while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) { $refNode = $refNode.NextSibling }
foreach ($ve in $valueEls) { Remove-NodeWithWhitespace $ve }
} else {
foreach ($child in $paramEl.ChildNodes) {
if ($child.NodeType -eq 'Element' -and $child.LocalName -in @('useRestriction','availableValue','denyIncompleteValues','use')) { $refNode = $child; break }
}
}
foreach ($v in $valueListItems) {
$fragXml = (Build-ParamValueXml -type $declaredType -value $v -indent $childIndent) -join "`n"
$nodes = Import-Fragment $xmlDoc $fragXml
foreach ($node in $nodes) { Insert-BeforeElement $paramEl $node $refNode $childIndent }
}
# Ensure <valueListAllowed>true</valueListAllowed> (schema order: after useRestriction, before availableValue/use)
$vlaEl = $null
foreach ($ch in $paramEl.ChildNodes) {
if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'valueListAllowed' -and $ch.NamespaceURI -eq $schNs) { $vlaEl = $ch; break }
}
if ($vlaEl) {
if ($vlaEl.InnerText.Trim() -ne 'true') { $vlaEl.InnerText = 'true' }
} else {
$refVla = $null
foreach ($child in $paramEl.ChildNodes) {
if ($child.NodeType -eq 'Element' -and $child.LocalName -in @('availableValue','denyIncompleteValues','use')) { $refVla = $child; break }
}
$nodes = Import-Fragment $xmlDoc "$childIndent<valueListAllowed>true</valueListAllowed>"
foreach ($node in $nodes) { Insert-BeforeElement $paramEl $node $refVla $childIndent }
}
$script:Dirty = $true; Write-Host "[OK] Parameter `"$paramName`": value set to list of $($valueListItems.Count) item(s)"
}
# Process availableValue — replace whole list with new items
if ($avPart) {
$avRest = ($avPart -replace '^availableValue=', '').Trim()
+107 -21
View File
@@ -1,4 +1,4 @@
# skd-edit v1.24 — Atomic 1C DCS editor (Python port)
# skd-edit v1.25 — Atomic 1C DCS editor (Python port)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
@@ -371,7 +371,18 @@ def parse_param_shorthand(s):
result["name"] = m.group(1).strip()
result["type"] = resolve_type_str(m.group(2).strip())
if m.group(3) is not None:
result["value"] = m.group(4).strip() if m.group(4) else ""
rhs = m.group(4)
if rhs and rhs.strip():
items = parse_value_list(rhs.strip())
if len(items) >= 2:
# Multi-value default → list; valueListAllowed implied
result["value"] = items
result["valueListAllowed"] = True
else:
# Scalar (single item, quotes stripped) or empty sentinel
result["value"] = items[0] if len(items) == 1 else ""
else:
result["value"] = ""
else:
result["name"] = s.strip()
@@ -631,14 +642,12 @@ def parse_output_param_shorthand(s):
return {"key": s.strip(), "value": ""}
def parse_available_value_list(s):
"""Returns list of {value, presentation} from comma-separated list.
Items can use single/double quotes (stripped). Quoted spans preserve commas/colons."""
if not s:
return []
# Tokenize by ',' respecting quoted spans
def split_quoted_csv(s):
"""Split on top-level commas, respecting single/double quoted spans.
Returns raw (un-stripped) item spans. Shared by availableValue and value-list parsing."""
items = []
if s is None:
return items
buf = []
in_quote = None
for ch in s:
@@ -656,12 +665,37 @@ def parse_available_value_list(s):
buf.append(ch)
if buf:
items.append("".join(buf))
return items
def strip_quotes(t):
t = t.strip()
if len(t) >= 2 and ((t[0] == "'" and t[-1] == "'") or (t[0] == '"' and t[-1] == '"')):
return t[1:-1]
return t
def strip_quotes(t):
"""Strip a single surrounding pair of matching quotes; trims first."""
t = t.strip()
if len(t) >= 2 and ((t[0] == "'" and t[-1] == "'") or (t[0] == '"' and t[-1] == '"')):
return t[1:-1]
return t
def parse_value_list(s):
"""Return list of value strings (quotes stripped) split by top-level commas.
No ':' handling — values may contain colons (e.g. dateTime 2024-01-01T12:30:00)."""
if s is None:
return []
result = []
for raw in split_quoted_csv(s):
v = strip_quotes(raw)
if v != "":
result.append(v)
return result
def parse_available_value_list(s):
"""Returns list of {value, presentation} from comma-separated list.
Items can use single/double quotes (stripped). Quoted spans preserve commas/colons."""
if not s:
return []
items = split_quoted_csv(s)
result = []
for raw in items:
@@ -1012,7 +1046,12 @@ def build_param_fragment(parsed, indent):
lines.append(f"{i}\t</valueType>")
vla = bool(parsed.get("valueListAllowed"))
if parsed["value"] is not None:
if isinstance(parsed["value"], list):
# Multi-value default (value-list): one <value> per item
for v in parsed["value"]:
for vl in build_param_value_xml(parsed.get("type", ""), v, f"{i}\t"):
lines.append(vl)
elif parsed["value"] is not None:
if is_empty_value(parsed["value"]):
empty_xml = build_empty_value_xml(parsed.get("type", ""), f"{i}\t", "", "value", vla)
if empty_xml:
@@ -1987,6 +2026,17 @@ elif operation == "modify-parameter":
simple_rest = rest[:av_idx].strip()
av_part = rest[av_idx:]
# Separate a multi-value value=... (list) — kv-regex below grabs only a single
# \S+ token, so a comma-separated list (with spaces) wouldn't be captured.
value_list_items = None
vl_idx = simple_rest.find("value=")
if vl_idx >= 0:
vl_rhs = simple_rest[vl_idx + len("value="):]
cand = parse_value_list(vl_rhs)
if len(cand) >= 2:
value_list_items = cand
simple_rest = simple_rest[:vl_idx].strip()
# Process simple key=value pairs (use, denyIncompleteValues, etc.)
if simple_rest:
for m in re.finditer(r'(\w+)=(\S+)', simple_rest):
@@ -2012,12 +2062,15 @@ elif operation == "modify-parameter":
else:
value_lines = build_param_value_xml(declared_type, value, child_indent)
frag_xml = "\n".join(value_lines)
was_existing = existing is not None
if existing is not None:
# Find next-element sibling as ref before removing
idx = list(param_el).index(existing)
ref_node = param_el[idx + 1] if idx + 1 < len(param_el) else None
remove_node_with_whitespace(existing)
# Collect ALL existing <value> (a param may carry a value-list) — scalar
# value= collapses them to one, so remove every <value>, not just the first.
all_value_els = [ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) == "value" and etree.QName(ch.tag).namespace == SCH_NS]
was_existing = len(all_value_els) > 0
if was_existing:
last_idx = list(param_el).index(all_value_els[-1])
ref_node = param_el[last_idx + 1] if last_idx + 1 < len(param_el) else None
for ve in all_value_els:
remove_node_with_whitespace(ve)
else:
ref_node = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) in ("useRestriction", "availableValue", "denyIncompleteValues", "use")), None)
if frag_xml:
@@ -2040,6 +2093,39 @@ elif operation == "modify-parameter":
insert_before_element(param_el, node, ref_node, child_indent)
dirty = True; print(f'[OK] Parameter "{param_name}": {key}={value} added')
# Process multi-value list (value=v1, v2, ...) — replace ALL <value>, ensure valueListAllowed=true
if value_list_items:
declared_type = ""
vt_el = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) == "valueType" and etree.QName(ch.tag).namespace == SCH_NS), None)
if vt_el is not None:
for tnode in vt_el:
if isinstance(tnode.tag, str) and local_name(tnode) == "Type":
declared_type = re.sub(r'^d\d+p\d+:', '', (tnode.text or "").strip())
break
# Remove ALL existing <value>; capture insertion ref after the last one
value_els = [ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) == "value" and etree.QName(ch.tag).namespace == SCH_NS]
if value_els:
last_idx = list(param_el).index(value_els[-1])
ref_node = param_el[last_idx + 1] if last_idx + 1 < len(param_el) else None
for ve in value_els:
remove_node_with_whitespace(ve)
else:
ref_node = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) in ("useRestriction", "availableValue", "denyIncompleteValues", "use")), None)
for v in value_list_items:
frag_xml = "\n".join(build_param_value_xml(declared_type, v, child_indent))
for node in import_fragment(xml_doc, frag_xml):
insert_before_element(param_el, node, ref_node, child_indent)
# Ensure <valueListAllowed>true</valueListAllowed> (schema order: after useRestriction, before availableValue/use)
vla_el = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) == "valueListAllowed" and etree.QName(ch.tag).namespace == SCH_NS), None)
if vla_el is not None:
if (vla_el.text or "").strip() != "true":
vla_el.text = "true"
else:
ref_vla = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) in ("availableValue", "denyIncompleteValues", "use")), None)
for node in import_fragment(xml_doc, f"{child_indent}<valueListAllowed>true</valueListAllowed>"):
insert_before_element(param_el, node, ref_vla, child_indent)
dirty = True; print(f'[OK] Parameter "{param_name}": value set to list of {len(value_list_items)} item(s)')
# Process availableValue
if av_part:
av_rest = av_part[len("availableValue="):].strip()
+9 -4
View File
@@ -1,7 +1,7 @@
---
name: skd-info
description: Анализ структуры схемы компоновки данных 1С (СКД) — наборы, поля, параметры, варианты. Используй для понимания отчёта — источник данных (запрос), доступные поля, параметры
argument-hint: <TemplatePath> [-Mode overview|query|fields|links|calculated|resources|params|variant|templates|trace|full] [-Name <dataset|variant|field|group>]
argument-hint: <TemplatePath> [-Mode overview|query|fields|links|calculated|resources|params|variant|templates|trace|full] [-Name <dataset|variant|field|group>] [-Raw]
allowed-tools:
- Bash
- Read
@@ -20,7 +20,8 @@ allowed-tools:
| `Mode` | Режим анализа (по умолчанию `overview`) |
| `Name` | Имя набора (query), поля (fields/calculated/resources/trace), варианта (variant) или группировки/поля (templates) |
| `Batch` | Номер пакета запроса, 0 = все (только query) |
| `Limit` / `Offset` | Пагинация (по умолчанию 150 строк) |
| `Raw` | (только query) сырой текст запроса целиком, без заголовков/оглавления/разделителей пакетов. Для выгрузки в `.sql` и возврата через `skd-edit set-query @file` |
| `Limit` / `Offset` | Пагинация (по умолчанию 150 строк; `-Raw` не усекается) |
| `OutFile` | Записать результат в файл (UTF-8 BOM) |
```powershell
@@ -31,6 +32,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/skd-info.ps1" -Temp
```powershell
... -Mode query -Name НоменклатураСЦенами
... -Mode query -Name ДанныеТ13 -Batch 3
... -Mode query -Name ДанныеТ13 -Raw -OutFile query.sql
... -Mode fields -Name КадастроваяСтоимость
... -Mode calculated -Name КоэффициентКи
... -Mode resources -Name СуммаНалога
@@ -45,7 +47,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/skd-info.ps1" -Temp
| Режим | Без `-Name` | С `-Name` |
|-------|-------------|-----------|
| `overview` | Навигационная карта схемы + подсказки Next | — |
| `query` | — | Текст запроса набора (с оглавлением батчей) |
| `query` | — | Текст запроса набора (с оглавлением батчей); `-Raw` — чистая выгрузка для правки |
| `fields` | Карта: имена полей по наборам | Деталь поля: набор, тип, роль, формат |
| `links` | Все связи наборов | — |
| `calculated` | Карта: имена вычисляемых полей | Выражение + заголовок + ограничения |
@@ -65,7 +67,10 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/skd-info.ps1" -Temp
3. `query -Name <набор>` — посмотреть текст SQL-запроса
4. `variant -Name <N>` — посмотреть группировки и фильтры варианта
Подробные примеры вывода каждого режима — в `modes-reference.md`.
Переработка запроса (round-trip): `query -Name <набор> -Raw -OutFile q.sql`
правка `q.sql``/skd-edit <tpl> -Operation set-query -Value "@q.sql"`. Флаг
`-Raw` отдаёт запрос целиком без декораций, поэтому выгрузка ↔ возврат
точны (включая многопакетные запросы с временными таблицами).
## Верификация
-246
View File
@@ -1,246 +0,0 @@
# /skd-info — полная справка по режимам
Компактное описание — в [SKILL.md](SKILL.md).
## overview (по умолчанию) — карта схемы
Компактная навигационная карта (10-25 строк). Показывает структуру и подсказывает следующие шаги:
```
=== DCS: ОсновнаяСхемаКомпоновкиДанных (362 lines) ===
Sources: ИсточникДанных1 (Local)
Datasets:
[Query] НоменклатураСЦенами 7 fields, query 40 lines
Calculated: 1
Resources: 1
Templates: 1 templates, 1 group bindings
Params: (none)
Variants:
[1] НоменклатураИЦены "Номенклатура и цены" Table(detail) 3 filters
[2] НоменклатураБезЦен "Номенклатура без цен" Group(detail) 2 filters
Next:
-Mode query query text
-Mode fields field tables by dataset
-Mode calculated calculated field expressions
-Mode resources resource aggregation
-Mode variant -Name <N> variant structure (1..2)
```
Для DataSetUnion — дерево наборов + связи:
```
Datasets:
[Union] РасчетНалогаНаИмущество 52 fields
├─ [Query] РасчетНалогаНаИмущество 51 fields, query 181 lines
├─ [Query] ДанныеПоКадастровой 29 fields, query 40 lines
├─ [Query] ДанныеПоСреднегодовой 34 fields, query 41 lines
Links: РасчетНалогаНаИмущество -> СостояниеОС (2 fields)
```
Параметры разделяются на видимые/скрытые:
```
Params: 18 (7 visible, 11 hidden): Период, Ответственный, ...
```
## query — текст запроса
`-Name <набор>` — имя DataSet (обязателен если наборов > 1).
Извлекает raw-текст запроса с деэкранированием XML (`&amp;``&`, `&gt;``>`). Для пакетных запросов — оглавление батчей:
```
=== Query: ДанныеТ13 (334 lines, 13 batches) ===
Batch 1: lines 1-8 → ПОМЕСТИТЬ Представления_Периоды
Batch 2: lines 9-26 → ПОМЕСТИТЬ Представления_СотрудникиОрганизации
...
--- Batch 1 ---
ВЫБРАТЬ
ДАТАВРЕМЯ(1, 1, 1) КАК Период
ПОМЕСТИТЬ Представления_Периоды
...
```
Фильтр по номеру батча: `-Batch 3` покажет только 3-й пакет.
## fields — поля наборов данных
Без `-Name` — карта: имена полей по наборам:
```
=== Fields map ===
СостояниеОС [Query] (3): Организация, ОсновноеСредство, ДатаСостояния
РасчетНалогаНаИмущество [Union] (52): ДоляСтоимостиЧислитель, ...
РасчетНалогаНаИмущество [Query] (51): КадастроваяСтоимость, ...
```
С `-Name <поле>` — детали конкретного поля:
```
=== Field: ДатаСостояния "Дата ввода в эксплуатацию" ===
Dataset: СостояниеОС [Query]
Format: ДФ=dd.MM.yyyy
```
Показывает: dataset, title, type, role, useRestriction, format, presentationExpression.
## links — связи наборов данных
```
=== Links (4) ===
РасчетНалогаНаИмущество -> СостояниеОС :
Организация -> Организация
ОсновноеСредство -> ОсновноеСредство
```
Группирует по парам наборов. Показывает поля связи и параметры.
## calculated — вычисляемые поля
Без `-Name` — карта: имена и заголовки:
```
=== Calculated fields (23) ===
ДоляСтоимости "Доля стоимости"
КоэффициентКи "Коэффициент Ки"
...
```
С `-Name <поле>` — полное выражение:
```
=== Calculated: ДоляСтоимости ===
Expression:
ВЫБОР КОГДА ... ТОГДА "1" ИНАЧЕ ... КОНЕЦ
Title: Доля стоимости
Restrict: condition
```
## resources — ресурсы (итоги по группировкам)
Без `-Name` — карта: имена полей, `*` = есть формулы по группировкам:
```
=== Resources (51) ===
НалоговаяБаза
КоэффициентКи *
...
* = has group-level formulas
```
С `-Name <поле>` — формулы агрегации:
```
=== Resource: ДатаСостояния ===
[ОсновноеСредство] ЕстьNull(ДатаСостояния, "")
```
## params — параметры схемы
```
=== Parameters (16) ===
Name Type Default Visible Expression
Период StandardPeriod LastMonth yes -
НачалоПериода DateTime - hidden &Период.ДатаНачала
Организация CatalogRef.Организации null yes -
```
## variant — варианты отчёта
Без `-Name` — список вариантов:
```
=== Variants (2) ===
[1] НоменклатураИЦены "Номенклатура и цены" Table(detail) 3 filters
[2] НоменклатураБезЦен "Номенклатура без цен" Group(detail) 2 filters
```
С `-Name <N|имя>` — структура конкретного варианта:
```
=== Variant [1]: НоменклатураИЦены "Номенклатура и цены" ===
Structure:
Table "Таблица"
├── Columns: [ТипЦен Items]
│ Selection: Auto, Цена
└── Rows: [Номенклатура Items]
Selection: Номенклатура, УИД, Auto
Filter:
[ ] Номенклатура InHierarchy [user]
[ ] ТипЦен Equal
[x] ВАрхиве = false "Исключая скрытые товары"
DataParams: КлючВарианта="НоменклатураИЦены"
Output: style=ЧерноБелый groups=Separately totalsH=None totalsV=None
```
## templates — привязки шаблонов вывода
Три типа привязок: `fieldTemplate` (к полю), `groupTemplate` (к группировке, Header/Footer), `groupHeaderTemplate` (заголовок группы).
Без `-Name` — карта привязок:
```
=== Templates (70 defined: 49 field, 37 group) ===
Field bindings (49): (all trivial)
ОстаточнаяСтоимостьНа0101, ОстаточнаяСтоимостьНа0102, ...
Group bindings (37):
ВидНалоговойБазы
Header -> Макет3 (1 rows, 1 params)
СреднегодоваяСтоимость2019
Footer -> Макет50 (1 rows) spacer
GroupHeader -> Макет40 (3 rows)
```
С `-Name <группировка|поле>` — содержимое шаблонов:
```
=== Templates: СреднегодоваяСтоимость2019 ===
Footer -> Макет50 [1 rows, 1 cells]:
Row 1: (empty)
GroupHeader -> Макет40 [3 rows, 78 cells]:
Row 1: "№ п/п" | "###Группировки1###" | "Инв. номер" | ...
Row 2: "01.01" | "01.02" | ... | "31.12"
Row 3: "1" | "2" | ... | "26"
```
Для field-привязок:
```
=== Field template: ОстаточнаяСтоимостьНа0101 -> Макет4 ===
[1 rows, 1 cells]
Row 1: {ОстаточнаяСтоимостьНа0101}
(all params trivial)
```
**Тривиальность выражений**: `Поле = Поле` и `Поле = Представление(Поле)` считаются тривиальными и НЕ выводятся. Показываются только нетривиальные — когда выражение содержит другое поле, вызов метода, пустую строку и т.д.
## trace — трассировка поля от заголовка до запроса
Ищет поле по dataPath ИЛИ заголовку (включая подстроку) и показывает полную цепочку происхождения за один вызов:
```
=== Trace: КоэффициентКи "Коэффициент Ки" ===
Dataset: (schema-level only, not in dataset fields)
Calculated:
ВЫБОР КОГДА ... ТОГДА 0 ИНАЧЕ ... КОНЕЦ
Operands:
КоличествоМесяцевИспользования -> РасчетНалогаНаИмущество [Query]
КоличествоМесяцевВладения -> РасчетНалогаНаИмущество [Query]
Resource:
[ОсновноеСредство] Сумма(КоэффициентКи)
```
Типичный сценарий: пользователь видит колонку "Коэффициент Ки" в отчёте и спрашивает как она считается. Один вызов `trace` показывает: формулу вычисления, откуда берутся операнды, как агрегируется в ресурс.
## Что не выводится
- XML namespace-декларации
- Обёртки v8:item/v8:lang/v8:content (извлекаем чистый текст)
- userSettingID (GUID-ы пользовательских настроек)
- Дефолтные periodAdditionBegin/End = 0001-01-01
- viewMode
+11 -3
View File
@@ -1,4 +1,4 @@
# skd-info v1.5 — Analyze 1C DCS structure
# skd-info v1.6 — Analyze 1C DCS structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory=$true)]
@@ -10,7 +10,8 @@ param(
[int]$Batch = 0,
[int]$Limit = 150,
[int]$Offset = 0,
[string]$OutFile
[string]$OutFile,
[switch]$Raw
)
$ErrorActionPreference = "Stop"
@@ -655,6 +656,13 @@ function Show-Query {
}
$rawQuery = Unescape-Xml $queryNode.InnerText
# Raw mode: emit verbatim query text only (no headers/TOC/batch split) for round-trip
if ($Raw) {
foreach ($ql in ($rawQuery.Trim() -split "`n")) { $lines.Add($ql.TrimEnd()) }
return
}
$dsNameStr = $targetDs.SelectSingleNode("s:name", $ns).InnerText
# Split into batches
@@ -1894,7 +1902,7 @@ if ($Offset -gt 0) {
$result = $result[$Offset..($totalLines - 1)]
}
if ($result.Count -gt $Limit) {
if (-not $Raw -and $result.Count -gt $Limit) {
$shown = $result[0..($Limit - 1)]
foreach ($l in $shown) { Write-Host $l }
Write-Host ""
+10 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# skd-info v1.5 — Analyze 1C DCS structure
# skd-info v1.6 — Analyze 1C DCS structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -278,6 +278,7 @@ def main():
parser.add_argument("-Limit", type=int, default=150)
parser.add_argument("-Offset", type=int, default=0)
parser.add_argument("-OutFile", default=None)
parser.add_argument("-Raw", action="store_true")
args = parser.parse_args()
# --- Resolve path ---
@@ -634,6 +635,13 @@ def main():
sys.exit(1)
raw_query = unescape_xml("".join(query_node.itertext()))
# Raw mode: emit verbatim query text only (no headers/TOC/batch split) for round-trip
if args.Raw:
for ql in raw_query.strip().split("\n"):
lines.append(ql.rstrip())
return
ds_name_str = (target_ds.find("s:name", NSMAP).text or "")
# Split into batches
@@ -1719,7 +1727,7 @@ def main():
sys.exit(0)
result = result[args.Offset:]
if len(result) > args.Limit:
if not args.Raw and len(result) > args.Limit:
shown = result[:args.Limit]
for line in shown:
print(line)
+4 -1
View File
@@ -5,9 +5,11 @@ Use this when the user asks to cover a 1C solution with automated regression tes
The runner is the same `run.mjs`. The mode is `test`:
```bash
node $RUN test [url] <dir|file> [flags]
node $RUN test <dir|file>... [flags]
```
Positional args are test paths (files and/or dirs, multiple allowed). URL is NOT positional — it comes from `webtest.config.mjs`; override with `--url=<url>`.
Tests live next to the project they cover (not inside the skill). Convention: `tests/` at the project root, with `_hooks.mjs` and `webtest.config.mjs` at the suite root. Tests are ES modules with `*.test.mjs` suffix.
## When to choose `test` over `exec`
@@ -362,6 +364,7 @@ Each `params` entry becomes its own test in the report. `{key}` placeholders in
node $RUN test tests/<app-name>/ # full app suite
node $RUN test tests/<app-name>/03-goods-receipt/ # one feature folder
node $RUN test tests/<app-name>/02-counterparties/01-create.test.mjs # one file
node $RUN test tests/<app-name>/02-x.test.mjs tests/<app-name>/05-y.test.mjs # several files
node $RUN test tests/<app-name>/ --tags=smoke # by tag (intersection)
node $RUN test tests/<app-name>/ --grep='накладн' # by name regex
node $RUN test tests/<app-name>/ --bail --retry=1 # stop on first fail, allow 1 retry
@@ -1,4 +1,4 @@
// web-test cli/commands/test v1.2 — regression test runner
// web-test cli/commands/test v1.3 — regression test runner
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import { existsSync, writeFileSync, mkdirSync } from 'fs';
import { resolve, dirname, basename, relative } from 'path';
@@ -20,11 +20,12 @@ export async function cmdTest(rawArgs) {
// Parse flags
const opts = { bail: false, retry: 0, timeout: 30000, report: null, format: 'json', screenshot: null, reportDir: null, record: false };
let tags = null, grep = null;
let tags = null, grep = null, urlFlag = null;
const positional = [];
for (const a of ownArgs) {
if (a.startsWith('--tags=')) tags = a.slice(7).split(',');
else if (a.startsWith('--grep=')) grep = new RegExp(a.slice(7), 'i');
else if (a.startsWith('--url=')) urlFlag = a.slice(6);
else if (a === '--bail') opts.bail = true;
else if (a.startsWith('--retry=')) opts.retry = parseInt(a.slice(8)) || 0;
else if (a.startsWith('--timeout=')) opts.timeout = parseInt(a.slice(10)) || 30000;
@@ -36,20 +37,28 @@ export async function cmdTest(rawArgs) {
else if (!a.startsWith('--')) positional.push(a);
}
// Determine URL and test path
let url, testPath;
if (positional.length === 2) {
url = positional[0];
testPath = resolve(positional[1]);
} else if (positional.length === 1) {
testPath = resolve(positional[0]);
} else {
die('Usage: node run.mjs test [url] <dir|file> [--tags=...] [--bail] [--retry=N] [--timeout=ms] [--report=path]');
// Positional args are ALWAYS test paths (one or many). URL comes from --url= or config
// (see webtest.config.mjs). This matches pytest/jest/playwright; a positional that looks
// like a URL is a mistake → fail fast with a hint instead of feeding it to page.goto().
const isUrl = (s) => /^https?:\/\//i.test(s);
let url = urlFlag || null;
const testPaths = [...positional];
if (testPaths.length === 0) {
die('Usage: node run.mjs test <dir|file>... [--url=URL] [--tags=...] [--grep=...] [--bail] [--retry=N] [--timeout=ms] [--report=path]');
}
for (const p of testPaths) {
if (existsSync(resolve(p))) continue;
if (isUrl(p)) {
die(`"${p}" looks like a URL — use --url=<url>; positional args are test paths.`);
}
die(`Test path not found: "${p}". To run a subset use --grep= / --tags=, or pass an existing dir/file.`);
}
// Load config if exists
const isFile = testPath.endsWith('.test.mjs');
const testDir = isFile ? dirname(testPath) : testPath;
// Load config if exists. config (webtest.config.mjs) and hooks (_hooks.mjs) resolve from
// the FIRST path's directory — list paths from the same suite folder.
const firstPath = resolve(testPaths[0]);
const isFile = firstPath.endsWith('.test.mjs');
const testDir = isFile ? dirname(firstPath) : firstPath;
const configPath = resolve(testDir, 'webtest.config.mjs');
let config = {};
if (existsSync(configPath)) {
@@ -110,8 +119,8 @@ export async function cmdTest(rawArgs) {
}
// Discover test files
const testFiles = discoverTests(testPath);
if (!testFiles.length) die(`No *.test.mjs files found in ${testPath}`);
const testFiles = discoverTests(testPaths);
if (!testFiles.length) die(`No *.test.mjs files found in ${testPaths.join(', ')}`);
// Import and filter tests
const tests = [];
@@ -1,10 +1,14 @@
// web-test cli/test-runner/discover v1.0 — test file discovery + state reset between tests
// web-test cli/test-runner/discover v1.1 — test file discovery + state reset between tests
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import { existsSync, readdirSync } from 'fs';
import { resolve } from 'path';
export function discoverTests(testPath) {
if (testPath.endsWith('.test.mjs')) return existsSync(testPath) ? [testPath] : [];
// Accepts a single path or an array of paths (files and/or dirs). Each .test.mjs file is
// taken directly; each directory is walked recursively (skipping _ / . prefixes). Results
// are deduped and sorted — sorting preserves the numeric-prefix order the suite relies on
// (00-, 01-, …) even when paths are listed out of order.
export function discoverTests(testPaths) {
const paths = Array.isArray(testPaths) ? testPaths : [testPaths];
const files = [];
function walk(dir) {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
@@ -14,8 +18,15 @@ export function discoverTests(testPath) {
else if (entry.name.endsWith('.test.mjs')) files.push(full);
}
}
walk(testPath);
return files.sort();
for (const p of paths) {
const full = resolve(p);
if (full.endsWith('.test.mjs')) {
if (existsSync(full)) files.push(full);
} else if (existsSync(full)) {
walk(full);
}
}
return [...new Set(files)].sort();
}
export async function resetState(ctx) {
+3 -2
View File
@@ -1,4 +1,4 @@
// web-test cli/util v1.1 — generic helpers for CLI commands
// web-test cli/util v1.2 — generic helpers for CLI commands
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
export function out(obj) {
@@ -85,7 +85,7 @@ Commands:
shot [file] Take screenshot (default: shot.png)
stop Logout and close browser
status Check session status
test [url] <dir|file> Run regression tests (*.test.mjs)
test <dir|file>... Run regression tests (*.test.mjs); accepts multiple paths
Options for exec:
--no-record Skip video recording (record() becomes no-op)
@@ -95,6 +95,7 @@ Global options (any command):
Default: on (env: WEB_TEST_PRESERVE_CLIPBOARD=0 to disable globally).
Options for test:
--url=URL Override the base URL (default: from webtest.config.mjs)
--tags=smoke,crud Filter tests by tags
--grep=pattern Filter tests by name (regex)
--bail Stop on first failure
@@ -1,4 +1,4 @@
// web-test forms/select-value v1.22 — Reference & composite-type value selection: selectValue, fillReferenceField, selection/type-dialog pickers.
// web-test forms/select-value v1.24 — Reference & composite-type value selection: selectValue, fillReferenceField, selection/type-dialog pickers.
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import {
@@ -22,6 +22,7 @@ import {
} from '../core/helpers.mjs';
import { pasteText } from '../core/clipboard.mjs';
import { getFormState } from './state.mjs';
import { filterList } from '../table/filter.mjs';
/**
* Scan visible grid rows for a text match (exact startsWith includes).
@@ -165,7 +166,15 @@ export async function pickFromSelectionForm(selFormNum, fieldName, search, origF
if (typeof search === 'object' && search) {
// Per-field advanced search via filterList(val, {field})
for (const [fld, val] of Object.entries(search)) {
try { await filterList(String(val), { field: fld }); } catch { /* proceed */ }
try {
await filterList(String(val), { field: fld });
} catch (e) {
// Re-throw programming errors (e.g. a missing import surfacing as
// ReferenceError) — only field-filter failures (not found / unsupported
// column) should be swallowed so we fall through to the re-scan.
if (e instanceof ReferenceError || e instanceof TypeError) throw e;
/* proceed */
}
}
} else if (searchLower) {
// Inline advanced search (Alt+F, "по части строки")
@@ -736,6 +745,21 @@ export async function selectValue(fieldName, searchText, { type } = {}) {
const regularItems = popupItems.filter(i => i.kind !== 'showAll');
const showAllItem = popupItems.find(i => i.kind === 'showAll');
if (searchText && typeof searchText !== 'string') {
// Object search ({field: value}) can't be matched against dropdown item
// text — close the typeahead popup and open the full selection form, which
// handles per-field advanced search (pickFromSelectionForm → filterList).
await page.keyboard.press('Escape');
await page.waitForTimeout(300);
const inputId = await findFieldInputId(formNum, btn.fieldName);
if (inputId) { await page.click(`[id="${inputId}"]`); await page.waitForTimeout(300); }
await page.keyboard.press('F4');
await page.waitForTimeout(ACTION_WAIT);
const formResult = await openFormAndPick();
if (formResult) return formResult;
throw new Error(`selectValue: object search ${JSON.stringify(searchText)} for "${btn.fieldName}" did not open a selection form`);
}
if (searchText) {
const target = normYo(searchText.toLowerCase());
// Try to find match among regular dropdown items
@@ -1,4 +1,4 @@
// web-test spreadsheet v1.19 — readSpreadsheet + helpers for SpreadsheetDocument (отчёты, печатные формы).
// web-test spreadsheet v1.20 — readSpreadsheet + helpers for SpreadsheetDocument (отчёты, печатные формы).
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import { page, ensureConnected } from '../core/state.mjs';
@@ -7,6 +7,7 @@ import { waitForStable } from '../core/wait.mjs';
import { getFormState } from '../forms/state.mjs';
import { returnFormState } from '../core/helpers.mjs';
import { scrollHorizontallyByKey } from '../core/scroll-horiz.mjs';
import { checkForErrors } from '../core/errors.mjs';
// --- Spreadsheet helpers (shared by readSpreadsheet and clickElement) ---
@@ -1,4 +1,4 @@
// web-test table/row-fill v1.22 — fillTableRow — заполнение строки табличной части/списка через Tab-навигацию и попутный выбор значений.
// web-test table/row-fill v1.23 — fillTableRow — заполнение строки табличной части/списка через Tab-навигацию и попутный выбор значений.
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import {
@@ -58,15 +58,46 @@ async function fillChoiceCell(formNum, text, { type = null, fieldLabel = '' } =
if (before && norm(before).includes(norm(text))) {
return { ok: true, method: 'skip', value: before };
}
// Try direct input; poll for the input value to settle on the pasted text (editable cell).
// Paste, then poll. Three outcomes, distinguished BEHAVIORALLY (not by value equality):
// (1) EDD autocomplete appears → reference/list cell → pick from the dropdown;
// (2) input changes to non-empty, no EDD → editable cell → leave value, method 'direct';
// (3) input unchanged (rejected) → НачалоВыбора pick-from-list → F4 selection form.
// A value-equality check on `after` is UNRELIABLE: numeric/date masks reformat the pasted
// text (grouping nbsp, decimal comma, padding) — e.g. "1234.56" → "1 234,56", "0,000"
// baseline. So we test "did the input change to non-empty" + "no autocomplete", never
// "does after contain text" (that false-negatives on reformatting → F4 → stray calculator).
await pasteText(text, { confirm: ['Control+a', 'Control+v'] });
let after = before, stuck = false;
let after = before, changed = false, eddSeen = false;
for (let i = 0; i < 6; i++) {
await page.waitForTimeout(100);
if (await isEddVisible()) { eddSeen = true; break; }
after = await page.evaluate(`document.activeElement?.value || ''`);
if (after !== before && norm(after).includes(norm(text))) { stuck = true; break; }
if (after !== before && after !== '') changed = true;
}
if (eddSeen) {
// Reference/list cell — pick a MATCHING item from the autocomplete. Only accept an
// exact (parenthetical-stripped) or substring match; never blind-pick items[0] — for a
// non-existent value 1C still lists unrelated entries, and picking the first silently
// writes the wrong reference. No match → fall through to the F4 selection form, which
// searches the full list and returns not_found if the value is truly absent.
const edd = await readEdd();
const items = (edd.items || []).map(i => i.name)
.filter(i => !/^Создать[\s:]/.test(i) && !/не найдено/i.test(i) && !/показать все/i.test(i));
const tgt = norm(text);
const pick = items.find(i => norm(i.replace(/\s*\([^)]*\)\s*$/, '')) === tgt)
|| items.find(i => norm(i).includes(tgt));
if (pick) {
await clickEddItemViaDispatch(pick);
await waitForStable();
return { ok: true, method: 'dropdown', value: pick.replace(/\s*\([^)]*\)\s*$/, '') };
}
// No matching item — dismiss the autocomplete and fall through to the F4 selection form.
await page.keyboard.press('Escape'); await page.waitForTimeout(200);
} else if (changed) {
// Editable cell — value lives in the INPUT; caller's Tab / end-of-row commit persists it.
return { ok: true, method: 'direct', value: after };
}
if (stuck) return { ok: true, method: 'direct', value: text };
// Text rejected (pick-from-list cell) — nothing typed to clear (field is not text-editable).
// Dismiss any autocomplete hint, then open the choice form via F4.
@@ -79,6 +110,15 @@ async function fillChoiceCell(formNum, text, { type = null, fieldLabel = '' } =
if (choiceForm !== null) break;
}
if (choiceForm === null) {
// F4 safety net: on an editable numeric/date cell mis-routed here, F4 opens a
// calculator/calendar (NOT a selection form). Close it — never leave the popup open
// (it blocks the UI) — and salvage: if the cell now holds a value, count it as 'direct'.
if (await findOpenPopup()) {
await page.keyboard.press('Escape');
for (let dw = 0; dw < 4; dw++) { await page.waitForTimeout(150); if (!(await findOpenPopup())) break; }
const nowVal = await page.evaluate(`document.activeElement?.value || ''`);
if (nowVal && nowVal !== before) return { ok: true, method: 'direct', value: nowVal };
}
return { ok: false, error: 'no_selection_form', message: `Cell "${fieldLabel || text}": F4 did not open a choice form` };
}
if (await isTypeDialog(choiceForm)) {
@@ -681,14 +721,28 @@ export async function fillTableRow(fields, { tab, add, row, table, scroll } = {}
let pick = realItems.find(i =>
normYo(i.replace(/\s*\([^)]*\)\s*$/, '').toLowerCase()) === tgt);
if (!pick) pick = realItems.find(i => normYo(i.toLowerCase()).includes(tgt));
if (!pick) pick = realItems[0];
// Click EDD item via dispatchEvent (bypasses div.surface overlay)
await clickEddItemViaDispatch(pick);
await waitForStable();
info.filled = true;
results.push({ field: matchedKey, cell: cell.fullName, ok: true,
method: 'dropdown', value: pick.replace(/\s*\([^)]*\)\s*$/, '') });
if (pick) {
// Click EDD item via dispatchEvent (bypasses div.surface overlay)
await clickEddItemViaDispatch(pick);
await waitForStable();
info.filled = true;
results.push({ field: matchedKey, cell: cell.fullName, ok: true,
method: 'dropdown', value: pick.replace(/\s*\([^)]*\)\s*$/, '') });
} else {
// EDD listed items but NONE matches the requested value. Do NOT blind-pick the
// first item — when the typed text has no hit, 1C still shows unrelated entries
// (recent/full list), so items[0] would silently write the wrong reference.
// Dismiss, clear the typed text, report not_found.
await page.keyboard.press('Escape');
await page.waitForTimeout(300);
await page.keyboard.press('Control+A');
await page.keyboard.press('Delete');
await page.waitForTimeout(200);
info.filled = true;
results.push({ field: matchedKey, cell: cell.fullName, ok: false,
error: 'not_found', message: `No match for "${text}" in autocomplete` });
}
} else {
// Only "Создать:" items — value not found in autocomplete
await page.keyboard.press('Escape');
+2 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env node
// web-test run v1.17 — CLI entry-point (распилено по cli/)
// web-test run v1.18 — CLI entry-point (распилено по cli/)
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
/**
* CLI runner for 1C web client automation.
@@ -14,7 +14,7 @@
* node src/run.mjs shot [file] take screenshot
* node src/run.mjs stop logout + close browser
* node src/run.mjs status check session
* node src/run.mjs test [url] <dir|file> run regression tests
* node src/run.mjs test <dir|file>... [--url] run regression tests
*
* Внутренности живут в cli/: util, session, exec-context, server,
* commands/{start,run,exec,shot,stop,status,test}, test-runner/*.
+20
View File
@@ -530,6 +530,26 @@ DataCompositionSchema
Стандартные варианты периодов (`v8:StandardPeriodVariant`): `Custom`, `Today`, `ThisWeek`, `ThisMonth`, `ThisQuarter`, `ThisYear`, `LastMonth`, `LastQuarter`, `LastYear` и др.
#### Значение-список (несколько значений по умолчанию)
Значением параметра может быть список — несколько элементов `<value>` подряд внутри
`<parameter>`, при `<valueListAllowed>true</valueListAllowed>`:
```xml
<parameter>
<name>ВидыСубконто</name>
<valueType>
<v8:Type xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config">d5p1:ChartOfCharacteristicTypesRef.ВидыСубконтоХозрасчетные</v8:Type>
</valueType>
<value xsi:type="dcscor:DesignTimeValue">ПланВидовХарактеристик.ВидыСубконтоХозрасчетные.Контрагенты</value>
<value xsi:type="dcscor:DesignTimeValue">ПланВидовХарактеристик.ВидыСубконтоХозрасчетные.Договоры</value>
<useRestriction>true</useRestriction>
<valueListAllowed>true</valueListAllowed>
</parameter>
```
Порядок элементов: `name, title, valueType, value*, useRestriction, …, valueListAllowed`.
---
## 9. Макеты областей (template)
+6
View File
@@ -339,6 +339,12 @@ XML-маппинг — по `<group>` на каждый элемент:
**Парсинг:** `"A: T = V"``name=A`, `type=T`, `value=V`. Значение `LastMonth` и другие варианты периодов → `v8:StandardPeriod` с `v8:variant`.
`<default>` может быть **списком** — несколько значений через запятую (с `'...'` для запятой внутри значения). В этом случае эмитятся несколько `<value>`, а `valueListAllowed=true` выводится автоматически (явный `@valueList` не нужен). Эквивалент объектной формы `"value": [ ... ]`.
```json
"parameters": ["Виды: ChartOfCharacteristicTypesRef.ВидыСубконтоХозрасчетные = ПланВидовХарактеристик.ВидыСубконтоХозрасчетные.Контрагенты, ПланВидовХарактеристик.ВидыСубконтоХозрасчетные.Договоры"]
```
### @autoDates
Флаг `@autoDates` в shorthand параметра автоматически генерирует два дополнительных параметра:
+6 -2
View File
@@ -12,11 +12,14 @@
## 1. Командная строка
```
node run.mjs test [url] <dir|file> [флаги]
node run.mjs test <dir|file>... [флаги]
```
Позиционные аргументы — это пути к тестам (файлы `*.test.mjs` и/или каталоги), можно указать несколько: `node run.mjs test a.test.mjs b.test.mjs dir/`. Файлы из каталогов обходятся рекурсивно; итоговый набор дедуплицируется и сортируется (порядок по числовым префиксам `00-`, `01-`, … сохраняется независимо от порядка аргументов). Путь, которого нет на диске, → ранняя понятная ошибка (аргумент, похожий на URL, подскажет про `--url=`).
| Флаг | По умолчанию | Описание |
|------|-------------|----------|
| `--url=URL` | (из конфига) | Переопределить базовый URL дефолтного контекста |
| `--tags=smoke,crud` | (все) | Фильтр тестов по тегам (пересечение) |
| `--grep=pattern` | (все) | Фильтр тестов по имени (регулярное выражение) |
| `--bail` | false | Остановиться при первом падении |
@@ -30,7 +33,7 @@ node run.mjs test [url] <dir|file> [флаги]
| `--record` | false | Записывать видео для каждого теста (mp4 в `--report-dir`) |
| `-- <hookArgs…>` | — | Всё после `--` пробрасывается в `_hooks.mjs` как `hookArgs` (см. §6.1) |
URL необязателен, если в каталоге тестов есть `webtest.config.mjs`. CLI URL переопределяет URL дефолтного контекста.
URL не передаётся позиционно — он берётся из `webtest.config.mjs` в каталоге тестов, а флаг `--url=` переопределяет URL дефолтного контекста. `webtest.config.mjs` и `_hooks.mjs` резолвятся от каталога **первого** пути, поэтому перечисляемые файлы должны лежать в одной папке сьюта.
### Валидация CLI
@@ -876,6 +879,7 @@ tests/myapp/
|--------|-----------|
| Обход | Рекурсивный; файлы и каталоги, имя которых начинается на `_` или `.`, пропускаются |
| Шаблон имени | Только `*.test.mjs` |
| Несколько путей | `node run.mjs test a.test.mjs b.test.mjs dir/` — наборы объединяются, дублируются и сортируются |
| Порядок | Сортировка по полному относительному пути (`sales/01` идёт до `warehouse/01`) |
| `file` в отчёте | `relative(testDir, file)` с разделителем `/`, например `sales/01-order-create.test.mjs` |
| Фильтр по пути с CLI | `node run.mjs test tests/myapp/sales/` запустит только подкаталог |
@@ -0,0 +1,18 @@
{
"name": "СКД: параметр со списком значений в шортхенде",
"params": { "outputPath": "Template.xml" },
"input": {
"dataSets": [{
"name": "Основной",
"query": "ВЫБРАТЬ Т.Поле ИЗ Регистр КАК Т",
"fields": ["Поле: string"]
}],
"parameters": [
"Виды: ChartOfCharacteristicTypesRef.ВидыСубконтоХозрасчетные = ПланВидовХарактеристик.ВидыСубконтоХозрасчетные.Контрагенты, ПланВидовХарактеристик.ВидыСубконтоХозрасчетные.Договоры"
]
},
"validatePath": "Template.xml",
"expect": {
"files": ["Template.xml"]
}
}
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
</dataSource>
<dataSet xsi:type="DataSetQuery">
<name>Основной</name>
<field xsi:type="DataSetFieldField">
<dataPath>Поле</dataPath>
<field>Поле</field>
<valueType>
<v8:Type>xs:string</v8:Type>
<v8:StringQualifiers>
<v8:Length>0</v8:Length>
<v8:AllowedLength>Variable</v8:AllowedLength>
</v8:StringQualifiers>
</valueType>
</field>
<dataSource>ИсточникДанных1</dataSource>
<query>ВЫБРАТЬ Т.Поле ИЗ Регистр КАК Т</query>
</dataSet>
<parameter>
<name>Виды</name>
<valueType>
<v8:Type xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config">d5p1:ChartOfCharacteristicTypesRef.ВидыСубконтоХозрасчетные</v8:Type>
</valueType>
<value xsi:type="dcscor:DesignTimeValue">ПланВидовХарактеристик.ВидыСубконтоХозрасчетные.Контрагенты</value>
<value xsi:type="dcscor:DesignTimeValue">ПланВидовХарактеристик.ВидыСубконтоХозрасчетные.Договоры</value>
<useRestriction>false</useRestriction>
<valueListAllowed>true</valueListAllowed>
</parameter>
<settingsVariant>
<dcsset:name>Основной</dcsset:name>
<dcsset:presentation xsi:type="v8:LocalStringType">
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Основной</v8:content>
</v8:item>
</dcsset:presentation>
<dcsset:settings xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows">
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
<dcsset:item xsi:type="dcsset:StructureItemGroup">
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
</dcsset:item>
</dcsset:settings>
</settingsVariant>
</DataCompositionSchema>
@@ -0,0 +1,24 @@
{
"name": "add-parameter список значений-дат — двоеточия в значениях не режутся",
"preRun": [
{
"script": "skd-compile/scripts/skd-compile",
"input": {
"dataSets": [{
"name": "Основной",
"query": "ВЫБРАТЬ Т.Поле ИЗ Регистр КАК Т",
"fields": ["Поле: string"]
}]
},
"args": { "-DefinitionFile": "{inputFile}", "-OutputPath": "{workDir}/Template.xml" }
}
],
"params": {
"templatePath": "Template.xml",
"operation": "add-parameter",
"value": "Даты: dateTime = 2024-01-01T00:00:00, 2024-06-15T12:30:45"
},
"expect": {
"files": ["Template.xml"]
}
}
@@ -0,0 +1,21 @@
{
"name": "add-parameter со списком значений (несколько <value> + valueListAllowed)",
"preRun": [
{
"script": "skd-compile/scripts/skd-compile",
"input": {
"dataSets": [{
"name": "Основной",
"query": "ВЫБРАТЬ Т.Поле ИЗ Регистр КАК Т",
"fields": ["Поле: string"]
}]
},
"args": { "-DefinitionFile": "{inputFile}", "-OutputPath": "{workDir}/Template.xml" }
}
],
"params": {
"templatePath": "Template.xml",
"operation": "add-parameter",
"value": "Виды: ChartOfCharacteristicTypesRef.ВидыСубконтоХозрасчетные = ПланВидовХарактеристик.ВидыСубконтоХозрасчетные.Контрагенты, ПланВидовХарактеристик.ВидыСубконтоХозрасчетные.Договоры"
}
}
@@ -0,0 +1,25 @@
{
"name": "modify-parameter value=список — скаляр заменяется списком + valueListAllowed добавлен",
"preRun": [
{
"script": "skd-compile/scripts/skd-compile",
"input": {
"dataSets": [{
"name": "Основной",
"query": "ВЫБРАТЬ Т.Поле ИЗ Регистр КАК Т",
"fields": ["Поле: string"]
}]
},
"args": { "-DefinitionFile": "{inputFile}", "-OutputPath": "{workDir}/Template.xml" }
},
{
"script": "skd-edit/scripts/skd-edit",
"args": { "-TemplatePath": "{workDir}/Template.xml", "-Operation": "add-parameter", "-Value": "Орг: CatalogRef.Организации = Справочник.Организации.X" }
}
],
"params": {
"templatePath": "Template.xml",
"operation": "modify-parameter",
"value": "Орг value=Справочник.Организации.X, Справочник.Организации.Y"
}
}
@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
</dataSource>
<dataSet xsi:type="DataSetQuery">
<name>Основной</name>
<field xsi:type="DataSetFieldField">
<dataPath>Поле</dataPath>
<field>Поле</field>
<valueType>
<v8:Type>xs:string</v8:Type>
<v8:StringQualifiers>
<v8:Length>0</v8:Length>
<v8:AllowedLength>Variable</v8:AllowedLength>
</v8:StringQualifiers>
</valueType>
</field>
<dataSource>ИсточникДанных1</dataSource>
<query>ВЫБРАТЬ Т.Поле ИЗ Регистр КАК Т</query>
</dataSet>
<parameter>
<name>Даты</name>
<valueType>
<v8:Type>xs:dateTime</v8:Type>
<v8:DateQualifiers>
<v8:DateFractions>DateTime</v8:DateFractions>
</v8:DateQualifiers>
</valueType>
<value xsi:type="xs:dateTime">2024-01-01T00:00:00</value>
<value xsi:type="xs:dateTime">2024-06-15T12:30:45</value>
<valueListAllowed>true</valueListAllowed>
</parameter>
<settingsVariant>
<dcsset:name>Основной</dcsset:name>
<dcsset:presentation xsi:type="v8:LocalStringType">
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Основной</v8:content>
</v8:item>
</dcsset:presentation>
<dcsset:settings xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows">
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
<dcsset:item xsi:type="dcsset:StructureItemGroup">
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
</dcsset:item>
</dcsset:settings>
</settingsVariant>
</DataCompositionSchema>
@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
</dataSource>
<dataSet xsi:type="DataSetQuery">
<name>Основной</name>
<field xsi:type="DataSetFieldField">
<dataPath>Поле</dataPath>
<field>Поле</field>
<valueType>
<v8:Type>xs:string</v8:Type>
<v8:StringQualifiers>
<v8:Length>0</v8:Length>
<v8:AllowedLength>Variable</v8:AllowedLength>
</v8:StringQualifiers>
</valueType>
</field>
<dataSource>ИсточникДанных1</dataSource>
<query>ВЫБРАТЬ Т.Поле ИЗ Регистр КАК Т</query>
</dataSet>
<parameter>
<name>Виды</name>
<valueType>
<v8:Type xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config">d5p1:ChartOfCharacteristicTypesRef.ВидыСубконтоХозрасчетные</v8:Type>
</valueType>
<value xsi:type="dcscor:DesignTimeValue">ПланВидовХарактеристик.ВидыСубконтоХозрасчетные.Контрагенты</value>
<value xsi:type="dcscor:DesignTimeValue">ПланВидовХарактеристик.ВидыСубконтоХозрасчетные.Договоры</value>
<valueListAllowed>true</valueListAllowed>
</parameter>
<settingsVariant>
<dcsset:name>Основной</dcsset:name>
<dcsset:presentation xsi:type="v8:LocalStringType">
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Основной</v8:content>
</v8:item>
</dcsset:presentation>
<dcsset:settings xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows">
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
<dcsset:item xsi:type="dcsset:StructureItemGroup">
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
</dcsset:item>
</dcsset:settings>
</settingsVariant>
</DataCompositionSchema>
@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
</dataSource>
<dataSet xsi:type="DataSetQuery">
<name>Основной</name>
<field xsi:type="DataSetFieldField">
<dataPath>Поле</dataPath>
<field>Поле</field>
<valueType>
<v8:Type>xs:string</v8:Type>
<v8:StringQualifiers>
<v8:Length>0</v8:Length>
<v8:AllowedLength>Variable</v8:AllowedLength>
</v8:StringQualifiers>
</valueType>
</field>
<dataSource>ИсточникДанных1</dataSource>
<query>ВЫБРАТЬ Т.Поле ИЗ Регистр КАК Т</query>
</dataSet>
<parameter>
<name>Орг</name>
<valueType>
<v8:Type xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config">d5p1:CatalogRef.Организации</v8:Type>
</valueType>
<value xsi:type="dcscor:DesignTimeValue">Справочник.Организации.X</value>
<value xsi:type="dcscor:DesignTimeValue">Справочник.Организации.Y</value>
<valueListAllowed>true</valueListAllowed>
</parameter>
<settingsVariant>
<dcsset:name>Основной</dcsset:name>
<dcsset:presentation xsi:type="v8:LocalStringType">
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Основной</v8:content>
</v8:item>
</dcsset:presentation>
<dcsset:settings xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows">
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
<dcsset:item xsi:type="dcsset:StructureItemGroup">
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
</dcsset:item>
</dcsset:settings>
</settingsVariant>
</DataCompositionSchema>
@@ -0,0 +1,22 @@
{
"name": "Raw-выгрузка многопакетного запроса без декораций",
"preRun": [
{
"script": "skd-compile/scripts/skd-compile",
"input": {
"dataSets": [{
"name": "Основной",
"query": "ВЫБРАТЬ 1 КАК Поле\nПОМЕСТИТЬ ВТ_Первая\n;\n////////////////////////////////////////////////////////////////////////////////\nВЫБРАТЬ Поле ИЗ ВТ_Первая КАК Т",
"fields": ["Поле"]
}]
},
"args": { "-DefinitionFile": "{inputFile}", "-OutputPath": "{workDir}/Template.xml" }
}
],
"params": { "templatePath": "Template.xml" },
"args_extra": ["-Mode", "query", "-Name", "Основной", "-Raw"],
"expect": {
"stdoutContains": ["ПОМЕСТИТЬ ВТ_Первая", "////", "ВЫБРАТЬ Поле ИЗ ВТ_Первая"],
"stdoutNotContains": ["=== Query", "--- Batch", "Batch 1: lines"]
}
}
@@ -0,0 +1,21 @@
{
"name": "Просмотр запроса (без -Raw) сохраняет декорации",
"preRun": [
{
"script": "skd-compile/scripts/skd-compile",
"input": {
"dataSets": [{
"name": "Основной",
"query": "ВЫБРАТЬ 1 КАК Поле\nПОМЕСТИТЬ ВТ_Первая\n;\n////////////////////////////////////////////////////////////////////////////////\nВЫБРАТЬ Поле ИЗ ВТ_Первая КАК Т",
"fields": ["Поле"]
}]
},
"args": { "-DefinitionFile": "{inputFile}", "-OutputPath": "{workDir}/Template.xml" }
}
],
"params": { "templatePath": "Template.xml" },
"args_extra": ["-Mode", "query", "-Name", "Основной"],
"expect": {
"stdoutContains": ["=== Query", "--- Batch", "Batch 1: lines"]
}
}
@@ -818,6 +818,14 @@ export const steps = [
// Редактируемая строковая колонка: у поля есть кнопка выбора, но НачалоВыбора пустой
// (F4 ничего не открывает), текст вводится напрямую — модель ячейки «Значение» Консоли запросов.
{ name: 'РедактируемаяСтрока', type: 'String', title: 'Редактируемая строка' },
// Редактируемые choice-ячейки Число и Дата (та же модель «Значение» КЗ): кнопка выбора +
// пустой НачалоВыбора, текст вводится напрямую и ПЕРЕФОРМАТИРУЕТСЯ маск-инпутом
// (1234.56 → «1 234,56»). Регресс-guard для fillChoiceCell — раньше includes-проверка
// рвалась о переформатирование → ложное F4 → калькулятор.
{ name: 'РедактируемоеЧисло', type: 'Number(15,2)', title: 'Редактируемое число' },
{ name: 'РедактируемаяДата', type: 'date', title: 'Редактируемая дата' },
// Булева колонка-флажок (отдельно от Картинка) — для fillTableRow toggle на дереве.
{ name: 'Булево', type: 'Boolean', title: 'Булево' },
]},
// Список значений для программного выбора (ПоказатьВыборЭлемента).
{ name: 'СписокТипов', type: 'ValueList' },
@@ -841,6 +849,17 @@ export const steps = [
// кнопка iCB есть, F4 ничего не открывает, текст редактируется напрямую (модель «Значение»).
{ input: 'ДеревоРедактируемаяСтрока', path: 'Дерево.РедактируемаяСтрока', title: 'Редактируемая строка',
choiceButton: true, on: ['StartChoice'], handlers: { StartChoice: 'ДеревоРедактируемаяСтрокаНачалоВыбора' } },
// Редактируемые choice-ячейки Число/Дата: кнопка iCB + пустой НачалоВыбора → текст
// редактируется напрямую, значение переформатируется маск-инпутом (модель «Значение» КЗ).
{ input: 'ДеревоРедактируемоеЧисло', path: 'Дерево.РедактируемоеЧисло', title: 'Редактируемое число',
choiceButton: true, on: ['StartChoice'], handlers: { StartChoice: 'ДеревоРедактируемоеЧислоНачалоВыбора' } },
{ input: 'ДеревоРедактируемаяДата', path: 'Дерево.РедактируемаяДата', title: 'Редактируемая дата',
choiceButton: true, on: ['StartChoice'], handlers: { StartChoice: 'ДеревоРедактируемаяДатаНачалоВыбора' } },
// Булево как ПОЛЕ ВВОДА с кнопкой выбора (не флажок): в ячейке выбор Да/Нет —
// fillTableRow идёт через dropdown-путь (как «Значение» типа Булево в Консоли
// запросов), не toggle. Кнопка iCB + пустой НачалоВыбора — единая модель «Значение».
{ input: 'ДеревоБулево', path: 'Дерево.Булево', title: 'Булево',
choiceButton: true, on: ['StartChoice'], handlers: { StartChoice: 'ДеревоБулевоНачалоВыбора' } },
]},
],
},
@@ -926,6 +945,25 @@ export const steps = [
\t// Текст вводится напрямую — модель ячейки «Значение» типовой Консоли запросов.
\tСтандартнаяОбработка = Ложь;
КонецПроцедуры
&НаКлиенте
Процедура ДеревоРедактируемоеЧислоНачалоВыбора(Элемент, ДанныеВыбора, СтандартнаяОбработка)
\t// Пустой обработчик — число редактируется напрямую (модель «Значение» КЗ).
\tСтандартнаяОбработка = Ложь;
КонецПроцедуры
&НаКлиенте
Процедура ДеревоРедактируемаяДатаНачалоВыбора(Элемент, ДанныеВыбора, СтандартнаяОбработка)
\t// Пустой обработчик — дата редактируется напрямую (модель «Значение» КЗ).
\tСтандартнаяОбработка = Ложь;
КонецПроцедуры
&НаКлиенте
Процедура ДеревоБулевоНачалоВыбора(Элемент, ДанныеВыбора, СтандартнаяОбработка)
\t// Пустой обработчик: кнопка выбора есть, F4 ничего не открывает; значение задаётся
\t// штатным списком Да/Нет поля ввода булева (модель «Значение» типа Булево в КЗ).
\tСтандартнаяОбработка = Ложь;
КонецПроцедуры
`,
},
+22
View File
@@ -69,6 +69,28 @@ export default async function({ navigateSection, openCommand, clickElement, sele
await closeForm({ save: false });
});
await step('object-search: selectValue({ Наименование }) выбирает через форму выбора', async () => {
// Регрессия объектного поиска { field: value }:
// 1) dropdown-путь 3A падал на searchText.toLowerCase() (объект, не строка);
// 2) pickFromSelectionForm (Шаг 2) вызывал filterList без импорта —
// ReferenceError тихо глотался catch'ем, поиск по полю не отрабатывал.
// Теперь объектный search уходит в форму выбора и фильтрует per-field.
await navigateSection('Склад');
await openCommand('Приходная накладная');
await clickElement('Создать');
const r = await selectValue('Контрагент', { 'Наименование': 'Север' });
log(`object-search: method=${r.selected?.method} errors=${JSON.stringify(r.errors)}`);
assert.equal(r.selected?.method, 'form', 'объектный поиск идёт через форму выбора');
assert.ok(!r.errors, 'без ошибок 1С');
const field = findField(r, 'Контрагент');
log(`Контрагент value='${field?.value}'`);
assert.includes(field?.value || '', 'Север', 'выбран контрагент с Наименование=Север');
await closeForm({ save: false });
});
await step('clear: selectValue с пустым search → Shift+F4', async () => {
await navigateSection('Склад');
await openCommand('Приходная накладная');
+14
View File
@@ -20,6 +20,20 @@ export default async function({ navigateSection, openCommand, getFormState, getC
assert.equal(submit.default, true, '«Сформировать» — кнопка по умолчанию');
});
await step('guard: readSpreadsheet до «Сформировать» → осмысленная ошибка, не ReferenceError', async () => {
// Регрессия: чтение несформированного отчёта идёт в ветку allCells.size===0,
// которая вызывает checkForErrors(). После рефакторинга на модули символ не был
// импортирован в spreadsheet.mjs → падало с "checkForErrors is not defined".
// Теперь должно бросать осмысленное сообщение (+ подсказка из инфо-панели 1С).
let threw = false, msg = '';
try { await readSpreadsheet(); }
catch (e) { threw = true; msg = e.message; }
log(`readBeforeGenerate: threw=${threw} msg=${msg}`);
assert.ok(threw, 'readSpreadsheet должен бросить — отчёт ещё не сформирован');
assert.includes(msg, 'no SpreadsheetDocument', 'осмысленное сообщение readSpreadsheet');
assert.ok(!/is not defined/.test(msg), 'не ReferenceError — checkForErrors импортирован');
});
await step('reset: сброс пользовательских настроек к стандартным', async () => {
// 1С хранит пользовательские настройки между сессиями — сбрасываем к дефолту,
// чтобы тест был идемпотентным независимо от предыдущих прогонов.
+48 -1
View File
@@ -14,6 +14,9 @@ export const timeout = 90000;
// + дискриминатор choice-ячейки (fillChoiceCell): ДеревоРедактируемаяСтрока (кнопка iCB,
// пустой НачалоВыбора, текст редактируется → method:'direct') vs ДеревоТипЗначения
// (РедактированиеТекста=Ложь, текст отвергается → форма выбора, method:'choice').
// + редактируемые choice-ячейки Число/Дата (РедактируемоеЧисло/РедактируемаяДата): маск-инпут
// переформатирует значение (1234.56 → «1 234,56») — регресс на баг с ложным F4→калькулятор.
// + булево как поле ввода (Булево, InputField, не флажок): выбор Да/Нет через dropdown-путь.
export default async function({ navigateLink, clickElement, closeForm, readTable, fillTableRow, assert, step, log }) {
@@ -27,7 +30,7 @@ export default async function({ navigateLink, clickElement, closeForm, readTable
await step('read-roots: на верхнем уровне видны группы (Товары, Услуги, БольшойСписок)', async () => {
const t = await readTable('Дерево');
log(`columns=${t.columns?.join(',')} rows=${t.rows?.length}`);
assert.deepEqual(t.columns, ['Номенклатура', 'Цена', 'Картинка', 'Флаг', 'Тип значения', 'Редактируемая строка'], 'колонки: Номенклатура + Цена + Картинка + Флаг + Тип значения + Редактируемая строка');
assert.deepEqual(t.columns, ['Номенклатура', 'Цена', 'Картинка', 'Флаг', 'Тип значения', 'Редактируемая строка', 'Редактируемое число', 'Редактируемая дата', 'Булево'], 'колонки: Номенклатура + Цена + Картинка + Флаг + Тип значения + Редактируемая строка + Редактируемое число + Редактируемая дата + Булево');
assert.equal(t.rows.length, 3, '3 корневые строки');
const names = t.rows.map(r => r['Номенклатура']);
assert.includes(names, 'Товары', 'есть Товары');
@@ -119,6 +122,50 @@ export default async function({ navigateLink, clickElement, closeForm, readTable
assert.equal(cell.error, 'not_found', 'error=not_found');
});
await step('choice-number: редактируемая choice-ячейка Число — paste переформатируется, method:direct', async () => {
// РедактируемоеЧисло — Number-колонка с кнопкой выбора (iCB) и пустым НачалоВыбора (модель
// «Значение» КЗ). Маск-инпут ПЕРЕФОРМАТИРУЕТ «1234.56» → «1 234,56» (nbsp-разделитель, запятая).
// Регресс на баг, где includes-проверка рвалась о переформатирование → ложное F4 → калькулятор.
// Теперь дискриминатор поведенческий (инпут изменился + нет EDD → direct), формат не важен.
const r = await fillTableRow({ 'Редактируемое число': '1234.56' }, { row: 1 });
log(`filled: ${JSON.stringify(r.filled)}`);
const cell = r.filled?.find(f => /Редактируемое число/i.test(f.field));
assert.ok(cell, 'поле Редактируемое число в результате');
assert.equal(cell.ok, true, 'ok=true');
assert.equal(cell.method, 'direct', 'method=direct (числовой маск-инпут, без F4/калькулятора)');
const t = await readTable('Дерево');
const tovar01 = t.rows.find(row => row['Номенклатура'] === 'Товар 01');
// 1С web использует неразрывный пробел как разделитель разрядов — убираем все пробелы перед сравнением.
assert.equal((tovar01['Редактируемое число'] || '').replace(/[\s\u00A0]/g, ''), '1234,56', 'число переформатировано в 1234,56');
});
await step('choice-date: редактируемая choice-ячейка Дата — method:direct', async () => {
// РедактируемаяДата — Date-колонка с кнопкой выбора и пустым НачалоВыбора. Та же модель «Значение».
const r = await fillTableRow({ 'Редактируемая дата': '15.06.2025' }, { row: 1 });
log(`filled: ${JSON.stringify(r.filled)}`);
const cell = r.filled?.find(f => /Редактируемая дата/i.test(f.field));
assert.ok(cell, 'поле Редактируемая дата в результате');
assert.equal(cell.ok, true, 'ok=true');
assert.equal(cell.method, 'direct', 'method=direct');
const t = await readTable('Дерево');
const tovar01 = t.rows.find(row => row['Номенклатура'] === 'Товар 01');
assert.equal(tovar01['Редактируемая дата'], '15.06.2025', 'дата записана');
});
await step('bool-input: булева ячейка-поле-ввода (не флажок) заполняется выбором Да', async () => {
// ДеревоБулево — InputField на булевом пути (НЕ CheckBoxField) с кнопкой выбора (iCB) и
// пустым НачалоВыбора: в ячейке выбор Да/Нет, fillTableRow идёт через dropdown-путь, а не
// toggle. Покрывает булево как поле ввода (модель «Значение» типа Булево в Консоли запросов).
const r = await fillTableRow({ 'Булево': 'Да' }, { row: 1 });
log(`filled: ${JSON.stringify(r.filled)}`);
const cell = r.filled?.find(f => /Булево/i.test(f.field));
assert.ok(cell, 'поле Булево в результате');
assert.equal(cell.ok, true, 'ok=true');
const t = await readTable('Дерево');
const tovar01 = t.rows.find(row => row['Номенклатура'] === 'Товар 01');
assert.equal(tovar01['Булево'], 'Да', 'Булево = Да');
});
await step('picture: колонка-картинка (pic:0/\'\') + кросс-проверка чекбоксом', async () => {
const t = await readTable('Дерево');
const t15 = t.rows.find(r => r['Номенклатура'] === 'Товар 15'); // Цена 1500 > 1000 → иконка
+3 -3
View File
@@ -190,9 +190,9 @@ export default async function({
// ── Focus-click skip checkbox: cluster booleans on right edge, click further right ──
await step('focus-click пропускает checkbox-ячейки при выборе focus-точки', async () => {
// После предыдущего шага viewport уехал вправо. Нужно сбросить — выходим из ТЧ
// и заходим заново через клик на Контрагент (вне грида).
await fillFields({ 'Комментарий': 'LongDoc' }); // вернёт к дефолтному viewport
// После предыдущего шага viewport уехал вправо. Нужно сбросить — выводим фокус
// из ТЧ кликом по полю «Комментарий» (вне грида), без перезаполнения значения.
await clickElement('Комментарий'); // фокус вне грида → дефолтный viewport
await wait(0.3);
const before = await readTable({ table: 'Товары', maxRows: 5 });
const bools0 = {
+5 -1
View File
@@ -11,6 +11,9 @@ node .claude/skills/web-test/scripts/run.mjs test tests/web-test/
# Один файл
node .claude/skills/web-test/scripts/run.mjs test tests/web-test/02-crud.test.mjs
# Несколько файлов (позиционные = пути к тестам, можно сколько угодно)
node .claude/skills/web-test/scripts/run.mjs test tests/web-test/04-selectvalue.test.mjs tests/web-test/11-report.test.mjs
# Несколько по фильтру тегов
node .claude/skills/web-test/scripts/run.mjs test tests/web-test/ --tags=table,smoke
@@ -18,7 +21,7 @@ node .claude/skills/web-test/scripts/run.mjs test tests/web-test/ --tags=table,s
node .claude/skills/web-test/scripts/run.mjs test tests/web-test/ --grep=multi
```
URL не указываем — берётся из `webtest.config.mjs` (`contexts.a.url` = `http://localhost:9191/webtest-runner/ru_RU`).
URL не передаём позиционно — берётся из `webtest.config.mjs` (`contexts.a.url` = `http://localhost:9191/webtest-runner/ru_RU`). Переопределить можно флагом `--url=<url>`.
Exit code: 0 = все прошли, 1 = есть падения.
@@ -26,6 +29,7 @@ Exit code: 0 = все прошли, 1 = есть падения.
| Флаг | Описание |
|---|---|
| `--url=URL` | Переопределить базовый URL (по умолчанию — из `webtest.config.mjs`) |
| `--tags=A,B` | Запустить только тесты с одним из тегов |
| `--grep=regex` | Фильтр по имени теста |
| `--bail` | Остановиться на первой ошибке |