@copilotkit/learning 0.0.1 → 1.81.0
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/README.md +63 -3
- package/dist/index.cjs +335 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -0
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +6 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +335 -34
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/scripts/check-size.mjs +2 -2
- package/src/__tests__/clicks.test.ts +499 -1
- package/src/__tests__/collector.test.ts +152 -0
- package/src/__tests__/inputs.test.ts +400 -1
- package/src/__tests__/redact.test.ts +4 -2
- package/src/__tests__/trajectory-collector.test.ts +105 -0
- package/src/clicks.ts +308 -3
- package/src/collector.ts +11 -5
- package/src/inputs.ts +167 -19
- package/src/redact.ts +7 -2
- package/src/trajectory-collector.ts +16 -2
- package/src/types.ts +6 -0
- package/CHANGELOG.md +0 -9
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it } from "vitest";
|
|
2
2
|
import { installClickCapture } from "../clicks";
|
|
3
|
+
import { createRedactor } from "../redact";
|
|
4
|
+
import type { Redactor } from "../redact";
|
|
3
5
|
import type { EnrichFn } from "../types";
|
|
4
6
|
|
|
5
7
|
interface Captured {
|
|
@@ -15,13 +17,16 @@ afterEach(() => {
|
|
|
15
17
|
document.body.innerHTML = "";
|
|
16
18
|
});
|
|
17
19
|
|
|
18
|
-
function setup(
|
|
20
|
+
function setup(
|
|
21
|
+
options: { trustAll?: boolean; enrich?: EnrichFn; redact?: Redactor } = {},
|
|
22
|
+
) {
|
|
19
23
|
const events: Captured[] = [];
|
|
20
24
|
uninstall = installClickCapture({
|
|
21
25
|
emit: (name, value) => events.push({ name, value }),
|
|
22
26
|
getRoute: () => "/deals/:id",
|
|
23
27
|
enrich: options.enrich,
|
|
24
28
|
isTrusted: options.trustAll === false ? undefined : () => true,
|
|
29
|
+
redact: options.redact,
|
|
25
30
|
});
|
|
26
31
|
return events;
|
|
27
32
|
}
|
|
@@ -200,3 +205,496 @@ describe("installClickCapture", () => {
|
|
|
200
205
|
expect(events).toMatchObject([]);
|
|
201
206
|
});
|
|
202
207
|
});
|
|
208
|
+
|
|
209
|
+
/** A combobox suggestion whose option names itself through its own text. */
|
|
210
|
+
const suggestion = (attributes = "") =>
|
|
211
|
+
`<input role="combobox" aria-controls="results"><ul id="results" role="listbox"><li id="option" role="option" aria-labelledby="option-text"${attributes}><span id="option-text">private search canary</span></li></ul>`;
|
|
212
|
+
|
|
213
|
+
describe("click target label", () => {
|
|
214
|
+
/** Clicks `#id` (or the element itself) and returns the captured target. */
|
|
215
|
+
function clickTarget(target: string | Element, redact?: Redactor) {
|
|
216
|
+
const events = setup({ redact });
|
|
217
|
+
click(typeof target === "string" ? byId(target) : target);
|
|
218
|
+
expect(events).toHaveLength(1);
|
|
219
|
+
return events[0]!.value.target as {
|
|
220
|
+
label?: string;
|
|
221
|
+
text: string;
|
|
222
|
+
action: string | null;
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
describe("names a control", () => {
|
|
227
|
+
it("uses aria-label on an icon-only button", () => {
|
|
228
|
+
document.body.innerHTML = `<button id="b" aria-label="Switch to dark mode"><svg><path></path></svg></button>`;
|
|
229
|
+
expect(clickTarget("b").label).toBe("Switch to dark mode");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("uses title", () => {
|
|
233
|
+
document.body.innerHTML = `<button id="b" title="Copy Code"><svg></svg></button>`;
|
|
234
|
+
expect(clickTarget("b").label).toBe("Copy Code");
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("uses the text of a button", () => {
|
|
238
|
+
document.body.innerHTML = `<button id="b">Approve</button>`;
|
|
239
|
+
expect(clickTarget("b").label).toBe("Approve");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("joins text from nested elements and collapses whitespace", () => {
|
|
243
|
+
document.body.innerHTML = `<button id="b">
|
|
244
|
+
<span>Thought</span>
|
|
245
|
+
<span>for <b>2</b> seconds</span>
|
|
246
|
+
</button>`;
|
|
247
|
+
expect(clickTarget("b").label).toBe("Thought for 2 seconds");
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("resolves aria-labelledby in order", () => {
|
|
251
|
+
document.body.innerHTML = `<h2 id="first">Invoice</h2><span id="second" hidden>Delete <i aria-hidden="true">icon</i></span><button id="b" aria-labelledby="second first" aria-label="Ignored">x</button>`;
|
|
252
|
+
expect(clickTarget("b").label).toBe("Delete Invoice");
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("uses the text of a tab", () => {
|
|
256
|
+
document.body.innerHTML = `<div id="tab" role="tab">Overview</div>`;
|
|
257
|
+
expect(clickTarget("tab").label).toBe("Overview");
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("uses the text of an element with a control role", () => {
|
|
261
|
+
document.body.innerHTML = `<div id="b" role="button">Retry</div>`;
|
|
262
|
+
expect(clickTarget("b").label).toBe("Retry");
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("uses the text of a link", () => {
|
|
266
|
+
document.body.innerHTML = `<a id="a" href="#settings">Account settings</a>`;
|
|
267
|
+
expect(clickTarget("a").label).toBe("Account settings");
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("uses the caption of a submit input", () => {
|
|
271
|
+
document.body.innerHTML = `<input id="s" type="submit" value="Save changes">`;
|
|
272
|
+
expect(clickTarget("s").label).toBe("Save changes");
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("uses alt of an image input and of an image", () => {
|
|
276
|
+
document.body.innerHTML = `<input id="i" type="image" alt="Search" src="search.png"><img id="img" alt="Company logo" src="logo.png">`;
|
|
277
|
+
expect(clickTarget("i").label).toBe("Search");
|
|
278
|
+
uninstall?.();
|
|
279
|
+
expect(clickTarget("img").label).toBe("Company logo");
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("names the button when its icon is clicked", () => {
|
|
283
|
+
document.body.innerHTML = `<button aria-label="Close dialog"><svg><path id="icon"></path></svg></button><button><svg><path id="icon2"></path></svg> Send</button>`;
|
|
284
|
+
expect(clickTarget("icon").label).toBe("Close dialog");
|
|
285
|
+
uninstall?.();
|
|
286
|
+
expect(clickTarget("icon2").label).toBe("Send");
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
describe("does not use user content", () => {
|
|
291
|
+
it("ignores title on a text input with a typed value, but keeps aria-label", () => {
|
|
292
|
+
document.body.innerHTML = `<input id="q" type="text" title="jane.private query"><input id="named" type="search" aria-label="Search customers" title="jane.private query">`;
|
|
293
|
+
(byId("q") as HTMLInputElement).value = "jane.private query";
|
|
294
|
+
(byId("named") as HTMLInputElement).value = "jane.private query";
|
|
295
|
+
expect(clickTarget("q").label).toBeUndefined();
|
|
296
|
+
uninstall?.();
|
|
297
|
+
expect(clickTarget("named").label).toBe("Search customers");
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("ignores title on a text area and a select", () => {
|
|
301
|
+
document.body.innerHTML = `<textarea id="t" title="Draft text">Draft text</textarea><select id="s" title="Chosen plan"><option>Chosen plan</option></select>`;
|
|
302
|
+
expect(clickTarget("t").label).toBeUndefined();
|
|
303
|
+
uninstall?.();
|
|
304
|
+
expect(clickTarget("s").label).toBeUndefined();
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("gives a contenteditable no label", () => {
|
|
308
|
+
document.body.innerHTML = `<div id="e" contenteditable="true" title="My private note">My private note</div>`;
|
|
309
|
+
expect(clickTarget("e").label).toBeUndefined();
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("gives containers no label from their text", () => {
|
|
313
|
+
document.body.innerHTML = `<div id="item" role="listitem">Jane Doe, diagnosis pending</div><ul><li id="li">Jane Doe, diagnosis pending</li></ul><div id="main" role="main">Jane Doe, diagnosis pending</div><div id="card" data-copilotkit-action="card.open">Jane Doe, diagnosis pending</div><label id="label">Jane Doe, diagnosis pending</label>`;
|
|
314
|
+
for (const id of ["item", "li", "main", "card", "label"]) {
|
|
315
|
+
const target = clickTarget(id);
|
|
316
|
+
expect(target.label).toBeUndefined();
|
|
317
|
+
expect(target.text).toContain("Jane Doe");
|
|
318
|
+
uninstall?.();
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("gives a control that contains an editable element no text label", () => {
|
|
323
|
+
document.body.innerHTML = `<div id="b" role="button">Rename to <input value="Q3 layoffs"> Q3 layoffs</div><div id="named" role="button" aria-label="Rename file">Q3 layoffs <span contenteditable="true">Q3 layoffs</span></div>`;
|
|
324
|
+
expect(clickTarget("b").label).toBeUndefined();
|
|
325
|
+
uninstall?.();
|
|
326
|
+
expect(clickTarget("named").label).toBe("Rename file");
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("gives a combobox suggestion that repeats the query no label, and keeps its action", () => {
|
|
330
|
+
document.body.innerHTML = `<input role="combobox" aria-autocomplete="list" aria-controls="suggestions"><ul id="suggestions" role="listbox"><li id="o" role="option">divorce lawyer near me</li><li id="named" role="option" aria-label="Suggestion 2" data-copilotkit-action="search.pick">divorce lawyer cost</li></ul>`;
|
|
331
|
+
const target = clickTarget("o");
|
|
332
|
+
expect(target.label).toBeUndefined();
|
|
333
|
+
uninstall?.();
|
|
334
|
+
const named = clickTarget("named");
|
|
335
|
+
expect(named.label).toBeUndefined();
|
|
336
|
+
expect(named.action).toBe("search.pick");
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("gives options of a datalist or a combobox-owned listbox no text label", () => {
|
|
340
|
+
document.body.innerHTML = `<div role="combobox" aria-owns="owned"><input></div><div id="owned" role="listbox"><div id="o1" role="option">private search one</div></div><input aria-autocomplete="list" aria-controls="controlled"><div id="controlled" role="listbox"><div id="o2" role="option">private search two</div></div><div role="combobox"><div role="listbox"><div id="o3" role="option">private search three</div></div></div><datalist id="dl"><option id="o4" role="option">private search four</option></datalist>`;
|
|
341
|
+
for (const id of ["o1", "o2", "o3", "o4"]) {
|
|
342
|
+
expect(clickTarget(id).label).toBeUndefined();
|
|
343
|
+
uninstall?.();
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("still names an option of a plain listbox", () => {
|
|
348
|
+
document.body.innerHTML = `<div id="list" role="listbox"><div id="o" role="option">Monthly</div></div>`;
|
|
349
|
+
expect(clickTarget("o").label).toBe("Monthly");
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
describe("never names a suggestion, from any source", () => {
|
|
353
|
+
it("ignores aria-labelledby that points at the option's own text", () => {
|
|
354
|
+
document.body.innerHTML = suggestion();
|
|
355
|
+
const { label } = clickTarget("option");
|
|
356
|
+
expect(label).toBeUndefined();
|
|
357
|
+
expect(label ?? "").not.toContain("canary");
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("ignores title on a suggestion", () => {
|
|
361
|
+
document.body.innerHTML = suggestion(' title="private search canary"');
|
|
362
|
+
expect(clickTarget("option").label).toBeUndefined();
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it("ignores aria-label on a suggestion", () => {
|
|
366
|
+
document.body.innerHTML = suggestion(
|
|
367
|
+
' aria-label="private search canary"',
|
|
368
|
+
);
|
|
369
|
+
expect(clickTarget("option").label).toBeUndefined();
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it("ignores a named icon inside a suggestion", () => {
|
|
373
|
+
document.body.innerHTML = `<input role="combobox" aria-controls="results"><ul id="results" role="listbox"><li role="option"><svg id="icon" role="img" aria-label="private search canary"></svg></li></ul>`;
|
|
374
|
+
expect(clickTarget("icon").label).toBeUndefined();
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it("ignores aria-labelledby elsewhere that points into a suggestion list", () => {
|
|
378
|
+
document.body.innerHTML = `${suggestion()}<button id="b1" aria-labelledby="option-text"><svg></svg></button><button id="b2" aria-labelledby="option"><svg></svg></button><button id="b3" aria-labelledby="results"><svg></svg></button>`;
|
|
379
|
+
for (const id of ["b1", "b2", "b3"]) {
|
|
380
|
+
const { label } = clickTarget(id);
|
|
381
|
+
expect(label).toBeUndefined();
|
|
382
|
+
expect(label ?? "").not.toContain("canary");
|
|
383
|
+
uninstall?.();
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it("still resolves aria-labelledby on other controls", () => {
|
|
388
|
+
document.body.innerHTML = `${suggestion()}<button id="b" aria-labelledby="x"><svg></svg></button><span id="x">Save</span>`;
|
|
389
|
+
expect(clickTarget("b").label).toBe("Save");
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it("excludes script, style, template, hidden, and aria-hidden content", () => {
|
|
394
|
+
document.body.innerHTML = `<button id="b">Save<script>leakone</script><style>.leaktwo{}</style><template>leakthree</template><span hidden>leakfour</span><span aria-hidden="true">leak<b>five</b></span></button>`;
|
|
395
|
+
const { label } = clickTarget("b");
|
|
396
|
+
expect(label).toBe("Save");
|
|
397
|
+
expect(label).not.toMatch(/leak|five/);
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
describe("cleans the name", () => {
|
|
402
|
+
it("replaces a URL in link text, including its token", () => {
|
|
403
|
+
document.body.innerHTML = `<a id="a" href="#reset">Open https://app.example.com/reset?token=Zx81Qp and www.example.org/a</a>`;
|
|
404
|
+
const target = clickTarget("a");
|
|
405
|
+
expect(target.label).toBe("Open [url] and [url]");
|
|
406
|
+
expect(target.label).not.toMatch(/Zx81Qp|example/);
|
|
407
|
+
expect(target.text).toContain("Zx81Qp");
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it("replaces an email", () => {
|
|
411
|
+
document.body.innerHTML = `<button id="b">Email jane.doe@example.com now</button>`;
|
|
412
|
+
const target = clickTarget("b");
|
|
413
|
+
expect(target.label).toBe("Email [email] now");
|
|
414
|
+
expect(target.label).not.toContain("jane.doe");
|
|
415
|
+
expect(target.text).toContain("jane.doe@example.com");
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it("replaces JWT-shaped and long token-like strings", () => {
|
|
419
|
+
const jwt = "eyJfixture.eyJpayload.placeholder";
|
|
420
|
+
const key = "tok9f3a8b2c7d1e6f4a0b5c9d2e8f7a1b3c";
|
|
421
|
+
document.body.innerHTML = `<button id="b" aria-label="Copy ${jwt} or ${key} for averyveryverylongwordwithoutdigits"></button>`;
|
|
422
|
+
const { label } = clickTarget("b");
|
|
423
|
+
expect(label).toBe(
|
|
424
|
+
"Copy [redacted] or [redacted] for averyveryverylongwordwithoutdigits",
|
|
425
|
+
);
|
|
426
|
+
expect(label).not.toMatch(/eyJ|tok9f3a/);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it("strips bidi control and zero-width characters", () => {
|
|
430
|
+
document.body.innerHTML = `<button id="b">\u202EAp\u200Bpro\u2066ve\u2069\uFEFF\u200F</button>`;
|
|
431
|
+
expect(clickTarget("b").label).toBe("Approve");
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it("caps an aria-label at 80 characters with an ellipsis", () => {
|
|
435
|
+
document.body.innerHTML = `<button id="b" aria-label="${"Long name ".repeat(20)}"></button>`;
|
|
436
|
+
const { label } = clickTarget("b");
|
|
437
|
+
expect(label).toHaveLength(80);
|
|
438
|
+
expect(label).toMatch(/^Long name Long name .*…$/);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it("scrubs a remembered password and credential pairs shown in a button", () => {
|
|
442
|
+
document.body.innerHTML = `<input id="pw" type="password"><button id="b">Your password hunter2secret, token=abc123</button>`;
|
|
443
|
+
const redact = createRedactor();
|
|
444
|
+
const field = byId("pw") as HTMLInputElement;
|
|
445
|
+
field.value = "hunter2secret";
|
|
446
|
+
redact.isPassword(field);
|
|
447
|
+
const { label } = clickTarget("b", redact);
|
|
448
|
+
expect(label).toBe("Your password [redacted], token=[redacted]");
|
|
449
|
+
expect(label).not.toMatch(/hunter2secret|abc123/);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it("omits the label when nothing is left after cleaning", () => {
|
|
453
|
+
document.body.innerHTML = `<button id="b" aria-label="\u200B \u202E"> <span aria-hidden="true">x</span></button>`;
|
|
454
|
+
expect(clickTarget("b")).not.toHaveProperty("label");
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
describe("review hardening", () => {
|
|
459
|
+
it("gives no text label to a link or role=button card with structured content", () => {
|
|
460
|
+
document.body.innerHTML = `<a id="a" href="#inbox-1"><div><h3>Jane Doe</h3><p>Re: biopsy results</p></div></a><div id="b" role="button" class="card"><h3>Jane Doe</h3><p>Pending diagnosis</p></div><button id="list"><ul><li>Jane Doe</li></ul></button>`;
|
|
461
|
+
for (const id of ["a", "b", "list"]) {
|
|
462
|
+
const target = clickTarget(id);
|
|
463
|
+
expect(target.label).toBeUndefined();
|
|
464
|
+
expect(target.text).toContain("Jane Doe");
|
|
465
|
+
uninstall?.();
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it("drops control text longer than 80 characters instead of cutting it", () => {
|
|
470
|
+
const text =
|
|
471
|
+
"Jane Doe asked about the results of her appointment last Tuesday at the clinic";
|
|
472
|
+
document.body.innerHTML = `<a id="a" href="#m">${text} again</a>`;
|
|
473
|
+
expect(clickTarget("a").label).toBeUndefined();
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it("keeps naming plain navigation links and buttons", () => {
|
|
477
|
+
document.body.innerHTML = `<nav><a id="a" href="#docs"><span>Docs</span></a></nav><button id="b"><span>Save</span> <span>Draft</span></button><a id="c" href="#c">Save<br>Draft</a>`;
|
|
478
|
+
expect(clickTarget("a").label).toBe("Docs");
|
|
479
|
+
uninstall?.();
|
|
480
|
+
expect(clickTarget("b").label).toBe("Save Draft");
|
|
481
|
+
uninstall?.();
|
|
482
|
+
expect(clickTarget("c").label).toBe("Save Draft");
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it("gives an anchor without href no text label", () => {
|
|
486
|
+
document.body.innerHTML = `<a id="a">Jane Doe private note</a>`;
|
|
487
|
+
expect(clickTarget("a").label).toBeUndefined();
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it("does not resolve aria-labelledby into editable content", () => {
|
|
491
|
+
document.body.innerHTML = `<div contenteditable="true"><h2 id="h1">My secret draft heading</h2></div><div role="textbox"><span id="h2">typed secret text</span></div><button id="b1" aria-labelledby="h1"><svg></svg></button><button id="b2" aria-labelledby="h2"><svg></svg></button>`;
|
|
492
|
+
for (const id of ["b1", "b2"]) {
|
|
493
|
+
expect(clickTarget(id).label).toBeUndefined();
|
|
494
|
+
uninstall?.();
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it("strips default-ignorable characters, so they cannot hide an email", () => {
|
|
499
|
+
document.body.innerHTML = `<button id="b1">Email jane.doe@exam\u00ADple.com</button><button id="b2">Email jane\u2060@example.com</button><button id="b3">Approve\u{E0049}\u{E0047}\u{E004E}</button><button id="b4">Save\uFE0F</button>`;
|
|
500
|
+
expect(clickTarget("b1").label).toBe("Email [email]");
|
|
501
|
+
uninstall?.();
|
|
502
|
+
expect(clickTarget("b2").label).toBe("Email [email]");
|
|
503
|
+
uninstall?.();
|
|
504
|
+
expect(clickTarget("b3").label).toBe("Approve");
|
|
505
|
+
uninstall?.();
|
|
506
|
+
expect(clickTarget("b4").label).toBe("Save");
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it("replaces emails with non-ASCII letters", () => {
|
|
510
|
+
document.body.innerHTML = `<button id="b1">Mail jürgen.schmidt@example.de</button><button id="b2">Mail jane@müller.de</button>`;
|
|
511
|
+
expect(clickTarget("b1").label).toBe("Mail [email]");
|
|
512
|
+
uninstall?.();
|
|
513
|
+
expect(clickTarget("b2").label).toBe("Mail [email]");
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it("names a decorative icon from its own name or its control", () => {
|
|
517
|
+
document.body.innerHTML = `<button><svg id="svg" role="img"><path></path></svg> Send</button><button><span id="span" role="presentation" class="icon"></span> Archive</button><button><svg id="named" role="img" aria-label="Close"></svg></button><div><svg id="lone" role="img"></svg></div>`;
|
|
518
|
+
const target = clickTarget("svg") as { label?: string; tag?: string };
|
|
519
|
+
expect(target).toMatchObject({ label: "Send", tag: "svg" });
|
|
520
|
+
uninstall?.();
|
|
521
|
+
expect(clickTarget("span").label).toBe("Archive");
|
|
522
|
+
uninstall?.();
|
|
523
|
+
expect(clickTarget("named").label).toBe("Close");
|
|
524
|
+
uninstall?.();
|
|
525
|
+
expect(clickTarget("lone").label).toBeUndefined();
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
it("uses alt and aria-label of images inside a control", () => {
|
|
529
|
+
document.body.innerHTML = `<a id="a" href="#home"><img alt="Home" src="home.png"></a><button id="b"><svg role="img" aria-label="Settings"></svg></button>`;
|
|
530
|
+
expect(clickTarget("a").label).toBe("Home");
|
|
531
|
+
uninstall?.();
|
|
532
|
+
expect(clickTarget("b").label).toBe("Settings");
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
it("treats tree items and options inside a combobox popup as suggestions", () => {
|
|
536
|
+
document.body.innerHTML = `<input role="combobox" aria-controls="tree"><ul id="tree" role="tree"><li id="t" role="treeitem">divorce lawyer near me</li></ul><input role="combobox" aria-controls="pop"><div id="pop"><div role="listbox"><div id="o" role="option">divorce lawyer cost</div></div></div>`;
|
|
537
|
+
expect(clickTarget("t").label).toBeUndefined();
|
|
538
|
+
uninstall?.();
|
|
539
|
+
expect(clickTarget("o").label).toBeUndefined();
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it("uses title only on controls", () => {
|
|
543
|
+
document.body.innerHTML = `<table><tr><td><span id="s" title="jane.doe@x.com Jane Doe">Jane…</span></td></tr></table><div id="item" role="listitem" title="Jane Doe">Jane</div><span id="named" aria-label="Status badge" title="Jane Doe">J</span>`;
|
|
544
|
+
expect(clickTarget("s").label).toBeUndefined();
|
|
545
|
+
uninstall?.();
|
|
546
|
+
expect(clickTarget("item").label).toBeUndefined();
|
|
547
|
+
uninstall?.();
|
|
548
|
+
expect(clickTarget("named").label).toBe("Status badge");
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it("scrubs a remembered password whose whitespace differs on screen", () => {
|
|
552
|
+
document.body.innerHTML = `<input id="pw" type="password"><button id="b">Show: correct
|
|
553
|
+
horse</button>`;
|
|
554
|
+
const redact = createRedactor();
|
|
555
|
+
const field = byId("pw") as HTMLInputElement;
|
|
556
|
+
field.value = "correct horse";
|
|
557
|
+
redact.isPassword(field);
|
|
558
|
+
const { label } = clickTarget("b", redact);
|
|
559
|
+
expect(label).toBe("Show: [redacted]");
|
|
560
|
+
expect(label).not.toContain("horse");
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
it("bounds the text walk on large or deep subtrees", () => {
|
|
564
|
+
document.body.innerHTML = `<a id="wide" href="#w">Inbox${"<span></span>".repeat(2000)}</a><a id="deep" href="#d">${"<span>".repeat(1500)}Deep${"</span>".repeat(1500)}</a>`;
|
|
565
|
+
expect(clickTarget("wide").label).toBeUndefined();
|
|
566
|
+
uninstall?.();
|
|
567
|
+
expect(clickTarget("deep").label).toBeUndefined();
|
|
568
|
+
});
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
describe("second review hardening", () => {
|
|
572
|
+
it("gives no text label to div fields that hold text through inline elements", () => {
|
|
573
|
+
document.body.innerHTML = `<a id="span" href="#p-9"><div><span>Jane Doe</span></div><div><span>HIV positive</span></div></a><a id="bold" href="#p-10"><div><b>Jane Doe</b></div><div><span><b>HIV positive</b></span></div></a>`;
|
|
574
|
+
for (const id of ["span", "bold"]) {
|
|
575
|
+
const target = clickTarget(id);
|
|
576
|
+
expect(target.label).toBeUndefined();
|
|
577
|
+
expect(target.text).toContain("HIV positive");
|
|
578
|
+
uninstall?.();
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
it("gives no text label to cards built from div and span fields", () => {
|
|
583
|
+
document.body.innerHTML = `<a id="a" href="#patients-9"><div><span>Jane Doe</span><span>DOB 1980-04-02</span><span>Oncology</span></div></a><div id="b" role="button"><div>Jane Doe</div><div>HIV positive</div></div>`;
|
|
584
|
+
for (const id of ["a", "b"]) {
|
|
585
|
+
const target = clickTarget(id);
|
|
586
|
+
expect(target.label).toBeUndefined();
|
|
587
|
+
expect(target.text).toContain("Jane Doe");
|
|
588
|
+
uninstall?.();
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
it("keeps naming controls with one or two text parts", () => {
|
|
593
|
+
document.body.innerHTML = `<button id="nested"><div><span>Save</span></div></button><button id="icon"><svg aria-hidden="true"><path d="M0"></path></svg><span>Save</span></button><button id="two"><span>Thought for</span> <span>2 seconds</span></button>`;
|
|
594
|
+
expect(clickTarget("nested").label).toBe("Save");
|
|
595
|
+
uninstall?.();
|
|
596
|
+
expect(clickTarget("icon").label).toBe("Save");
|
|
597
|
+
uninstall?.();
|
|
598
|
+
expect(clickTarget("two").label).toBe("Thought for 2 seconds");
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
it("scrubs a remembered password that differs in whitespace or invisible characters", () => {
|
|
602
|
+
for (const password of ["tr0ub 4dor", "tr0ub\u00AD4dor"]) {
|
|
603
|
+
document.body.innerHTML = `<input id="pw" type="password"><button id="b">Show ${password}</button>`;
|
|
604
|
+
const redact = createRedactor();
|
|
605
|
+
const field = byId("pw") as HTMLInputElement;
|
|
606
|
+
field.value = password;
|
|
607
|
+
redact.isPassword(field);
|
|
608
|
+
const { label } = clickTarget("b", redact);
|
|
609
|
+
expect(label).toBe("Show [redacted]");
|
|
610
|
+
uninstall?.();
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
it("keeps dotted names and redacts dotted tokens", () => {
|
|
615
|
+
document.body.innerHTML = `<button id="name">Open settings.notifications.preferences</button><button id="digits">Use a1b2c3d4e5.f6g7h8i9j0.k1l2m3n4o5</button><button id="long">Use abcdefghijklmnopq.rstuvwxyzabcdefgh.ijklmnopqrstuvwxy</button>`;
|
|
616
|
+
expect(clickTarget("name").label).toBe(
|
|
617
|
+
"Open settings.notifications.preferences",
|
|
618
|
+
);
|
|
619
|
+
uninstall?.();
|
|
620
|
+
expect(clickTarget("digits").label).toBe("Use [redacted]");
|
|
621
|
+
uninstall?.();
|
|
622
|
+
expect(clickTarget("long").label).toBe("Use [redacted]");
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
it("does not split words at inline formatting", () => {
|
|
626
|
+
document.body.innerHTML = `<button id="b">S<u>ave</u></button><a id="a" href="#docs">Java<mark>Script</mark> guide</a>`;
|
|
627
|
+
expect(clickTarget("b").label).toBe("Save");
|
|
628
|
+
uninstall?.();
|
|
629
|
+
expect(clickTarget("a").label).toBe("JavaScript guide");
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
it("allows one paragraph, and headings inside summary", () => {
|
|
633
|
+
document.body.innerHTML = `<a id="a" href="#start"><div><p class="framer-text">Get started</p></div></a><details><summary id="s"><h3>Shipping and returns</h3></summary></details><a id="two" href="#two"><p>Jane Doe</p><p>Pending</p></a>`;
|
|
634
|
+
expect(clickTarget("a").label).toBe("Get started");
|
|
635
|
+
uninstall?.();
|
|
636
|
+
expect(clickTarget("s").label).toBe("Shipping and returns");
|
|
637
|
+
uninstall?.();
|
|
638
|
+
expect(clickTarget("two").label).toBeUndefined();
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
it("names an svg by its aria-label or title without walking inside it", () => {
|
|
642
|
+
document.body.innerHTML = `<button id="label"><svg aria-label="Delete"><path></path></svg></button><button id="title"><svg><title>Download</title><path></path></svg></button><a id="logo" href="#home"><svg>${"<path></path>".repeat(1200)}</svg><span>Home</span></a>`;
|
|
643
|
+
expect(clickTarget("label").label).toBe("Delete");
|
|
644
|
+
uninstall?.();
|
|
645
|
+
expect(clickTarget("title").label).toBe("Download");
|
|
646
|
+
uninstall?.();
|
|
647
|
+
expect(clickTarget("logo").label).toBe("Home");
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
it("does not name a native checkbox from its label text", () => {
|
|
651
|
+
document.body.innerHTML = `<input type="checkbox" id="t"><label for="t">Buy milk for Jane</label><input type="checkbox" id="named" aria-label="Mark done"><label for="named">Buy milk for Jane</label>`;
|
|
652
|
+
expect(clickTarget("t").label).toBeUndefined();
|
|
653
|
+
uninstall?.();
|
|
654
|
+
expect(clickTarget("named").label).toBe("Mark done");
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
it("does not read text slotted into a web component", () => {
|
|
658
|
+
document.body.innerHTML = `<my-button id="host">Save</my-button>`;
|
|
659
|
+
const root = byId("host").attachShadow({ mode: "open" });
|
|
660
|
+
root.innerHTML = `<button id="inner"><slot></slot></button>`;
|
|
661
|
+
const events = setup();
|
|
662
|
+
click(root.getElementById("inner")!, {
|
|
663
|
+
detail: 1,
|
|
664
|
+
clientX: 10,
|
|
665
|
+
clientY: 10,
|
|
666
|
+
composed: true,
|
|
667
|
+
});
|
|
668
|
+
expect(events[0]?.value.target).not.toHaveProperty("label");
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
it("cuts long aria-labelledby text instead of dropping it", () => {
|
|
672
|
+
document.body.innerHTML = `<span id="l">${"word ".repeat(70)}</span><button id="b" aria-labelledby="l">Go</button>`;
|
|
673
|
+
const { label } = clickTarget("b");
|
|
674
|
+
expect(label).toMatch(/^word word .*…$/);
|
|
675
|
+
expect(Array.from(label!)).toHaveLength(80);
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
it("measures the 80-character limit in code points", () => {
|
|
679
|
+
const emoji = "\u{1F600}".repeat(41);
|
|
680
|
+
document.body.innerHTML = `<button id="b">${emoji}</button>`;
|
|
681
|
+
expect(clickTarget("b").label).toBe(emoji);
|
|
682
|
+
});
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
it("drops a named control inside data-copilotkit-ignore", () => {
|
|
686
|
+
document.body.innerHTML = `<div data-copilotkit-ignore><button id="b" aria-label="Delete account">Delete account</button></div>`;
|
|
687
|
+
const events = setup();
|
|
688
|
+
click(byId("b"));
|
|
689
|
+
expect(events).toEqual([]);
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
it("labels the pointerdown target when a re-render moves the click to body", () => {
|
|
693
|
+
document.body.innerHTML = `<button id="b">Open row</button>`;
|
|
694
|
+
const events = setup();
|
|
695
|
+
pointerDown(byId("b"));
|
|
696
|
+
byId("b").remove();
|
|
697
|
+
click(document.body);
|
|
698
|
+
expect(events[0]?.value.target).toMatchObject({ label: "Open row" });
|
|
699
|
+
});
|
|
700
|
+
});
|