[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

fix(browser): fix trace highlight of shadow dom on webdriverio (#10227)

authored by

Hiroshi Ogawa and committed by
GitHub
(Apr 29, 2026, 6:34 PM +0900) b01afd26 0dccf3d6

+77 -51
+68 -36
test/browser/specs/trace.test.ts
··· 141 141 "entries": [ 142 142 { 143 143 "element": { 144 + "_pwSelector": "internal:role=button", 144 145 "locator": "getByRole('button')", 145 - "selector": " body > button", 146 + "selector": "html > body > button", 146 147 }, 147 148 "kind": "mark", 148 149 "location": "exotic.test.ts:58", ··· 166 167 "entries": [ 167 168 { 168 169 "element": { 170 + "_pwSelector": "internal:testid=[data-testid="trace-canvas"s]", 169 171 "locator": "getByTestId('trace-canvas')", 170 - "selector": " body > canvas", 172 + "selector": "html > body > canvas", 171 173 }, 172 174 "kind": "mark", 173 175 "location": "exotic.test.ts:17", ··· 191 193 "entries": [ 192 194 { 193 195 "element": { 196 + "_pwSelector": "internal:role=button[name="Custom element button"i]", 194 197 "locator": "getByRole('button', { name: 'Custom element button' })", 195 198 "selector": ">>>html > body > trace-widget > button", 196 199 }, ··· 198 201 "location": "exotic.test.ts:47", 199 202 "name": "custom element rendered", 200 203 "snapshot": { 201 - "selectorError": "Unexpected token "" while parsing selector """, 202 - "selectorResolution": "error", 204 + "selectorResolution": "matched", 203 205 }, 204 206 }, 205 207 { ··· 217 219 "entries": [ 218 220 { 219 221 "element": { 222 + "_pwSelector": "internal:role=button[name="Shadow button"i]", 220 223 "locator": "getByRole('button', { name: 'Shadow button' })", 221 224 "selector": ">>>html > body > section > button", 222 225 }, ··· 224 227 "location": "exotic.test.ts:28", 225 228 "name": "shadow button rendered", 226 229 "snapshot": { 227 - "selectorError": "Unexpected token "" while parsing selector """, 228 - "selectorResolution": "error", 230 + "selectorResolution": "matched", 229 231 }, 230 232 }, 231 233 { ··· 245 247 "entries": [ 246 248 { 247 249 "element": { 250 + "_pwSelector": "internal:role=button[name="Hello"i]", 248 251 "locator": "getByRole('button', { name: 'Hello' })", 249 - "selector": " body > button", 252 + "selector": "html > body > button", 250 253 }, 251 254 "kind": "action", 252 255 "location": "expect.test.ts:25", ··· 284 287 "entries": [ 285 288 { 286 289 "element": { 290 + "_pwSelector": "internal:role=button", 287 291 "locator": "getByRole('button')", 288 - "selector": " body > button", 292 + "selector": "html > body > button", 289 293 }, 290 294 "kind": "expect", 291 295 "location": "expect.test.ts:15", ··· 310 314 "entries": [ 311 315 { 312 316 "element": { 317 + "_pwSelector": "internal:role=button", 313 318 "locator": "getByRole('button')", 314 - "selector": " body > button", 319 + "selector": "html > body > button", 315 320 }, 316 321 "kind": "expect", 317 322 "location": "expect.test.ts:10", ··· 351 356 "entries": [ 352 357 { 353 358 "element": { 359 + "_pwSelector": "internal:role=button[name="Hello"i]", 354 360 "locator": "getByRole('button', { name: 'Hello' })", 355 - "selector": " body > button", 361 + "selector": "html > body > button", 356 362 }, 357 363 "kind": "mark", 358 364 "location": "mark.test.ts:24", ··· 376 382 "entries": [ 377 383 { 378 384 "element": { 385 + "_pwSelector": "internal:role=button", 379 386 "locator": "getByRole('button')", 380 - "selector": " body > button", 387 + "selector": "html > body > button", 381 388 }, 382 389 "kind": "mark", 383 390 "location": "mark.test.ts:10", ··· 440 447 "entries": [ 441 448 { 442 449 "element": { 450 + "_pwSelector": "internal:role=button", 443 451 "locator": "getByRole('button')", 444 - "selector": " body > button", 452 + "selector": "html > body > button", 445 453 }, 446 454 "kind": "mark", 447 455 "location": "mark.test.ts:29", ··· 467 475 "entries": [ 468 476 { 469 477 "element": { 478 + "_pwSelector": "internal:attr=[alt="external trace asset"i]", 470 479 "locator": "getByAltText('external trace asset')", 471 - "selector": " body > img", 480 + "selector": "html > body > img", 472 481 }, 473 482 "kind": "mark", 474 483 "location": "resources.test.ts:20", ··· 492 501 "entries": [ 493 502 { 494 503 "element": { 504 + "_pwSelector": "internal:attr=[alt="local trace asset"i]", 495 505 "locator": "getByAltText('local trace asset')", 496 - "selector": " body > img", 506 + "selector": "html > body > img", 497 507 }, 498 508 "kind": "mark", 499 509 "location": "resources.test.ts:12", ··· 519 529 "entries": [ 520 530 { 521 531 "element": { 532 + "_pwSelector": "internal:role=list", 522 533 "locator": "getByRole('list')", 523 - "selector": " body > ul", 534 + "selector": "html > body > ul", 524 535 }, 525 536 "kind": "mark", 526 537 "location": "retry.test.ts:29", ··· 542 553 "entries": [ 543 554 { 544 555 "element": { 556 + "_pwSelector": "internal:role=list", 545 557 "locator": "getByRole('list')", 546 - "selector": " body > ul", 558 + "selector": "html > body > ul", 547 559 }, 548 560 "kind": "mark", 549 561 "location": "retry.test.ts:29", ··· 566 578 "entries": [ 567 579 { 568 580 "element": { 581 + "_pwSelector": "internal:role=list", 569 582 "locator": "getByRole('list')", 570 - "selector": " body > ul", 583 + "selector": "html > body > ul", 571 584 }, 572 585 "kind": "mark", 573 586 "location": "retry.test.ts:29", ··· 590 603 "entries": [ 591 604 { 592 605 "element": { 606 + "_pwSelector": "internal:role=list", 593 607 "locator": "getByRole('list')", 594 - "selector": " body > ul", 608 + "selector": "html > body > ul", 595 609 }, 596 610 "kind": "mark", 597 611 "location": "retry.test.ts:29", ··· 614 628 "entries": [ 615 629 { 616 630 "element": { 631 + "_pwSelector": "internal:role=list", 617 632 "locator": "getByRole('list')", 618 - "selector": " body > ul", 633 + "selector": "html > body > ul", 619 634 }, 620 635 "kind": "mark", 621 636 "location": "retry.test.ts:29", ··· 640 655 "entries": [ 641 656 { 642 657 "element": { 658 + "_pwSelector": "internal:role=list", 643 659 "locator": "getByRole('list')", 644 - "selector": " body > ul", 660 + "selector": "html > body > ul", 645 661 }, 646 662 "kind": "mark", 647 663 "location": "retry.test.ts:18", ··· 663 679 "entries": [ 664 680 { 665 681 "element": { 682 + "_pwSelector": "internal:role=list", 666 683 "locator": "getByRole('list')", 667 - "selector": " body > ul", 684 + "selector": "html > body > ul", 668 685 }, 669 686 "kind": "mark", 670 687 "location": "retry.test.ts:18", ··· 687 704 "entries": [ 688 705 { 689 706 "element": { 707 + "_pwSelector": "internal:role=list", 690 708 "locator": "getByRole('list')", 691 - "selector": " body > ul", 709 + "selector": "html > body > ul", 692 710 }, 693 711 "kind": "mark", 694 712 "location": "retry.test.ts:18", ··· 713 731 "entries": [ 714 732 { 715 733 "element": { 734 + "_pwSelector": "internal:role=list", 716 735 "locator": "getByRole('list')", 717 - "selector": " body > ul", 736 + "selector": "html > body > ul", 718 737 }, 719 738 "kind": "mark", 720 739 "location": "retry.test.ts:36", ··· 736 755 "entries": [ 737 756 { 738 757 "element": { 758 + "_pwSelector": "internal:role=list", 739 759 "locator": "getByRole('list')", 740 - "selector": " body > ul", 760 + "selector": "html > body > ul", 741 761 }, 742 762 "kind": "mark", 743 763 "location": "retry.test.ts:36", ··· 760 780 "entries": [ 761 781 { 762 782 "element": { 783 + "_pwSelector": "internal:role=list", 763 784 "locator": "getByRole('list')", 764 - "selector": " body > ul", 785 + "selector": "html > body > ul", 765 786 }, 766 787 "kind": "mark", 767 788 "location": "retry.test.ts:36", ··· 785 806 "entries": [ 786 807 { 787 808 "element": { 809 + "_pwSelector": "internal:role=list", 788 810 "locator": "getByRole('list')", 789 - "selector": " body > ul", 811 + "selector": "html > body > ul", 790 812 }, 791 813 "kind": "mark", 792 814 "location": "retry.test.ts:36", ··· 811 833 "entries": [ 812 834 { 813 835 "element": { 836 + "_pwSelector": "internal:role=list", 814 837 "locator": "getByRole('list')", 815 - "selector": " body > ul", 838 + "selector": "html > body > ul", 816 839 }, 817 840 "kind": "mark", 818 841 "location": "retry.test.ts:22", ··· 834 857 "entries": [ 835 858 { 836 859 "element": { 860 + "_pwSelector": "internal:role=list", 837 861 "locator": "getByRole('list')", 838 - "selector": " body > ul", 862 + "selector": "html > body > ul", 839 863 }, 840 864 "kind": "mark", 841 865 "location": "retry.test.ts:22", ··· 858 882 "entries": [ 859 883 { 860 884 "element": { 885 + "_pwSelector": "internal:role=list", 861 886 "locator": "getByRole('list')", 862 - "selector": " body > ul", 887 + "selector": "html > body > ul", 863 888 }, 864 889 "kind": "mark", 865 890 "location": "retry.test.ts:22", ··· 981 1006 "entries": [ 982 1007 { 983 1008 "element": { 1009 + "_pwSelector": "internal:role=button[name="First pseudo state"i]", 984 1010 "locator": "getByRole('button', { name: 'First pseudo state' })", 985 - "selector": " body > button:nth-child(1)", 1011 + "selector": "html > body > button:nth-child(1)", 986 1012 }, 987 1013 "kind": "expect", 988 1014 "location": "styles.test.ts:101", ··· 994 1020 }, 995 1021 { 996 1022 "element": { 1023 + "_pwSelector": "internal:role=button[name="First pseudo state"i]", 997 1024 "locator": "getByRole('button', { name: 'First pseudo state' })", 998 - "selector": " body > button:nth-child(1)", 1025 + "selector": "html > body > button:nth-child(1)", 999 1026 }, 1000 1027 "kind": "action", 1001 1028 "location": "styles.test.ts:104", ··· 1007 1034 }, 1008 1035 { 1009 1036 "element": { 1037 + "_pwSelector": "internal:role=button[name="Second pseudo state"i]", 1010 1038 "locator": "getByRole('button', { name: 'Second pseudo state' })", 1011 - "selector": " body > button:nth-child(2)", 1039 + "selector": "html > body > button:nth-child(2)", 1012 1040 }, 1013 1041 "kind": "action", 1014 1042 "location": "styles.test.ts:105", ··· 1020 1048 }, 1021 1049 { 1022 1050 "element": { 1051 + "_pwSelector": "internal:label="Focused pseudo state"i", 1023 1052 "locator": "getByLabel('Focused pseudo state')", 1024 - "selector": " body > input", 1053 + "selector": "html > body > input", 1025 1054 }, 1026 1055 "kind": "action", 1027 1056 "location": "styles.test.ts:106", ··· 1033 1062 }, 1034 1063 { 1035 1064 "element": { 1065 + "_pwSelector": "internal:label="Focus within pseudo state"i", 1036 1066 "locator": "getByLabel('Focus within pseudo state')", 1037 - "selector": " body > label > input", 1067 + "selector": "html > body > label > input", 1038 1068 }, 1039 1069 "kind": "action", 1040 1070 "location": "styles.test.ts:107", ··· 1080 1110 "entries": [ 1081 1111 { 1082 1112 "element": { 1113 + "_pwSelector": "internal:role=button", 1083 1114 "locator": "getByRole('button')", 1084 - "selector": " body > main > button", 1115 + "selector": "html > body > main > button", 1085 1116 }, 1086 1117 "kind": "mark", 1087 1118 "location": "viewport.test.ts:31", ··· 1105 1136 "entries": [ 1106 1137 { 1107 1138 "element": { 1139 + "_pwSelector": "internal:testid=[data-testid="scroll-box"s]", 1108 1140 "locator": "getByTestId('scroll-box')", 1109 - "selector": " body > section", 1141 + "selector": "html > body > section", 1110 1142 }, 1111 1143 "kind": "mark", 1112 1144 "location": "viewport.test.ts:47",
+5
packages/browser/src/client/tester/locators.ts
··· 355 355 return { 356 356 selector: this.selector, 357 357 locator: this.asLocator(), 358 + _pwSelector: this._pwSelector, 358 359 } 359 360 } 360 361 ··· 429 430 export interface SerializedLocator { 430 431 selector: string 431 432 locator: string 433 + /** 434 + * @internal 435 + */ 436 + _pwSelector?: string 432 437 } 433 438 434 439 function createStrictModeViolationError(
+4 -15
packages/browser/src/client/tester/trace.ts
··· 15 15 16 16 export interface BrowserTraceEntry { 17 17 name: string 18 - // not used yet for UI but tested 19 18 kind: BrowserTraceEntryKind 20 19 status?: BrowserTraceEntryStatus 21 20 startTime: number ··· 85 84 startTime?: number 86 85 }, 87 86 ): void { 88 - // TODO: trace-view currently receives selectors after locator/action resolution, 89 - // so provider-specific lowered selectors can leak into snapshot lookup. Preserve 90 - // the original locator selector, or record the target node id before lowering. 91 - // for example, this causes shadow dom selectors to fail with `>>>` marker. 92 - // For now, remove trivial `html >` prefix generated by convertElementToCssSelector. 93 - // this is also necessary to `engine.querySelector + document.documentElement` 94 - // to find an element on webdriverio 95 - if (options.element?.selector?.startsWith('html >')) { 96 - options.element.selector = options.element.selector.slice(6) 97 - } 98 87 const attemptInfo = getBrowserState().browserTraceAttempts.get(task.id)! 99 88 const relativeStartTime = (options.startTime ?? now()) - attemptInfo.startTime 100 - const snapshot = takeSnapshot(options.element?.selector) 89 + const snapshot = takeSnapshot(options.element) 101 90 const entry: BrowserTraceEntry = { 102 91 ...options, 103 92 startTime: relativeStartTime, ··· 117 106 // selector engine inside the snapshot iframe at view time via injected script. 118 107 // Our approach resolves at collection time (same moment as snapshot) — simpler but 119 108 // requires Mirror plumbing. nodeId-based lookup also works across shadow DOM, unlike querySelector. 120 - function takeSnapshot(selector?: string): TraceSnapshot { 109 + function takeSnapshot(serializedLocator?: SerializedLocator): TraceSnapshot { 121 110 const { snapshot, createMirror } = getBrowserState().browserTraceDomSnapshot! 122 111 const traceView = getBrowserState().config.browser.traceView 123 112 const engine = getBrowserState().selectorEngine! ··· 144 133 const ids = Array.from(elements, el => mirror.getId(el)).filter(id => id !== -1) 145 134 result.pseudoClassIds[className] = ids 146 135 } 147 - if (selector) { 136 + if (serializedLocator) { 148 137 try { 149 138 const el = engine.querySelector( 150 - engine.parseSelector(selector), 139 + engine.parseSelector(serializedLocator._pwSelector ?? serializedLocator.selector), 151 140 document.documentElement, 152 141 false, 153 142 )