Files
Aislo/resources/tester/helper_ui_fill_viewport.cjs
T

136 lines
4.1 KiB
JavaScript

/* 창 아래 끝까지 칸(`ui_template/ui_template_data_table.ts` · PLAN 199-1) 단위검증 헬퍼 —
* 트랜스파일해 가짜 DOM(요소 트리 · 창 · ResizeObserver)에서 돌리고 판정할 값을 JSON 으로 냄. */
const fs = require("fs");
const path = require("path");
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
const file = path.join(__dirname, "..", "..", "ui_template", "ui_template_data_table.ts");
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
}).outputText;
// ── 가짜 DOM ──
const listeners = { resize: new Set() };
const observers = [];
const win = {
innerHeight: 900,
scrollY: 0,
addEventListener: (t, f) => listeners[t]?.add(f),
removeEventListener: (t, f) => listeners[t]?.delete(f),
};
class RO {
constructor(fn) {
this.fn = fn;
this.targets = new Set();
observers.push(this);
}
observe(el) {
this.targets.add(el);
}
disconnect() {
this.targets.clear();
}
}
function el(name, css = {}, height = 0) {
const node = {
name,
css,
height,
top: 0,
parentElement: null,
nextElementSibling: null,
isConnected: false,
style: {},
getBoundingClientRect() {
return { top: this.top - win.scrollY, height: this.height };
},
};
return node;
}
function link(parent, ...kids) {
kids.forEach((k, i) => {
k.parentElement = parent;
k.nextElementSibling = kids[i + 1] ?? null;
});
parent.kids = kids;
return parent;
}
const sty = (n) => ({
rowGap: n.css.rowGap ?? "normal",
paddingBottom: n.css.paddingBottom ?? "0px",
borderBottomWidth: n.css.borderBottomWidth ?? "0px",
marginBottom: n.css.marginBottom ?? "0px",
});
const doc = { body: null };
const box = { exports: {} };
const sandbox = {
window: win,
document: doc,
getComputedStyle: sty,
ResizeObserver: RO,
};
new Function("exports", "module", "require", ...Object.keys(sandbox), js)(
box.exports,
box,
(p) => (p.endsWith(".css") ? {} : require(p)),
...Object.values(sandbox),
);
const m = box.exports;
// 문서: html > body(padding-bottom 8) > page(padding-bottom 16 · gap 8) > [tabs 40] [card(padding-bottom 12) > [root][note 40]]
const html = el("html");
const body = el("body", { paddingBottom: "8px" });
const page = el("page", { paddingBottom: "16px", rowGap: "8px" });
const tabs = el("tabs", {}, 40);
const card = el("card", { paddingBottom: "12px", marginBottom: "4px" });
const root = el("root");
const note = el("note", {}, 40);
link(html, body);
link(body, page);
link(page, tabs, card);
link(card, root, note);
doc.body = body;
root.top = 200;
const out = { clamp: m.fillHeight(900, 200, 68), small: m.fillHeight(300, 250, 100) };
// 아래 자리 = note(40 + 줄 사이 0) + card 여백(12) + card 바깥 여백(4) + page 아래(16) + body 아래(8)
out.below = m.spaceBelow(root);
out.belowExpected = 40 + 0 + 12 + 4 + 16 + 8;
// 아직 문서에 안 붙음 — 높이 안 건드림
let fits = 0;
const stop = m.fillViewport(root, () => (fits += 1));
out.beforeAttach = root.style.maxHeight ?? null;
out.observed =
observers.some((o) => o.targets.has(root)) && observers.some((o) => o.targets.has(body));
// 붙음 → 관찰자가 부름
[html, body, page, tabs, card, root, note].forEach((n) => (n.isConnected = true));
observers.forEach((o) => o.fn());
out.first = root.style.maxHeight;
out.fits = fits;
// 창 크기 바뀜 → 다시 셈
win.innerHeight = 600;
[...listeners.resize].forEach((f) => f());
out.afterResize = root.style.maxHeight;
// 스크롤해도 같은 값(문서 좌표 기준)
win.scrollY = 80;
[...listeners.resize].forEach((f) => f());
out.afterScroll = root.style.maxHeight;
win.scrollY = 0;
// 아래 형제가 늘어남 → 그만큼 줄어듦
note.height = 100;
observers.forEach((o) => o.fn());
out.afterNoteGrow = root.style.maxHeight;
// 칸이 떨어지면 멈춤
root.isConnected = false;
[...listeners.resize].forEach((f) => f());
out.listenersAfterDetach = listeners.resize.size;
stop();
console.log(JSON.stringify(out));