From 9ef6298b220a56de460467e3218350a4ea83b214 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 9 Oct 2026 22:46:17 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat(b08):=20=EB=A9=94=EC=9D=B8=20=ED=83=AD?= =?UTF-8?q?=20=3D=20=EA=B5=AC=EC=A1=B0=EB=AC=BC=20=EC=A7=91=EA=B3=84?= =?UTF-8?q?=ED=91=9C=20=C2=B7=20=EC=9E=90=EC=9E=AC=20=C2=B7=20=ED=86=A0?= =?UTF-8?q?=EA=B3=B5=20=C2=B7=20=EC=88=98=EB=A6=AC=20=C2=B7=20=EC=9E=90?= =?UTF-8?q?=EC=9E=AC=20=ED=83=AD=20=EC=95=88=20=EC=B4=9D=EA=B4=84=20=C2=B7?= =?UTF-8?q?=20=EC=BD=98=ED=81=AC=EB=A6=AC=ED=8A=B8=EC=86=8C=EC=9A=94=20?= =?UTF-8?q?=C2=B7=20=EC=9E=90=EC=9E=AC=EC=88=98=EB=9F=89=20=C2=B7=20?= =?UTF-8?q?=EC=A2=8C=EC=B8=A1=20=EC=B9=B4=EB=93=9C=20=EB=91=98=EB=A1=9C(16?= =?UTF-8?q?0-1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01STqEqztUthVFBmEP18wBSz --- B08_Quantity/B08_Quantity_UI_Options.ts | 10 +++++---- B08_Quantity/B08_Quantity_UI_Page.ts | 26 ++++++++++++------------ resources/tester/test_122_b08_options.py | 17 ++++++++++++++-- ui_template/ui_template_locale_b4.ts | 3 ++- 4 files changed, 36 insertions(+), 20 deletions(-) diff --git a/B08_Quantity/B08_Quantity_UI_Options.ts b/B08_Quantity/B08_Quantity_UI_Options.ts index 599fe4035..a6f645812 100644 --- a/B08_Quantity/B08_Quantity_UI_Options.ts +++ b/B08_Quantity/B08_Quantity_UI_Options.ts @@ -1,9 +1,10 @@ /* ============================================================================= * B08_Quantity_UI_Options.ts - * 좌측 패널(PLAN 122) — 메인 탭과 같은 컨테이너 셋(구조물 · 토공 · 수리집수면적표 · 접이) + [저장] 줄. + * 좌측 패널(PLAN 122 · 160-1) — 메인 탭과 같은 컨테이너 넷(구조물 집계표 · 자재 · 토공 · 수리집수면적표 · 접이) + * + [저장] 줄. * - * 구조물 › 배관 1본 길이(m) — 정본 = 프로젝트 층 M02 구조물집계표 변수 `파형강관_1본_m` - * 구조물 › 할증 모래 · 자갈 · 시멘트 %(PLAN 124) — 총괄자재수량집계표 그 세 줄 · 0~100 + * 구조물 집계표 › 배관 1본 길이(m) — 정본 = 프로젝트 층 M02 구조물집계표 변수 `파형강관_1본_m` + * 자재 › 할증 모래 · 자갈 · 시멘트 %(PLAN 124) — 총괄자재수량집계표 그 세 줄 · 0~100 * 토공 › 연암 % · 경암 %(암 전체 기준 · 한쪽 고치면 다른 쪽 = 100 − 값 · 0~100) * 수리집수면적표 — 빈 자리(다음 옵션) * 조작 = 캐시 `b08-options` 즉시 + 열린 표가 같은 식으로 바로 다시 셈(`…_Options_Calc`) · @@ -232,7 +233,8 @@ export function createOptionsPanel(projectId: string | null, onSaved: () => void const panel = document.createElement("div"); panel.className = "ui-sidebar-panel b08-options"; panel.append( - card("structure", "B08_Quantity_Tab_Structure", one.root, ...surchargeBlock), + card("summary", "B08_Quantity_Tab_StructureSummary", one.root), + card("material", "B08_Quantity_Tab_Materials", ...surchargeBlock), card("earthwork", "B08_Quantity_Tab_Earthwork", rockRow, hint("암 전체 기준 · 두 칸 합 100")), card("hydrology", "B08_Quantity_Tab_Hydrology", hint("다음 옵션 자리")), actions, diff --git a/B08_Quantity/B08_Quantity_UI_Page.ts b/B08_Quantity/B08_Quantity_UI_Page.ts index 779595939..ab156c57c 100644 --- a/B08_Quantity/B08_Quantity_UI_Page.ts +++ b/B08_Quantity/B08_Quantity_UI_Page.ts @@ -2,9 +2,9 @@ * B08_Quantity_UI_Page.ts * 로그인 후 08: 5차 워크플로우 (수량 산출) — 위 탭 화면 (PLAN 61) * - * 탭 = 구조물 · 토공 · 수리집수면적표 (PLAN 95-1). 위 탭 단추 = 공용 기본 크기(md) · 보조 탭 단추 = sm(PLAN 69-1). - * 구조물 아래 보조 탭 = 구조물 집계표(61-1) · 총괄자재수량집계표 · 콘크리트소요자재집계표 · - * 자재수량집계표(셋 = 121-1 `B08_Quantity_UI_Material`) — 구조물도는 집계표 행을 눌러 여는 모달(95-3). + * 탭 = 구조물 집계표(61-1) · 자재 · 토공 · 수리집수면적표 (PLAN 95-1 · 160-1). 위 탭 단추 = 공용 + * 기본 크기(md) · 보조 탭 단추 = sm(PLAN 69-1). 구조물도는 집계표 행을 눌러 여는 모달(95-3). + * 자재 아래 보조 탭 = 총괄자재수량집계표 · 콘크리트소요자재집계표 · 자재수량집계표(121-1 `…_UI_Material`). * 옛 수량 산출 코드(`old_code/B08_Quantity/`)는 가져오지 않음 — 새로 씀. * 좌측 패널 = 메인 탭과 같은 컨테이너 셋 + [저장] 줄(`B08_Quantity_UI_Options` · PLAN 122). * ========================================================================== */ @@ -28,16 +28,16 @@ function L(key: keyof typeof ui_locales): string { } const TABS = [ - { id: "structure", label: "B08_Quantity_Tab_Structure" }, + { id: "summary", label: "B08_Quantity_Tab_StructureSummary" }, + { id: "material", label: "B08_Quantity_Tab_Materials" }, { id: "earthwork", label: "B08_Quantity_Tab_Earthwork" }, { id: "hydrology", label: "B08_Quantity_Tab_Hydrology" }, ] as const; type TabId = (typeof TABS)[number]["id"]; -/** 구조물 탭 보조 탭 — 실무 엑셀 차례. */ +/** 자재 탭 보조 탭 — 실무 엑셀 차례. */ const SHEETS = [ - { id: "summary", label: "B08_Quantity_Tab_StructureSummary" }, { id: "material", label: "B08_Quantity_Tab_MaterialTotal" }, { id: "concrete", label: "B08_Quantity_Tab_ConcreteMaterial" }, { id: "quantity", label: "B08_Quantity_Tab_MaterialQuantity" }, @@ -52,8 +52,8 @@ function pendingNote(): HTMLElement { return pending; } -/** 구조물 탭 — 보조 탭 줄 + 몸통(보조 탭마다 새 몸통 · 늦은 응답이 다른 보조 탭을 덮지 않게). */ -function renderStructure(host: HTMLElement, projectId: string | null): void { +/** 자재 탭 — 보조 탭 줄 + 몸통(보조 탭마다 새 몸통 · 늦은 응답이 다른 보조 탭을 덮지 않게). */ +function renderMaterials(host: HTMLElement, projectId: string | null): void { const tabs = document.createElement("div"); tabs.className = "b08-quantity__tabs b08-structure__tabs"; tabs.setAttribute("role", "tablist"); @@ -76,18 +76,18 @@ function renderStructure(host: HTMLElement, projectId: string | null): void { const body = document.createElement("div"); view.replaceChildren(body); if (!projectId) body.append(pendingNote()); - else if (picked === "summary") void renderStructureSummary(body, projectId); else void renderMaterialSheet(body, projectId, picked); }; host.replaceChildren(tabs, view); - select("summary"); + select("material"); } /** 탭마다 새 몸통 — 늦게 온 응답이 다른 탭 화면을 덮지 않게(떨어진 몸통에만 그림). */ function renderTab(host: HTMLElement, tab: TabId, projectId: string | null): void { const view = document.createElement("div"); host.replaceChildren(view); - if (tab === "structure") return renderStructure(view, projectId); + if (tab === "material") return renderMaterials(view, projectId); + if (tab === "summary" && projectId) return void renderStructureSummary(view, projectId); if (tab === "hydrology" && projectId) return void renderHydro(view, projectId); if (tab === "earthwork" && projectId) return void renderEarthwork(view, projectId); view.append(pendingNote()); @@ -103,7 +103,7 @@ export function renderB08Quantity(root: HTMLElement): void { panel.className = "b08-quantity__panel"; panel.setAttribute("role", "tabpanel"); - let current: TabId = "structure"; + let current: TabId = "summary"; const select = (tab: TabId): void => { current = tab; tabBar.replaceChildren( @@ -125,7 +125,7 @@ export function renderB08Quantity(root: HTMLElement): void { const body = document.createElement("div"); body.className = "b08-quantity"; body.append(tabBar, panel); - select("structure"); + select("summary"); const layout = createWorkflowLayout({ title: L("B08_Quantity_Title"), diff --git a/resources/tester/test_122_b08_options.py b/resources/tester/test_122_b08_options.py index ddb457020..fa255f5d9 100644 --- a/resources/tester/test_122_b08_options.py +++ b/resources/tester/test_122_b08_options.py @@ -139,8 +139,21 @@ def test_좌측_패널_컨테이너_셋과_저장_줄(): page = (ROOT / "B08_Quantity/B08_Quantity_UI_Page.ts").read_text(encoding="utf-8") panel = (ROOT / "B08_Quantity/B08_Quantity_UI_Options.ts").read_text(encoding="utf-8") assert "leftPanel: createOptionsPanel(projectId, () => select(current))" in page - for tab in ("structure", "earthwork", "hydrology"): - assert f'card("{tab}", "B08_Quantity_Tab_' in panel, tab + # 160-1 — 메인 탭 차례 = 구조물 집계표 · 자재 · 토공 · 수리 · 카드도 같은 차례(1본 → 집계표 · 할증 → 자재) + tabs = ("summary", "material", "earthwork", "hydrology") + ids = re.findall(r'\{ id: "(\w+)", label: "B08_Quantity_Tab_', page) + assert ids == [ + *tabs, + "material", + "concrete", + "quantity", + ] # 자재 탭 안 = 총괄 · 콘크리트소요 · 자재수량 + assert 'select("material");' in page and 'select("summary");' in page + assert [panel.index(f'card("{tab}", "B08_Quantity_Tab_') for tab in tabs] == sorted( + panel.index(f'card("{tab}", "B08_Quantity_Tab_') for tab in tabs + ) + assert 'card("summary", "B08_Quantity_Tab_StructureSummary", one.root),' in panel + assert 'card("material", "B08_Quantity_Tab_Materials", ...surchargeBlock),' in panel assert "ui-collapsible ui-sidebar-section ui-sidebar-card" in panel assert ( 'actions.className = "ui-sidebar-actions"' in panel and "attachCollapsible(panel)" in panel diff --git a/ui_template/ui_template_locale_b4.ts b/ui_template/ui_template_locale_b4.ts index 2d52a5331..970bc5ae4 100644 --- a/ui_template/ui_template_locale_b4.ts +++ b/ui_template/ui_template_locale_b4.ts @@ -19,7 +19,7 @@ export const ui_locales_b4 = { ], B08_Quantity_Tab_Hydrology: ["수리집수면적표", "Hydraulic Catchment Table"], B08_Quantity_Tab_Pending: ["차례대로 만들 탭입니다.", "This tab comes later, in order."], - B08_Quantity_Tab_Structure: ["구조물", "Structure"], + B08_Quantity_Tab_Materials: ["자재", "Materials"], B08_Quantity_Tab_MaterialTotal: ["총괄자재수량집계표", "Total Material Quantity Table"], B08_Quantity_Tab_ConcreteMaterial: [ "콘크리트소요자재집계표", @@ -59,6 +59,7 @@ export const ui_locales_b4 = { ], B08_Quantity_Summary_Total: ["계", "Total"], B08_Quantity_Summary_Unlinked: ["미연결", "Unlinked"], + B08_Quantity_Summary_Unassigned: ["미지정", "Unassigned"], B08_Quantity_Link_Title: ["조합 연결", "Link combinations"], B08_Quantity_Link_Body: ["본체", "Body"], B08_Quantity_Link_Linked: ["연결됨", "Linked"], From f6acf1eda04f385052fc070218c41703cfc9bfd7 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 9 Oct 2026 22:47:56 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat(b08):=20=EA=B5=AC=EC=A1=B0=EB=AC=BC=20?= =?UTF-8?q?=EC=A7=91=EA=B3=84=ED=91=9C=20=E3=80=8C=EB=AF=B8=EC=A7=80?= =?UTF-8?q?=EC=A0=95=E3=80=8D=20=EC=97=B4=20=E2=80=94=20=EC=B8=A1=EC=A0=90?= =?UTF-8?q?=20=EB=B9=88=20=EC=A1=B0=ED=95=A9=20=EC=B9=B8=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=C2=B7=20=EA=B0=99=EC=9D=80=20=EC=A1=B0=ED=95=A9=20?= =?UTF-8?q?=EC=97=B4=20=EC=B9=B8=20=ED=91=9C=EA=B8=B0(160-2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01STqEqztUthVFBmEP18wBSz --- B08_Quantity/B08_Quantity_Summary_Data.ts | 21 +++++ B08_Quantity/B08_Quantity_UI_Summary.ts | 22 +++++- resources/tester/test_160_2_b08_unassigned.py | 77 +++++++++++++++++++ .../tester/test_61_1_b08_summary_table.py | 2 +- 4 files changed, 119 insertions(+), 3 deletions(-) create mode 100644 resources/tester/test_160_2_b08_unassigned.py diff --git a/B08_Quantity/B08_Quantity_Summary_Data.ts b/B08_Quantity/B08_Quantity_Summary_Data.ts index 8be14f298..ec4800b5c 100644 --- a/B08_Quantity/B08_Quantity_Summary_Data.ts +++ b/B08_Quantity/B08_Quantity_Summary_Data.ts @@ -6,6 +6,7 @@ * 열 = 저장된 구조물 조합(본체 `summary_item` + 그룹 `group_items`) — 같은 층 · 주인 · 키는 한 열 * 조합 = 프로젝트 안 사본이 있는 것만(`linkedOnly` · 서버 `linked_snapshots` · PLAN 143-1) * 미연결 시설 = 서버 `unlinked`(셈한 칸 기준) — 그 측점도 줄(값 빈칸 · 「미연결」 · PLAN 154-1) + * 「미지정」 열 글 · 같은 조합 열 칸 = 빈 조합 칸(`unassignedText` · `unassignedCombos` · 160-2) * * 읽기만 — B05/B06 저장 자료(structures.json · pipe_points.json)를 고치지 않는다. * 런타임 import 는 `common_util_summary_item` 하나 — 시험이 두 파일을 이어 붙여 돌림. @@ -102,6 +103,26 @@ export interface ComboColumn { export const comboId = (item: SummaryItem): string => `${item.layer}|${item.owner ?? ""}|${item.key}`; +/** 측점 「미지정」 칸 글 — 시설마다 「이름: 빈 칸 · 빈 칸」(칸 이름 = 그룹 · 본체 = `body` · 160-2). */ +export const unassignedText = (facilities: readonly UnlinkedFacility[], body: string): string => + facilities + .map((f) => { + const slots = f.slots.filter((slot) => !slot.linked).map((slot) => slot.group ?? body); + return slots.length ? `${f.name}: ${slots.join(" · ")}` : f.name; + }) + .join(" / "); + +/** 빈 칸에 고른 조합 id — 같은 조합 열이 있으면 그 칸에 「미지정」(160-2). */ +export function unassignedCombos(facilities: readonly UnlinkedFacility[]): Set { + const ids = new Set(); + for (const f of facilities) + for (const slot of f.slots) { + const item = slot.linked ? null : readSummaryItem(slot.item); + if (item) ids.add(comboId(item)); + } + return ids; +} + function stationOf(thing: PlacedItem): number | null { const value = thing.chainage_m ?? thing.start_m; return typeof value === "number" && Number.isFinite(value) ? value : null; diff --git a/B08_Quantity/B08_Quantity_UI_Summary.ts b/B08_Quantity/B08_Quantity_UI_Summary.ts index 16098f2a7..daf25240b 100644 --- a/B08_Quantity/B08_Quantity_UI_Summary.ts +++ b/B08_Quantity/B08_Quantity_UI_Summary.ts @@ -6,7 +6,8 @@ * 구조물 = GET …/route/structures · 배관 = GET …/drainage/pipe-points (읽기만) * 머리 두 줄 = 조합 구분(경로 첫 칸) · 조합 이름(나머지 경로) — 사본 manifest 경로(집계 응답 `linked`) * 조합 · 측점 = 프로젝트 안 사본이 있는 것만(`linkedOnly` · PLAN 143-1) — 미연결 시설은 그 측점 - * 줄만 「· 미연결」(값 빈칸 · 서버 `unlinked` · PLAN 154-1) + * 줄만 「· 미연결」(값 빈칸 · 서버 `unlinked` · PLAN 154-1) · 측점 옆 「미지정」 열 = 그 측점 빈 조합 칸 + * · 같은 조합 열이 있으면 그 칸에도 「미지정」(160-2 · 화면만) * 횡단배수 열 · 칸 = GET …/quantity/summary(서버 단독 · PLAN 98 · 120) — 머리 구조물 · 형식 · 규격 * 3단 + 단위 줄 · 「관 공」 맨 윗 머리 = 관에 이은 조합 경로(있으면) · 서버가 펼친 종류(`types`)는 * 조합 열에서 뺌 · 다른 구조물 열은 조합 그대로 @@ -37,6 +38,8 @@ import { type LinkedSnapshot, type PlacedItem, type UnlinkedFacility, + unassignedCombos, + unassignedText, unlinkedAt, widthOf, } from "./B08_Quantity_Summary_Data"; @@ -153,9 +156,22 @@ function buildTable( : labels[i], ); const stationHead = L("B08_Quantity_Summary_Station"); + // 160-2 — 빈 조합 칸 이름 · 같은 조합 열 칸에도 「미지정」(미연결 시설이 있을 때만 열) + const missingHead = L("B08_Quantity_Summary_Unassigned"); + const here = stations.map((station) => unlinkedAt(unlinked, station)); + const missing = here.map((at) => unassignedText(at, L("B08_Quantity_Link_Body"))); + const hasMissing = missing.some(Boolean); + const comboIds = new Set(columns.map((column) => column.id)); + const markOf = (at: readonly UnlinkedFacility[]): Record => + Object.fromEntries( + [...unassignedCombos(at)].filter((id) => comboIds.has(id)).map((id) => [id, missingHead]), + ); const table = createDataTable({ columns: [ { key: "station", label: stationHead, width: widthOf(stationHead, ...shown) }, + ...(hasMissing + ? [{ key: "unassigned", label: missingHead, width: widthOf(missingHead, ...missing) }] + : []), ...columns.map((column) => ({ key: column.id, label: column.name, @@ -169,11 +185,13 @@ function buildTable( // 조합 열 칸은 아직 비움 — 사용자가 채울 자리라 「-」 대신 빈칸 · 배관 칸 = 서버 값 rows: stations.map((station, i) => ({ station: shown[i], + ...(hasMissing ? { unassigned: missing[i] } : {}), + ...markOf(here[i]), ...values.get(stationKey(station)), })), total, onRowClick: (i) => onRowClick(stations[i], labels[i]), - frozen: 1, + frozen: hasMissing ? 2 : 1, empty: "", caption: L("B08_Quantity_Tab_StructureSummary"), }); diff --git a/resources/tester/test_160_2_b08_unassigned.py b/resources/tester/test_160_2_b08_unassigned.py new file mode 100644 index 000000000..7b9a37433 --- /dev/null +++ b/resources/tester/test_160_2_b08_unassigned.py @@ -0,0 +1,77 @@ +"""160-2 B08 구조물 집계표 「미지정」 — 측점 옆 열 글 · 같은 조합 열 칸 표기(화면만). + +`B08_Quantity_Summary_Data.ts` 를 `common_util_summary_item.ts` 와 이어 붙여 node 로 돌림(61-1 시험과 같은 꼴). +""" + +import json +from pathlib import Path + +from test_61_1_b08_summary_table import _item, _run + +ROOT = Path(__file__).resolve().parents[2] + + +def _slot(copy: str, group, item=None, linked=False) -> dict: + return {"copy": copy, "group": group, "item": item, "linked": linked} + + +FACILITIES = [ + { + "id": "pipe@100", + "type_id": "pipe", + "name": "배관", + "facility": "pipe", + "chainage_m": 100.0, + "slots": [ + _slot("pipe@100", None), + _slot("pipe@100__gpipe", "관", _item("SS2")), + _slot("pipe@100__gin", "관 보호공(유입)", _item("SS3"), linked=True), + ], + }, + { + "id": "s1", + "type_id": "ford_bridge", + "name": "세월교", + "facility": None, + "chainage_m": 100.004, + "slots": [_slot("s1__gfordout", "유출 구조", {"key": "깨짐"})], # 모양 틀림 = 조합 없음 + }, + { + "id": "s2", + "type_id": "box", + "name": "BOX", + "facility": None, + "chainage_m": 160.0, + "slots": [_slot("s2", None, _item("SS1"), linked=True)], # 빈 칸 없음 = 이름만 + }, +] + + +def test_미지정_글과_조합_칸(): + out = _run( + f"\nconst f = {json.dumps(FACILITIES, ensure_ascii=False)};" + "\nconst at = (s) => unlinkedAt(f, s);" + "\nconsole.log(JSON.stringify({" + " t100: unassignedText(at(100), '본체'), t160: unassignedText(at(160), '본체')," + " t0: unassignedText(at(0), '본체')," + " c100: [...unassignedCombos(at(100))], c160: [...unassignedCombos(at(160))] }));" + ) + assert out["t100"] == "배관: 본체 · 관 / 세월교: 유출 구조" + assert out["t160"] == "BOX" and out["t0"] == "" + assert out["c100"] == ["system||SS2"] # 연결된 칸 · 고른 조합 없는 칸은 뺌 + assert out["c160"] == [] + + +def test_화면_연결(): + summary = (ROOT / "B08_Quantity/B08_Quantity_UI_Summary.ts").read_text(encoding="utf-8") + assert ( + 'unassignedText(at, L("B08_Quantity_Link_Body"))' in summary + ) # 칸 이름 = 모달과 같은 규칙 + assert ( + 'key: "unassigned", label: missingHead' in summary + and "frozen: hasMissing ? 2 : 1" in summary + ) + assert "...markOf(here[i])," in summary and "filter((id) => comboIds.has(id))" in summary + assert '${L("B08_Quantity_Summary_Unlinked")}' in summary # 「· 미연결」 측점 표기 그대로 + locale = (ROOT / "ui_template/ui_template_locale_b4.ts").read_text(encoding="utf-8") + assert 'B08_Quantity_Summary_Unassigned: ["미지정", "Unassigned"]' in locale diff --git a/resources/tester/test_61_1_b08_summary_table.py b/resources/tester/test_61_1_b08_summary_table.py index bfba8a6f8..8922b2a81 100644 --- a/resources/tester/test_61_1_b08_summary_table.py +++ b/resources/tester/test_61_1_b08_summary_table.py @@ -96,5 +96,5 @@ def test_집계표는_공용_표(): # PLAN 70-2 — 공용 많은 자료 표 · 측점 열 고정 · 칸은 빈칸(「—」 아님) ui = (ROOT / "B08_Quantity" / "B08_Quantity_UI_Summary.ts").read_text(encoding="utf-8") assert "createDataTable(" in ui and "@ui/ui_template_data_table" in ui - assert "frozen: 1" in ui and 'empty: ""' in ui and "onRowClick" in ui + assert "frozen: hasMissing ? 2 : 1" in ui and 'empty: ""' in ui and "onRowClick" in ui assert 'document.createElement("table")' not in ui