fix(ui): 돋보기 스크롤 따라감 — 다시 켤 때 원을 먼저 보여 스크롤 옮김이 먹게 · 켠 채 스크롤은 rAF 한 번에 칸 · 페이지 위치 맞춤 · 0.5 초 뒤 다시 뜸 (221-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QND7Co43B24zK2P937rdxt
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
/* 돋보기(`ui_template/ui_template_loupe.ts` · 계획서 218-1 · 220-1 휠 배율 · 221-1 CAD 빼기) 단위검증 헬퍼 —
|
||||
/* 돋보기(`ui_template/ui_template_loupe.ts` · 계획서 218-1 · 220-1 휠 배율 · 221-1 CAD 빼기 · 221-2 스크롤) 단위검증 헬퍼 —
|
||||
* 그 자리에서 트랜스파일해 Node 로 돌리고 값을 JSON 으로 냄. 가짜 창 · 가짜 시계 · 가짜 요소로 판정. */
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
@@ -37,6 +37,8 @@ function fakeWindow(doc) {
|
||||
(win.calls = win.calls.filter((c) => c.type !== type || c.fn !== fn));
|
||||
win.setTimeout = (fn, ms) => (win.timers.push({ id: ++win.seq, at: win.now + ms, fn }), win.seq);
|
||||
win.clearTimeout = (id) => (win.timers = win.timers.filter((t) => t.id !== id));
|
||||
win.requestAnimationFrame = (fn) => win.setTimeout(fn, 16); // 한 화면 틀
|
||||
win.cancelAnimationFrame = win.clearTimeout;
|
||||
win.tick = (ms) => {
|
||||
win.now += ms;
|
||||
for (const t of win.timers.filter((t) => t.at <= win.now)) {
|
||||
@@ -374,7 +376,7 @@ class El {
|
||||
this.style = {};
|
||||
this.dataset = {};
|
||||
this.attrs = {};
|
||||
this.scrollTop = opts.scrollTop || 0;
|
||||
this.st = opts.scrollTop || 0;
|
||||
this.scrollLeft = 0;
|
||||
this.width = opts.width || 0;
|
||||
this.height = opts.height || 0;
|
||||
@@ -391,6 +393,14 @@ class El {
|
||||
getBoundingClientRect() {
|
||||
return this.rect;
|
||||
}
|
||||
// 실제 DOM 처럼 숨긴(hidden) 조상 아래에선 스크롤을 적어도 안 먹음
|
||||
get scrollTop() {
|
||||
return this.st;
|
||||
}
|
||||
set scrollTop(v) {
|
||||
for (let e = this; e; e = e.parent) if (e.hidden) return;
|
||||
this.st = v;
|
||||
}
|
||||
get classList() {
|
||||
return { contains: (k) => this.className.split(" ").includes(k) };
|
||||
}
|
||||
@@ -470,6 +480,8 @@ function makePage() {
|
||||
});
|
||||
cadBox.className = "cad-host";
|
||||
cadBox.append(new El("IFRAME"), new El("A"));
|
||||
scroller.scrollHeight = 900; // 스크롤될 수 있는 칸
|
||||
scroller.clientHeight = 300;
|
||||
scroller.append(canvas, blank, input, check);
|
||||
app.append(scroller, offscreen, table, svg, frame, cadBox);
|
||||
return app;
|
||||
@@ -581,4 +593,46 @@ function fakeDocument(app) {
|
||||
r.hide.reopenLabel = [label.textContent, label.hidden, stage.style.transform];
|
||||
r.hide.reuseRoot = doc.body.children.length;
|
||||
}
|
||||
{
|
||||
// 221-2 스크롤 — 스크롤한 뒤 켬 · 켠 채 스크롤(칸 · 문서)
|
||||
const app = makePage();
|
||||
const scroller = app.children[0];
|
||||
const doc = fakeDocument(app);
|
||||
const win = fakeWindow(doc);
|
||||
win.MutationObserver = class {
|
||||
observe() {}
|
||||
disconnect() {}
|
||||
};
|
||||
const view = m.createLoupeView(win);
|
||||
const cloneScroller = () => doc.body.children[0].children[0].children[0].children[0].children[0];
|
||||
const page = () => doc.body.children[0].children[0].children[0];
|
||||
view.show(500, 400);
|
||||
view.hide();
|
||||
scroller.scrollTop = 0; // 0 에서 켬 → 스크롤 칸으로 잡힘
|
||||
view.show(500, 400);
|
||||
const zero = cloneScroller().scrollTop;
|
||||
view.hide();
|
||||
scroller.scrollTop = 260; // 꺼진 동안 스크롤해 둠 → 다시 켬(숨긴 원을 다시 씀)
|
||||
view.show(500, 400);
|
||||
const before = cloneScroller().scrollTop;
|
||||
// 켠 채 스크롤 — 다음 화면 틀에 위치만 · 같은 복제
|
||||
const first = cloneScroller();
|
||||
scroller.scrollTop = 400;
|
||||
app.rect = { ...app.rect, top: -50, bottom: 750 }; // 문서도 50 내려감
|
||||
const scroll = () => win.calls.filter((c) => c.type === "scroll").forEach((c) => c.fn({}));
|
||||
scroll();
|
||||
scroll(); // 한 틀에 한 번으로 묶음
|
||||
const rafs = win.timers.filter((t) => t.at === win.now + 16).length;
|
||||
win.tick(16);
|
||||
const during = {
|
||||
top: cloneScroller().scrollTop,
|
||||
same: cloneScroller() === first,
|
||||
pageTop: page().style.top,
|
||||
};
|
||||
win.tick(484); // 0.5 초 뒤 다시 뜸 — 비워 둔 칸을 채움 · 스크롤은 그대로
|
||||
const recaptured = { same: cloneScroller() === first, top: cloneScroller().scrollTop };
|
||||
view.hide();
|
||||
const n = win.timers.length;
|
||||
r.scroll = { zero, before, rafs, during, recaptured, timersAfterHide: n };
|
||||
}
|
||||
console.log(JSON.stringify(r));
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""돋보기 스크롤 따라감(계획서 221-2) 회귀검증 — 스크롤해 둔 뒤 켜도 원 = 지금 화면 ·
|
||||
켠 채 스크롤하면 다음 화면 틀(rAF 한 번)에 위치만 맞춤 · 0.5 초 뒤 다시 뜸 · 끄면 예약 없음.
|
||||
|
||||
218-1 헬퍼(`helper_218_1_loupe.cjs`)의 스크롤 장면을 판정함(숨긴 원 안에선 스크롤이 안 먹는 실제 동작 흉내).
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def s():
|
||||
proc = subprocess.run(
|
||||
["node", os.path.join(HERE, "helper_218_1_loupe.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)["scroll"]
|
||||
|
||||
|
||||
def test_스크롤한_뒤_켬(s):
|
||||
assert s["zero"] == 0
|
||||
assert s["before"] == 260 # 다시 켤 때도 지금 스크롤 그대로
|
||||
|
||||
|
||||
def test_켠_채_스크롤_위치만_맞춤(s):
|
||||
assert s["rafs"] == 1 # 스크롤 두 번 → 한 틀에 한 번
|
||||
assert s["during"] == {"top": 400, "same": True, "pageTop": "-50px"} # 칸 · 문서 스크롤
|
||||
|
||||
|
||||
def test_켠_채_스크롤_0_5초_뒤_다시_뜸(s):
|
||||
assert s["recaptured"] == {"same": False, "top": 400}
|
||||
assert s["timersAfterHide"] == 0
|
||||
Reference in New Issue
Block a user