From fd834b6f34bab7a1fd97372ecd5283e47768135a Mon Sep 17 00:00:00 2001 From: haa Date: Wed, 7 Oct 2026 09:03:54 +0200 Subject: [PATCH 1/2] docs: wheel drag refresh fix brief --- .loom/design/task-brief-wheel-drag-refresh.md | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 .loom/design/task-brief-wheel-drag-refresh.md diff --git a/.loom/design/task-brief-wheel-drag-refresh.md b/.loom/design/task-brief-wheel-drag-refresh.md new file mode 100644 index 0000000..f4f46db --- /dev/null +++ b/.loom/design/task-brief-wheel-drag-refresh.md @@ -0,0 +1,118 @@ +# Devin Brief: 拖拽提交后 UI 不刷新(P0,Linux 真机实测抓到) + +## 项目 +`/home/haa/sites/promptkey`,当前 `master` HEAD `baa37fe`(2.0.4)。新建分支 `fix/wheel-drag-commit-no-refresh`。 + +## 背景 + +我在 **Linux 上用 Playwright 真实驱动前端**(mock `__TAURI__` IPC),跑完整个冒烟套件。16/17 通过,**抓到一个 P0 真 bug** —— 这正是你在 Windows 上装 2.0.4 后拖排序会遇到的"拖了没反应"。 + +## 实测证据(完整链路,逐环都有) + +``` +MID-DRAG: {target row gets `.dragging`, insertion slot appears} ✅ 交互识别正常 +AFTER DROP: set_pin_order 收到 { ids: [2, 1] } ✅ 顺序计算完全正确 +FINAL DOM: #wheelMirror 顺序仍是 ["1","2"] ❌ 提交后没有重新渲染 +``` + +**即:拖拽逻辑对、提交参数对,唯一断点是"提交成功后 UI 没刷新"。** + +这解释了为什么 2.0.4 的 e2e 用例声称"断言 DOM reorder + `set_pin_order` 被调用"却全绿 —— 见下方"测试盲区"。 + +## 疑似位置 + +`src/js/views/prompts.js` 的拖拽提交函数(PR #14 的 pointer 实现,约 `commitPinOrder` / drop handler 一带): + +```js +try { + await ipc('set_pin_order', { ids: pins }); + await loadPrompts(); rebuildIndex(); renderPrompts(); // ← 这条链看起来有,但实测没生效 +} catch { /* toasted */ } +``` + +请先**逐行读实际代码**,查清为什么刷新没发生。至少这几种可能,请逐一证伪/证实: + +1. **刷新被调用了但抛错被 catch 吞掉**(`loadPrompts` 内部依赖某个 mock 里没有的东西,或某个调用 throw) +2. **`renderPrompts()` 只重渲染 `#grid`,根本不碰 `#wheelMirror`** —— 那顺序就永远不更新。**这个可能性最大,优先查** +3. `wheelPrompts()` 的排序在 `wheelSort !== 'manual'` 时不认 `inject_order`,mock 切了 manual 但真实路径有问题 +4. 提交后 `state.prompts` 被替换但 mirror 的渲染走的是别的缓存 +5. drop 之后某个 return 提前退出,刷新语句根本没执行到 + +## 修复要求 + +1. **提交成功后 `#wheelMirror` 必须立即反映新顺序**(不需要刷新页面、不需要切页面再切回) +2. **顺序要真的持久化** —— 重载页面后顺序不变(我的 mock 里已实现"真后端"语义,能验证这点) +3. **失败时回滚视觉状态**:IPC 失败要恢复到拖拽前的顺序,不能停在中间态 +4. 我实测发现:**拖拽必须按在 `.drag` 句柄上才触发**(按行 body 无效)—— 这是 PR #14 的合理设计,**不要改掉**,但请在交付里确认这是有意为之,并考虑要不要给用户更明显的视觉提示(句柄 hover/光标变化),你论证 + +## 附带:修测试盲区(必须) + +2.0.4 的 e2e 号称断言"DOM reorder",却漏掉了这个 bug。原因是: + +- e2e 的 mock `set_pin_order` 返回 `null`,**不真的持久化顺序** +- 且断言只看"调用发生",**没断言松手后 DOM 顺序真的变了** + +请修 `tests/e2e/wheel_sort_drag_e2e.py`: + +1. mock 的 `set_pin_order` 必须**真的写回顺序**(真后端语义) +2. 断言必须包含:**松手后 `#wheelMirror` 的 DOM 顺序确实变化** +3. 再断言一次 `set_pin_order` 收到的 `ids` 顺序 = DOM 新顺序 +4. 加一个失败回滚用例:mock `set_pin_order` reject → UI 恢复原顺序 +5. 加一个"刷新后顺序仍在"用例(重新 load 页面) + +--- + +# 硬约束 + +- **我在 Linux 上给你复现步骤**(见下),你可以自己复现验证 +- 无 cargo,本机不编译,CI 全绿即可 +- 不得回归 Windows 行为 +- 一个 commit `fix(wheel): re-render mirror after pin order commit`,push,PR `--base master`,**不合并** +- 遵守 LOOM 流程 + +## 我在 Linux 上的复现方式(你可以照做) + +```bash +cd /home/haa/sites/promptkey/src && python3 -m http.server 8913 # 后台起静态服务 +# 然后用 Playwright + mock __TAURI__(mock 必须实现 set_pin_order 真写回) +# 我的冒烟脚本在 /tmp/pk_linux_smoke.py,可直接参考或复用 +``` + +关键:**mock 一定要让 `set_pin_order` 真的更新数据**,否则这个 bug 测不出来(这正是原 e2e 漏掉它的原因)。 + +--- + +## 最终交付 + +``` +## 根因 +(为什么提交后不刷新,逐行证据) + +## 修复 +(改了什么;为什么这样改) + +## 测试盲区修正 +(e2e 怎么改的;现在能抓住这个 bug 吗——**请真的跑一次证明**) + +## 复现验证 +(你在 Linux 上跑的证据:拖拽前/中/后 DOM + set_pin_order 参数 + 刷新后顺序) + +## 顺带观察 +(拖拽句柄的视觉提示、以及你发现的任何其他问题) +``` + +```json +COMPLETION_NOTIFY +source: assistant +task: promptkey-wheel-drag-refresh +deliverables: +- root cause of "commits but UI never re-renders" proven with code lines +- mirror re-renders immediately after successful commit and survives reload +- failure rolls back visual order +- e2e mock now persists order and asserts post-drop DOM order + received ids + reload persistence + rollback +- e2e actually re-run to prove it now catches the bug (or would have) +- CI green +- branch fix/wheel-drag-commit-no-refresh + PR against master (not merged) +status: success +errors: none +``` From ffa60cc36a33701d1cbdad0c474f52b98d73c8c6 Mon Sep 17 00:00:00 2001 From: haa Date: Wed, 7 Oct 2026 09:28:21 +0200 Subject: [PATCH 2/2] fix(wheel): re-render mirror after pin order commit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pinOrder() treated inject_order <= 0 as "unordered" (sentinel 9999), so a committed write of position 0 — a 0-based writer or a pack-authored weight of "0" (inject_order is user-facing pack data per GUI_RENOVATION_GUIDE) — silently sorted to the tail. The commit chain itself was sound: set_pin_order persists, wheelSort flips to manual, and loadPrompts+rebuildIndex+renderPrompts re-renders #wheelMirror — but the fresh sort mapped the new first item to the tail, so the DOM appeared to never update. Any finite inject_order is an explicit position now. Strengthen wheel_sort_drag_e2e so this class of bug cannot pass again: mock backend is durable (localStorage-backed, 1-based writes matching Rust), post-drop asserts submitted ids == rendered DOM order for pointer and keyboard commits, a rejected commit must roll back the visual order with no mid-state residue, the committed order must survive page.reload(), and inject_order "0" must rank first not last. Verified: reverting the refresh chain FAILs the new DOM-order checks; reverting pinOrder FAILs the "0" check. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .loom/state.json | 2 +- .loom/tasks.json | 103 ++++++++++++++++++++++ src/js/store.js | 2 +- tests/e2e/wheel_sort_drag_e2e.py | 145 ++++++++++++++++++++++++++----- 4 files changed, 228 insertions(+), 24 deletions(-) diff --git a/.loom/state.json b/.loom/state.json index 599279e..7ba8066 100644 --- a/.loom/state.json +++ b/.loom/state.json @@ -4,7 +4,7 @@ "name": "promptkey", "status": "complete", "created_at": "2026-10-06T08:16:56.246Z", - "updated_at": "2026-10-07T06:08:09.960Z" + "updated_at": "2026-10-07T07:31:11.309Z" }, "understanding": { "confirmed": [ diff --git a/.loom/tasks.json b/.loom/tasks.json index aa4929b..96374c8 100644 --- a/.loom/tasks.json +++ b/.loom/tasks.json @@ -1034,6 +1034,109 @@ "created_at": "2026-10-07T05:55:04.721Z", "updated_at": "2026-10-07T06:08:09.959Z", "completed_at": "2026-10-07T06:08:09.959Z" + }, + { + "id": "TASK-011", + "title": "Fix wheel-mirror stale order after pin-order commit (pinOrder parse + e2e blind spots)", + "outcome": "After a pointer/keyboard reorder commit, #wheelMirror immediately renders the persisted order, the order survives reload, a rejected commit rolls back the visual order, and the e2e proves each of these against a durable mock backend.", + "acceptance": [ + { + "criterion": "store.js pinOrder() honors any finite inject_order (including 0 / weight-0 pack data); only missing/unparseable values get the 9999 tail sentinel", + "verify_by": "read src/js/store.js pinOrder; e2e 'inject_order 0 ranks first' case", + "evidence": "src/js/store.js:93 `return Number.isFinite(n) ? n : 9999;` — 0-based and '0' weight writes now rank as explicit positions" + }, + { + "criterion": "e2e mock set_pin_order is durable (localStorage-backed) and writes 1-based inject_order strings matching Rust (i+1).to_string()", + "verify_by": "read tests/e2e/wheel_sort_drag_e2e.py INIT block", + "evidence": "tests/e2e/wheel_sort_drag_e2e.py INIT: db in localStorage '__pk_e2e_prompts__', writes String(i+1), persist() after each mutation" + }, + { + "criterion": "e2e asserts post-drop DOM order equals submitted set_pin_order ids for pointer AND keyboard commits", + "verify_by": "python3 tests/e2e/wheel_sort_drag_e2e.py — 'submitted ids == rendered DOM order' checks", + "evidence": "'submitted ids == rendered DOM order' + 'keyboard commit: submitted ids == rendered DOM order' checks PASS" + }, + { + "criterion": "e2e covers reload persistence, reject-rollback (DOM + backend data unchanged, no mid-state), and inject_order=0 ordering", + "verify_by": "python3 tests/e2e/wheel_sort_drag_e2e.py ALL PASS (26 checks)", + "evidence": "'order persisted after reload', 'failed commit: DOM rolls back / no mid-state residue / backend data untouched', \"inject_order '0' ranks first, not last\" — all PASS (26/26)" + }, + { + "criterion": "e2e provably catches the bug class: with the refresh chain removed it FAILs on named DOM-order checks; with pinOrder reverted it FAILs the '0' check", + "verify_by": "local runs recorded in PR body / session evidence", + "evidence": "Broken-prompts.js run: FAIL 'DOM re-rendered to committed order', 'submitted ids == rendered DOM order', 'sort toggle now shows manual', + 2 keyboard-path FAILs. Reverted pinOrder run: FAIL \"inject_order '0' ranks first\" (got ['1','3','4','2'])" + }, + { + "criterion": "CI green; PR opened against master, not merged", + "verify_by": "gh pr checks on fix/wheel-drag-commit-no-refresh", + "evidence": "PR #16 https://github.com/Haaaiawd/PromptKey/pull/16 — cargo check/clippy/test pass, frontend static pass, playwright e2e pass; open, not merged" + } + ], + "done_when": [], + "boundaries": [ + "No change to Rust backend or schema; inject_order stays TEXT 1-based writes", + "No change to drag arming rules: mouse may drag from anywhere on a row, touch/pen only from the .drag grip", + "Windows behavior must not regress" + ], + "depends_on": [], + "reads": [ + ".loom/design/task-brief-wheel-drag-refresh.md", + "src/js/views/prompts.js", + "src/js/store.js", + "src/main.rs", + "tests/e2e/wheel_sort_drag_e2e.py" + ], + "touches": [ + "src/js/store.js", + "tests/e2e/wheel_sort_drag_e2e.py" + ], + "implements": ".loom/design/task-brief-wheel-drag-refresh.md", + "design_exemption": "", + "capability_hooks": [], + "capability_exemption": "Bugfix within existing design-system/interaction scope — sort-parse defect + test strengthening; no new professional capability surface.", + "integrity_version": 1, + "covers": [], + "status": "done", + "progress": { + "completed": [], + "current": "complete", + "next": "" + }, + "evidence": [ + "Local: python3 tests/e2e/wheel_sort_drag_e2e.py 26/26 ALL PASS; hotkey_status 10/10; hotkey_recorder 22/22; no_tauri 13/13; user's Linux smoke script 16/17 -> 17/17 unchanged. Negative proofs: refresh chain removed -> 5 named FAILs; pinOrder reverted -> inject_order-0 FAIL. CI: PR #16 all checks green.", + { + "type": "acceptance_results", + "results": [ + { + "criterion": "store.js pinOrder() honors any finite inject_order (including 0 / weight-0 pack data); only missing/unparseable values get the 9999 tail sentinel", + "evidence": "src/js/store.js:93 `return Number.isFinite(n) ? n : 9999;` — 0-based and '0' weight writes now rank as explicit positions" + }, + { + "criterion": "e2e mock set_pin_order is durable (localStorage-backed) and writes 1-based inject_order strings matching Rust (i+1).to_string()", + "evidence": "tests/e2e/wheel_sort_drag_e2e.py INIT: db in localStorage '__pk_e2e_prompts__', writes String(i+1), persist() after each mutation" + }, + { + "criterion": "e2e asserts post-drop DOM order equals submitted set_pin_order ids for pointer AND keyboard commits", + "evidence": "'submitted ids == rendered DOM order' + 'keyboard commit: submitted ids == rendered DOM order' checks PASS" + }, + { + "criterion": "e2e covers reload persistence, reject-rollback (DOM + backend data unchanged, no mid-state), and inject_order=0 ordering", + "evidence": "'order persisted after reload', 'failed commit: DOM rolls back / no mid-state residue / backend data untouched', \"inject_order '0' ranks first, not last\" — all PASS (26/26)" + }, + { + "criterion": "e2e provably catches the bug class: with the refresh chain removed it FAILs on named DOM-order checks; with pinOrder reverted it FAILs the '0' check", + "evidence": "Broken-prompts.js run: FAIL 'DOM re-rendered to committed order', 'submitted ids == rendered DOM order', 'sort toggle now shows manual', + 2 keyboard-path FAILs. Reverted pinOrder run: FAIL \"inject_order '0' ranks first\" (got ['1','3','4','2'])" + }, + { + "criterion": "CI green; PR opened against master, not merged", + "evidence": "PR #16 https://github.com/Haaaiawd/PromptKey/pull/16 — cargo check/clippy/test pass, frontend static pass, playwright e2e pass; open, not merged" + } + ], + "at": "2026-10-07T07:31:11.294Z" + } + ], + "created_at": "2026-10-07T07:25:40.131Z", + "updated_at": "2026-10-07T07:31:11.305Z", + "completed_at": "2026-10-07T07:31:11.305Z" } ] } diff --git a/src/js/store.js b/src/js/store.js index d97858c..0c854bd 100644 --- a/src/js/store.js +++ b/src/js/store.js @@ -90,7 +90,7 @@ export function wheelPrompts() { } function pinOrder(p) { const n = parseInt(p.inject_order, 10); - return Number.isFinite(n) && n > 0 ? n : 9999; + return Number.isFinite(n) ? n : 9999; } const VAR_RE = /\{\{\s*([a-zA-Z_][\w.-]*)\s*\}\}/g; diff --git a/tests/e2e/wheel_sort_drag_e2e.py b/tests/e2e/wheel_sort_drag_e2e.py index d1f6cb2..df3bcfc 100644 --- a/tests/e2e/wheel_sort_drag_e2e.py +++ b/tests/e2e/wheel_sort_drag_e2e.py @@ -7,17 +7,26 @@ never drag. This test drives REAL pointer input (mouse.move/down/move/up), so it fails if the rows do not physically reorder. +The mock backend is durable: prompts live in localStorage and set_pin_order +writes inject_order through (1-based strings, same as Rust's +`(i + 1).to_string()`), so a committed order survives page.reload() exactly +like the real SQLite backend — and a UI that commits but never re-renders is +caught, not masked. + Covers: 1. plain click on a mirror row does not call set_pin_order - 2. dragging a row to a new slot reorders the DOM and calls set_pin_order - once with the exact new id sequence - 3. a drag landing in the original slot commits nothing - 4. committing from 'auto' (the default mode) flips wheelSort to manual, - persists pk-wheel-sort, and the new order survives the reload that - follows the drop - 5. the drawer no longer renders #fApps / #fOrder, and update_prompt passes + 2. dragging a row to a new slot reorders the DOM, calls set_pin_order once + with the exact new id sequence, and submitted ids == rendered DOM order + 3. a committed order survives a full page reload (durable mock backend) + 4. a rejected set_pin_order rolls back: DOM returns to the pre-drag order, + no mid-state residue, backend data untouched + 5. a drag landing in the original slot commits nothing + 6. the drawer no longer renders #fApps / #fOrder, and update_prompt passes app_scopes_json / inject_order through instead of wiping them - 6. keyboard reorder (ArrowDown on the grip) commits via the same path + 7. keyboard reorder (ArrowDown on the grip) commits via the same path + 8. inject_order "0" is an explicit position (ranks first, not last) — + regression guard for pinOrder's "only >0 counts" bug, where a 0-based or + weight-0 write silently sorted to the tail and the mirror never moved Run: python3 tests/e2e/wheel_sort_drag_e2e.py Reqs: python3 -m playwright (browsers installed) @@ -66,30 +75,41 @@ INIT = """ localStorage.setItem('pk-lang', 'zh-CN'); window.__CALLS__ = []; -window.__PROMPTS__ = %s; +// Durable mock backend: the db lives in localStorage so writes survive a +// page reload exactly like the real SQLite backend. A window-scoped array +// would silently reset on reload and could never test persistence. +const DB_KEY = '__pk_e2e_prompts__'; +let db = JSON.parse(localStorage.getItem(DB_KEY) || 'null'); +if (!db) { + db = JSON.parse(JSON.stringify(%s)); + localStorage.setItem(DB_KEY, JSON.stringify(db)); +} +window.__PROMPTS__ = db; +const persist = () => localStorage.setItem(DB_KEY, JSON.stringify(db)); window.__TAURI__ = { core: { invoke: (cmd, args) => { - const db = window.__PROMPTS__; switch (cmd) { case 'get_prompts_view': return Promise.resolve(JSON.parse(JSON.stringify(db))); case 'set_pin_order': window.__CALLS__.push({ cmd, ids: [...args.ids] }); + if (window.__FAIL_PIN_ORDER__) return Promise.reject('mock backend down'); args.ids.forEach((id, i) => { const p = db.find(x => x.id === id); if (p) p.inject_order = String(i + 1); }); + persist(); return Promise.resolve(null); case 'update_prompt': { window.__CALLS__.push({ cmd, prompt: args.prompt }); const p = db.find(x => x.id === args.prompt.id); - if (p) Object.assign(p, args.prompt); + if (p) { Object.assign(p, args.prompt); persist(); } return Promise.resolve(null); } case 'toggle_prompt_pin': { const p = db.find(x => x.id === args.id); - if (p) p.is_pinned = p.is_pinned ? 0 : 1; + if (p) { p.is_pinned = p.is_pinned ? 0 : 1; persist(); } return Promise.resolve(null); } case 'check_service_status': return Promise.resolve(true); @@ -127,10 +147,43 @@ def mirror_names(page): "#wheelMirror .wm-item .nm", "els => els.map(e => e.textContent)") +def mirror_ids(page): + return page.eval_on_selector_all( + "#wheelMirror .wm-item", "els => els.map(e => e.dataset.wheelId)") + + def set_pin_order_calls(page): return page.evaluate("window.__CALLS__.filter(c => c.cmd === 'set_pin_order')") +def wait_mirror_order(page, names, timeout=3000): + """Bounded wait for the mirror to show `names`; returns False instead of + raising so a missing re-render reports as a named FAIL, not a crash.""" + try: + page.wait_for_function( + "[...document.querySelectorAll('#wheelMirror .wm-item .nm')]" + ".map(e => e.textContent).join() === " + json.dumps(",".join(names)), + timeout=timeout) + return True + except Exception: + return False + + +def drag_row_to(page, wheel_id, target_loc, target_y_offset): + """Real pointer drag: press the row's grip, move past the threshold, drop + on the target row at target_y_offset ('top' = above it, 'bottom' = below).""" + row = page.locator(f'.wm-item[data-wheel-id="{wheel_id}"] .drag') + rb = row.bounding_box() + tb = target_loc.bounding_box() + sx, sy = rb["x"] + rb["width"] / 2, rb["y"] + rb["height"] / 2 + ty = tb["y"] + 3 if target_y_offset == "top" else tb["y"] + tb["height"] - 2 + page.mouse.move(sx, sy) + page.mouse.down() + page.mouse.move(sx, sy - 20, steps=4) + page.mouse.move(tb["x"] + tb["width"] / 2, ty, steps=12) + page.mouse.up() + + def main(): srv, port = serve() with sync_playwright() as p: @@ -175,15 +228,44 @@ def main(): calls = set_pin_order_calls(page) check("drop calls set_pin_order once with [1,4,2,3]", calls == [{"cmd": "set_pin_order", "ids": [1, 4, 2, 3]}], str(calls)) - page.wait_for_function( - "[...document.querySelectorAll('#wheelMirror .wm-item .nm')]" - ".map(e => e.textContent).join() === 'Alpha,Delta,Beta,Gamma'") - check("DOM order persisted after reload", mirror_names(page) == ["Alpha", "Delta", "Beta", "Gamma"]) + check("DOM re-rendered to committed order", + wait_mirror_order(page, ["Alpha", "Delta", "Beta", "Gamma"]), + str(mirror_names(page))) + check("submitted ids == rendered DOM order", + [str(i) for i in calls[0]["ids"]] == mirror_ids(page), + f"ids={calls[0]['ids']} dom={mirror_ids(page)}") check("auto -> manual flip persisted", page.evaluate("localStorage.getItem('pk-wheel-sort')") == "manual") check("sort toggle now shows manual", "手动" in (page.locator("#wheelSortBtn").text_content() or "")) - # ---- 3. drag back to the same slot commits nothing ---- + # ---- 3. committed order survives a real page reload ---- + page.reload() + page.wait_for_selector("#wheelMirror .wm-item") + page.wait_for_timeout(200) + check("order persisted after reload", + mirror_names(page) == ["Alpha", "Delta", "Beta", "Gamma"], + str(mirror_names(page))) + check("manual sort restored after reload", + "手动" in (page.locator("#wheelSortBtn").text_content() or "")) + + # ---- 4. rejected commit rolls back the visual order ---- + page.evaluate("window.__FAIL_PIN_ORDER__ = true") + drag_row_to(page, 3, page.locator('.wm-item[data-wheel-id="1"]'), "top") + page.wait_for_function("window.__CALLS__.filter(c => c.cmd === 'set_pin_order').length === 1") + page.wait_for_timeout(300) + check("failed commit: DOM rolls back to pre-drag order", + mirror_names(page) == ["Alpha", "Delta", "Beta", "Gamma"], + str(mirror_names(page))) + check("failed commit: no mid-state residue", + page.locator("#wheelMirror .wm-insert").count() == 0 + and page.locator("#wheelMirror .wm-item.dragging").count() == 0) + stored = page.evaluate( + "Object.fromEntries(window.__PROMPTS__.filter(p => p.is_pinned).map(p => [p.id, p.inject_order]))") + check("failed commit: backend data untouched", + stored == {"1": "1", "2": "3", "3": "4", "4": "2"}, str(stored)) + page.evaluate("window.__FAIL_PIN_ORDER__ = false") + + # ---- 5. drag back to the same slot commits nothing ---- row3 = page.locator('.wm-item[data-wheel-id="3"]') b3 = row3.bounding_box() page.mouse.move(b3["x"] + b3["width"] / 2, b3["y"] + b3["height"] / 2) @@ -194,7 +276,7 @@ def main(): check("no-op drop: still exactly one set_pin_order", len(set_pin_order_calls(page)) == 1) - # ---- 4. drawer: internal fields gone, save preserves them ---- + # ---- 6. drawer: internal fields gone, save preserves them ---- page.click('.card[data-id="1"]') page.wait_for_selector("#drawer.show") check("drawer has no #fApps input", page.locator("#fApps").count() == 0) @@ -208,19 +290,38 @@ def main(): check("save preserves app_scopes_json", upd["app_scopes_json"] == '["Code.exe"]', str(upd)) check("save preserves inject_order", upd["inject_order"] == "1", str(upd)) - # ---- 5. keyboard reorder on the grip ---- + # ---- 7. keyboard reorder on the grip ---- page.locator('.wm-item[data-wheel-id="1"] .drag').focus() page.keyboard.press("ArrowDown") page.wait_for_function("window.__CALLS__.filter(c => c.cmd === 'set_pin_order').length === 2") calls = set_pin_order_calls(page) check("ArrowDown on grip commits [4,1,2,3]", calls[-1]["ids"] == [4, 1, 2, 3], str(calls[-1])) - page.wait_for_function( - "[...document.querySelectorAll('#wheelMirror .wm-item .nm')]" - ".map(e => e.textContent).join() === 'Delta,Alpha,Beta,Gamma'") + check("keyboard commit: DOM re-rendered to committed order", + wait_mirror_order(page, ["Delta", "Alpha", "Beta", "Gamma"]), + str(mirror_names(page))) + check("keyboard commit: submitted ids == rendered DOM order", + [str(i) for i in calls[-1]["ids"]] == mirror_ids(page), + f"ids={calls[-1]['ids']} dom={mirror_ids(page)}") check("focus returned to moved row's grip", page.evaluate("document.activeElement?.dataset?.drag") == "1") + # ---- 8. inject_order "0" is an explicit position ---- + # Rewrite the durable db: give pinned prompts spaced weights and set + # Beta's inject_order to "0". A parse that only accepts >0 would dump + # Beta to the 9999 tail; it must rank FIRST. + page.evaluate("""() => { + const db = JSON.parse(localStorage.getItem('__pk_e2e_prompts__')); + for (const p of db) if (p.is_pinned) p.inject_order = String(p.id * 10); + db.find(p => p.id === 2).inject_order = '0'; + localStorage.setItem('__pk_e2e_prompts__', JSON.stringify(db)); + }""") + page.reload() + page.wait_for_selector("#wheelMirror .wm-item") + page.wait_for_timeout(200) + check("inject_order '0' ranks first, not last", + mirror_ids(page) == ["2", "1", "3", "4"], str(mirror_ids(page))) + browser.close() srv.shutdown()