mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 00:51:59 +00:00
像小计的行只认夹在明细中间的;可以在配方里声明「没有小计行」,不再每期确认
复核发现(C-subtotal-suspect-false-positive):没配小计时,_suspect_word 对只有「标签文字列 + 数字列」的列表 不加限制,「其余文字列为空」自然成立,首列以 合计/总计/小计/汇总/累计 开头的普通数据行都被报 subtotal_suspect, 最后一行也报(说明却写「夹在明细中间」)。这是必勾的确认项:存量配方(「累计充值」「累计消费」这类键值清单) 升级后每一期导入都多一项,工作流里「按配方导入一期」直接被 files/imports._needs_people 挡下;确认又记不住, 只能用 declare_subtotals 把真数据改成小计。 改法: - 执行器(recipe_engine._watch_suspect / _settle_suspects):这一行本身像小计(首列以合计字样开头、其余文字和 日期列空着、有数)之外,还要上一行明细在某个文字或日期列上有值、这一行那一列空着,并且下一行明细在同一列上 也有值(或者下面紧跟着合计行——最后一组的小计也在列表里面)。候选先挂着,等下一行明细再定;列表到头了还挂着的 不报。两列的键值清单因此不会报;上下明细那一列本来就空着的也不报。 - 配方新增 ListRows.no_subtotals(缺省 false,规范化时省略,存量配方的哈希不变):人确认过这张列表没有小计行, 以合计字样开头的行照常是数据,执行器不再报 subtotal_suspect。它是放宽,出确认项 no_subtotals:<块>,签名只看 这个开关,配上(或改过)的那一次确认一次,之后各期不再出。和 subtotals 二选一,同时配了报 subtotals_conflict。 - 配方面板「小计行」里没勾「有小计行」时多一个勾选框「没有小计行(以合计字样开头的行也是数据,不再提醒)」; 勾上「有小计行」时自动去掉它。subtotal_suspect 的说明改成「上下明细都有值的文字列在这一行空着」,并说明以后 不想再确认去哪里勾。 - check-manage 加一步:勾选「没有小计行」发 PUT,no_subtotals 为 true。 测试:test_recipe_p5 加复核的两列反例(门票收入 / 累计收入 / 汇总调整 / 餐饮收入;最后一行「合计调整项」)、 最后一行和上下明细那一列空着的反例、夹在中间和紧跟合计行的正例、no_subtotals 的导入、确认项、签名和冲突校验; 去掉「要有下文」的条件回跑两条失败。
This commit is contained in:
@@ -274,7 +274,7 @@ _FIELD_LABEL = {
|
||||
"parts": "组成列", "claims": "认领", "a": "一方", "b": "另一方", "reason": "理由",
|
||||
"ignore_rows": "按行标签忽略的行", "ignore_columns": "按表头忽略的列", "ignore_outside": "按同一行文字忽略的数字",
|
||||
"label": "标签", "anchor": "同一行的文字",
|
||||
"subtotals": "小计行", "scope": "小计管哪些明细", "group_column": "分组的列", "period": "每行属于哪一期",
|
||||
"subtotals": "小计行", "no_subtotals": "没有小计行", "scope": "小计管哪些明细", "group_column": "分组的列", "period": "每行属于哪一期",
|
||||
"column": "日期列", "check": "核对方式", "synthesize": "加统计期列",
|
||||
}
|
||||
|
||||
@@ -1036,11 +1036,14 @@ class _Checker:
|
||||
|
||||
def check_subtotals(self, base: tuple[Any, ...], block: ListBlock, names: list[str]) -> None:
|
||||
"""期 5:小计行的标签列、分组列要是这个列表的列;按组小计要给分组列;小计和合计的开头词不能互为前缀
|
||||
(「合计」「合计小计」:一行就分不清是小计还是合计)。"""
|
||||
(「合计」「合计小计」:一行就分不清是小计还是合计);配了小计又声明没有小计(no_subtotals)是矛盾的。"""
|
||||
sub = block.rows.subtotals
|
||||
if sub is None:
|
||||
return
|
||||
sp = base + ("rows", "subtotals")
|
||||
if block.rows.no_subtotals:
|
||||
self.add(base + ("rows", "no_subtotals"), "subtotals_conflict",
|
||||
"配了小计行,又声明了这张列表没有小计行:二选一")
|
||||
if sub.label_column not in names:
|
||||
self.add(sp + ("label_column",), "column_unknown",
|
||||
f"小计标签所在的列「{sub.label_column}」不在这个列表的列里{self._near(sub.label_column, names)}")
|
||||
|
||||
@@ -527,6 +527,7 @@ def _signatures(recipe: Recipe, sheet_name: Callable[[SheetRecipe], str]) -> dic
|
||||
subt = block.rows.subtotals
|
||||
out[f"subtotals:{block.id}"] = _sig(
|
||||
None if subt is None else (subt.label_column, subt.pick, subt.scope, subt.group_column, subt.keep_as))
|
||||
out[f"no_subtotals:{block.id}"] = _sig(block.rows.no_subtotals)
|
||||
period = block.period
|
||||
out[f"list_period:{block.id}"] = _sig(None if period is None else (period.column, period.synthesize))
|
||||
for r in recipe.relations:
|
||||
@@ -1076,6 +1077,10 @@ def _recipe_items(bag: _Bag, recipe: Recipe, ex: dict[str, Any], facts: Any, *,
|
||||
nums = [c.name for c in b.columns if c.type in ("INTEGER", "REAL")]
|
||||
detail = f"{scope};按明细核对的列:{'、'.join(nums)}" if nums else f"{scope};这个块没有数字列,不核对"
|
||||
bag.add(f"subtotals:{b.id}", f"{prefix}改作核对、不进数据{keep}", detail)
|
||||
elif b.rows.no_subtotals:
|
||||
# 声明没有小计行是放宽:以合计字样开头的行从此照常进数据,不再报「像小计」。配上(或改过)的那一次确认
|
||||
bag.add(f"no_subtotals:{b.id}", f"「{b.id}」没有小计行:以合计字样开头的行都按数据导入,不再提醒像小计的行",
|
||||
"如果其实有穿插的小计,对这些列求和会把小计重复算进去")
|
||||
period = b.period
|
||||
if period is not None and period.column:
|
||||
bag.add(f"list_period:{b.id}", f"「{b.id}」每一行的「{period.column}」都要在本期统计期内",
|
||||
|
||||
@@ -1679,9 +1679,14 @@ class _ListRun:
|
||||
#: 期 5:加统计期起止两列(ListPeriod.synthesize):每行末尾两格占位,统计期定下来以后由 _Sink 换成真值
|
||||
self.synth = block.period is not None and block.period.synthesize is not None
|
||||
#: 期 5:没配小计时,像夹在明细中间的小计的数据行(行号)和命中的合计字样:照常按数据导入,收尾时报
|
||||
#: subtotal_suspect 要人确认(带修复按钮:改作小计核对)
|
||||
#: subtotal_suspect 要人确认(带修复按钮:改作小计核对)。人声明过没有小计(no_subtotals)的不看
|
||||
self.suspects: list[int] = []
|
||||
self.suspect_word: str | None = None
|
||||
self.watch_suspects = block.rows.subtotals is None and not block.rows.no_subtotals
|
||||
#: 上一行普通明细在哪些文字、日期列(列序号,不含标签所在的首列)上有值;还没有明细时是空集
|
||||
self.prev_filled: frozenset[int] = frozenset()
|
||||
#: 等着下一行明细确认的候选:(行号, 合计字样, 上一行明细有值、这一行空着的列)
|
||||
self.pending: list[tuple[int, str, frozenset[int]]] = []
|
||||
|
||||
# ---------------- 喂入 ----------------
|
||||
|
||||
@@ -2069,6 +2074,8 @@ class _ListRun:
|
||||
label = inb.get(self.colmap.get(self.total_idx, -1))
|
||||
pick = match_key(block.rows.total_row.pick) # type: ignore[union-attr]
|
||||
if label is not None and match_key(_text(label.value)).startswith(pick):
|
||||
if self.watch_suspects:
|
||||
self._settle_suspects(None)
|
||||
self._total_row(r, inb, cells, label)
|
||||
return
|
||||
if self.sub_idx is not None:
|
||||
@@ -2076,15 +2083,43 @@ class _ListRun:
|
||||
if label is not None and match_key(_text(label.value)).startswith(self.sub_key):
|
||||
self._subtotal_row(r, inb, cells, label)
|
||||
return
|
||||
elif (word := self._suspect_word(inb)) is not None:
|
||||
self.suspects.append(r)
|
||||
self.suspect_word = self.suspect_word or word
|
||||
elif self.watch_suspects:
|
||||
self._watch_suspect(r, inb)
|
||||
self._record(r, inb, cells, fills)
|
||||
|
||||
def _watch_suspect(self, r: int, inb: dict[int, GridCell]) -> None:
|
||||
"""期 5:没配小计、也没声明没有小计时,认夹在明细中间的小计(照常按数据导入,收尾时报 subtotal_suspect)。
|
||||
|
||||
要同时满足才算:首列以合计字样开头、其余文字和日期列都空着、至少一格有数(_suspect_word);上一行明细在某个
|
||||
文字或日期列上有值、这一行那一列空着;下一行明细在同一列上也有值,或者下面紧跟着合计行。所以候选先挂着,
|
||||
等下一行明细来了再定(_settle_suspects)。
|
||||
|
||||
为什么这么严(复核反例):只有「标签列 + 数字列」的键值清单(「累计收入 300」「汇总调整 5」),「其余文字列
|
||||
为空」自然成立,什么也说明不了;放在最后、后面没有明细的行也不是「夹在中间」。报错了就是每期都要勾的
|
||||
确认项,按配方导入一期的无人值守运行会被它挡住。"""
|
||||
filled = frozenset(i for i, col in enumerate(self.cols)
|
||||
if col.type in ("TEXT", "DATE") and (c := self.colmap.get(i)) is not None
|
||||
and c != self.c1 and c in inb)
|
||||
word = self._suspect_word(inb) if self.prev_filled else None
|
||||
if word is not None:
|
||||
self.pending.append((r, word, self.prev_filled))
|
||||
return
|
||||
self._settle_suspects(filled)
|
||||
self.prev_filled = filled
|
||||
|
||||
def _settle_suspects(self, filled: frozenset[int] | None) -> None:
|
||||
"""挂着的候选有了下文:filled 是紧跟着的那行明细有值的文字、日期列(None 是紧跟着合计行)。上一行明细有值、
|
||||
候选空着的列,下一行明细也有值,才算夹在明细中间。"""
|
||||
for row, word, gap in self.pending:
|
||||
if filled is None or gap & filled:
|
||||
self.suspects.append(row)
|
||||
self.suspect_word = self.suspect_word or word
|
||||
self.pending = []
|
||||
|
||||
def _suspect_word(self, inb: dict[int, GridCell]) -> str | None:
|
||||
"""期 5:这一行像不像夹在明细中间的小计(配方没配 subtotals 时才看):首列是以合计字样(TOTAL_WORDS)开头的文字,
|
||||
其余文字、日期列都空着,至少一格有数。像的话返回那个字样。以前这种行静默当数据导入,求和就把小计重复算了
|
||||
进去(H2);现在照常导入,但收尾时报出来要人确认,并给「改作小计核对」的修复按钮。合计行已经在前面认走了。"""
|
||||
"""期 5:这一行本身像不像小计:首列是以合计字样(TOTAL_WORDS)开头的文字,其余文字、日期列都空着,至少一格
|
||||
有数。像的话返回那个字样。以前这种行静默当数据导入,求和就把小计重复算了进去(H2);现在照常导入,但收尾时
|
||||
报出来要人确认,并给「改作小计核对」的修复按钮。合计行已经在前面认走了。上下文的条件见 _watch_suspect。"""
|
||||
cell = inb.get(self.c1)
|
||||
if cell is None or cell.formula is not None or not isinstance(cell.value, str):
|
||||
return None
|
||||
@@ -2110,10 +2145,10 @@ class _ListRun:
|
||||
cells = [self.sh.coord(r, self.c1) for r in self.suspects]
|
||||
rows = _rows_text(self.suspects)
|
||||
word = self.suspect_word or ""
|
||||
where = (f"{self.where}:第 {rows} 行像夹在明细中间的小计(「{label}」列以「{word}」开头、其余文字列为空),"
|
||||
"按数据导入了")
|
||||
where = (f"{self.where}:第 {rows} 行像夹在明细中间的小计(「{label}」列以「{word}」开头,上下明细都有值的"
|
||||
"文字列在这一行空着),按数据导入了")
|
||||
msg = (f"{where}。如果它们是小计,对这一列求和会把小计重复算进去:请用修复按钮改作小计核对;"
|
||||
"确实是数据时,确认后照常导入")
|
||||
"确实是数据时,确认后照常导入;以后各期不想再确认,在配方的「小计行」里勾选「没有小计行」")
|
||||
self.eng.problems.add("subtotal_suspect", msg, where, cells[:CELLS_MAX], sheet=self.sh.name,
|
||||
fix_args={"block": self.block.id, "label_column": label, "pick": word,
|
||||
"rows": list(self.suspects), "cells": cells[:CELLS_MAX]})
|
||||
|
||||
@@ -323,6 +323,9 @@ class ListRows(_M):
|
||||
total_row: TotalRow | None = None
|
||||
#: 期 5:穿插在明细中间的小计行(Subtotals)。默认 None = 期 4 的行为(像小计的行照常是数据或起草失败)
|
||||
subtotals: Subtotals | None = None
|
||||
#: 期 5:人确认过这张列表没有小计行:以合计字样开头的行就是数据(「累计充值」这类键值清单),执行器不再报
|
||||
#: subtotal_suspect,每期也就不用再勾那一项确认。和 subtotals 二选一(静态校验 subtotals_conflict)
|
||||
no_subtotals: bool = False
|
||||
|
||||
|
||||
class ListValues(_M):
|
||||
|
||||
@@ -476,3 +476,62 @@ def test_ordinary_rows_starting_with_a_total_word_are_not_flagged():
|
||||
rows = [("商品", "门店", "金额"), ("累计积分卡", "门店一", 10), ("普通卡", "门店二", 20)]
|
||||
ex = _run(_book(rows), _list([("商品", "TEXT"), ("门店", "TEXT"), ("金额", "INTEGER")]))
|
||||
assert ex.ok and not [p for p in ex.problems if p.code == "subtotal_suspect"]
|
||||
|
||||
|
||||
def _suspects(data: list[tuple[Any, ...]], columns: list[tuple[str, str]], **block: Any) -> list[str]:
|
||||
ex = _run(_book(data), _list(columns, **block))
|
||||
assert ex.ok, [p.message for p in ex.problems]
|
||||
return [c for p in ex.problems if p.code == "subtotal_suspect" for c in p.cells]
|
||||
|
||||
|
||||
def test_a_two_column_key_value_list_is_never_flagged():
|
||||
"""复核反例:只有「标签文字列 + 数字列」的键值清单,「其余文字列为空」自然成立,不能据此说它像小计——
|
||||
「累计收入」「汇总调整」是普通的数据行。最后一行以合计字样开头也一样。"""
|
||||
cols = [("项目", "TEXT"), ("金额", "INTEGER")]
|
||||
assert _suspects([("项目", "金额"), ("门票收入", 100), ("累计收入", 300), ("汇总调整", 5), ("餐饮收入", 50)],
|
||||
cols) == []
|
||||
assert _suspects([("项目", "金额"), ("门票收入", 100), ("餐饮收入", 50), ("合计调整项", 5)], cols) == []
|
||||
|
||||
|
||||
def test_only_rows_between_details_are_flagged():
|
||||
"""像小计的行要夹在明细中间:上下相邻的明细在某个文字列上有值、这一行空着;后面还得有明细(或者是合计行)。
|
||||
放在最后、后面没有明细的不报(那不是「夹在中间」);上下的明细那一列本来就空着的也不报。"""
|
||||
cols = [("地区", "TEXT"), ("门店", "TEXT"), ("金额", "INTEGER")]
|
||||
head = ("地区", "门店", "金额")
|
||||
# 最后一行:后面没有明细,也没有合计行
|
||||
assert _suspects([head, ("分区甲", "门店一", 100), ("分区甲", "门店二", 200), ("小计", None, 300)], cols) == []
|
||||
# 上面的明细「门店」也空着:不是因为小计才空的
|
||||
assert _suspects([head, ("分区甲", None, 100), ("累计调整", None, 5), ("分区乙", "门店三", 50)], cols) == []
|
||||
# 下面的明细「门店」空着:同样不报
|
||||
assert _suspects([head, ("分区甲", "门店一", 100), ("累计调整", None, 5), ("分区乙", None, 50)], cols) == []
|
||||
# 夹在明细中间:报
|
||||
assert _suspects([head, ("分区甲", "门店一", 100), ("小计", None, 100), ("分区乙", "门店三", 50)],
|
||||
cols) == ["明细!A3"]
|
||||
# 最后一组的小计后面紧跟合计行:也在列表里面,报
|
||||
total = {"rows": {"total_row": {"label_column": "地区", "pick": "合计"}}}
|
||||
assert _suspects([head, ("分区甲", "门店一", 100), ("小计", None, 100), ("合计", None, 100)], cols,
|
||||
**total) == ["明细!A3"]
|
||||
|
||||
|
||||
def test_declaring_no_subtotals_keeps_such_rows_as_data_without_asking(tmp_path):
|
||||
"""人确认过「这张列表没有小计行」(rows.no_subtotals):以合计字样开头的行照常是数据,不再每期报 subtotal_suspect、
|
||||
不再每期要勾确认;这个声明本身是放宽,配上的那一次要确认一次。和 subtotals 只能二选一。"""
|
||||
from app.data import recipe_confirm
|
||||
|
||||
recipe = _list(STORE_COLS, rows={"total_row": {"label_column": "地区", "pick": "合计"}, "no_subtotals": True})
|
||||
assert validate_recipe(recipe, facts=None, origin="rules")[1] == []
|
||||
db = str(tmp_path / "n.db")
|
||||
ex = _run(_book(STORES), recipe, db)
|
||||
assert ex.ok and not [p for p in ex.problems if p.code == "subtotal_suspect"]
|
||||
assert _rows(db, 'SELECT COUNT(*) FROM "明细"') == [(6,)] # 两行「小计」照常是数据
|
||||
items = recipe_confirm.confirm_items(recipe_confirm.ConfirmContext(kind="first", recipe=recipe, extraction=ex,
|
||||
checks=[]))
|
||||
ids = [i.id for i in items]
|
||||
assert "no_subtotals:列表1" in ids and not [i for i in ids if i.startswith("subtotal_suspect:")]
|
||||
sigs = recipe_confirm.recipe_item_signatures(Recipe.model_validate(recipe))
|
||||
assert sigs["no_subtotals:列表1"] != recipe_confirm.recipe_item_signatures(
|
||||
Recipe.model_validate(_list(STORE_COLS)))["no_subtotals:列表1"]
|
||||
both = _stores_recipe()
|
||||
both["sheets"][0]["blocks"][0]["rows"]["no_subtotals"] = True
|
||||
probs = validate_recipe(both, facts=None, origin="rules")[1]
|
||||
assert [p.code for p in probs] == ["subtotals_conflict"] and probs[0].path.endswith("/rows/no_subtotals")
|
||||
|
||||
@@ -1644,6 +1644,8 @@ export const RECIPE_TEXT = {
|
||||
subtotalScope: '一行小计管哪些明细',
|
||||
subtotalGroupColumn: '按哪一列分组',
|
||||
subtotalKeep: '原表小计另存为表(留空则只核对)',
|
||||
/** 人确认过列表里没有小计(ListRows.no_subtotals):像小计的行照常是数据,不再每期报「像小计的行」要人确认 */
|
||||
noSubtotals: '没有小计行(以合计字样开头的行也是数据,不再提醒)',
|
||||
listPeriod: '按期累积时每一行属于哪一期',
|
||||
listPeriodColumn: '按哪个日期列核对',
|
||||
listPeriodHint: '列表要按期累积,每一行都要说得出属于哪一期;所在工作表要配置统计期(表内没有时,每期试运行时录入)。导入模式另在「导入模式」中选',
|
||||
|
||||
@@ -72,7 +72,7 @@ export function fillRecipeDefaults(input: Recipe | null | undefined): Recipe | n
|
||||
block.extra_columns ??= 'reject'
|
||||
block.ignore_columns ??= []
|
||||
block.merged_data ??= 'reject'
|
||||
block.rows = { blank_rows: 'stop', total_row: null, subtotals: null, ...(isObj(block.rows) ? block.rows : {}) }
|
||||
block.rows = { blank_rows: 'stop', total_row: null, subtotals: null, no_subtotals: false, ...(isObj(block.rows) ? block.rows : {}) }
|
||||
if (isObj(block.rows.total_row)) block.rows.total_row.keep_as ??= null
|
||||
if (isObj(block.rows.subtotals)) {
|
||||
block.rows.subtotals = { scope: 'since_previous', group_column: null, keep_as: null, ...block.rows.subtotals }
|
||||
@@ -891,9 +891,23 @@ function ListEditor({ s, b, block, edit }: { s: number; b: number; block: any; e
|
||||
x.rows.subtotals = on
|
||||
? { label_column: arr(x.columns)[0]?.name ?? '', pick: '小计', scope: 'since_previous', group_column: null, keep_as: null }
|
||||
: null
|
||||
// 有小计和「没有小计行」只能二选一(后端静态校验 subtotals_conflict)
|
||||
if (on) x.rows.no_subtotals = false
|
||||
})}>
|
||||
{RECIPE_TEXT.subtotalsOn}
|
||||
</Check>
|
||||
{!subtotals && (
|
||||
<div className="mt-1">
|
||||
<Check path={`${base}/rows/no_subtotals`} checked={block.rows?.no_subtotals === true}
|
||||
onChange={(on) => edit((r) => {
|
||||
const x = at(r)
|
||||
x.rows = isObj(x.rows) ? x.rows : {}
|
||||
x.rows.no_subtotals = on
|
||||
})}>
|
||||
{RECIPE_TEXT.noSubtotals}
|
||||
</Check>
|
||||
</div>
|
||||
)}
|
||||
{subtotals && (
|
||||
<div className="mt-1 grid grid-cols-1 gap-2 md:grid-cols-3" data-list-subtotals={String(block.id)}>
|
||||
<FieldWrap path={`${base}/rows/subtotals/label_column`} label={RECIPE_TEXT.subtotalLabelColumn}>
|
||||
|
||||
@@ -2301,13 +2301,21 @@ await section('数据 · 表格:按配方导入', async () => {
|
||||
await until(async () => puts().length === 5, 4000)
|
||||
check('切换「数据中间的空行」:发 PUT,blank_rows 变成跳过', puts()[4]?.body?.recipe?.sheets?.[1]?.blocks?.[0]?.rows?.blank_rows === 'skip')
|
||||
await page.waitForTimeout(300)
|
||||
// 期 5:没配小计时可以声明「没有小计行」(以合计字样开头的行照常是数据,不再每期报「像小计的行」要人确认)
|
||||
replies.recipe.push(echo())
|
||||
await panel.locator('[data-field="/sheets/1/blocks/0/rows/no_subtotals"]').check()
|
||||
await until(async () => puts().length === 6, 4000)
|
||||
const noSub = puts()[5]?.body?.recipe?.sheets?.[1]?.blocks?.[0]?.rows
|
||||
check('勾选「没有小计行」:发 PUT,no_subtotals 为 true、没有配小计', noSub?.no_subtotals === true && !noSub?.subtotals,
|
||||
JSON.stringify(noSub ?? {}))
|
||||
await page.waitForTimeout(300)
|
||||
// 有问题的配方:一条对得上字段(单位),一条对不上(挂到面板顶部)
|
||||
replies.recipe.push(echo({ recipe_problems: [
|
||||
{ path: '/tables/0/units/分区甲', code: 'unit_label_conflict', message: '表「日客流量」:列「分区甲」的单位与标签「分区甲(人次)」不一致' },
|
||||
{ path: '/sheets/0/blocks/0/label_offset', code: 'schema', message: '第 1 个工作表的交叉表:标签列偏移只能是紧挨日期的左侧一列' },
|
||||
] }))
|
||||
await panel.locator('[data-field="/tables/0/units/分区甲"]').selectOption('人')
|
||||
await until(async () => puts().length === 6, 4000)
|
||||
await until(async () => puts().length === 7, 4000)
|
||||
await page.waitForTimeout(300)
|
||||
const trialBtn = wizard().locator('[data-trial-run]')
|
||||
check('配方有问题时「试运行」禁用', await trialBtn.isDisabled())
|
||||
@@ -2321,8 +2329,8 @@ await section('数据 · 表格:按配方导入', async () => {
|
||||
await panel.locator('[data-recipe-paste-toggle]').click()
|
||||
await panel.locator('[data-recipe-paste] textarea').fill(JSON.stringify(FLOW_RECIPE))
|
||||
await panel.locator('[data-recipe-paste] button', { hasText: '保存配方' }).click()
|
||||
await until(async () => puts().length === 7, 4000)
|
||||
check('「粘贴配方」发 PUT,body 就是粘贴的配方', JSON.stringify(puts()[6]?.body?.recipe) === JSON.stringify(FLOW_RECIPE))
|
||||
await until(async () => puts().length === 8, 4000)
|
||||
check('「粘贴配方」发 PUT,body 就是粘贴的配方', JSON.stringify(puts()[7]?.body?.recipe) === JSON.stringify(FLOW_RECIPE))
|
||||
await page.waitForTimeout(300)
|
||||
check('……问题清掉以后「试运行」可用', await trialBtn.isEnabled())
|
||||
// 连续修改:上一次 PUT 还没回来就接着改。上一次回来时,排在后面的修改不能被服务端那份盖掉,
|
||||
|
||||
Reference in New Issue
Block a user