<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: El Pixel</title>
    <description>The latest articles on DEV Community by El Pixel (elpixel).</description>
    <link>https://dev.to/elpixel</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F14980%2F129afd7c-b27c-4d9d-a8b4-5c8aa4729b05.png</url>
      <title>DEV Community: El Pixel</title>
      <link>https://dev.to/elpixel</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/elpixel"/>
    <language>en</language>
    <item>
      <title>Your Angular Test Passed. Could the User Click the Button?</title>
      <dc:creator>Vladimir</dc:creator>
      <pubDate>Thu, 01 Oct 2026 11:52:07 +0000</pubDate>
      <link>https://dev.to/elpixel/your-angular-test-passed-could-the-user-click-the-button-36na</link>
      <guid>https://dev.to/elpixel/your-angular-test-passed-could-the-user-click-the-button-36na</guid>
      <description>&lt;p&gt;A visible Angular button was covered by a decorative layer. The handler still worked. Our test clicked the button, observed the expected status, and passed. A pointer could not reach the button's center.&lt;/p&gt;

&lt;p&gt;Running the same synthetic test in Chromium did not change the result. The missing observation was whether the pointer could actually reach its target.&lt;/p&gt;

&lt;p&gt;We examined six component scenarios, with expected behavior and one isolated mutation defined before writing each test. We chose the mutations to examine different mechanisms; this is a small demonstration setup, not a random sample of application defects.&lt;/p&gt;

&lt;p&gt;The configurations were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A:&lt;/strong&gt; jsdom with &lt;code&gt;@testing-library/user-event&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;B:&lt;/strong&gt; headless Chromium in Vitest Browser Mode with the same synthetic interaction library.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;C:&lt;/strong&gt; the same Chromium with actions from &lt;code&gt;vitest/browser&lt;/code&gt; through the Playwright provider.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The synthetic/provider distinction, including a covered button, is already explained in the &lt;a href="https://cookbook.marmicode.io/angular/testing/vitest-browser-mode" rel="noopener noreferrer"&gt;Marmicode Cookbook&lt;/a&gt;. Here we add six predefined mutations, a separate hit test in B, and the correction of our own geometry oracle.&lt;/p&gt;

&lt;p&gt;In our form, programmatic focus, and specified Tab sequence, jsdom was sufficient. Actual resize notifications and popover geometry needed browser layout. The covered-button case required checking the action itself, either with a provider click or an explicit browser hit test. The later B hit test is separate from the original matrix.&lt;/p&gt;

&lt;h2&gt;
  
  
  What counted as a complete check
&lt;/h2&gt;

&lt;p&gt;Before a test could be credited with detecting a mutation, it had to pass on the working version. We then examined how much of the expected behavior it actually checked.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;bug_detected&lt;/code&gt;: the working version passed, and the defective version violated the full predefined criterion.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;partial&lt;/code&gt;: the test checked part of that criterion.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;unsupported&lt;/code&gt;: in this fixture, the full criterion could not be verified reliably without substituting the behavior under investigation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;unsupported&lt;/code&gt; refers to the complete behavior in this fixture. A mock can check the response to a size or the argument passed to &lt;code&gt;observe()&lt;/code&gt;, but those are different assertions. Neither &lt;code&gt;partial&lt;/code&gt; nor &lt;code&gt;unsupported&lt;/code&gt; means &lt;code&gt;false_negative&lt;/code&gt;, a missed defect in a complete check.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario and user criterion&lt;/th&gt;
&lt;th&gt;A: jsdom + synthetic&lt;/th&gt;
&lt;th&gt;B: Chromium + synthetic&lt;/th&gt;
&lt;th&gt;C: Chromium + provider&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;S1. Form:&lt;/strong&gt; reject an invalid email, then submit a valid one&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bug_detected&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bug_detected&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bug_detected&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;S2. Dialog focus:&lt;/strong&gt; move focus to Close when opening, and return it to Open when closing&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bug_detected&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bug_detected&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bug_detected&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;S3. Tab order:&lt;/strong&gt; visit the available controls and skip the hidden one&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bug_detected&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bug_detected&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bug_detected&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;S4. Container width:&lt;/strong&gt; update after the inner container shrinks without a viewport change&lt;/td&gt;
&lt;td&gt;&lt;code&gt;unsupported&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bug_detected&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bug_detected&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;S5. Popover near the edge:&lt;/strong&gt; switch sides and keep the entire interactive rectangle inside the viewport&lt;/td&gt;
&lt;td&gt;&lt;code&gt;unsupported&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bug_detected&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bug_detected&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;S6. Covered button:&lt;/strong&gt; allow a pointer click at the center and update the status&lt;/td&gt;
&lt;td&gt;&lt;code&gt;partial&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;partial&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bug_detected&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The table describes the original test adapters: the actions, waits, and assertions written for each configuration. The later S6 check using &lt;code&gt;elementFromPoint&lt;/code&gt; is excluded; it is discussed separately below.&lt;/p&gt;

&lt;p&gt;The components, styles, and mutations stayed the same across configurations. The adapters differed: in S1, A/B used &lt;code&gt;type&lt;/code&gt; and C used &lt;code&gt;fill&lt;/code&gt;; in S3, A/B called &lt;code&gt;user-event.tab&lt;/code&gt; and C sent a browser Tab. The results therefore cannot be attributed to a single, fully isolated property of the environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where these scenarios fit in the testing pyramid
&lt;/h3&gt;

&lt;p&gt;In all six scenarios, we mounted an individual component through TestBed and tested it together with its template and DOM events. In scope, these are component integration tests. We did not run a complete application with routing, a backend, and a user flow. Browser Mode alone does not turn such a test into an E2E test.&lt;/p&gt;

&lt;p&gt;For the specified behavior in S1–S3, jsdom was sufficient. S4–S5 needed browser dimensions and notifications; S6 needed a check that the click was possible. Separate unit tests in these scenarios can check a validator, the state chosen for a width, or a position calculation. Those tests are useful, but they do not confirm the component's interaction with the browser. E2E tests would be needed for a complete application flow; the experiment did not investigate that level.&lt;/p&gt;

&lt;h2&gt;
  
  
  S1–S3: form, focus, and Tab
&lt;/h2&gt;

&lt;p&gt;In &lt;strong&gt;S1&lt;/strong&gt;, the form rejected &lt;code&gt;bad.example&lt;/code&gt;, displayed an error, and kept the submission count at zero. It then accepted &lt;code&gt;ok@example.com&lt;/code&gt;. The mutation removed the format check while retaining the empty-field check. All three configurations detected it. Validation was handled by the application with &lt;code&gt;novalidate&lt;/code&gt;; the browser's native validation message was outside the scenario.&lt;/p&gt;

&lt;p&gt;For &lt;strong&gt;S2&lt;/strong&gt;, we checked focus: after the dialog opened and rendering settled, focus had to move to Close; after closing, it had to return to Open. The mutation removed the focus change on opening. Checking &lt;code&gt;document.activeElement&lt;/code&gt; detected it in A, B, and C.&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;S3&lt;/strong&gt;, the test pressed Tab three times from a known starting position. The expected sequence was First, Second, End. Between the last two controls was another control in a section with &lt;code&gt;display: none&lt;/code&gt;. Replacing that property with &lt;code&gt;opacity: 0&lt;/code&gt; left the control in the Tab order while making it visually hidden. Both &lt;code&gt;user-event.tab&lt;/code&gt; in jsdom and the browser action detected the change.&lt;/p&gt;

&lt;p&gt;These tests needed no layout measurements. Their results concern programmatic focus and the specified Tab sequence. We did not test a focus trap, &lt;code&gt;visibility: hidden&lt;/code&gt;, &lt;code&gt;inert&lt;/code&gt;, or other forms of keyboard navigation. A component using those ways of restricting interaction needs separate Tab-order and focus checks. S3 is not a complete accessibility test.&lt;/p&gt;

&lt;h2&gt;
  
  
  S4: which container ResizeObserver watches
&lt;/h2&gt;

&lt;p&gt;The component changed its label according to its container's width. At 360 px, it displayed Wide. After shrinking to 240 px, below the 300 px threshold, it had to display Narrow. The viewport stayed unchanged, and the outer container remained 400 px wide.&lt;/p&gt;

&lt;p&gt;In the working implementation, &lt;code&gt;ResizeObserver&lt;/code&gt; watched the inner container. The mutation switched it to the outer one. The relevant width changed, but the observer was watching a different element.&lt;/p&gt;

&lt;p&gt;In the Angular component, the notification updated a signal whose value appeared in the template. An excerpt from the working implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;observer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ResizeObserver&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;compact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;contentRect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nativeElement&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The observer was disconnected when the component was destroyed. The source already had a separate hook for that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;ngOnDestroy&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;disconnect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Manually passing a width of 240 px to the callback can check that it switches the component's state. But that test delivers the notification regardless of which element the browser observes. Without a separate check of the observed target, this substitution does not check the error in our scenario.&lt;/p&gt;

&lt;p&gt;A more targeted mock test could record the argument to &lt;code&gt;observe()&lt;/code&gt; and compare it with the inner container. Such a spy could detect this exact mutation in jsdom. It would check which element the component subscribed to. That is a useful implementation check, but it does not confirm the entire user scenario: after the container actually shrank, the browser delivered a notification and the label updated. This spy was not part of the main series.&lt;/p&gt;

&lt;p&gt;In B and C, we actually shrank the inner container and waited for the label to change. Both tests detected the mutation. For A, the complete scenario was &lt;code&gt;unsupported&lt;/code&gt;: the fixture had no independent layout measurements or browser notification.&lt;/p&gt;

&lt;h2&gt;
  
  
  S5: where the popover ended up
&lt;/h2&gt;

&lt;p&gt;The component opened a popover next to its trigger, the element it was anchored to. In the normal position, the popover had to appear on the right. When the trigger moved near the right edge, to &lt;code&gt;left = 740&lt;/code&gt; in an 800 × 600 CSS px viewport, the popover had to open on the left.&lt;/p&gt;

&lt;p&gt;We checked the side and the entire interactive rectangle against the viewport, with a one-pixel tolerance. The mutation disabled the side switch. Opening in the normal position still worked, but near the edge, the popover's right boundary reached 948 px.&lt;/p&gt;

&lt;p&gt;After the tests were corrected, B and C detected the violation. In A, the complete scenario was &lt;code&gt;unsupported&lt;/code&gt;: it required measuring the popover's actual position.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why the first browser test passed on the defect
&lt;/h3&gt;

&lt;p&gt;After opening, Angular added the popover to the DOM, while its position was calculated later through &lt;code&gt;requestAnimationFrame&lt;/code&gt;. In between, the element existed but retained coordinates from the previous opening. The test could check that old rectangle and pass on the defective version.&lt;/p&gt;

&lt;p&gt;We corrected the wait in two steps. First, we waited for the trigger to move to the edge before opening. That was not enough: we also needed to wait for the popover's own position to update. Only then did the test check its side and all four viewport boundaries.&lt;/p&gt;

&lt;p&gt;The wait for the update allowed both the correct and incorrect new positions. It therefore did not give the test its expected answer. The subsequent assertions about the side and boundaries detected the error.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuvu5h7skgts2ia34xcpj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuvu5h7skgts2ia34xcpj.png" alt="S5: move the trigger, open the popover, wait for its position to update, then check the side and all four boundaries." width="799" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The browser provided real coordinates, but our test read them too early. After correcting the waits and completing the assertions, we repeated the final series. We retained the early false passes in the deviations log and excluded them from the matrix.&lt;/p&gt;

&lt;h3&gt;
  
  
  How we corrected S5: Angular, the wait code, and the 500 threshold
&lt;/h3&gt;

&lt;p&gt;All components were mounted through Angular &lt;code&gt;TestBed&lt;/code&gt;. The helper created a fixture, appended its element to the document, and ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;detectChanges&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;whenStable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the component setup actually used in the experiment.&lt;/p&gt;

&lt;p&gt;Starting with Angular 21, zoneless is enabled by default. TestBed's mode depends on whether &lt;code&gt;zone.js&lt;/code&gt; is loaded: with it, Zone-based change detection is used by default; without it, zoneless is used. An explicit provider can change that choice. The fixture's saved configuration did not load ZoneJS.&lt;/p&gt;

&lt;p&gt;For new zoneless tests, &lt;a href="https://angular.dev/guide/zoneless#using-zoneless-in-testbed" rel="noopener noreferrer"&gt;Angular recommends&lt;/a&gt; waiting for &lt;code&gt;fixture.whenStable()&lt;/code&gt; where possible, allowing Angular to perform the scheduled update. Calling &lt;code&gt;detectChanges()&lt;/code&gt; manually forces change detection and can hide a missing notification of a state change. The excerpt above preserves the original helper rather than replacing it with a new implementation.&lt;/p&gt;

&lt;p&gt;User actions then changed the component's state. In S5, opening the popover looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;opened&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;place&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The popover appeared through Angular &lt;code&gt;@if&lt;/code&gt;. In the next animation frame, &lt;code&gt;place()&lt;/code&gt; measured the trigger and popover and updated the coordinate signal. Fixture setup with &lt;code&gt;whenStable()&lt;/code&gt; happened before these user actions. In the opening test, we separately waited for the required geometry change. Angular stability and the popover's correct position are different conditions: &lt;code&gt;whenStable()&lt;/code&gt; is not an assertion about an element's side or boundaries.&lt;/p&gt;

&lt;p&gt;We added the same waits in B and C. This excerpt from the corrected B test conveys the sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buttons&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;until&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBoundingClientRect&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;740&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;until&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;popup&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;popup&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeLessThan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBoundingClientRect&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;popup&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeGreaterThanOrEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;popup&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeLessThanOrEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;801&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;popup&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeGreaterThanOrEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;popup&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeLessThanOrEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;601&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;buttons[0]&lt;/code&gt; moves the trigger to the edge, &lt;code&gt;trigger&lt;/code&gt; opens the popover, and &lt;code&gt;popup()&lt;/code&gt; reads its rectangle. The shared &lt;code&gt;until&lt;/code&gt; helper polled the condition every 20 ms until it succeeded or timed out. The excerpts preserve the code actually used. In your own test, you can express the wait with the built-in &lt;a href="https://vitest.dev/api/vi.html#vi-waitfor" rel="noopener noreferrer"&gt;vi.waitFor&lt;/a&gt; or &lt;a href="https://vitest.dev/api/expect.html#poll" rel="noopener noreferrer"&gt;expect.poll&lt;/a&gt;; the choice of helper was not investigated here.&lt;/p&gt;

&lt;p&gt;Before repositioning, the popover stayed in its old location: its left boundary was roughly 148 px from the screen's left edge. Checking the position at that point would read the result of the previous opening.&lt;/p&gt;

&lt;p&gt;The number 500 is an intermediate threshold here. While the coordinate is roughly 148, the test waits. Once the popover moves toward the trigger near the right edge, the wait ends. Both the correct and defective new positions pass this threshold, so &lt;code&gt;left &amp;gt;= 500&lt;/code&gt; alone does not mean that the component works correctly.&lt;/p&gt;

&lt;p&gt;The correctness check follows: the popover must be to the left of the trigger and stay inside the screen. The defective version passes the wait but then fails because its right boundary reaches 948 px. The threshold of 500 was chosen for the two positions in this fixture; it cannot be copied unchanged into other tests.&lt;/p&gt;

&lt;p&gt;After refining the waits, adding the vertical boundaries, and correcting the CSS, we repeated the final series. The early logs were retained in the deviations log and excluded from the matrix.&lt;/p&gt;

&lt;h2&gt;
  
  
  S6: three ways to check a covered button
&lt;/h2&gt;

&lt;p&gt;A visible Confirm button, 160 × 48 px, had to change the status from Pending to Confirmed after one pointer click at its center. The code excerpts retain the fixture's original Russian labels: &lt;code&gt;Подтвердить&lt;/code&gt; (Confirm), &lt;code&gt;Ожидает&lt;/code&gt; (Pending), and &lt;code&gt;Подтверждено&lt;/code&gt; (Confirmed).&lt;/p&gt;

&lt;p&gt;A decorative layer covered the center. In the working version, it had &lt;code&gt;pointer-events: none&lt;/code&gt;. The mutation changed the value to &lt;code&gt;auto&lt;/code&gt;: the button and handler stayed the same, but the layer intercepted clicks.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbqfjik7z8khhyg4gf0lr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbqfjik7z8khhyg4gf0lr.png" alt="S6: synthetic click calls the handler, provider click detects interception, and a separate exploratory hit test finds the covering layer." width="800" height="874"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The original A/B test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fixture&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CoveredButtonComponent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nativeElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;HTMLButtonElement&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nativeElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;output&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;HTMLOutputElement&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Ожидает&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;userEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;until&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Подтверждено&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;userEvent&lt;/code&gt; is imported from &lt;code&gt;@testing-library/user-event&lt;/code&gt;. The handler ran and the status changed even on the defective version. The original A/S6 and B/S6 therefore received &lt;code&gt;partial&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;user-event&lt;/code&gt; models interaction sequences and performs its own checks. Its &lt;code&gt;pointerEventsCheck&lt;/code&gt; checks whether an element has or inherits &lt;code&gt;pointer-events: none&lt;/code&gt;. &lt;a href="https://testing-library.com/docs/user-event/options/#pointereventscheck" rel="noopener noreferrer"&gt;Testing Library: pointerEventsCheck&lt;/a&gt;. Separately, the pointer API documentation says that the library does not determine whether the described action is possible at the specified point in the layout. &lt;a href="https://testing-library.com/docs/user-event/pointer/" rel="noopener noreferrer"&gt;Testing Library: pointer&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The button itself did not have &lt;code&gt;pointer-events: none&lt;/code&gt; in S6. The problem was a different layer above it. The target passed to &lt;code&gt;click&lt;/code&gt; was still the button, and the test only checked the status change afterward. Even real Chromium did not add a pointer hit check to that code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Provider click
&lt;/h3&gt;

&lt;p&gt;In C, the action used &lt;code&gt;page&lt;/code&gt; from &lt;code&gt;vitest/browser&lt;/code&gt;. The corresponding excerpt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fixture&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CoveredButtonComponent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nativeElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;output&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;HTMLOutputElement&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Ожидает&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Подтвердить&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;until&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Подтверждено&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The click was called without &lt;code&gt;force&lt;/code&gt;. The working version passed. On the defective version, the provider reported that &lt;code&gt;.decoration&lt;/code&gt; intercepted pointer events and could not perform the action. The actionability check detected the obstacle before the click.&lt;/p&gt;

&lt;h3&gt;
  
  
  A separate B test with elementFromPoint
&lt;/h3&gt;

&lt;p&gt;After the main series, we added an exploratory B check using &lt;code&gt;document.elementFromPoint&lt;/code&gt;. The method returns the topmost element at a given point. We calculated the center from the button's actual rectangle. This function came from that test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hitAtCentre&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBoundingClientRect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeGreaterThan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeGreaterThan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;elementFromPoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;hitButton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We performed the check before and after the synthetic click. In the working version, the center &lt;code&gt;(80, 24)&lt;/code&gt; contained the button or its descendant. The defective version returned &lt;code&gt;.decoration&lt;/code&gt;, even though the handler still changed the status. The test required &lt;code&gt;hitButton: true&lt;/code&gt; in both measurements and detected the overlap.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;S6 check&lt;/th&gt;
&lt;th&gt;Observation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Original A/B: synthetic click and status&lt;/td&gt;
&lt;td&gt;The handler changes state after the event is delivered.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C: provider click without &lt;code&gt;force&lt;/code&gt; and status&lt;/td&gt;
&lt;td&gt;The action passes actionability checks, executes, and changes the status.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B + hit test (exploratory, outside the main matrix)&lt;/td&gt;
&lt;td&gt;The button or its descendant is at the center before and after the click. The defective layer violates this assertion.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;B now had two different tests. The original only checked the status change after a synthetic click. The later test also asked which element was at the click point and detected the overlap.&lt;/p&gt;

&lt;p&gt;The main table describes the first set of tests. B/S6 therefore remains &lt;code&gt;partial&lt;/code&gt; there, and the added test's result is reported separately. This is not a limitation of Browser Mode: the browser provided the data for a hit test, but the original test did not request it. Checking a single point is also not equivalent to the provider's full set of actionability checks.&lt;/p&gt;

&lt;p&gt;In this scenario, the &lt;strong&gt;environment&lt;/strong&gt; provided geometry information, the &lt;strong&gt;action&lt;/strong&gt; determined how interaction happened, and the &lt;strong&gt;test oracle&lt;/strong&gt; defined what counted as a correct result. A status change was enough to check the handler, but not the promised pointer click.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to check your own test
&lt;/h2&gt;

&lt;p&gt;Look for a defect that would still leave your test green:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What the test already checks&lt;/th&gt;
&lt;th&gt;What it might miss&lt;/th&gt;
&lt;th&gt;What observation to add&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The dialog appeared&lt;/td&gt;
&lt;td&gt;Focus stayed on the opener&lt;/td&gt;
&lt;td&gt;Check &lt;code&gt;activeElement&lt;/code&gt; after opening and focus restoration after closing.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The component reacts to a supplied size&lt;/td&gt;
&lt;td&gt;The observer subscribed to a different container&lt;/td&gt;
&lt;td&gt;Actually resize the intended container and wait for the response.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The popover exists&lt;/td&gt;
&lt;td&gt;It did not switch sides or extends beyond the viewport&lt;/td&gt;
&lt;td&gt;Wait for layout to update, then check its position and boundaries.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The status changed after a synthetic click&lt;/td&gt;
&lt;td&gt;Another layer blocks the button&lt;/td&gt;
&lt;td&gt;Perform a provider action or an explicit browser hit test at the specified point.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For a form, programmatic focus, or a specific Tab order, first check whether your existing jsdom test has enough observations. In a keyboard scenario, perform the Tab sequence: calling &lt;code&gt;.focus()&lt;/code&gt; on the expected elements does not check the transitions.&lt;/p&gt;

&lt;p&gt;When a requirement depends on actual dimensions, position, or a browser notification, check those data in the browser. A mock can complement that check with a test of the logic. The test's name and description should reflect the part of the behavior it confirms.&lt;/p&gt;

&lt;p&gt;Check any chosen defect alongside the working version. If both fail, first investigate the test and its conditions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Experiment conditions
&lt;/h2&gt;

&lt;p&gt;The tests ran on Windows 11. B and C used headless Chromium with an 800 × 600 CSS px viewport and a scale factor of 1.&lt;/p&gt;

&lt;p&gt;We chose six defects ourselves and tested them in a small fixture. The results explain what these tests check. They do not tell us how often these errors occur in working applications or how many defects the same approach will find in another project. We did not run the tests on other operating systems or browser engines. Large test suites and E2E flows were not tested either.&lt;/p&gt;

&lt;p&gt;These materials cannot support a speed comparison: the old local runs performed different amounts of work. We did not measure memory, CI behavior, or migration costs. There are no conclusions about flakiness, either. One early cold run of S4/S5 timed out; we did not establish the cause.&lt;/p&gt;

&lt;p&gt;Before writing the article, we checked the saved results and logs. There was no independent repeat on a different machine with a clean installation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Versions and saved results
&lt;/h3&gt;

&lt;p&gt;The fixture used the following versions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Angular&lt;/td&gt;
&lt;td&gt;22.2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vitest&lt;/td&gt;
&lt;td&gt;5.0.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@vitest/browser-playwright&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5.0.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;jsdom&lt;/td&gt;
&lt;td&gt;30.1.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Playwright&lt;/td&gt;
&lt;td&gt;1.63.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@testing-library/user-event&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;14.6.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chromium&lt;/td&gt;
&lt;td&gt;153.0.8010.12, revision 1243&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node.js&lt;/td&gt;
&lt;td&gt;24.19.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Before the final series, we increased the condition wait to 5 seconds and the test timeout to 15 seconds.&lt;/p&gt;

&lt;p&gt;We retained the protocol, code, patches, lockfile, configuration, and run results. To verify the table, we re-examined the archive and checked 21 runs against their JSON reports: 3 runs of the working versions and 18 with mutations. The result was unchanged. For S6, we also examined the text logs: the provider reported that the layer intercepted the click. A test-failed flag alone would not have been enough to support that explanation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;In the original A/B tests for S6, the handler ran and the covered button passed the test. The overlap was detected when we checked whether the click was possible. Start there when choosing an environment: what must the user be able to do, and what observation in the test confirms it?&lt;/p&gt;

&lt;p&gt;Originally published on &lt;a href="https://el-pixel.com/angular-vitest-user-behavior/" rel="noopener noreferrer"&gt;El Pixel&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>vitest</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
