fix(h5): prevent native scrolling from stealing diagonal image swipes

This commit is contained in:
xsl
2026-10-13 00:42:01 +08:00
parent 4c478b16e3
commit 98935dd168
10 changed files with 197 additions and 171 deletions
@@ -4,10 +4,13 @@
- 图片区域可横向跟手切换;说明文字不触发横滑,页签仍可点击或键盘切换。
- 当前试用横向优先:移动超过 12px 且横向位移大于纵向的 2/3 即进入横滑;只有纵向位移达到横向的 1.5 倍时才锁定竖滑。一次手势锁定后不重新判方向。之前上线的竖向优先版本要求横向大于纵向的 1.5 倍才横滑。
- 修正浏览器抢占:仅改方向比例时,`touch-action: pan-y` 仍可能让浏览器提前滚动并发送 `pointercancel`。现在仅报告详情卡启用非被动 `touchmove`,与 Pointer Events 共用同一方向锁;图片区域单指横滑时取消原生滚动,竖滑和文字区域不拦截。不通过 `pointermove.preventDefault()` 或指针捕获来假定原生滚动已被阻止。
- 横滑过程中冻结卡片当前高度,包括图片/字体延迟加载导致的尺寸更新;松手或取消后再恢复当前页签高度,并沿用平滑高度过渡。
- 首尾保留阻尼反馈,但回弹位移最大为 16 CSS px,不允许拉出大片空白。
- 保留浏览器原生纵向滚动、图片点击放大、取消复位与无障碍页签行为。不尝试拦截微信系统返回手势。
验证:`node h5/test/report-touch-layout.mjs` 检查真实移动触摸横滑、斜滑、文字区不切页、图片与文字区竖向滚动、拖动期间高度不变、边缘位移上限和原有布局/交互;`node h5/test/profile-interaction.mjs` 回归共用滑动组件的基础信息页。
新增回归覆盖横移 60px/纵移 75px、横移 80px/纵移 90px 的六段真实触摸路径(之前均会触发 `pointercancel` 并竖滚)。现在每组三张报告卡都检查松手前跟手位移、无浏览器取消、正确切页,以及拖动中/松手后竖向位置均不变。本地报告、基础信息交互和算法 86 条断言均通过;不调用付费检测或写入报告。
之前的竖向优先版本已发布;当前横向优先版本已通过本地报告交互测试,按用户要求发版。发布沿用 Git bundle 快进同步至 `/home/ubuntu/juzi`,重启 `juzi-h5.service`,再经公网运行同一报告交互测试并检查服务健康及静态资源一致性。不代用户推送 origin,不包含未追踪的 `new_feedback/`。最终手机微信体验仍需真机复验。
+2 -3
View File
@@ -40,13 +40,12 @@
"38": "FaceLandmark68Net",
"39": "UI交互界面调整",
"40": "第四轮反馈修改与验收",
"41": "vizCompose",
"41": "showScreen",
"42": "layer_at",
"43": "TinyYolov2Base",
"44": "start-local.sh",
"45": "PredictAllAgeAndGenderWithFaceAlignmentTask",
"46": "PredictAllFaceExpressionsWithFaceAlignmentTask",
"47": "readPhotoFile",
"48": "已修改",
"49": "mobile-select/README.md",
"50": "call_skinanalyze_3view.sh",
@@ -63,7 +62,7 @@
"61": "报告布局与跟手交互调整",
"62": "提交前 Review(2026-10-11)",
"63": "e2e-browser.mjs",
"64": "play",
"64": "start",
"65": "微信登录接入与公网部署说明",
"66": ".runAndExtendWithFaceDetections",
"67": "ui-20261009.mjs",
+1 -1
View File
@@ -1 +1 @@
{"0": "e93608cf288712b8", "1": "cb6ea50bd4e4a5ca", "2": "fd2eb9185c61f1fe", "3": "1f737ca8edb978f2", "4": "56bf12952b0de42b", "5": "7eecef0f27ae2903", "6": "23e6fea2f88a24d1", "7": "ca1cdb7d0ae4e71e", "8": "a689fcb62e7754af", "9": "5b3557ee5c2bf900", "10": "2b037735e1f5b0f6", "11": "4a490abd5e503318", "12": "aeca274694e158d2", "13": "7e3b558a587a4ebe", "14": "bf75bfc99d4f9e34", "15": "6dfa695990b6977c", "16": "95add6a975c0cb72", "17": "e042d7fe7351bcb9", "18": "f93d5d85e671fcee", "19": "39abb1eef19df707", "20": "aecf2fce44ba49c5", "21": "6079f456ac9d4b2e", "22": "02495fa16ac642c3", "23": "90fac00055070ce2", "24": "f85b5b1634224c86", "25": "46e8db3e6e3f37ea", "26": "a8588d8e77a6ae93", "27": "eab427ce9a5219e9", "28": "037cf5fc8fd22dd0", "29": "c608f1c6492e1cb1", "30": "73c13793fca09f56", "31": "fbbcfc059751cc6a", "32": "f6bcf56c4589316b", "33": "75f750c5b7d9f63d", "34": "f024ff7942935690", "35": "241be5abeab1da6f", "36": "a7d375041779bad4", "37": "cb8f26249485324a", "38": "9a25b1230b5fe649", "39": "fdd95375b4a5ddb3", "40": "5e546676ab96de03", "41": "a005abe41943c7cd", "42": "916170b9f5d7346b", "43": "110bce64414980d4", "44": "852c1bb2446edd9c", "45": "92ab3e5134ed8685", "46": "65718282b3c32109", "47": "4e91cfdf55362604", "48": "cf2f2424bc00436f", "49": "1f976711cda158dc", "50": "bbb2b57d72922b16", "51": "eb6a182204867275", "52": "2ec3c8c088568ba9", "53": "68c45056378341a7", "54": "c58d41e8e580fdc6", "55": "68c52c80f38f8b2c", "56": "9ee2ad45dc27851d", "57": "f2a40b5f1b29f669", "58": "d8c090d6837ee23a", "59": "891f59cda3708718", "60": "a505b7de6257a4e8", "61": "5209ce2a267bf33b", "62": "1366ae24c7e5897d", "63": "c256b43f75d7809e", "64": "0adfe3c7d77dcbe1", "65": "bdee3c4a37df52f8", "66": "46204fb2e4d50c53", "67": "3e126d8deaf6e200", "68": "6b91f3679541de79", "69": "e511846eeba03076", "70": "be0c8310abda2467", "71": "589a2d5a76f3880a", "72": "c84f196a7bd35861", "73": "2b887bf601ccfb9f", "74": "ae2a154311e1f53a", "75": "69fdd23278518884", "76": "3c4c278d6026bc3a", "77": "b4e08c87a078a832", "78": "71f300fdfae6e5a6", "79": "86f177bf0a7dd07c", "80": "315325cde1352e98", "81": "37c559741825be74", "82": "200f25fbad7af8fb", "83": "1284897282eb47a3", "84": "2fb17901aa87e0e7", "85": "4bc370be8afd7db1", "86": "386c6fcc7be918e0", "87": "e5fa7717989eda80", "88": "8b5ab5232cef3457", "89": "9e3363b80eeceeea", "90": "dd1c246e19a2d032", "91": "83b0fd3b14363e4d", "92": "fced244d93677d90", "93": "5a9014c4a7f87699", "94": "713d04996c4627df", "95": "59c31f858c7b6970", "96": "4195478d5c528371", "99": "23f1d406f6c779ae", "100": "e42f384dfedcff89", "102": "4dba2da8e6a57bef", "103": "6ffbe07b521a061b", "104": "e114a7aebaaca2f1", "105": "af50c66d16c81bba", "106": "8bc72c179793e1c2", "107": "d9c195d9ccdb137e", "108": "e0fc0a42e33bda91", "109": "4afb1715f62865b1", "110": "45259f492e4f2efb", "111": "b34758de9b00285f", "113": "0d99f86b9e41c4bb", "114": "b9d7c266afa84437", "116": "d1ec12eee057e765", "117": "02c78dd73c398bfa", "123": "b091e3eb82b456eb", "124": "c96276581a8c98d4", "125": "e4f29216745ec758", "127": "746b316833e9c203"}
{"0": "e93608cf288712b8", "1": "cb6ea50bd4e4a5ca", "2": "3427a6e8a9c7cb3e", "3": "1f737ca8edb978f2", "4": "56bf12952b0de42b", "5": "7eecef0f27ae2903", "6": "23e6fea2f88a24d1", "7": "ca1cdb7d0ae4e71e", "8": "a689fcb62e7754af", "9": "5b3557ee5c2bf900", "10": "2b037735e1f5b0f6", "11": "4a490abd5e503318", "12": "aeca274694e158d2", "13": "7e3b558a587a4ebe", "14": "bf75bfc99d4f9e34", "15": "6dfa695990b6977c", "16": "95add6a975c0cb72", "17": "e042d7fe7351bcb9", "18": "f93d5d85e671fcee", "19": "39abb1eef19df707", "20": "aecf2fce44ba49c5", "21": "6079f456ac9d4b2e", "22": "02495fa16ac642c3", "23": "90fac00055070ce2", "24": "f85b5b1634224c86", "25": "46e8db3e6e3f37ea", "26": "a8588d8e77a6ae93", "27": "eab427ce9a5219e9", "28": "037cf5fc8fd22dd0", "29": "c608f1c6492e1cb1", "30": "73c13793fca09f56", "31": "fbbcfc059751cc6a", "32": "f6bcf56c4589316b", "33": "75f750c5b7d9f63d", "34": "f024ff7942935690", "35": "241be5abeab1da6f", "36": "a7d375041779bad4", "37": "cb8f26249485324a", "38": "9a25b1230b5fe649", "39": "fdd95375b4a5ddb3", "40": "5e546676ab96de03", "41": "71cbe5098f4c1b6f", "42": "916170b9f5d7346b", "43": "110bce64414980d4", "44": "852c1bb2446edd9c", "45": "92ab3e5134ed8685", "46": "65718282b3c32109", "48": "cf2f2424bc00436f", "49": "1f976711cda158dc", "50": "bbb2b57d72922b16", "51": "eb6a182204867275", "52": "2ec3c8c088568ba9", "53": "68c45056378341a7", "54": "c58d41e8e580fdc6", "55": "68c52c80f38f8b2c", "56": "9ee2ad45dc27851d", "57": "f2a40b5f1b29f669", "58": "d8c090d6837ee23a", "59": "891f59cda3708718", "60": "a505b7de6257a4e8", "61": "5209ce2a267bf33b", "62": "1366ae24c7e5897d", "63": "c256b43f75d7809e", "64": "a01bccd7f7c24c78", "65": "bdee3c4a37df52f8", "66": "46204fb2e4d50c53", "67": "3e126d8deaf6e200", "68": "6b91f3679541de79", "69": "e511846eeba03076", "70": "be0c8310abda2467", "71": "589a2d5a76f3880a", "72": "c84f196a7bd35861", "73": "2b887bf601ccfb9f", "74": "ae2a154311e1f53a", "75": "69fdd23278518884", "76": "3c4c278d6026bc3a", "77": "b4e08c87a078a832", "78": "71f300fdfae6e5a6", "79": "86f177bf0a7dd07c", "80": "315325cde1352e98", "81": "37c559741825be74", "82": "200f25fbad7af8fb", "83": "1284897282eb47a3", "84": "2fb17901aa87e0e7", "85": "4bc370be8afd7db1", "86": "386c6fcc7be918e0", "87": "e5fa7717989eda80", "88": "8b5ab5232cef3457", "89": "9e3363b80eeceeea", "90": "dd1c246e19a2d032", "91": "83b0fd3b14363e4d", "92": "fced244d93677d90", "93": "5a9014c4a7f87699", "94": "713d04996c4627df", "95": "59c31f858c7b6970", "96": "4195478d5c528371", "99": "23f1d406f6c779ae", "100": "e42f384dfedcff89", "102": "4dba2da8e6a57bef", "103": "6ffbe07b521a061b", "104": "e114a7aebaaca2f1", "105": "af50c66d16c81bba", "106": "8bc72c179793e1c2", "107": "d9c195d9ccdb137e", "108": "e0fc0a42e33bda91", "109": "4afb1715f62865b1", "110": "52f28eb30e80851f", "111": "b34758de9b00285f", "113": "0d99f86b9e41c4bb", "114": "b9d7c266afa84437", "116": "d1ec12eee057e765", "117": "cc191db3bbde70d7", "123": "b091e3eb82b456eb", "124": "c96276581a8c98d4", "125": "e4f29216745ec758", "127": "746b316833e9c203"}
+21 -26
View File
@@ -1,16 +1,16 @@
# Graph Report - juzi (2026-10-13)
## Corpus Check
- 107 files · ~4,905,535 words
- 107 files · ~4,905,775 words
- Verdict: corpus is large enough that graph structure adds value.
## Summary
- 1457 nodes · 2904 edges · 117 communities (92 shown, 25 thin omitted)
- 1457 nodes · 2904 edges · 116 communities (91 shown, 25 thin omitted)
- Extraction: 93% EXTRACTED · 7% INFERRED · 0% AMBIGUOUS · INFERRED: 210 edges (avg confidence: 0.57)
- Token cost: 0 input · 0 output
## Graph Freshness
- Built from commit: `f1e64565`
- Built from commit: `4c478b16`
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
- Run `graphify update .` after code changes (no API cost).
@@ -54,13 +54,12 @@
- FaceLandmark68Net
- UI交互界面调整
- 第四轮反馈修改与验收
- vizCompose
- showScreen
- layer_at
- TinyYolov2Base
- start-local.sh
- PredictAllAgeAndGenderWithFaceAlignmentTask
- PredictAllFaceExpressionsWithFaceAlignmentTask
- readPhotoFile
- 已修改
- mobile-select/README.md
- call_skinanalyze_3view.sh
@@ -74,7 +73,7 @@
- 报告布局与跟手交互调整
- 提交前 Review(2026-10-11)
- e2e-browser.mjs
- play
- start
- 微信登录接入与公网部署说明
- .runAndExtendWithFaceDetections
- ui-20261009.mjs
@@ -147,7 +146,7 @@
## Import Cycles
- None detected.
## Communities (117 total, 25 thin omitted)
## Communities (116 total, 25 thin omitted)
### Community 0 - "face-api.js"
Cohesion: 0.02
@@ -158,8 +157,8 @@ Cohesion: 0.25
Nodes (7): 发现并修复, 可复现命令, 已通过的检查, 线上闭环测试(不含真实微信授权), 结论, 话术及仓库同步, 验证边界
### Community 2 - "app.js"
Cohesion: 0.13
Nodes (22): cancelSkinDrag(), esc(), goSkin(), loadAppFont(), loginAllowed(), loginBrowser(), loginSucceeded(), loginWechat() (+14 more)
Cohesion: 0.11
Nodes (22): cancelSkinDrag(), esc(), goSkin(), openHistory(), renderHistoryList(), renderReport(), renderViz(), restartScanCycle() (+14 more)
### Community 3 - "skin-score.js"
Cohesion: 0.12
@@ -289,9 +288,9 @@ Nodes (4): 1meAI测肤旷视UI问题反馈, UI交互界面调整, 交互, **设
Cohesion: 0.22
Nodes (8): PSD 来源, 前端参数及限制, 后台校准留痕, 已确认并实现, 生产部署及只读验收, 皱纹(紧塑力)原文, 第四轮反馈修改与验收, 验证
### Community 41 - "vizCompose"
Cohesion: 0.18
Nodes (11): renderReport(), renderViz(), setDim(), vizCollectMarks(), vizCompose(), vizFaceCrop(), vizLoadImage(), vizMapURL() (+3 more)
### Community 41 - "showScreen"
Cohesion: 0.22
Nodes (11): loadAppFont(), loginAllowed(), loginBrowser(), loginSucceeded(), loginWechat(), openOverlay(), prepareScanArtwork(), renderAccountStatus() (+3 more)
### Community 42 - "layer_at"
Cohesion: 0.18
@@ -301,10 +300,6 @@ Nodes (11): list, bounds(), layer_at(), main(), save_image(), composite(), main(
Cohesion: 0.50
Nodes (3): HOST, PORT, start-local.sh script
### Community 47 - "readPhotoFile"
Cohesion: 0.40
Nodes (5): checkStill(), faceLuma(), hideCamStatus(), readPhotoFile(), showCamStatus()
### Community 48 - "已修改"
Cohesion: 0.22
Nodes (8): 12 日第二批:明确项修改, 公网部署与复核(2026-10-12,北京时间), 准备捕捉提醒, 字重, 已修改, 拍照展示, 首页高清批注, 验证与状态
@@ -341,9 +336,9 @@ Nodes (4): 仍需公网/业务确认, 已发现并修复, 提交前 Review(202
Cohesion: 0.29
Nodes (4): EXPECTED, fails, pageErrors, pwRoot
### Community 64 - "play"
Cohesion: 0.22
Nodes (9): busyWithFlow(), decode(), drain(), load(), play(), preload(), start(), stopCur() (+1 more)
### Community 64 - "start"
Cohesion: 0.29
Nodes (7): decode(), drain(), load(), preload(), start(), stopCur(), unlock()
### Community 65 - "微信登录接入与公网部署说明"
Cohesion: 0.29
@@ -442,8 +437,8 @@ Cohesion: 0.24
Nodes (12): bindBackdropClose(), closeLoginSuccess(), closeOverlay(), handleCamHidden(), leaveCamera(), photosByView(), popHistorySilently(), requestAnalyze() (+4 more)
### Community 110 - "doCapture"
Cohesion: 0.15
Nodes (24): armNudge(), beginDetect(), captureFromVideo(), captureHaptic(), captureStageHint(), detectFrame(), detTick(), doCapture() (+16 more)
Cohesion: 0.17
Nodes (22): armNudge(), busyWithFlow(), captureFromVideo(), captureHaptic(), captureStageHint(), checkStill(), doCapture(), dropHint() (+14 more)
### Community 111 - "i"
Cohesion: 0.12
@@ -454,8 +449,8 @@ Cohesion: 0.14
Nodes (17): boxPredictionLayer(), convLayer(), convWithBatchNorm(), depthwiseConvLayer(), depthwiseSeparableConv$1(), fullyConnectedLayer(), getStridesForLayerIdx(), leaky() (+9 more)
### Community 117 - "startCamera"
Cohesion: 0.29
Nodes (8): applyMirror(), effMirror(), getCamStream(), handleCamVisible(), onCamTrackEnded(), renderCamStep(), startCamera(), waitVideoStable()
Cohesion: 0.15
Nodes (17): applyMirror(), beginDetect(), detectFrame(), detTick(), effMirror(), enableFallback(), getCamStream(), handleCamVisible() (+9 more)
### Community 123 - "评分圈内居中、基础信息页固定与正脸语音修复"
Cohesion: 0.40
@@ -473,11 +468,11 @@ Nodes (5): height(), FaceLandmark68NetBase(), getSizesForScale(), normalize$1(),
## Suggested Questions
_Questions this graph is uniquely positioned to answer:_
- **Why does `height()` connect `height` to `app.js`, `customer-feedback.js`, `scan-placeholder.mjs`, `extractFaceTensors`, `__awaiter`, `profile-interaction.mjs`, `extractFaces`, `.forwardInput`?**
- **Why does `height()` connect `height` to `app.js`, `customer-feedback.js`, `scan-placeholder.mjs`, `extractFaceTensors`, `showScreen`, `__awaiter`, `profile-interaction.mjs`, `extractFaces`, `.forwardInput`?**
_High betweenness centrality (0.062) - this node is a cross-community bridge._
- **Why does `v()` connect `n` to `report-ui.js`, `vizCompose`, `C`, `face-api.js`?**
- **Why does `v()` connect `n` to `report-ui.js`, `face-api.js`, `app.js`, `C`?**
_High betweenness centrality (0.045) - this node is a cross-community bridge._
- **Why does `k()` connect `k` to `face-api.js`, `t`, `n`, `vizCompose`, `lib/accounts.js`?**
- **Why does `k()` connect `k` to `face-api.js`, `app.js`, `t`, `n`, `lib/accounts.js`?**
_High betweenness centrality (0.039) - this node is a cross-community bridge._
- **Are the 24 inferred relationships involving `n()` (e.g. with `newRecordId()` and `ae()`) actually correct?**
_`n()` has 24 INFERRED edges - model-reasoned connections that need verification._
File diff suppressed because one or more lines are too long
+116 -116
View File
@@ -909,8 +909,8 @@
"_callable": true,
"_origin": "ast",
"id": "h5_js_app_toast",
"community": 110,
"community_name": "doCapture",
"community": 117,
"community_name": "startCamera",
"norm_label": "toast()"
},
{
@@ -921,8 +921,8 @@
"_callable": true,
"_origin": "ast",
"id": "h5_js_app_loadappfont",
"community": 2,
"community_name": "app.js",
"community": 41,
"community_name": "showScreen",
"norm_label": "loadappfont()"
},
{
@@ -933,8 +933,8 @@
"_callable": true,
"_origin": "ast",
"id": "h5_js_app_renderaccountstatus",
"community": 2,
"community_name": "app.js",
"community": 41,
"community_name": "showScreen",
"norm_label": "renderaccountstatus()"
},
{
@@ -945,8 +945,8 @@
"_callable": true,
"_origin": "ast",
"id": "h5_js_app_loginsucceeded",
"community": 2,
"community_name": "app.js",
"community": 41,
"community_name": "showScreen",
"norm_label": "loginsucceeded()"
},
{
@@ -969,8 +969,8 @@
"_callable": true,
"_origin": "ast",
"id": "h5_js_app_showscreen",
"community": 2,
"community_name": "app.js",
"community": 41,
"community_name": "showScreen",
"norm_label": "showscreen()"
},
{
@@ -993,8 +993,8 @@
"_callable": true,
"_origin": "ast",
"id": "h5_js_app_setdetailtab",
"community": 2,
"community_name": "app.js",
"community": 41,
"community_name": "showScreen",
"norm_label": "setdetailtab()"
},
{
@@ -1005,8 +1005,8 @@
"_callable": true,
"_origin": "ast",
"id": "h5_js_app_resetreport",
"community": 2,
"community_name": "app.js",
"community": 41,
"community_name": "showScreen",
"norm_label": "resetreport()"
},
{
@@ -1053,8 +1053,8 @@
"_callable": true,
"_origin": "ast",
"id": "h5_js_app_preparescanartwork",
"community": 2,
"community_name": "app.js",
"community": 41,
"community_name": "showScreen",
"norm_label": "preparescanartwork()"
},
{
@@ -1065,8 +1065,8 @@
"_callable": true,
"_origin": "ast",
"id": "h5_js_app_openoverlay",
"community": 2,
"community_name": "app.js",
"community": 41,
"community_name": "showScreen",
"norm_label": "openoverlay()"
},
{
@@ -1281,8 +1281,8 @@
"_callable": true,
"_origin": "ast",
"id": "h5_js_app_enablefallback",
"community": 110,
"community_name": "doCapture",
"community": 117,
"community_name": "startCamera",
"norm_label": "enablefallback()"
},
{
@@ -1305,8 +1305,8 @@
"_callable": true,
"_origin": "ast",
"id": "h5_js_app_readphotofile",
"community": 47,
"community_name": "readPhotoFile",
"community": 110,
"community_name": "doCapture",
"norm_label": "readphotofile()"
},
{
@@ -1317,8 +1317,8 @@
"_callable": true,
"_origin": "ast",
"id": "h5_js_app_checkstill",
"community": 47,
"community_name": "readPhotoFile",
"community": 110,
"community_name": "doCapture",
"norm_label": "checkstill()"
},
{
@@ -1329,8 +1329,8 @@
"_callable": true,
"_origin": "ast",
"id": "h5_js_app_showcamstatus",
"community": 47,
"community_name": "readPhotoFile",
"community": 117,
"community_name": "startCamera",
"norm_label": "showcamstatus()"
},
{
@@ -1341,8 +1341,8 @@
"_callable": true,
"_origin": "ast",
"id": "h5_js_app_hidecamstatus",
"community": 47,
"community_name": "readPhotoFile",
"community": 110,
"community_name": "doCapture",
"norm_label": "hidecamstatus()"
},
{
@@ -1354,7 +1354,7 @@
"_origin": "ast",
"id": "h5_js_app_decode",
"community": 64,
"community_name": "play",
"community_name": "start",
"norm_label": "decode()"
},
{
@@ -1366,7 +1366,7 @@
"_origin": "ast",
"id": "h5_js_app_load",
"community": 64,
"community_name": "play",
"community_name": "start",
"norm_label": "load()"
},
{
@@ -1378,7 +1378,7 @@
"_origin": "ast",
"id": "h5_js_app_preload",
"community": 64,
"community_name": "play",
"community_name": "start",
"norm_label": "preload()"
},
{
@@ -1390,7 +1390,7 @@
"_origin": "ast",
"id": "h5_js_app_unlock",
"community": 64,
"community_name": "play",
"community_name": "start",
"norm_label": "unlock()"
},
{
@@ -1402,7 +1402,7 @@
"_origin": "ast",
"id": "h5_js_app_stopcur",
"community": 64,
"community_name": "play",
"community_name": "start",
"norm_label": "stopcur()"
},
{
@@ -1414,7 +1414,7 @@
"_origin": "ast",
"id": "h5_js_app_start",
"community": 64,
"community_name": "play",
"community_name": "start",
"norm_label": "start()"
},
{
@@ -1426,7 +1426,7 @@
"_origin": "ast",
"id": "h5_js_app_drain",
"community": 64,
"community_name": "play",
"community_name": "start",
"norm_label": "drain()"
},
{
@@ -1437,8 +1437,8 @@
"_callable": true,
"_origin": "ast",
"id": "h5_js_app_busywithflow",
"community": 64,
"community_name": "play",
"community": 110,
"community_name": "doCapture",
"norm_label": "busywithflow()"
},
{
@@ -1449,8 +1449,8 @@
"_callable": true,
"_origin": "ast",
"id": "h5_js_app_play",
"community": 64,
"community_name": "play",
"community": 110,
"community_name": "doCapture",
"norm_label": "play()"
},
{
@@ -1497,8 +1497,8 @@
"_callable": true,
"_origin": "ast",
"id": "h5_js_app_loadfaceapi",
"community": 110,
"community_name": "doCapture",
"community": 117,
"community_name": "startCamera",
"norm_label": "loadfaceapi()"
},
{
@@ -1509,8 +1509,8 @@
"_callable": true,
"_origin": "ast",
"id": "h5_js_app_loadmodels",
"community": 110,
"community_name": "doCapture",
"community": 117,
"community_name": "startCamera",
"norm_label": "loadmodels()"
},
{
@@ -1617,8 +1617,8 @@
"_callable": true,
"_origin": "ast",
"id": "h5_js_app_maybestartdetect",
"community": 110,
"community_name": "doCapture",
"community": 117,
"community_name": "startCamera",
"norm_label": "maybestartdetect()"
},
{
@@ -1641,8 +1641,8 @@
"_callable": true,
"_origin": "ast",
"id": "h5_js_app_dettick",
"community": 110,
"community_name": "doCapture",
"community": 117,
"community_name": "startCamera",
"norm_label": "dettick()"
},
{
@@ -1653,8 +1653,8 @@
"_callable": true,
"_origin": "ast",
"id": "h5_js_app_detectframe",
"community": 110,
"community_name": "doCapture",
"community": 117,
"community_name": "startCamera",
"norm_label": "detectframe()"
},
{
@@ -1677,8 +1677,8 @@
"_callable": true,
"_origin": "ast",
"id": "h5_js_app_faceluma",
"community": 47,
"community_name": "readPhotoFile",
"community": 110,
"community_name": "doCapture",
"norm_label": "faceluma()"
},
{
@@ -1725,8 +1725,8 @@
"_callable": true,
"_origin": "ast",
"id": "h5_js_app_rendercamstep",
"community": 117,
"community_name": "startCamera",
"community": 110,
"community_name": "doCapture",
"norm_label": "rendercamstep()"
},
{
@@ -1773,8 +1773,8 @@
"_callable": true,
"_origin": "ast",
"id": "h5_js_app_shutter",
"community": 110,
"community_name": "doCapture",
"community": 117,
"community_name": "startCamera",
"norm_label": "shutter()"
},
{
@@ -1809,8 +1809,8 @@
"_callable": true,
"_origin": "ast",
"id": "h5_js_app_setdim",
"community": 41,
"community_name": "vizCompose",
"community": 2,
"community_name": "app.js",
"norm_label": "setdim()"
},
{
@@ -1833,8 +1833,8 @@
"_callable": true,
"_origin": "ast",
"id": "h5_js_app_vizfacecrop",
"community": 41,
"community_name": "vizCompose",
"community": 2,
"community_name": "app.js",
"norm_label": "vizfacecrop()"
},
{
@@ -1845,8 +1845,8 @@
"_callable": true,
"_origin": "ast",
"id": "h5_js_app_vizcollectmarks",
"community": 41,
"community_name": "vizCompose",
"community": 2,
"community_name": "app.js",
"norm_label": "vizcollectmarks()"
},
{
@@ -1857,8 +1857,8 @@
"_callable": true,
"_origin": "ast",
"id": "h5_js_app_vizscalemarks",
"community": 41,
"community_name": "vizCompose",
"community": 2,
"community_name": "app.js",
"norm_label": "vizscalemarks()"
},
{
@@ -1869,8 +1869,8 @@
"_callable": true,
"_origin": "ast",
"id": "h5_js_app_vizmapurl",
"community": 41,
"community_name": "vizCompose",
"community": 2,
"community_name": "app.js",
"norm_label": "vizmapurl()"
},
{
@@ -1881,8 +1881,8 @@
"_callable": true,
"_origin": "ast",
"id": "h5_js_app_vizloadimage",
"community": 41,
"community_name": "vizCompose",
"community": 2,
"community_name": "app.js",
"norm_label": "vizloadimage()"
},
{
@@ -1893,8 +1893,8 @@
"_callable": true,
"_origin": "ast",
"id": "h5_js_app_vizcompose",
"community": 41,
"community_name": "vizCompose",
"community": 2,
"community_name": "app.js",
"norm_label": "vizcompose()"
},
{
@@ -1905,8 +1905,8 @@
"_callable": true,
"_origin": "ast",
"id": "h5_js_app_renderviz",
"community": 41,
"community_name": "vizCompose",
"community": 2,
"community_name": "app.js",
"norm_label": "renderviz()"
},
{
@@ -1917,8 +1917,8 @@
"_callable": true,
"_origin": "ast",
"id": "h5_js_app_renderreport",
"community": 41,
"community_name": "vizCompose",
"community": 2,
"community_name": "app.js",
"norm_label": "renderreport()"
},
{
@@ -1929,8 +1929,8 @@
"_callable": true,
"_origin": "ast",
"id": "h5_js_app_begindetect",
"community": 110,
"community_name": "doCapture",
"community": 117,
"community_name": "startCamera",
"norm_label": "begindetect()"
},
{
@@ -1949,7 +1949,7 @@
"label": "renderHistoryList()",
"file_type": "code",
"source_file": "h5/js/app.js",
"source_location": "L1628",
"source_location": "L1629",
"_callable": true,
"_origin": "ast",
"id": "h5_js_app_renderhistorylist",
@@ -1961,7 +1961,7 @@
"label": "openHistory()",
"file_type": "code",
"source_file": "h5/js/app.js",
"source_location": "L1649",
"source_location": "L1650",
"_callable": true,
"_origin": "ast",
"id": "h5_js_app_openhistory",
@@ -1973,43 +1973,43 @@
"label": "loginAllowed()",
"file_type": "code",
"source_file": "h5/js/app.js",
"source_location": "L1666",
"source_location": "L1667",
"_callable": true,
"_origin": "ast",
"id": "h5_js_app_loginallowed",
"community": 2,
"community_name": "app.js",
"community": 41,
"community_name": "showScreen",
"norm_label": "loginallowed()"
},
{
"label": "loginBrowser()",
"file_type": "code",
"source_file": "h5/js/app.js",
"source_location": "L1671",
"source_location": "L1672",
"_callable": true,
"_origin": "ast",
"id": "h5_js_app_loginbrowser",
"community": 2,
"community_name": "app.js",
"community": 41,
"community_name": "showScreen",
"norm_label": "loginbrowser()"
},
{
"label": "loginWechat()",
"file_type": "code",
"source_file": "h5/js/app.js",
"source_location": "L1680",
"source_location": "L1681",
"_callable": true,
"_origin": "ast",
"id": "h5_js_app_loginwechat",
"community": 2,
"community_name": "app.js",
"community": 41,
"community_name": "showScreen",
"norm_label": "loginwechat()"
},
{
"label": "handleCamHidden()",
"file_type": "code",
"source_file": "h5/js/app.js",
"source_location": "L1694",
"source_location": "L1695",
"_callable": true,
"_origin": "ast",
"id": "h5_js_app_handlecamhidden",
@@ -2021,7 +2021,7 @@
"label": "handleCamVisible()",
"file_type": "code",
"source_file": "h5/js/app.js",
"source_location": "L1704",
"source_location": "L1705",
"_callable": true,
"_origin": "ast",
"id": "h5_js_app_handlecamvisible",
@@ -2033,7 +2033,7 @@
"label": "lock()",
"file_type": "code",
"source_file": "h5/js/app.js",
"source_location": "L1720",
"source_location": "L1721",
"_callable": true,
"_origin": "ast",
"id": "h5_js_app_lock",
@@ -5927,7 +5927,7 @@
"label": "aligned()",
"file_type": "code",
"source_file": "h5/test/report-touch-layout.mjs",
"source_location": "L106",
"source_location": "L114",
"_callable": true,
"_origin": "ast",
"id": "h5_test_report_touch_layout_aligned",
@@ -6419,8 +6419,8 @@
"_callable": true,
"_origin": "ast",
"id": "h5_vendor_faceapi_face_api_w",
"community": 41,
"community_name": "vizCompose",
"community": 2,
"community_name": "app.js",
"norm_label": "w()"
},
{
@@ -8351,8 +8351,8 @@
"_callable": true,
"_origin": "ast",
"id": "h5_vendor_faceapi_face_api_rh",
"community": 41,
"community_name": "vizCompose",
"community": 2,
"community_name": "app.js",
"norm_label": "rh()"
},
{
@@ -18996,7 +18996,7 @@
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "h5/js/app.js",
"source_location": "L1694",
"source_location": "L1695",
"weight": 1.0,
"_origin": "ast",
"source": "h5_js_app",
@@ -19007,7 +19007,7 @@
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "h5/js/app.js",
"source_location": "L1704",
"source_location": "L1705",
"weight": 1.0,
"_origin": "ast",
"source": "h5_js_app",
@@ -19117,7 +19117,7 @@
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "h5/js/app.js",
"source_location": "L1720",
"source_location": "L1721",
"weight": 1.0,
"_origin": "ast",
"source": "h5_js_app",
@@ -19128,7 +19128,7 @@
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "h5/js/app.js",
"source_location": "L1666",
"source_location": "L1667",
"weight": 1.0,
"_origin": "ast",
"source": "h5_js_app",
@@ -19139,7 +19139,7 @@
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "h5/js/app.js",
"source_location": "L1671",
"source_location": "L1672",
"weight": 1.0,
"_origin": "ast",
"source": "h5_js_app",
@@ -19161,7 +19161,7 @@
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "h5/js/app.js",
"source_location": "L1680",
"source_location": "L1681",
"weight": 1.0,
"_origin": "ast",
"source": "h5_js_app",
@@ -19227,7 +19227,7 @@
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "h5/js/app.js",
"source_location": "L1649",
"source_location": "L1650",
"weight": 1.0,
"_origin": "ast",
"source": "h5_js_app",
@@ -19337,7 +19337,7 @@
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "h5/js/app.js",
"source_location": "L1628",
"source_location": "L1629",
"weight": 1.0,
"_origin": "ast",
"source": "h5_js_app",
@@ -19792,7 +19792,7 @@
"context": "call",
"confidence": "EXTRACTED",
"source_file": "h5/js/app.js",
"source_location": "L1668",
"source_location": "L1669",
"weight": 1.0,
"_origin": "ast",
"source": "h5_js_app_loginallowed",
@@ -19888,7 +19888,7 @@
"context": "call",
"confidence": "EXTRACTED",
"source_file": "h5/js/app.js",
"source_location": "L1676",
"source_location": "L1677",
"weight": 1.0,
"_origin": "ast",
"source": "h5_js_app_loginbrowser",
@@ -19936,7 +19936,7 @@
"context": "call",
"confidence": "EXTRACTED",
"source_file": "h5/js/app.js",
"source_location": "L1650",
"source_location": "L1651",
"weight": 1.0,
"_origin": "ast",
"source": "h5_js_app_openhistory",
@@ -20116,7 +20116,7 @@
"context": "call",
"confidence": "EXTRACTED",
"source_file": "h5/js/app.js",
"source_location": "L1701",
"source_location": "L1702",
"weight": 1.0,
"_origin": "ast",
"source": "h5_js_app_handlecamhidden",
@@ -20284,7 +20284,7 @@
"context": "call",
"confidence": "EXTRACTED",
"source_file": "h5/js/app.js",
"source_location": "L1708",
"source_location": "L1709",
"weight": 1.0,
"_origin": "ast",
"source": "h5_js_app_handlecamvisible",
@@ -20368,7 +20368,7 @@
"context": "call",
"confidence": "EXTRACTED",
"source_file": "h5/js/app.js",
"source_location": "L1702",
"source_location": "L1703",
"weight": 1.0,
"_origin": "ast",
"source": "h5_js_app_handlecamhidden",
@@ -20728,7 +20728,7 @@
"context": "call",
"confidence": "EXTRACTED",
"source_file": "h5/js/app.js",
"source_location": "L1697",
"source_location": "L1698",
"weight": 1.0,
"_origin": "ast",
"source": "h5_js_app_handlecamhidden",
@@ -21196,7 +21196,7 @@
"context": "call",
"confidence": "EXTRACTED",
"source_file": "h5/js/app.js",
"source_location": "L1707",
"source_location": "L1708",
"weight": 1.0,
"_origin": "ast",
"source": "h5_js_app_handlecamvisible",
@@ -21280,7 +21280,7 @@
"context": "call",
"confidence": "EXTRACTED",
"source_file": "h5/js/app.js",
"source_location": "L1640",
"source_location": "L1641",
"weight": 1.0,
"_origin": "ast",
"source": "h5_js_app_renderhistorylist",
@@ -21592,7 +21592,7 @@
"context": "call",
"confidence": "EXTRACTED",
"source_file": "h5/js/app.js",
"source_location": "L1651",
"source_location": "L1652",
"weight": 1.0,
"_origin": "ast",
"source": "h5_js_app_openhistory",
@@ -21604,7 +21604,7 @@
"context": "call",
"confidence": "EXTRACTED",
"source_file": "h5/js/app.js",
"source_location": "L1672",
"source_location": "L1673",
"weight": 1.0,
"_origin": "ast",
"source": "h5_js_app_loginbrowser",
@@ -21616,7 +21616,7 @@
"context": "call",
"confidence": "EXTRACTED",
"source_file": "h5/js/app.js",
"source_location": "L1681",
"source_location": "L1682",
"weight": 1.0,
"_origin": "ast",
"source": "h5_js_app_loginwechat",
@@ -28014,7 +28014,7 @@
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "h5/test/report-touch-layout.mjs",
"source_location": "L106",
"source_location": "L114",
"weight": 1.0,
"_origin": "ast",
"source": "h5_test_report_touch_layout",
@@ -50555,5 +50555,5 @@
}
],
"hyperedges": [],
"built_at_commit": "f1e64565b99a7a97a2ebf42c6065c92f6379eb57"
"built_at_commit": "4c478b16e3f1ed3d6f3ca339259d4610ee395ef5"
}
+8 -8
View File
@@ -40,8 +40,8 @@
"semantic_hash": ""
},
"h5/js/app.js": {
"mtime": 1791822609.9707172,
"ast_hash": "7841b52a3f703c468951211b93003dfe",
"mtime": 1791823225.0391843,
"ast_hash": "4787d84f22ac0257204e8ddbdd421140",
"semantic_hash": ""
},
"h5/js/head-pose.js": {
@@ -1920,13 +1920,13 @@
"semantic_hash": ""
},
"h5/js/swipe-track.js": {
"mtime": 1791822609.9707172,
"ast_hash": "79f7941362f09f5a43ca7c2ca73bef06",
"mtime": 1791823225.0391843,
"ast_hash": "9df914772c4bd2a38bb2235bdee9afc9",
"semantic_hash": ""
},
"h5/test/report-touch-layout.mjs": {
"mtime": 1791822609.9707172,
"ast_hash": "90a721a135acb4f63b30bf9b4c9653be",
"mtime": 1791823225.0391843,
"ast_hash": "04b77c2480c0817f28825218429a20ae",
"semantic_hash": ""
},
"h5/test/scan-placeholder.mjs": {
@@ -1965,8 +1965,8 @@
"semantic_hash": ""
},
"docs/\u62a5\u544a\u9875\u6ed1\u52a8\u9632\u51b2\u7a81-2026-10-13.md": {
"mtime": 1791822609.9707172,
"ast_hash": "17fc9e4b6fedfda7fb62763a8c878656",
"mtime": 1791823307.6808388,
"ast_hash": "c9d9f9a9d7642f923b091bbb0d1ad8a8",
"semantic_hash": ""
},
"docs/\u62a5\u544a\u9876\u90e8\u8bc4\u5206\u4e0e\u5e74\u9f84\u6392\u7248-2026-10-13.md": {
+1
View File
@@ -1522,6 +1522,7 @@
}
var swipe = SwipeTrack.create(viewport, track, { count: panels.length, gestureTarget: card,
gestureSelector: '.viz-frame', startThreshold: 12, directionRatio: 2 / 3, maxOverscroll: 16,
preventNativeHorizontalPan: true,
onStart: function () { viewport.style.height = viewport.getBoundingClientRect().height + 'px'; },
onCancel: height, onSelect: function (i) { setDetailTab(card, String(i)); } });
detailSwipes.set(card, { swipe: swipe, height: height });
+26 -11
View File
@@ -18,6 +18,20 @@ window.SwipeTrack = (function () {
if (instant) { void track.offsetWidth; track.style.transition = ''; }
}
function cancel() { go(index, true); if (options.onCancel) options.onCancel(index); }
function lockDirection(dx, dy) {
if (drag.vertical) return false;
if (drag.horizontal) return true;
var threshold = options.startThreshold || 8, ratio = options.directionRatio || 1;
if (Math.max(Math.abs(dx), Math.abs(dy)) <= threshold) return false;
// Touch and pointer events must share the same decision, regardless of
// which event stream the browser dispatches first.
if (Math.abs(dy) * ratio >= Math.abs(dx)) { drag.vertical = true; return false; }
drag.horizontal = true;
if (options.onStart) options.onStart(index);
viewport.classList.add('dragging'); track.classList.add('dragging');
try { target.setPointerCapture(drag.id); } catch (_) {}
return true;
}
target.addEventListener('pointerdown', function (e) {
if (drag.id != null || e.isPrimary === false || e.button !== 0 || !viewport.clientWidth) return;
suppressUntil = 0; // A fresh tap is intentional, not the preceding drag's click.
@@ -31,17 +45,7 @@ window.SwipeTrack = (function () {
target.addEventListener('pointermove', function (e) {
if (e.pointerId !== drag.id || drag.vertical) return;
var dx = e.clientX - drag.startX, dy = e.clientY - drag.startY;
if (!drag.horizontal) {
var threshold = options.startThreshold || 8, ratio = options.directionRatio || 1;
if (Math.max(Math.abs(dx), Math.abs(dy)) <= threshold) return;
// Each caller chooses its diagonal bias. Once locked, neither direction
// can steal this gesture before the finger lifts.
if (Math.abs(dy) * ratio >= Math.abs(dx)) { drag.vertical = true; return; }
drag.horizontal = true;
if (options.onStart) options.onStart(index);
viewport.classList.add('dragging'); track.classList.add('dragging');
try { target.setPointerCapture(e.pointerId); } catch (_) {}
}
if (!lockDirection(dx, dy)) return;
if (e.cancelable) e.preventDefault();
var elapsed = e.timeStamp - drag.lastAt;
if (elapsed > 0) drag.velocity = (e.clientX - drag.lastX) / elapsed;
@@ -53,6 +57,17 @@ window.SwipeTrack = (function () {
track.style.transform = 'translate3d(' + offset + 'px,0,0)';
if (options.onMove) options.onMove(Math.max(0, Math.min(options.count - 1, -offset / drag.width)));
});
if (options.preventNativeHorizontalPan) {
// preventDefault on pointermove does not stop native viewport panning.
// Cancel only the single-touch horizontal gesture before the browser
// starts scrolling and sends pointercancel. Vertical/text/multi-touch
// gestures keep their native behavior; taps still open image zoom.
target.addEventListener('touchmove', function (e) {
if (drag.id == null || e.touches.length !== 1) return;
var touch = e.touches[0];
if (lockDirection(touch.clientX - drag.startX, touch.clientY - drag.startY) && e.cancelable) e.preventDefault();
}, { passive: false });
}
window.addEventListener('pointerup', function (e) {
if (e.pointerId !== drag.id) return;
var horizontal = drag.horizontal, dx = e.clientX - drag.startX, next = index;
+16 -3
View File
@@ -65,7 +65,7 @@ try {
const touch = (type, x, y, timestamp) => cdp.send('Input.dispatchTouchEvent', {
type, ...(timestamp ? { timestamp } : {}), touchPoints: /touchEnd|touchCancel/.test(type) ? [] : [{ x, y, id: 1 }]
});
async function drag(card, dx, { dy = 0, cancel = false, pause = 0, flick = false, verify = false, text = false } = {}) {
async function drag(card, dx, { dy = 0, cancel = false, pause = 0, flick = false, verify = false, text = false, steps = 2, horizontal = false } = {}) {
const frame = card.locator('.dpanel.active ' + (text ? '.ditem-text' : '.viz-frame'));
await frame.scrollIntoViewIfNeeded(); await page.waitForTimeout(400);
const box = await frame.boundingBox(), x = box.x + box.width / 2, y = box.y + box.height / 2;
@@ -73,10 +73,17 @@ try {
const scrollBefore = await page.locator('#s-report').evaluate(el => el.scrollTop);
const beforeHeight = await card.locator('.detail-viewport').evaluate(el => el.getBoundingClientRect().height);
await touch('touchStart', x, y, now);
await touch('touchMove', x + dx / 2, y + dy / 2, now && now + .02);
await touch('touchMove', x + dx, y + dy, now && now + .04);
for (let step = 1; step <= steps; step++) {
await touch('touchMove', x + dx * step / steps, y + dy * step / steps, now && now + .04 * step / steps);
if (steps > 2) await page.waitForTimeout(20);
}
const liveOffset = await offset(card), limit = await card.locator('.detail-viewport').evaluate(el => el.clientWidth * (el.querySelectorAll('.dpanel').length - 1));
assert(liveOffset <= 17 && liveOffset >= -limit - 17, 'edge resistance caps overscroll at 16px');
if (horizontal) {
assert(Math.abs(liveOffset - before - dx) < 3, 'diagonal drag remains finger-following before release');
assert.equal(await card.locator('.detail-panels.dragging').count(), 1, 'browser does not cancel diagonal image gesture');
assert(Math.abs(await page.locator('#s-report').evaluate(el => el.scrollTop) - scrollBefore) < 1, 'diagonal image gesture does not scroll the report');
}
if (verify) {
assert(Math.abs(await offset(card) - before - dx) < 3, 'report follows finger before release');
assert.equal(await selected(card), previous, 'selection commits only on release');
@@ -97,6 +104,7 @@ try {
if (pause) await page.waitForTimeout(pause);
await touch(cancel ? 'touchCancel' : 'touchEnd', 0, 0, now && now + .05);
await page.waitForTimeout(400);
if (horizontal) assert(Math.abs(await page.locator('#s-report').evaluate(el => el.scrollTop) - scrollBefore) < 1, 'horizontal release has no vertical momentum');
if (dy <= -80 && Math.abs(dx) < 10) {
assert(await page.locator('#s-report').evaluate(el => el.scrollTop) > scrollBefore + 10, 'vertical gesture actually scrolls the report');
}
@@ -121,6 +129,11 @@ try {
await drag(card, 100, { cancel: true }); assert.equal(await selected(card), '1', 'cancellation restores selection');
await drag(card, 2, { dy: -90 }); assert.equal(await selected(card), '1', 'vertical scrolling preserves tab');
await drag(card, 65, { dy: -60 }); assert.equal(await selected(card), '0', 'diagonal image gesture prioritizes horizontal switching');
for (const [dx, dy] of [[-60, -75], [-80, -90]]) {
await drag(card, dx, { dy, steps: 6, horizontal: true });
assert.equal(await selected(card), '1', 'vertical-heavy diagonal still uses configured horizontal priority');
await card.locator('.dtab').first().click(); await page.waitForTimeout(400);
}
await card.locator('.dtab').nth(1).click(); await page.waitForTimeout(400);
await drag(card, 100, { text: true }); assert.equal(await selected(card), '1', 'horizontal dragging on analysis text does not switch tabs');
await drag(card, 2, { dy: -90, text: true }); assert.equal(await selected(card), '1', 'reading text keeps vertical scrolling and current tab');