@usercase/mcp 0.2.4-next.5 → 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.
- package/mcp/path.js +34 -8
- 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
|
-
|
|
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
|
|
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.
|
|
226
|
-
await this.shot(file, pickH
|
|
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.
|
|
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.
|
|
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",
|