@monoes/monobrowse 1.0.5 → 1.0.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monoes/monobrowse",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "Native browser automation via Chrome DevTools Protocol — the engine powering monomind browse",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,39 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { deriveBoxOutput } from '../cli/commands.js';
3
+ import { getElementBox } from '../browser/snapshot.js';
4
+ import type { ElementRef } from '../browser/types.js';
5
+
6
+ describe('getElementBox (issue #15: center-vs-top-left contract)', () => {
7
+ it('averages the CDP content quad\'s four corners into a center point', async () => {
8
+ // Content quad order per CDP DOM.getBoxModel: [x1,y1 top-left, x2,y2
9
+ // top-right, x3,y3 bottom-right, x4,y4 bottom-left]. A 372x37 box whose
10
+ // top-left is (454, 513.5) — matching the issue's own reproduction data.
11
+ const client = {
12
+ send: vi.fn().mockResolvedValue({
13
+ model: {
14
+ content: [454, 513.5, 826, 513.5, 826, 550.5, 454, 550.5],
15
+ width: 372,
16
+ height: 37,
17
+ },
18
+ }),
19
+ };
20
+ const ref = { backendDOMNodeId: 1 } as unknown as ElementRef;
21
+ const box = await getElementBox(client as any, 'session-1', ref);
22
+ expect(box).toEqual({ x: 640, y: 532, width: 372, height: 37 });
23
+ });
24
+ });
25
+
26
+ describe('deriveBoxOutput (issue #15: get box exposing both conventions)', () => {
27
+ it('derives true top-left x/y from the center point, alongside explicit centerX/centerY', () => {
28
+ const center = { x: 640, y: 532, width: 372, height: 37 };
29
+ const result = deriveBoxOutput(center);
30
+ expect(result).toEqual({
31
+ x: 454, y: 513.5, width: 372, height: 37,
32
+ centerX: 640, centerY: 532,
33
+ });
34
+ });
35
+
36
+ it('returns null when there is no box (element not in DOM)', () => {
37
+ expect(deriveBoxOutput(null)).toBeNull();
38
+ });
39
+ });
@@ -217,6 +217,17 @@ export async function getObjectIdForRef(
217
217
  return result.object?.objectId ?? null;
218
218
  }
219
219
 
220
+ /**
221
+ * Returns the element's content box — but `x`/`y` are the CENTER point
222
+ * (averaged across all four corners of CDP's content quad), not the
223
+ * top-left origin a `getBoundingClientRect()`-style box normally implies.
224
+ * This is deliberate: every caller in this file (clickElement, fillElement,
225
+ * hoverElement, dragElement, ...) uses box.x/box.y directly as the
226
+ * interaction target, which should land at the element's center. Do NOT
227
+ * add width/2 or height/2 to these values — that double-offsets away from
228
+ * the element (see issue #15). If you need the true top-left origin, derive
229
+ * it as `x - width / 2, y - height / 2`.
230
+ */
220
231
  export async function getElementBox(
221
232
  client: CdpClient,
222
233
  sessionId: string,
@@ -777,7 +777,8 @@ const getCommand: Command = {
777
777
  const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
778
778
  const ref = _refs.get(refKey);
779
779
  if (!ref) throw new Error(`Ref @${refKey} not found`);
780
- value = await browser.getElementBox(client, sessionId, ref);
780
+ const center = await browser.getElementBox(client, sessionId, ref);
781
+ value = deriveBoxOutput(center);
781
782
  break;
782
783
  }
783
784
  case 'styles': {
@@ -1591,8 +1592,11 @@ const tapCommand: Command = {
1591
1592
  const ref = await browser.resolveRef(client, sessionId, _refs, key);
1592
1593
  const box = await browser.getElementBox(client, sessionId, ref);
1593
1594
  if (!box) throw new Error(`Cannot get bounds for @${key}`);
1594
- x = Math.round(box.x + box.width / 2);
1595
- y = Math.round(box.y + box.height / 2);
1595
+ // box.x/box.y from getElementBox() ARE already the center point (see
1596
+ // its own doc comment) — adding width/2 here double-offset the tap
1597
+ // target away from the element (issue #15).
1598
+ x = Math.round(box.x);
1599
+ y = Math.round(box.y);
1596
1600
  } else {
1597
1601
  const posJson = await browser.evaluateJs(client, sessionId,
1598
1602
  `(function(){var el=document.querySelector(${JSON.stringify(arg)});if(!el)return null;var r=el.getBoundingClientRect();return JSON.stringify({x:r.left+r.width/2,y:r.top+r.height/2});})()`) as string | null;
@@ -2497,8 +2501,30 @@ function tokenizeBatchCommand(input: string): string[] {
2497
2501
  * only its own known --flags are consumed from the front; everything after
2498
2502
  * them is taken verbatim as one argument, untouched by tokenization.
2499
2503
  *
2504
+ /**
2505
+ * `get box`'s output shape: browser.getElementBox() returns the element's
2506
+ * CENTER point under x/y (correct for internal click-target callers), but
2507
+ * `get box` is a bounding-box accessor where x/y conventionally means the
2508
+ * top-left origin — a caller computing its own center as `box.x + width/2`
2509
+ * would otherwise double-offset away from the element (issue #15). Expose
2510
+ * both conventions, explicitly labeled, so neither is ambiguous.
2511
+ *
2500
2512
  * Exported for direct unit testing — not part of the CLI's public API.
2501
2513
  */
2514
+ export function deriveBoxOutput(
2515
+ center: { x: number; y: number; width: number; height: number } | null
2516
+ ): { x: number; y: number; width: number; height: number; centerX: number; centerY: number } | null {
2517
+ if (!center) return null;
2518
+ return {
2519
+ x: center.x - center.width / 2,
2520
+ y: center.y - center.height / 2,
2521
+ width: center.width,
2522
+ height: center.height,
2523
+ centerX: center.x,
2524
+ centerY: center.y,
2525
+ };
2526
+ }
2527
+
2502
2528
  export function parseBatchCommandLine(cmdStr: string): { subName: string; subArgs: string[]; flags: Record<string, unknown> } {
2503
2529
  const trimmed = cmdStr.trim();
2504
2530
  const evalMatch = trimmed.match(/^eval\b\s*/);