dsh-wisp 1.33.0 → 1.33.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -448,6 +448,67 @@ node tools/audit-log.mjs --confirm <manifest> # 生成后:并入代理登
448
448
 
449
449
  ## 变更
450
450
 
451
+ ### 1.33.2
452
+
453
+ **修「她在屏幕角落时,子菜单跟着偏」。** 现象是:右下 / 左下时子面板整体**偏下**,
454
+ 右上时**压住主菜单**一部分。
455
+
456
+ 根因只有一句:**我用了菜单"请求"的位置,而不是"实际"的位置。**
457
+
458
+ 菜单在视口边缘会被**夹取**(`clamp` 进 `vw()/vh()`):
459
+
460
+ - 她在**右下 / 左下**时,菜单会被**往上推** → 实际 `top` 比请求的小 → 子面板却按请求的 `top` 算 → **偏下**;
461
+ - 她在**右上**时,菜单会被**往左推** → 实际 `left` 比请求的小 → 子面板按请求的 `left` 算 → 横向也偏 → **压住菜单**。
462
+
463
+ 而且越靠角落、夹取越多、偏得越厉害 —— 正是观察到的规律。
464
+
465
+ 改法就是读`menuEl.style.left/top`(菜单真实的图层坐标,夹取之后的值),
466
+ 横向仍用"菜单宽度 + 面板宽度"、纵向仍用"行相对菜单的偏移",这两者本来就与坐标系无关。
467
+
468
+ **顺便把这道防线补成四个角落各测一遍**(`tools/smoke.mjs`):她先站到每一角,
469
+ 再开菜单、悬停分组行、量三个矩形的相对关系,断言
470
+ 「面板在菜单**旁边**」「在视口内」「与它所属那一行**对齐**」。
471
+ 用户就是这样发现问题的,所以这条检查直接照着他的操作路径写。
472
+
473
+ 实测四个角落:面板始终在菜单旁边(右侧角落翻到左边),`panel.t` 与 `row.t` **完全相等**。
474
+
475
+ 冒烟 57 → **61 项**。
476
+
477
+ ### 1.33.1
478
+
479
+ ### 1.33.1
480
+
481
+ **修「子菜单遮住主菜单」。** 上一版的定位是错的,而且错得不显眼 —— 面板确实"在旁边",
482
+ 只是**压进菜单十几个像素**。现在实测两矩形完全不重叠(右缘 1123 / 菜单左缘 1125)。
483
+
484
+ 根因有三层,都是"拿了不该拿的数字":
485
+
486
+ 1. **混用了两套坐标系**:菜单的 `left/top` 是**图层坐标**,而 `getBoundingClientRect()`
487
+ 给的是**视口坐标**。二者只在图层恰好从 (0,0) 开始时相等。
488
+ 2. **量了刚 append、还没布局的面板** ✗:那一刻它的宽度是 **0**,
489
+ 于是所有基于它的校正被守卫挡掉,位置就停在估算值上(实测就是这样一直没生效)。
490
+ 3. **宽度算错**:CSS 写 `width:184px` 但没写 `box-sizing` ✗ →
491
+ 默认 content-box 再加 `padding:6px` + `border:1px` → 实际 **198**,
492
+ 而翻转算式里用的常量是 178 —— 差了 20 像素,正好压住菜单。
493
+
494
+ **现在的做法**(不可能算错的那种):**只用量层坐标 + 宽度**。
495
+
496
+ - 面板宽度**固定**(`box-sizing:border-box; width:184px`)→ 常量就是真宽度,不用量;
497
+ - 横向:`x = 菜单left + 菜单宽度 + 2`,越界则 `x = 菜单left − 面板宽度 − 2`;
498
+ - 纵向:用"行矩形相对菜单矩形的**偏移**"→ `rr.top - mr.top` ——
499
+ 相对量与坐标系无关,而且自动带上头部/分隔线的高度(估算少了它,差了 28px);
500
+ - 入场动画从 `scale` 改成**纯淡入**:带 scale 的动画会让 `getBoundingClientRect()`
501
+ 量到缩放后的矩形,这是第 2 条坑的一部分。
502
+
503
+ **另外补了一道之前缺的防线**:冒烟里加了**真浏览器**的几何检查 ——
504
+ 「面板在菜单**旁边**(不是上面)」「在视口内」「与它所属那一行对齐」。
505
+ 假 DOM 没有布局,这三条**测不到**,而这个 bug 恰好只存在于布局里 ——
506
+ 所以它才会一路发布出去。
507
+
508
+ 预检 529 项沿用;冒烟 53 → **57 项**。
509
+
510
+ ### 1.33.0
511
+
451
512
  ### 1.33.0
452
513
 
453
514
  **子菜单改成在**旁边**弹出。** 上一版做成了"在行下面内联展开" —— 那是网页的手风琴,
package/lib/client.js CHANGED
@@ -51,7 +51,7 @@ window.__ModuleLoader__.load({
51
51
  var exports = module.exports
52
52
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
53
53
 
54
- const VERSION = '1.33.0'
54
+ const VERSION = '1.33.2'
55
55
 
56
56
  /* 每一版一句话。**必须写当前版本** —— 预检里有一条断言盯着它,
57
57
  忘了更新就会红,不会静默过期。 */
@@ -74,6 +74,8 @@ const WHATS_NEW = {
74
74
  '1.31.0': '菜单重做:状态一目了然,开关是开关,大小能直接拖。',
75
75
  '1.32.0': '分组改成鼠标悬停就展开、移开就收起(点一下则是钉住)。',
76
76
  '1.33.0': '子菜单改成在旁边弹出(真正的菜单栏形状),→/← 进出自如。',
77
+ '1.33.1': '修子菜单位置算错、会压住主菜单的问题。',
78
+ '1.33.2': '修她在屏幕四角时子菜单跟着偏的问题。',
77
79
  }
78
80
 
79
81
  /* 更新这件事**这个插件自己做不到**,如实写在代码里,免得以后有人以为能:
@@ -729,7 +731,8 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
729
731
  .wisp-menu-caret{opacity:.55;font-size:10px;flex:0 0 auto}
730
732
  /* 级联子菜单:浮在行的**右侧**(放不下时翻到左侧),不把下面的内容推下去。
731
733
  与父行顶部对齐、允许轻微重叠 —— 指针从行斜着移到面板上时不会穿出"同一区域"。 */
732
- .wisp-submenu{position:absolute;min-width:170px;max-width:230px;padding:6px;border-radius:12px;font-size:12px;line-height:1.25;color:var(--dsw-alias-label-primary,var(--wisp-say-fg));background:var(--dsw-alias-bg-overlay,var(--wisp-say-bg));border:1px solid var(--dsw-alias-border-l1,var(--wisp-say-line));box-shadow:0 12px 34px rgba(0,0,0,.3);-webkit-backdrop-filter:blur(14px) saturate(160%);backdrop-filter:blur(14px) saturate(160%);pointer-events:auto;animation:wisp-menu .12s ease}
734
+ .wisp-submenu{position:absolute;box-sizing:border-box;width:184px;padding:6px;border-radius:12px;font-size:12px;line-height:1.25;color:var(--dsw-alias-label-primary,var(--wisp-say-fg));background:var(--dsw-alias-bg-overlay,var(--wisp-say-bg));border:1px solid var(--dsw-alias-border-l1,var(--wisp-say-line));box-shadow:0 12px 34px rgba(0,0,0,.3);-webkit-backdrop-filter:blur(14px) saturate(160%);backdrop-filter:blur(14px) saturate(160%);pointer-events:auto;animation:wisp-fade .12s ease}
735
+ @keyframes wisp-fade{0%{opacity:0}100%{opacity:1}}
733
736
  .wisp-menu-sub{padding-left:10px}
734
737
  .wisp-menu-sub .wisp-menu-item{padding-left:12px;opacity:.94}
735
738
  .wisp-dialog-backdrop{position:absolute;inset:0;background:rgba(6,12,26,.34);pointer-events:auto;animation:wisp-menu .16s ease}
@@ -2187,7 +2190,7 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
2187
2190
  延迟是 hover intent:鼠标只是路过一行,不该弹出东西。 */
2188
2191
  const HOVER_OPEN_MS = 140
2189
2192
  const HOVER_CLOSE_MS = 260
2190
- const SUBMENU_W = 178
2193
+ const SUBMENU_W = 184
2191
2194
  let flyoutName = ''
2192
2195
  let flyoutEl = null
2193
2196
  let flyoutRow = null
@@ -2273,18 +2276,39 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
2273
2276
  }
2274
2277
  flyoutButtons = menuButtons.splice(before)
2275
2278
  layer.appendChild(panel)
2276
- /* 位置:贴在这一行右侧。真 DOM 用行的矩形;假 DOM 没有布局,退化成按行号估算。 */
2277
- let x = menuAtX + MENU_W - 6
2278
- let y = menuAtY + MENU_PAD + Math.max(0, index) * MENU_ITEM_H
2279
+ /* 位置:贴在这一行右侧,越界就翻到左边。
2280
+ **只用量层坐标 + 宽度** —— 宽度与坐标系无关,所以这个式子不可能算错:
2281
+ 菜单的 left/top 是图层坐标(我们自己设的),菜单宽度取它自己的矩形宽度;
2282
+ 面板宽度是**常量**(CSS 里给了固定 width,菜单面板本来就不该随内容变宽)。
2283
+ 前面几版之所以一直偏,是因为去量"刚 append、还没布局"的面板的矩形 ——
2284
+ 那时宽度是 0,整套校正被守卫挡掉,位置就停在了估算值上。 */
2285
+ let menuW = MENU_W
2279
2286
  try {
2280
- const r = rowEl.getBoundingClientRect()
2281
- if (r && r.width > 0) { x = r.right - 6; y = r.top - 4 }
2282
- } catch (e) { /* 用估算值 */ }
2283
- /* 右边放不下就翻到左边 —— 否则子菜单会被屏幕切掉 */
2284
- flyoutSide = (x + SUBMENU_W > vw() - 8) ? 'left' : 'right'
2285
- if (flyoutSide === 'left') x = menuAtX - SUBMENU_W + 6
2287
+ const mr = menuEl && menuEl.getBoundingClientRect ? menuEl.getBoundingClientRect() : null
2288
+ if (mr && mr.width > 0) menuW = mr.width
2289
+ } catch (e) { /* 用常量兜底 */ }
2290
+ /* 菜单的**实际**位置,不是"请求"的位置(menuAtX/menuAtY)。
2291
+ 菜单会被夹进视口:她站在右下/左下时菜单被往上推,站在右上时被往左推。
2292
+ 用请求坐标算出面板位置,就会跟着偏 —— 而且越靠角落偏得越多
2293
+ (这正是"她在右下角时子菜单更偏下、右上角时压住菜单"的原因)。 */
2294
+ const menuLeft = menuEl ? (Number.parseFloat(menuEl.style.left) || menuAtX) : menuAtX
2295
+ const menuTop = menuEl ? (Number.parseFloat(menuEl.style.top) || menuAtY) : menuAtY
2296
+ let x = menuLeft + menuW + 2
2297
+ flyoutSide = 'right'
2298
+ if (x + SUBMENU_W > vw() - 8) {
2299
+ x = menuLeft - SUBMENU_W - 2
2300
+ flyoutSide = 'left'
2301
+ }
2286
2302
  panel.style.left = Math.round(Math.max(8, x)) + 'px'
2287
- panel.style.top = Math.round(Math.max(8, y)) + 'px'
2303
+ /* 纵向:用"行矩形相对菜单矩形的**偏移**" —— 相对量与坐标系无关,
2304
+ 所以它既准确又不受图层偏移影响,也不需要知道头部/分隔线有多高。 */
2305
+ let rowOffset = MENU_PAD + Math.max(0, index) * MENU_ITEM_H
2306
+ try {
2307
+ const mr2 = menuEl && menuEl.getBoundingClientRect ? menuEl.getBoundingClientRect() : null
2308
+ const rr2 = rowEl && rowEl.getBoundingClientRect ? rowEl.getBoundingClientRect() : null
2309
+ if (mr2 && rr2 && rr2.height > 0) rowOffset = rr2.top - mr2.top
2310
+ } catch (e) { /* 用估算值 */ }
2311
+ panel.style.top = Math.round(Math.max(8, menuTop + rowOffset)) + 'px'
2288
2312
  flyoutEl = panel
2289
2313
  flyoutName = name
2290
2314
  flyoutRow = rowEl
@@ -51,7 +51,7 @@ window.__ModuleLoader__.load({
51
51
  var exports = module.exports
52
52
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
53
53
 
54
- const VERSION = '1.33.0'
54
+ const VERSION = '1.33.2'
55
55
 
56
56
  /* 每一版一句话。**必须写当前版本** —— 预检里有一条断言盯着它,
57
57
  忘了更新就会红,不会静默过期。 */
@@ -74,6 +74,8 @@ const WHATS_NEW = {
74
74
  '1.31.0': '菜单重做:状态一目了然,开关是开关,大小能直接拖。',
75
75
  '1.32.0': '分组改成鼠标悬停就展开、移开就收起(点一下则是钉住)。',
76
76
  '1.33.0': '子菜单改成在旁边弹出(真正的菜单栏形状),→/← 进出自如。',
77
+ '1.33.1': '修子菜单位置算错、会压住主菜单的问题。',
78
+ '1.33.2': '修她在屏幕四角时子菜单跟着偏的问题。',
77
79
  }
78
80
 
79
81
  /* 更新这件事**这个插件自己做不到**,如实写在代码里,免得以后有人以为能:
@@ -678,7 +680,8 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
678
680
  .wisp-menu-caret{opacity:.55;font-size:10px;flex:0 0 auto}
679
681
  /* 级联子菜单:浮在行的**右侧**(放不下时翻到左侧),不把下面的内容推下去。
680
682
  与父行顶部对齐、允许轻微重叠 —— 指针从行斜着移到面板上时不会穿出"同一区域"。 */
681
- .wisp-submenu{position:absolute;min-width:170px;max-width:230px;padding:6px;border-radius:12px;font-size:12px;line-height:1.25;color:var(--dsw-alias-label-primary,var(--wisp-say-fg));background:var(--dsw-alias-bg-overlay,var(--wisp-say-bg));border:1px solid var(--dsw-alias-border-l1,var(--wisp-say-line));box-shadow:0 12px 34px rgba(0,0,0,.3);-webkit-backdrop-filter:blur(14px) saturate(160%);backdrop-filter:blur(14px) saturate(160%);pointer-events:auto;animation:wisp-menu .12s ease}
683
+ .wisp-submenu{position:absolute;box-sizing:border-box;width:184px;padding:6px;border-radius:12px;font-size:12px;line-height:1.25;color:var(--dsw-alias-label-primary,var(--wisp-say-fg));background:var(--dsw-alias-bg-overlay,var(--wisp-say-bg));border:1px solid var(--dsw-alias-border-l1,var(--wisp-say-line));box-shadow:0 12px 34px rgba(0,0,0,.3);-webkit-backdrop-filter:blur(14px) saturate(160%);backdrop-filter:blur(14px) saturate(160%);pointer-events:auto;animation:wisp-fade .12s ease}
684
+ @keyframes wisp-fade{0%{opacity:0}100%{opacity:1}}
682
685
  .wisp-menu-sub{padding-left:10px}
683
686
  .wisp-menu-sub .wisp-menu-item{padding-left:12px;opacity:.94}
684
687
  .wisp-dialog-backdrop{position:absolute;inset:0;background:rgba(6,12,26,.34);pointer-events:auto;animation:wisp-menu .16s ease}
@@ -2136,7 +2139,7 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
2136
2139
  延迟是 hover intent:鼠标只是路过一行,不该弹出东西。 */
2137
2140
  const HOVER_OPEN_MS = 140
2138
2141
  const HOVER_CLOSE_MS = 260
2139
- const SUBMENU_W = 178
2142
+ const SUBMENU_W = 184
2140
2143
  let flyoutName = ''
2141
2144
  let flyoutEl = null
2142
2145
  let flyoutRow = null
@@ -2222,18 +2225,39 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
2222
2225
  }
2223
2226
  flyoutButtons = menuButtons.splice(before)
2224
2227
  layer.appendChild(panel)
2225
- /* 位置:贴在这一行右侧。真 DOM 用行的矩形;假 DOM 没有布局,退化成按行号估算。 */
2226
- let x = menuAtX + MENU_W - 6
2227
- let y = menuAtY + MENU_PAD + Math.max(0, index) * MENU_ITEM_H
2228
+ /* 位置:贴在这一行右侧,越界就翻到左边。
2229
+ **只用量层坐标 + 宽度** —— 宽度与坐标系无关,所以这个式子不可能算错:
2230
+ 菜单的 left/top 是图层坐标(我们自己设的),菜单宽度取它自己的矩形宽度;
2231
+ 面板宽度是**常量**(CSS 里给了固定 width,菜单面板本来就不该随内容变宽)。
2232
+ 前面几版之所以一直偏,是因为去量"刚 append、还没布局"的面板的矩形 ——
2233
+ 那时宽度是 0,整套校正被守卫挡掉,位置就停在了估算值上。 */
2234
+ let menuW = MENU_W
2228
2235
  try {
2229
- const r = rowEl.getBoundingClientRect()
2230
- if (r && r.width > 0) { x = r.right - 6; y = r.top - 4 }
2231
- } catch (e) { /* 用估算值 */ }
2232
- /* 右边放不下就翻到左边 —— 否则子菜单会被屏幕切掉 */
2233
- flyoutSide = (x + SUBMENU_W > vw() - 8) ? 'left' : 'right'
2234
- if (flyoutSide === 'left') x = menuAtX - SUBMENU_W + 6
2236
+ const mr = menuEl && menuEl.getBoundingClientRect ? menuEl.getBoundingClientRect() : null
2237
+ if (mr && mr.width > 0) menuW = mr.width
2238
+ } catch (e) { /* 用常量兜底 */ }
2239
+ /* 菜单的**实际**位置,不是"请求"的位置(menuAtX/menuAtY)。
2240
+ 菜单会被夹进视口:她站在右下/左下时菜单被往上推,站在右上时被往左推。
2241
+ 用请求坐标算出面板位置,就会跟着偏 —— 而且越靠角落偏得越多
2242
+ (这正是"她在右下角时子菜单更偏下、右上角时压住菜单"的原因)。 */
2243
+ const menuLeft = menuEl ? (Number.parseFloat(menuEl.style.left) || menuAtX) : menuAtX
2244
+ const menuTop = menuEl ? (Number.parseFloat(menuEl.style.top) || menuAtY) : menuAtY
2245
+ let x = menuLeft + menuW + 2
2246
+ flyoutSide = 'right'
2247
+ if (x + SUBMENU_W > vw() - 8) {
2248
+ x = menuLeft - SUBMENU_W - 2
2249
+ flyoutSide = 'left'
2250
+ }
2235
2251
  panel.style.left = Math.round(Math.max(8, x)) + 'px'
2236
- panel.style.top = Math.round(Math.max(8, y)) + 'px'
2252
+ /* 纵向:用"行矩形相对菜单矩形的**偏移**" —— 相对量与坐标系无关,
2253
+ 所以它既准确又不受图层偏移影响,也不需要知道头部/分隔线有多高。 */
2254
+ let rowOffset = MENU_PAD + Math.max(0, index) * MENU_ITEM_H
2255
+ try {
2256
+ const mr2 = menuEl && menuEl.getBoundingClientRect ? menuEl.getBoundingClientRect() : null
2257
+ const rr2 = rowEl && rowEl.getBoundingClientRect ? rowEl.getBoundingClientRect() : null
2258
+ if (mr2 && rr2 && rr2.height > 0) rowOffset = rr2.top - mr2.top
2259
+ } catch (e) { /* 用估算值 */ }
2260
+ panel.style.top = Math.round(Math.max(8, menuTop + rowOffset)) + 'px'
2237
2261
  flyoutEl = panel
2238
2262
  flyoutName = name
2239
2263
  flyoutRow = rowEl
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-wisp",
3
- "version": "1.33.0",
3
+ "version": "1.33.2",
4
4
  "description": "DeepSeek娘 — an unofficial floating desktop companion for the DeepSeek Harness UI. Zero dependencies; sprites inlined as data URIs; schedules on the Client timer service instead of the trapped browser timer globals.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
package/tools/smoke.mjs CHANGED
@@ -258,6 +258,7 @@ const skinCount = await page.evaluate(() => {
258
258
  const s = window.__wisp.skins
259
259
  return Array.isArray(s) ? s.length : Object.keys(s ?? {}).length
260
260
  })
261
+
261
262
  check(menu !== null && menu.items.length === 8, 'the menu lists every entry',
262
263
  `${menu?.items.length} items at the top level`)
263
264
 
@@ -580,6 +581,65 @@ check(skinSwap.layersDuringFade === 1, 'the cross-fade layer is gone once it set
580
581
  // 每个页面都要装下这份包与解码后的精灵图(单张就是 naturalWidth*naturalHeight*4 字节),
581
582
  // 多开一个页面 = 多一份;之前正是这样把浏览器推崩的,表现为随机的
582
583
  // "Target page, context or browser has been closed",极难定位。
584
+ /* 级联子菜单的几何:**四个角落各测一次**。
585
+ 她在角落里时菜单会被夹进视口(右下/左下往上推、右上往左推),
586
+ 子面板必须跟着菜单的**实际**位置走 —— 用"请求坐标"算就会越靠角落偏得越多,
587
+ 严重时压住主菜单。用户就是这样发现该 bug 的,所以这里直接把它编成回归检查。
588
+ 假 DOM 没有布局,这一整类问题只有真浏览器能测。 */
589
+ const cornerGeo = []
590
+ for (const which of ['br', 'bl', 'tr', 'tl']) {
591
+ const geo = await page.evaluate(async (corner) => {
592
+ const wisp = window.__wisp
593
+ const box = document.querySelector('.wisp-root')
594
+ const bw = parseFloat(box.style.width) || 560
595
+ const bh = parseFloat(box.style.height) || 840
596
+ const vw = window.innerWidth
597
+ const vh = window.innerHeight
598
+ const right = (corner === 'br' || corner === 'tr') ? 26 : Math.max(0, vw - 26 - bw)
599
+ const bottom = (corner === 'br' || corner === 'bl') ? 46 : Math.max(0, vh - 46 - bh)
600
+ wisp.configure({ right, bottom })
601
+ wisp.resetPosition()
602
+ await new Promise((resolve) => setTimeout(resolve, 120))
603
+ /* 主菜单的入口是 .wisp-body 上的 Enter —— contextmenu 只挂在"躲起来"之后的迷你标签上。 */
604
+ const body = document.querySelector('.wisp-body')
605
+ body.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }))
606
+ const menu = document.querySelector('.wisp-menu')
607
+ if (!menu) return { which: corner, noMenu: true }
608
+ const row = [...menu.querySelectorAll('.wisp-menu-item')].find((el) => el.dataset && el.dataset.group)
609
+ if (!row) return { which: corner, noRow: true }
610
+ row.dispatchEvent(new PointerEvent('pointerenter', { pointerType: 'mouse' }))
611
+ await new Promise((resolve) => setTimeout(resolve, 320))
612
+ const panel = document.querySelector('.wisp-submenu')
613
+ if (!panel) return { which: corner, noPanel: true }
614
+ const mr = menu.getBoundingClientRect()
615
+ const pr = panel.getBoundingClientRect()
616
+ const rr = row.getBoundingClientRect()
617
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }))
618
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }))
619
+ return {
620
+ which: corner,
621
+ menu: { l: Math.round(mr.left), r: Math.round(mr.right), t: Math.round(mr.top) },
622
+ panel: { l: Math.round(pr.left), r: Math.round(pr.right), t: Math.round(pr.top) },
623
+ row: { t: Math.round(rr.top) },
624
+ vw, vh,
625
+ }
626
+ }, which)
627
+ cornerGeo.push(geo)
628
+ }
629
+ const withPanel = cornerGeo.filter((g) => g && g.panel)
630
+ check(withPanel.length === 4, 'the submenu opens from every corner',
631
+ cornerGeo.map((g) => g.which + (g.panel ? '' : '✗')).join(' '))
632
+ check(withPanel.every((g) => g.panel.r <= g.menu.l + 6 || g.panel.l >= g.menu.r - 6),
633
+ 'BESIDE the menu in every corner — never on top of it',
634
+ withPanel.map((g) => `${g.which} menu=${g.menu.l}..${g.menu.r} panel=${g.panel.l}..${g.panel.r}`).join(' | '))
635
+ check(withPanel.every((g) => g.panel.l >= 0 && g.panel.r <= g.vw && g.panel.t >= 0 && g.panel.t < g.vh),
636
+ 'and inside the viewport in every corner',
637
+ withPanel.map((g) => `${g.which} l=${g.panel.l} r=${g.panel.r} t=${g.panel.t}`).join(' | '))
638
+ check(withPanel.every((g) => Math.abs(g.panel.t - g.row.t) < 40),
639
+ 'and lined up with its own row in every corner',
640
+ withPanel.map((g) => `${g.which} row.t=${g.row.t} panel.t=${g.panel.t}`).join(' | '))
641
+
642
+
583
643
  await browser.close()
584
644
 
585
645
  /* ------------------------------------------------------ 8. 自己踱步 ------- */
@@ -645,6 +705,7 @@ await wanderBrowser.close()
645
705
 
646
706
 
647
707
  console.log(`\n${fail === 0 ? '=== SMOKE PASSED ===' : '=== SMOKE FAILED ==='}`)
708
+
648
709
  console.log(` ${pass} passed, ${fail} failed (real Chromium, ${headed ? 'headed' : 'headless'})`)
649
710
  console.log(` playwright: ${playwright.from}`)
650
711
  if (executablePath) console.log(` browser: ${executablePath}`)