feat(b05·b06): 종단 누가토량 알약 — 바탕 불투명 · 두 테마 글자 대비 4.5 넘음(붉은 계통 그대로) · 그래프 영역 안 끌어 옮김(마우스 · 손가락 pointer 한 길 · 끝 막힘 · 자리 = 영역 안 비율 · 화면 설정 한 칸 B05 · B06 공용) · 끈 뒤 click 삼킴 · 바닥 서브패널 위 (309)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
This commit is contained in:
@@ -101,6 +101,8 @@ export const STATE_REGISTRY = {
|
||||
},
|
||||
/** B06 횡단 카드 한 줄 카드 수(138-3 · 1~6 · 기본 3). */
|
||||
"cross-columns": { bucket: "pref", scope: "global" },
|
||||
/** B05 · B06 종단 누가토량 알약 자리(309-2) — 그래프 영역 안 비율 `{"x":0~1,"y":0~1}` · 없으면 처음 자리. */
|
||||
"mass-haul-badge-pos": { bucket: "pref", scope: "global" },
|
||||
"section-panel-height": {
|
||||
bucket: "pref",
|
||||
scope: "global",
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
* 만들어 `bodyWrap` 에 붙이고 종단 스크롤과 잇는 데까지 — 그리기 · 높이는 종단도 몫.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createMassHaulBadge } from "@util/common_util_mass_haul_badge";
|
||||
import { attachMassHaulBadgeDrag, createMassHaulBadge } from "@util/common_util_mass_haul_badge";
|
||||
import { readStateRaw, writeStateRaw } from "../b_page_state";
|
||||
import { createHaulPlanPrefetch } from "../../B05_Profile/B05_Profile_Api_HaulPlan";
|
||||
import { createProfileTableOverlay } from "../../B05_Profile/B05_Profile_UI_Profile_TableOverlay";
|
||||
import {
|
||||
@@ -31,6 +32,11 @@ export function createProfileOverlays(deps: ProfileOverlaysDeps) {
|
||||
// 그래프 위 도구줄 · 측점 ▲ 줄(바닥 47px) 아래로 내려 겹치지 않게 한다(289).
|
||||
massBadge.root.style.setProperty("--mass-haul-badge-top", "52px");
|
||||
bodyWrap.append(massBadge.root);
|
||||
// 그래프 안 끌어 옮김(309-2) — 자리는 화면 설정 한 칸(B05 · B06 같은 값)
|
||||
attachMassHaulBadgeDrag(massBadge.root, body, {
|
||||
read: () => readStateRaw("mass-haul-badge-pos"),
|
||||
write: (value) => writeStateRaw("mass-haul-badge-pos", value),
|
||||
});
|
||||
let massHaulSummary: MassHaulSummaryValues | null = null;
|
||||
// 유토 배분은 **서버가 만든다**(2026-09-06) — 편집이 멈추면 뒤에서 받아 두고, 도착하면
|
||||
// 곡선을 다시 그린다. 그 산식이 브라우저 번들에 안 실리는 것이 이 구조의 목적이다.
|
||||
|
||||
@@ -382,29 +382,35 @@
|
||||
/* Y축 띠(고정 축) 오른쪽에 놓는다 — 8px 로 두면 축 라벨 위에 겹쳐 값이 잘린다
|
||||
(2026-09-06 실측). 축 폭이 다른 화면은 이 변수를 덮어쓴다. */
|
||||
left: var(--mass-haul-badge-left, 86px);
|
||||
z-index: 4;
|
||||
/* 바닥 서브패널(유토곡선 · 테이블 5) · 손잡이(6) 위 — 어디로 끌어도 보이고 다시 잡힘(309-2) */
|
||||
z-index: 7;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4, 4px);
|
||||
/* 큰 알약(289) — 글자 · 여백 한 단 위 */
|
||||
padding: var(--spacing-4, 4px) var(--spacing-12, 12px);
|
||||
/* 붉은 계통(289-2) — 테두리 · 글자 · 옅은 바탕 = 위험색 섞음(밝음 · 어두움 같이) */
|
||||
/* 붉은 계통(289-2) — 테두리 · 글자 · 옅은 바탕 = 위험색 섞음(밝음 · 어두움 같이)
|
||||
바탕은 불투명(309-1) — 그래프 선 · 채움이 비치지 않게 · 글자 대비 = 두 테마 모두 4.5 : 1 넘음 */
|
||||
border: 1px solid color-mix(in srgb, var(--color-danger) 55%, transparent);
|
||||
border-radius: var(--radius-pills, 999px);
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--color-danger) 6%,
|
||||
color-mix(in srgb, var(--color-surface-raised) 88%, transparent)
|
||||
);
|
||||
background: color-mix(in srgb, var(--color-danger) 8%, var(--color-surface-raised));
|
||||
color: color-mix(in srgb, var(--color-danger) 70%, var(--color-text-body));
|
||||
font-size: var(--pill-font-size-lg);
|
||||
line-height: var(--leading-body-sm);
|
||||
white-space: nowrap;
|
||||
pointer-events: auto;
|
||||
/* 그래프 안 끌어 옮기기(309-2) — 손가락 끌기가 화면 밀기 · 글 고름으로 새지 않게 */
|
||||
cursor: grab;
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.mass-haul-badge.is-dragging {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.mass-haul-badge em {
|
||||
color: color-mix(in srgb, var(--color-danger) 55%, var(--color-text-secondary));
|
||||
color: color-mix(in srgb, var(--color-danger) 55%, var(--color-text-body));
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
|
||||
@@ -84,3 +84,100 @@ export function createMassHaulBadge(): MassHaulBadge {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** 알약 자리 저장 창구(화면 설정 한 칸) — 값 = `{"x":0~1,"y":0~1}` 글. */
|
||||
export interface MassHaulBadgePlace {
|
||||
read: () => string | null;
|
||||
write: (value: string) => void;
|
||||
}
|
||||
|
||||
/** 끈 것으로 보는 움직임(px) — 이 안이면 그냥 누름(지금 동작 그대로). */
|
||||
const DRAG_SLOP_PX = 4;
|
||||
|
||||
/**
|
||||
* 알약 끌어 옮기기(309-2) — 마우스 · 손가락 같은 pointer 길 · `area`(그래프 스크롤러)의 보이는 상자 안에서만.
|
||||
* 영역 = 알약 처음 자리(Y축 띠 오른쪽 · 측점 ▲ 줄 아래)가 왼쪽 위 끝 · 스크롤러 보이는 오른쪽 · 아래 끝까지.
|
||||
* 자리 = 영역 안 비율 — 폭 · 높이가 바뀌거나 다시 그려도 같은 비율. 알약은 그래프 형제라 끌기가 그래프 고름에 안 닿음.
|
||||
*/
|
||||
export function attachMassHaulBadgeDrag(
|
||||
root: HTMLElement,
|
||||
area: HTMLElement,
|
||||
place: MassHaulBadgePlace,
|
||||
): void {
|
||||
let origin: { x: number; y: number } | null = null;
|
||||
const bounds = () => {
|
||||
if (!origin) {
|
||||
const [left, top] = [root.style.left, root.style.top];
|
||||
root.style.left = root.style.top = "";
|
||||
origin = { x: root.offsetLeft, y: root.offsetTop };
|
||||
[root.style.left, root.style.top] = [left, top];
|
||||
}
|
||||
const w = area.offsetLeft + area.clientWidth - root.offsetWidth - origin.x;
|
||||
const h = area.offsetTop + area.clientHeight - root.offsetHeight - origin.y;
|
||||
return { ...origin, w: Math.max(0, w), h: Math.max(0, h) };
|
||||
};
|
||||
const clamp = (v: number): number => Math.min(1, Math.max(0, v));
|
||||
const put = (x: number, y: number): void => {
|
||||
const b = bounds();
|
||||
root.style.left = `${Math.round(b.x + clamp(x) * b.w)}px`;
|
||||
root.style.top = `${Math.round(b.y + clamp(y) * b.h)}px`;
|
||||
};
|
||||
const restore = (): void => {
|
||||
if (root.hidden || !root.isConnected) return;
|
||||
try {
|
||||
const v = JSON.parse(place.read() ?? "null") as { x?: unknown; y?: unknown } | null;
|
||||
if (typeof v?.x === "number" && typeof v.y === "number") put(v.x, v.y);
|
||||
} catch {
|
||||
/* 못 읽는 값 = 처음 자리 */
|
||||
}
|
||||
};
|
||||
new ResizeObserver(restore).observe(area);
|
||||
new MutationObserver(restore).observe(root, { attributeFilter: ["hidden"] });
|
||||
|
||||
let drag: { id: number; px: number; py: number; left: number; top: number } | null = null;
|
||||
let moved: { x: number; y: number } | null = null;
|
||||
root.addEventListener("pointerdown", (e) => {
|
||||
if (e.button !== 0) return;
|
||||
drag = {
|
||||
id: e.pointerId,
|
||||
px: e.clientX,
|
||||
py: e.clientY,
|
||||
left: root.offsetLeft,
|
||||
top: root.offsetTop,
|
||||
};
|
||||
moved = null;
|
||||
root.setPointerCapture(e.pointerId);
|
||||
});
|
||||
root.addEventListener("pointermove", (e) => {
|
||||
if (!drag || e.pointerId !== drag.id) return;
|
||||
const dx = e.clientX - drag.px;
|
||||
const dy = e.clientY - drag.py;
|
||||
if (!moved && Math.hypot(dx, dy) < DRAG_SLOP_PX) return;
|
||||
const b = bounds();
|
||||
moved = {
|
||||
x: b.w ? clamp((drag.left + dx - b.x) / b.w) : 0,
|
||||
y: b.h ? clamp((drag.top + dy - b.y) / b.h) : 0,
|
||||
};
|
||||
root.classList.add("is-dragging");
|
||||
put(moved.x, moved.y);
|
||||
});
|
||||
const end = (e: PointerEvent): void => {
|
||||
if (!drag || e.pointerId !== drag.id) return;
|
||||
drag = null;
|
||||
root.classList.remove("is-dragging");
|
||||
if (moved) place.write(JSON.stringify({ x: +moved.x.toFixed(4), y: +moved.y.toFixed(4) }));
|
||||
};
|
||||
root.addEventListener("pointerup", end);
|
||||
root.addEventListener("pointercancel", end);
|
||||
// 끈 뒤 따라오는 click 은 삼킴 — 안 끈 누름은 그대로 지나감
|
||||
root.addEventListener(
|
||||
"click",
|
||||
(e) => {
|
||||
if (!moved) return;
|
||||
moved = null;
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
},
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -70,9 +70,13 @@ def test_badge_red_tone_uses_theme_danger():
|
||||
badge = _block(css, ".mass-haul-badge")
|
||||
assert "border: 1px solid color-mix(in srgb, var(--color-danger) 55%, transparent)" in badge
|
||||
assert "color: color-mix(in srgb, var(--color-danger) 70%, var(--color-text-body))" in badge
|
||||
assert "var(--color-danger) 6%" in badge
|
||||
# 바탕 불투명(309-1) — 그래프 선 · 채움이 비치지 않게
|
||||
assert (
|
||||
"background: color-mix(in srgb, var(--color-danger) 8%, var(--color-surface-raised))"
|
||||
in badge
|
||||
)
|
||||
em = _block(css, ".mass-haul-badge em")
|
||||
assert "color-mix(in srgb, var(--color-danger) 55%, var(--color-text-secondary))" in em
|
||||
assert "color-mix(in srgb, var(--color-danger) 55%, var(--color-text-body))" in em
|
||||
over = _block(css, ".mass-haul-badge.is-over")
|
||||
assert "border-color: var(--color-danger)" in over
|
||||
assert "var(--color-danger) 12%" in over
|
||||
@@ -83,9 +87,9 @@ def test_badge_red_tone_uses_theme_danger():
|
||||
|
||||
def test_badge_red_tone_contrast_both_themes():
|
||||
for name, t in THEMES.items():
|
||||
bg = _mix(t["danger"], 6, t["surface"])
|
||||
bg = _mix(t["danger"], 8, t["surface"])
|
||||
text = _mix(t["danger"], 70, t["body"])
|
||||
caption = _mix(t["danger"], 55, t["secondary"])
|
||||
caption = _mix(t["danger"], 55, t["body"])
|
||||
over_bg = _mix(t["danger"], 12, t["surface"])
|
||||
over_value = _mix(t["danger"], 85, t["body"]) # 음수 값 = 위험색 더 많이
|
||||
assert _contrast(text, bg) >= 4.5, name
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
"""B05 · B06 종단 누가토량 알약 — 테마 색 · 그래프 안 끌기(PLAN 309 · 요구 202) — 실제 브라우저(5174).
|
||||
|
||||
① 색: 밝음 · 어두움 × 보통 · 음수 — 바탕 불투명 · 글자(제목 · 값) 대비 4.5 넘음 · 붉은 계통(빨강이 가장 큼)
|
||||
② 끌기(마우스): 움직임 · 끈 뒤 click 은 삼킴 · 안 끈 누름은 지나감 · 영역 끝 막힘(오른쪽 아래 · 왼쪽 위 = 처음 자리)
|
||||
③ 자리 기억: 화면 설정 한 칸(계정 올리기 PUT 에 실림 — 시험은 가로채 서버에 안 씀) · 다시 열어도 · 창 크기 바꿔도 같은 비율
|
||||
④ B06 도 같은 자리 · 손가락 끌기(터치 pointer)도 같은 길
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import test_device_audit as audit # noqa: E402
|
||||
|
||||
KEY = "aislo:pref:mass-haul-badge-pos"
|
||||
# 그래프 고름 · 종단 편집 흔적 — 고른 것 수 · 되돌리기 단추 켜짐
|
||||
PICKED = (
|
||||
"[document.querySelectorAll('.b05-route-profile__body :is(.is-selected, .is-picked,"
|
||||
" .is-active)').length, location.hash]"
|
||||
)
|
||||
BOX = (
|
||||
"(() => { const b = document.querySelector('.mass-haul-badge'), a = b.parentElement"
|
||||
".querySelector('.b05-route-profile__body'); const r = b.getBoundingClientRect(),"
|
||||
" q = a.getBoundingClientRect(); return {l: r.left, t: r.top, r: r.right, b: r.bottom,"
|
||||
" w: r.width, h: r.height, ar: q.left + a.clientWidth, ab: q.top + a.clientHeight,"
|
||||
" al: q.left, at: q.top}; })()"
|
||||
)
|
||||
CONTRAST = r"""
|
||||
(theme) => {
|
||||
document.documentElement.dataset.theme = theme;
|
||||
const parse = (s) => { const m = s.match(/[\d.]+/g).map(Number);
|
||||
const k = s.startsWith('color(') ? 255 : 1;
|
||||
return {r: m[0] * k, g: m[1] * k, b: m[2] * k, a: m.length > 3 ? m[3] : 1}; };
|
||||
const lum = (c) => { const ch = [c.r, c.g, c.b].map((v) => { v /= 255;
|
||||
return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4; });
|
||||
return 0.2126 * ch[0] + 0.7152 * ch[1] + 0.0722 * ch[2]; };
|
||||
const ratio = (x, y) => { const [h, l] = [lum(x), lum(y)].sort((p, q) => q - p);
|
||||
return (h + 0.05) / (l + 0.05); };
|
||||
const badge = document.querySelector('.mass-haul-badge');
|
||||
const was = badge.classList.contains('is-over');
|
||||
const out = [];
|
||||
for (const over of [false, true]) {
|
||||
badge.classList.toggle('is-over', over);
|
||||
const bg = parse(getComputedStyle(badge).backgroundColor);
|
||||
for (const t of ['em', 'strong']) {
|
||||
const fg = parse(getComputedStyle(badge.querySelector(t)).color);
|
||||
out.push({over, t, alpha: bg.a, ratio: ratio(fg, bg), red: fg.r > fg.g && fg.r > fg.b});
|
||||
}
|
||||
}
|
||||
badge.classList.toggle('is-over', was);
|
||||
return out;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def _ctx(browser, cred, touch=False, saved=None):
|
||||
ctx = browser.new_context(
|
||||
viewport={"width": 1440, "height": 900}, has_touch=touch, is_mobile=False
|
||||
)
|
||||
audit._login(ctx, cred)
|
||||
pid = audit._project_id(ctx)
|
||||
ctx.add_init_script(f"localStorage.setItem('frd_current_project_id', {json.dumps(pid)})")
|
||||
pushed = []
|
||||
|
||||
# 계정 취향 — 받기는 빈 값(처음 자리부터) · 올리기는 가로채 서버에 안 씀
|
||||
def prefs(route):
|
||||
if route.request.method == "PUT":
|
||||
pushed.append(json.loads(route.request.post_data or "{}").get("prefs", {}))
|
||||
body = {"prefs": saved or {}} if route.request.method == "GET" else {"ok": True}
|
||||
route.fulfill(status=200, content_type="application/json", body=json.dumps(body))
|
||||
|
||||
ctx.route("**/api/dashboard/me/ui-prefs", prefs)
|
||||
return ctx, pushed
|
||||
|
||||
|
||||
def _drag(page, dx, dy):
|
||||
b = page.evaluate(BOX)
|
||||
x, y = b["l"] + b["w"] / 2, b["t"] + b["h"] / 2
|
||||
page.mouse.move(x, y)
|
||||
page.mouse.down()
|
||||
page.mouse.move(x + dx / 2, y + dy / 2, steps=4)
|
||||
page.mouse.move(x + dx, y + dy, steps=4)
|
||||
page.mouse.up()
|
||||
|
||||
|
||||
def _saved(page):
|
||||
return json.loads(page.evaluate(f"localStorage.getItem({json.dumps(KEY)})") or "null")
|
||||
|
||||
|
||||
@pytest.mark.skipif(not audit._server_up(), reason="sub1 화면 서버(5174) 꺼짐")
|
||||
def test_mass_haul_badge_color_and_drag():
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
cred = json.loads((audit.ROOT / "tmp" / "aislo_cred.json").read_text(encoding="utf-8"))
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(channel="msedge", headless=True)
|
||||
ctx, pushed = _ctx(browser, cred)
|
||||
page = ctx.new_page()
|
||||
audit._open(page, "b05-profile")
|
||||
page.wait_for_selector(".mass-haul-badge:not([hidden])", timeout=30000)
|
||||
|
||||
# ①
|
||||
for theme in ("light", "dark"):
|
||||
for row in page.evaluate(CONTRAST, theme):
|
||||
assert row["alpha"] == 1 and row["ratio"] >= 4.5 and row["red"], (theme, row)
|
||||
page.evaluate("document.documentElement.removeAttribute('data-theme')")
|
||||
|
||||
# ② 안 끈 누름 = 지나감 · 끈 뒤 click = 삼킴
|
||||
page.evaluate(
|
||||
"window.__clicks = 0; document.addEventListener('click', (e) =>"
|
||||
" e.target.closest?.('.mass-haul-badge') && window.__clicks++)"
|
||||
)
|
||||
start = page.evaluate(BOX)
|
||||
picked = page.evaluate(PICKED)
|
||||
page.click(".mass-haul-badge")
|
||||
assert page.evaluate("window.__clicks") == 1
|
||||
assert page.evaluate(BOX)["l"] == start["l"]
|
||||
assert _saved(page) is None
|
||||
_drag(page, 200, 60)
|
||||
moved = page.evaluate(BOX)
|
||||
assert abs(moved["l"] - start["l"] - 200) <= 2 and abs(moved["t"] - start["t"] - 60) <= 2
|
||||
assert page.evaluate("window.__clicks") == 1
|
||||
_drag(page, 5000, 5000) # 오른쪽 아래 끝 막힘
|
||||
end = page.evaluate(BOX)
|
||||
assert (
|
||||
end["ar"] - 1 <= end["r"] <= end["ar"] + 1
|
||||
and end["ab"] - 1 <= end["b"] <= end["ab"] + 1
|
||||
)
|
||||
assert _saved(page) == {"x": 1, "y": 1}
|
||||
_drag(page, -5000, -5000) # 왼쪽 위 끝 = 처음 자리
|
||||
assert page.evaluate(BOX)["l"] == start["l"] and page.evaluate(BOX)["t"] == start["t"]
|
||||
assert _saved(page) == {"x": 0, "y": 0}
|
||||
assert page.evaluate(PICKED) == picked # 끌기가 그래프 고름 · 편집을 안 일으킴
|
||||
|
||||
# ③ 비율 기억 · 계정 올리기에 실림 · 다시 열기 · 창 크기
|
||||
_drag(page, 300, 100)
|
||||
ratio = _saved(page)
|
||||
assert 0 < ratio["x"] < 1 and 0 < ratio["y"] < 1
|
||||
page.wait_for_timeout(2000) # 올리기 모음 1.5초
|
||||
assert any(KEY in push for push in pushed)
|
||||
audit._open(page, "b05-profile")
|
||||
page.wait_for_selector(".mass-haul-badge:not([hidden])", timeout=30000)
|
||||
b = page.evaluate(BOX)
|
||||
x0, y0 = start["l"] - start["al"], start["t"] - start["at"]
|
||||
span_x = b["ar"] - b["al"] - x0 - b["w"]
|
||||
span_y = b["ab"] - b["at"] - y0 - b["h"]
|
||||
assert abs((b["l"] - b["al"] - x0) - ratio["x"] * span_x) <= 2, (b, ratio)
|
||||
assert abs((b["t"] - b["at"] - y0) - ratio["y"] * span_y) <= 2, (b, ratio)
|
||||
page.set_viewport_size({"width": 1200, "height": 800})
|
||||
page.wait_for_timeout(800)
|
||||
b = page.evaluate(BOX)
|
||||
span_x = b["ar"] - b["al"] - x0 - b["w"]
|
||||
assert abs((b["l"] - b["al"] - x0) - ratio["x"] * span_x) <= 2, (b, ratio)
|
||||
assert b["r"] <= b["ar"] + 1 and b["b"] <= b["ab"] + 1
|
||||
ctx.close()
|
||||
|
||||
# ④ B06 같은 자리(계정 값으로 받음) · 손가락 끌기
|
||||
tctx, _ = _ctx(browser, cred, touch=True, saved={KEY: json.dumps(ratio)})
|
||||
tp = tctx.new_page()
|
||||
audit._open(tp, "b06-section")
|
||||
tp.wait_for_selector(".mass-haul-badge:not([hidden])", timeout=30000)
|
||||
b = tp.evaluate(BOX)
|
||||
span_x = b["ar"] - b["al"] - x0 - b["w"]
|
||||
assert abs((b["l"] - b["al"] - x0) - ratio["x"] * span_x) <= 2, (b, ratio)
|
||||
cdp = tctx.new_cdp_session(tp)
|
||||
x, y = b["l"] + b["w"] / 2, b["t"] + b["h"] / 2
|
||||
cdp.send(
|
||||
"Input.dispatchTouchEvent", {"type": "touchStart", "touchPoints": [{"x": x, "y": y}]}
|
||||
)
|
||||
for i in range(1, 6):
|
||||
cdp.send(
|
||||
"Input.dispatchTouchEvent",
|
||||
{"type": "touchMove", "touchPoints": [{"x": x - 20 * i, "y": y - 8 * i}]},
|
||||
)
|
||||
cdp.send("Input.dispatchTouchEvent", {"type": "touchEnd", "touchPoints": []})
|
||||
after = tp.evaluate(BOX)
|
||||
assert abs(after["l"] - b["l"] + 100) <= 2 and abs(after["t"] - b["t"] + 40) <= 2, (
|
||||
b,
|
||||
after,
|
||||
)
|
||||
assert _saved(tp) != ratio
|
||||
browser.close()
|
||||
Reference in New Issue
Block a user