@usercase/mcp 0.2.4-next.6 → 0.2.4-next.7

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.
Files changed (2) hide show
  1. package/mcp/path.js +34 -8
  2. package/package.json +1 -1
package/mcp/path.js CHANGED
@@ -37,8 +37,33 @@ const pathOnly = (u) => {
37
37
  * 요소 하나를 SDK 가 맞출 수 있는 모양으로. 브라우저 안에서 돈다.
38
38
  * XPath 는 SDK main/utils.ts getElementXPath 와 같은 방식(같은 이름 형제 사이 순번, svg 는 name())
39
39
  */
40
- function describeInPage(n, pickSel) {
41
- const pick = (n.closest && n.closest(pickSel)) || n;
40
+ function describeInPage(n, { sel: pickSel, lift = false }) {
41
+ /**
42
+ * onClick 달린 div 로 만든 버튼은 태그로 안 잡힌다. 그러면 누른 조각(글자 span)만 기록돼, 같은 버튼의 아이콘을
43
+ * 누른 참여자는 다른 길로 갔다고 나왔다(10/27 bluetags: "Save" 글자 대 북마크 아이콘). 클릭을 받는 요소까지 올린다:
44
+ * React 가 onClick 을 붙인 가장 가까운 것, 없으면 손가락 커서가 이어지는 데까지. 둘 다 조작을 하나 더 품거나
45
+ * 처음 것보다 훨씬 크면(카드 전체) 멈춘다 — 아무 데나 눌러도 맞게 되면 안 된다
46
+ */
47
+ const liftInPage = (e) => {
48
+ const area = (x) => {
49
+ const r = x.getBoundingClientRect();
50
+ return r.width * r.height;
51
+ };
52
+ const base = Math.max(area(e), 1);
53
+ const fits = (x) => area(x) <= base * 6 && !Array.from(x.querySelectorAll(pickSel)).some((c) => !c.contains(e) && !e.contains(c));
54
+ const reactClick = (x) => {
55
+ for (const k in x) if (k.startsWith("__reactProps") && x[k] && typeof x[k].onClick === "function") return true;
56
+ return false;
57
+ };
58
+ for (let x = e; x && x !== document.body; x = x.parentElement) {
59
+ if (reactClick(x)) return fits(x) ? x : e;
60
+ }
61
+ let top = e;
62
+ for (let x = e.parentElement; x && x !== document.body && getComputedStyle(x).cursor === "pointer" && fits(x); x = x.parentElement) top = x;
63
+ return top;
64
+ };
65
+ const hit = n.closest && n.closest(pickSel);
66
+ const pick = hit || (lift ? liftInPage(n) : n);
42
67
  const xp = (e) => {
43
68
  const parts = [];
44
69
  while (e && e.nodeType === 1) {
@@ -215,15 +240,16 @@ class PathRecorder {
215
240
  return this.screen(
216
241
  `Not pressed — ${blocked}. If this is where the participant's task ends (e.g. the final "Pay" or "Submit" button), call path_click again with press=false: it is recorded as the final click without pressing it.`,
217
242
  );
218
- const pickH = await el.evaluateHandle((n, sel) => (n.closest && n.closest(sel)) || n, PICK_SEL);
219
- const info = await el.evaluate(describeInPage, PICK_SEL);
243
+ const info = await el.evaluate(describeInPage, { sel: PICK_SEL, lift: true });
220
244
  if (!info.xpath) return this.screen(`Could not work out where [${index}] is in the page. Try another element.`);
245
+ // 기록한 XPath 로 다시 찾은 것을 찍고 누른다 — SDK 가 참여자 화면에서 찾을 바로 그 요소다
246
+ const pickH = (await c.page.evaluateHandle(byXPathInPage, info.xpath)).asElement() || el;
221
247
  const at = pathOnly(c.page.url());
222
248
  let step = null;
223
249
  if (record) {
224
250
  const file = path.join(c.dir, `${String(c.steps.length + 1).padStart(2, "0")}-click.jpg`);
225
- await pickH.asElement().scrollIntoViewIfNeeded({ timeout: 5000 }).catch(() => {});
226
- await this.shot(file, pickH.asElement());
251
+ await pickH.scrollIntoViewIfNeeded({ timeout: 5000 }).catch(() => {});
252
+ await this.shot(file, pickH);
227
253
  step = { type: "XPATH", url: at, concrete: at, xpath: info.xpath, classes: info.classes, ids: info.ids, label: info.label, tag: info.tag, image: file, pressed: press };
228
254
  c.steps.push(step);
229
255
  }
@@ -232,7 +258,7 @@ class PathRecorder {
232
258
  if (!press) return this.screen(`Recorded "${info.label || info.tag}" as the final click, without pressing it. The path ends here — call path_check next.`);
233
259
  const before = c.page.url();
234
260
  try {
235
- await pickH.asElement().click({ timeout: 10000 });
261
+ await pickH.click({ timeout: 10000 });
236
262
  } catch (e) {
237
263
  if (step) c.steps.pop();
238
264
  c.actions.pop();
@@ -251,7 +277,7 @@ class PathRecorder {
251
277
  const c = this.need();
252
278
  const el = c.view && c.view.items[index] ? await handleAt(c.page, index) : null;
253
279
  if (!el) return this.screen(`[${index}] is not on the screen any more.`);
254
- const info = await el.evaluate(describeInPage, "input,textarea,select,[contenteditable]");
280
+ const info = await el.evaluate(describeInPage, { sel: "input,textarea,select,[contenteditable]" });
255
281
  await el.fill(text, { timeout: 10000 });
256
282
  c.actions.push({ do: "type", xpath: info.xpath, text });
257
283
  c.checked = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usercase/mcp",
3
- "version": "0.2.4-next.6",
3
+ "version": "0.2.4-next.7",
4
4
  "description": "Virtual users that browse your website and tell you what they thought — an MCP server for Claude Code",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://usercase.io",