返回 DeepSeek-Reasonix
submission-native-input.mjs
根目录 / desktop / frontend / bench / submission-native-input.mjs
1 import assert from "node:assert/strict";
2 import { execFileSync } from "node:child_process";
3
4 // XTest input targets only the disposable Electron window in an isolated Xvfb
5 // display. It must never run against the developer's active desktop session.
6 export async function verifyNativeReaderInput(page, evidence = {}) {
7 evidence.status = "running";
8 assert.equal(process.platform, "linux");
9 assert.ok(process.env.DISPLAY, "native input requires an isolated Xvfb display");
10 const xdo = (...args) => execFileSync("xdotool", args.map(String), { encoding: "utf8", timeout: 10_000 }).trim();
11 const windows = xdo("search", "--onlyvisible", "--name", "^Reasonix Handoff Native$").split("\n");
12 assert.equal(windows.length, 1, "exactly one disposable native window is required");
13 const windowId = windows[0];
14 xdo("windowfocus", "--sync", windowId);
15 const frame = () => page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))));
16 const scroll = page.locator(".chat-flow-scroll");
17 await scroll.evaluate(el => {
18 el.focus();
19 window.nativeSamples = [];
20 window.nativeSampling = true;
21 window.nativePhase = "position";
22 window.nativePointerEvents = [];
23 for (const type of ["pointerdown", "pointerup", "pointermove"]) el.addEventListener(type, event => {
24 window.nativePointerEvents.push({ type, x: event.clientX, y: event.clientY, buttons: event.buttons, target: event.target === el });
25 });
26 const sample = () => {
27 const box = el.getBoundingClientRect();
28 const rows = [...el.querySelectorAll("[data-chat-kind]")];
29 const visible = rows.some(row => { const rect = row.getBoundingClientRect(); return rect.height > 0 && rect.bottom > box.top && rect.top < box.bottom; });
30 window.nativeSamples.push({ phase: window.nativePhase, top: el.scrollTop, height: el.scrollHeight, viewport: el.clientHeight, blank: !visible });
31 if (window.nativeSampling) requestAnimationFrame(sample);
32 };
33 sample();
34 });
35 const top = () => scroll.evaluate(el => el.scrollTop);
36 try {
37 xdo("key", "--clearmodifiers", "End");
38 await page.waitForFunction(() => { const el = document.querySelector(".chat-flow-scroll"); return el.scrollHeight - el.clientHeight - el.scrollTop <= 1; });
39 const box = await scroll.boundingBox();
40 assert.ok(box);
41 const center = { x: Math.round(box.x + box.width / 2), y: Math.round(box.y + box.height / 2) };
42 xdo("mousemove", "--window", windowId, center.x, center.y);
43 await page.waitForFunction(() => window.nativePointerEvents.some(event => event.type === "pointermove"));
44 const observed = await page.evaluate(() => window.nativePointerEvents.filter(event => event.type === "pointermove").at(-1));
45 // Window-manager borders can offset X11 window-relative and DOM client
46 // coordinates. Measure that transform with a harmless move over content.
47 const offset = { x: center.x - observed.x, y: center.y - observed.y };
48 evidence.coordinateOffset = offset;
49 const beforeWheel = await top();
50 await page.evaluate(() => { window.nativePhase = "wheel"; });
51 xdo("click", "--repeat", 4, "--delay", 50, 4);
52 await page.waitForFunction(before => document.querySelector(".chat-flow-scroll").scrollTop < before - 50, beforeWheel);
53 await frame();
54 const afterWheel = await top();
55 evidence.wheel = { before: beforeWheel, after: afterWheel };
56 await scroll.evaluate(el => el.focus());
57 const beforeKeyboard = await top();
58 await page.evaluate(() => { window.nativePhase = "keyboard"; });
59 xdo("key", "--clearmodifiers", "Page_Up");
60 await page.waitForFunction(before => document.querySelector(".chat-flow-scroll").scrollTop < before - 50, beforeKeyboard);
61 await frame();
62 const afterKeyboard = await top();
63 evidence.keyboard = { before: beforeKeyboard, after: afterKeyboard };
64 await page.evaluate(() => { window.nativePhase = "position"; });
65 xdo("key", "--clearmodifiers", "End");
66 await page.waitForFunction(() => { const el = document.querySelector(".chat-flow-scroll"); return el.scrollHeight - el.clientHeight - el.scrollTop <= 1; });
67 const track = await scroll.evaluate(el => {
68 const box = el.getBoundingClientRect();
69 // clientLeft includes the reserved left gutter with stable both-edges.
70 const gutter = el.offsetWidth - el.clientWidth - el.clientLeft - parseFloat(getComputedStyle(el).borderRightWidth);
71 const thumb = Math.max(gutter, (box.height - 2 * gutter) * el.clientHeight / el.scrollHeight);
72 return { x: box.right - gutter / 2, y: box.bottom - gutter - thumb / 2, to: box.top + box.height / 2, gutter, top: el.scrollTop };
73 });
74 assert.ok(track.gutter > 0, "native scrollbar must be exposed");
75 evidence.track = track;
76 evidence.phase = "drag-start";
77 await page.evaluate(() => { window.nativePhase = "scrollbar"; });
78 xdo("mousemove", "--window", windowId, Math.round(track.x + offset.x), Math.round(track.y + offset.y));
79 xdo("mousedown", 1);
80 try {
81 for (let step = 1; step <= 12; step++) {
82 xdo("mousemove", "--window", windowId, Math.round(track.x + offset.x), Math.round(track.y + (track.to - track.y) * step / 12 + offset.y));
83 // Do not await renderer frames inside a native pointer transaction:
84 // the scrollbar's modal drag loop may defer JS until mouse release.
85 xdo("sleep", "0.03");
86 }
87 } finally { xdo("mouseup", 1); }
88 evidence.phase = "drag-released";
89 await page.waitForFunction(before => document.querySelector(".chat-flow-scroll").scrollTop < before - 100, track.top);
90 await frame();
91 evidence.phase = "drag-observed";
92 assert.equal(await scroll.getAttribute("data-scroll-mode"), "reader");
93 const samples = await page.evaluate(() => { window.nativeSampling = false; return window.nativeSamples; });
94 const heights = samples.map(sample => sample.height);
95 const extent = { initial: heights[0], min: Math.min(...heights), max: Math.max(...heights), final: heights.at(-1) };
96 const blankFrames = samples.filter(sample => sample.blank).length;
97 const reverseDisplacement = Math.max(0, ...samples.slice(1).map((sample, index) =>
98 sample.phase !== "position" && sample.phase === samples[index].phase ? sample.top - samples[index].top : 0));
99 assert.equal(blankFrames, 0, "native input must not expose blank transcript frames");
100 assert.ok(extent.max - extent.final <= Math.max(96, samples.at(-1).viewport * 0.5), "native input must not collapse the scroll range");
101 return Object.assign(evidence, { status: "passed", method: "X11 XTest through xdotool", wheel: { before: beforeWheel, after: afterWheel },
102 keyboard: { before: beforeKeyboard, after: afterKeyboard }, scrollbar: { before: track.top, after: await top(), gutter: track.gutter },
103 extent, blankFrames, reverseDisplacement, samples });
104 } finally {
105 Object.assign(evidence, await page.evaluate(() => {
106 window.nativeSampling = false;
107 return { samples: window.nativeSamples, pointerEvents: window.nativePointerEvents, screen: {
108 innerWidth, innerHeight, outerWidth, outerHeight, devicePixelRatio, screenX, screenY,
109 } };
110 }));
111 }
112 }
113
113 lines Plain Text