<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/pretty-feed-v3.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Martin Grandrath</title><description>Blog about programming, IT, and whatever else crosses my mind</description><link>https://www.martin-grandrath.de/</link><language>en</language><item><title>How to use Jest matchers with Vitest</title><link>https://www.martin-grandrath.de/blog/2024-07-07_how-to-use-jest-matchers-with-vitest.html</link><guid isPermaLink="true">https://www.martin-grandrath.de/blog/2024-07-07_how-to-use-jest-matchers-with-vitest.html</guid><description>Vitest can be used as a drop-in replacement for Jest. Using Jest matchers and assertions with Vitest takes just a little bit of setup configuration.</description><pubDate>Sun, 07 Jul 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A while ago I migrated a test suite from &lt;a href=&quot;https://jestjs.io/&quot;&gt;Jest&lt;/a&gt; to &lt;a href=&quot;https://vitest.dev/&quot;&gt;Vitest&lt;/a&gt;. Every thing went fine except that I needed to find out how we could continue to use &lt;a href=&quot;https://github.com/testing-library/jest-dom&quot;&gt;jest-dom&lt;/a&gt; with Vitest. Jest-dom is useful when writing tests for DOM elements with &lt;a href=&quot;https://testing-library.com/&quot;&gt;testing-library&lt;/a&gt;. It provides handy assertions like for example &lt;code&gt;expect(someButton).toBeDisabled()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Luckily Vitest is largely compatible with Jest and so Jest matchers can be used as is.&lt;/p&gt;
&lt;p&gt;First we need to create a setup file. I use the name &lt;code&gt;vitest-setup.mjs&lt;/code&gt; but it can be anything you like. Just make sure it matches the configuration below.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// vitest-setup.mjs

import { expect } from &quot;vitest&quot;;
import * as matchers from &quot;@testing-library/jest-dom/matchers&quot;;

expect.extend(matchers);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next we add the setup file to the configuration.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// vitest.config.mjs

import { defineConfig } from &quot;vitest/dist/config&quot;;

export default defineConfig({
  test: {
    setupFiles: [&quot;./vitest-setup.mjs&quot;],
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In case we are using TypeScript we need to add the additional matchers to Vitest&apos;s types.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// vitest-testing-library-matchers.d.ts

import &quot;vitest&quot;;
import type { TestingLibraryMatchers } from &quot;@testing-library/jest-dom/types/matchers&quot;;

declare module &quot;vitest&quot; {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  interface Assertion&amp;lt;T = any&amp;gt; extends TestingLibraryMatchers&amp;lt;T, void&amp;gt; {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ensure that TypeScript picks up this declaration file.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// tsconfig.json

{
  &quot;include&quot;: [&quot;*.d.ts&quot;, …],
  …
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we can write tests that make use of Jest-dom. 🎉&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// greeting.spec.tsx

import { describe, expect, it } from &quot;vitest&quot;;
import { render, screen } from &quot;@testing-library/react&quot;;
import { Greeting } from &quot;./greeting&quot;;

describe(&quot;&amp;lt;Greeting&amp;gt;&quot;, () =&amp;gt; {
  it(&quot;should say Hello&quot;, () =&amp;gt; {
    render(&amp;lt;Greeting name=&quot;Vitest&quot; /&amp;gt;);
    const heading = screen.getByRole(&quot;heading&quot;);
    expect(heading).toHaveTextContent(&quot;Hello, Vitest!&quot;);
  });
});
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>100% code coverage? Easy!</title><link>https://www.martin-grandrath.de/blog/2024-06-23_100-percent-code-coverage-easy.html</link><guid isPermaLink="true">https://www.martin-grandrath.de/blog/2024-06-23_100-percent-code-coverage-easy.html</guid><description>The code coverage metric can easily be manipulated without actually improving the test quality. Be mindful what it does and doesn&apos;t measure.</description><pubDate>Sun, 23 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&quot;Code coverage&quot; measures the amount of production code (usually as a percentage) that is executed when running the test suite. It is often used as a proxy metric for assessing the overall test quality of a code base. Code analysis tools like &lt;a href=&quot;https://de.wikipedia.org/wiki/SonarQube&quot;&gt;SonarQube&lt;/a&gt; are able to, for example, fail the build pipeline if the code coverage metric falls below a certain threshold.&lt;/p&gt;
&lt;p&gt;There is however a subtle shortcoming of this metric that too few people seem to be aware of. As I said it measures the amount of production code that is &lt;em&gt;executed&lt;/em&gt; when running the test suite. It does not take into account whether the test does anything meaningful with that.&lt;/p&gt;
&lt;p&gt;Let&apos;s create a small example to illustrate the issue. Take this straightforward &lt;a href=&quot;https://en.wikipedia.org/wiki/Fizz_buzz&quot;&gt;FizzBuzz&lt;/a&gt; implementation and an accompanying test that tests nothing:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// fizz-buzz.ts
export const fizzBuzz = (n: number): string =&amp;gt; {
  const fizz = n % 3 ? &quot;Fizz&quot; : &quot;&quot;;
  const buzz = n % 5 ? &quot;Buzz&quot; : &quot;&quot;;

  return fizz + buzz || String(n);
};

// fizz-buzz.test.ts
describe(&quot;fizzBuzz&quot;, () =&amp;gt; {
  it(&quot;implements Fizz Buzz&quot;, () =&amp;gt; {
    expect(1).toEqual(1);
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Running the test (e.g. running &lt;code&gt;vitest&lt;/code&gt; with the &lt;code&gt;--coverage&lt;/code&gt; flag) results in a code coverage of 0% – no surprise there. But what happens if we call the &lt;code&gt;fizzBuzz&lt;/code&gt; function inside our test?&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// fizz-buzz.test.ts
import { fizzBuzz } from &quot;./fizz-buzz.ts&quot;

describe(&quot;fizzBuzz&quot;, () =&amp;gt; {
  it(&quot;implements Fizz Buzz&quot;, () =&amp;gt; {
    // call to `fizzBuzz` without doing anything with its result
    fizzBuzz(1);

    expect(1).toEqual(1);
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we measure a code coverage of 100% of lines and 66.66% of branches:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;--------------|---------|----------|---------|---------|-------------------
File          | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
--------------|---------|----------|---------|---------|-------------------
All files     |     100 |    66.66 |     100 |     100 |                   
 fizz-buzz.ts |     100 |    66.66 |     100 |     100 | 2-3               
--------------|---------|----------|---------|---------|-------------------
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can take this further by adding more calls to our test that cover all the different cases:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// fizz-buzz.test.ts
import { fizzBuzz } from &quot;./fizz-buzz.ts&quot;

describe(&quot;fizzBuzz&quot;, () =&amp;gt; {
  it(&quot;implements Fizz Buzz&quot;, () =&amp;gt; {
    fizzBuzz(1);
    fizzBuzz(3);
    fizzBuzz(5);
    fizzBuzz(15);

    expect(1).toEqual(1);
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now our code coverage passes with flying colors:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;--------------|---------|----------|---------|---------|-------------------
File          | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
--------------|---------|----------|---------|---------|-------------------
All files     |     100 |      100 |     100 |     100 |                   
 fizz-buzz.ts |     100 |      100 |     100 |     100 |                   
--------------|---------|----------|---------|---------|-------------------
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The point I&apos;m trying to make is that you need to be mindful when it comes to code coverage as a quality metric. Be aware that a development team needs to care about code and test quality. Measuring code coverage and making it visible is a sensible thing to do but requiring a minimum coverage value might do more harm than good if a team has no stakes or interest in actual code quality.&lt;/p&gt;
</content:encoded></item><item><title>Testing touch gestures with Playwright</title><link>https://www.martin-grandrath.de/blog/2024-05-01_testing-touch-gestures-with-playwright.html</link><guid isPermaLink="true">https://www.martin-grandrath.de/blog/2024-05-01_testing-touch-gestures-with-playwright.html</guid><description>Recently I needed to find a way to implement an automated Playwright test that ensured that the pinch-to-zoom gesture worked on a page in mobile browsers. I was able to find a solution using the Chrome DevTools Protocol and the VisualViewport API.</description><pubDate>Wed, 01 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Recently I needed to find a way to implement an automated test that ensured that the pinch-to-zoom gesture (i.e. placing two fingers on the screen and moving them apart to zoom in) worked on a page in mobile browsers. &quot;But,&quot; I hear you object, &quot;why would you want to test pinch-to-zoom? It&apos;s a native browser feature. It doesn&apos;t make sense to test it.&quot; Yes, it is true that the zoom gesture is a browser feature. And it is also true that we don&apos;t need to test it&apos;s implementation. But it is also the case that our own page can contain code that disables the native behavior. In our particular case we were including a third party library that contained some CSS that disabled some gestures in order for that library to handle them itself. This piece of CSS sneaked in as part of an update and kept unnoticed for quite some time. When we realized that zooming in on the page did not work anymore we decided to look into creating an automated test to prevent this issue to come up again in the future.&lt;/p&gt;
&lt;h2&gt;Playwright and the Chrome DevTools Protocol&lt;/h2&gt;
&lt;p&gt;To simulate a pinch-to-zoom gesture we need to dispatch two &lt;code&gt;touchstart&lt;/code&gt; events (one for the thumb and one for the index finger) followed by &lt;code&gt;touchmove&lt;/code&gt; events that represent moving the fingers apart and finally two &lt;code&gt;touchend&lt;/code&gt; events for lifting the fingers up and thus ending the gesture. Unfortunately Playwright does not support these touch events directly (as of version 1.43.1). While the &lt;a href=&quot;https://playwright.dev/docs/api/class-mouse&quot;&gt;&lt;code&gt;Mouse&lt;/code&gt; class&lt;/a&gt; offers methods like &lt;code&gt;down()&lt;/code&gt;, &lt;code&gt;move()&lt;/code&gt; and &lt;code&gt;up()&lt;/code&gt; for clicking and dragging with a mouse, the &lt;a href=&quot;https://playwright.dev/docs/api/class-touchscreen&quot;&gt;&lt;code&gt;Touchscreen&lt;/code&gt; class&lt;/a&gt; only implements a &lt;code&gt;tap()&lt;/code&gt; method for simple one-finger taps on the screen.&lt;/p&gt;
&lt;p&gt;We found a workaround for this limitation though. Playwright provides the &lt;a href=&quot;https://playwright.dev/docs/api/class-cdpsession&quot;&gt;&lt;code&gt;CDPSession&lt;/code&gt; class&lt;/a&gt;. It enables Playwright tests to issue raw &lt;a href=&quot;https://chromedevtools.github.io/devtools-protocol/&quot;&gt;Chrome DevTools Protocol&lt;/a&gt; (CDP) commands. Among other things these commands include a variety of &lt;a href=&quot;https://chromedevtools.github.io/devtools-protocol/tot/Input/&quot;&gt;input events&lt;/a&gt;. It&apos;s important to point out that the Chrome DevTools Protocol is browser dependent and can not be used for testing Firefox. This was acceptable in our use case because we wanted to detect if some code (CSS or otherwise) broke the native zoom behavior. Of course we would not detect an issue with pinch-to-zoom that only affects non-Chrome browsers. While this is certainly not ideal it&apos;s better than nothing and it was able to detect the actual issue at hand.&lt;/p&gt;
&lt;p&gt;CDP provides a single command for triggering pinch-to-zoom gestures: &lt;a href=&quot;https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-synthesizePinchGesture&quot;&gt;&lt;code&gt;Input.synthesizePinchGesture&lt;/code&gt;&lt;/a&gt;. It accepts coordinates for the center of the gesture as well as a scale factor which determines whether the page should be zoomed in (i.e. moving the fingers apart) or zoomed out (i.e. moving the fingers towards each other). Unfortunately it is marked as experimental and we had issues with it in our CI/CD pipeline. Instead we simulated the pinch gesture using three &lt;a href=&quot;https://chromedevtools.github.io/devtools-protocol/1-3/Input/#method-dispatchTouchEvent&quot;&gt;&lt;code&gt;Input.dispatchTouchEvent&lt;/code&gt;&lt;/a&gt; commands: the first puts two fingers on the screen (&lt;code&gt;touchStart&lt;/code&gt;), the second moves them apart (&lt;code&gt;touchMove&lt;/code&gt;), and the third lifts them from the screen (&lt;code&gt;touchEnd&lt;/code&gt;).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// The point where the thumb and index finger start the gesture
const pinchStart = { x: 200, y: 200 };

// The point where the thumb ends the gesture
const thumbPinchEnd = { x: 100, y: 100 };

// The point where the index finger ends the gesture
const indexFingerPinchEnd = { x: 300, y: 300 };

const cdpSession = await page.context().newCDPSession(page);
await cdpSession.send(&quot;Input.dispatchTouchEvent&quot;, {
  type: &quot;touchStart&quot;,
  touchPoints: [pinchStart, pinchStart],
});
await cdpSession.send(&quot;Input.dispatchTouchEvent&quot;, {
  type: &quot;touchMove&quot;,
  touchPoints: [thumbPinchEnd, indexFingerPinchEnd],
});
await cdpSession.send(&quot;Input.dispatchTouchEvent&quot;, {
  type: &quot;touchEnd&quot;,
  touchPoints: [],
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Determining if the page is zoomed in&lt;/h2&gt;
&lt;p&gt;What has been sort of complicated in the past and required a fair bit of knowledge about browser geometry and things like &lt;a href=&quot;https://www.quirksmode.org/mobile/viewports.html&quot;&gt;the difference between the visual and the layout viewport&lt;/a&gt; has become rather straightforward with the introduction of the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/VisualViewport&quot;&gt;&lt;code&gt;VisualViewport&lt;/code&gt; API&lt;/a&gt;. It provides a &lt;code&gt;scale&lt;/code&gt; property that tells you the scale factor right away. &lt;code&gt;1&lt;/code&gt; if the page is not zoomed at all, a number between &lt;code&gt;0&lt;/code&gt; and &lt;code&gt;1&lt;/code&gt; if it is zoomed out and a number greater than &lt;code&gt;1&lt;/code&gt; if it is zoomed in.&lt;/p&gt;
&lt;p&gt;We can query this value from the browser by using Playwright&apos;s &lt;code&gt;evaluate&lt;/code&gt; method:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const scaleFactor = await page.evaluate(() =&amp;gt; {
  return visualViewport?.scale;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We use the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining&quot;&gt;optional chaining operator&lt;/a&gt; &lt;code&gt;?&lt;/code&gt; because the &lt;code&gt;visualViewport&lt;/code&gt; object can be &lt;code&gt;null&lt;/code&gt; (in case the document is not fully active) and otherwise a type aware IDE will complain. Because Playwright waits for the page to be active when we use &lt;code&gt;await page.goto(…)&lt;/code&gt; we don&apos;t need to worry about that. If you use TypeScript you can alternatively use the &lt;a href=&quot;https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#non-null-assertion-operator-postfix-&quot;&gt;non-null assertion operator&lt;/a&gt; &lt;code&gt;!&lt;/code&gt; instead.&lt;/p&gt;
&lt;p&gt;The final test:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { test, expect } from &quot;@playwright/test&quot;;

const pinchToZoomGesture = async (page) =&amp;gt; {
  const pinchStart = { x: 200, y: 200 };
  const thumbPinchEnd = { x: 100, y: 100 };
  const indexFingerPinchEnd = { x: 300, y: 300 };

  const cdpSession = await page.context().newCDPSession(page);
  await cdpSession.send(&quot;Input.dispatchTouchEvent&quot;, {
    type: &quot;touchStart&quot;,
    touchPoints: [pinchStart, pinchStart],
  });
  await cdpSession.send(&quot;Input.dispatchTouchEvent&quot;, {
    type: &quot;touchMove&quot;,
    touchPoints: [thumbPinchEnd, indexFingerPinchEnd],
  });
  await cdpSession.send(&quot;Input.dispatchTouchEvent&quot;, {
    type: &quot;touchEnd&quot;,
    touchPoints: [],
  });
};

test(&quot;page supports pinch-to-zoom gesture&quot;, async ({ page, browserName }) =&amp;gt; {
  // We need to skip this test on non-Chromium browsers because the Chrome
  // DevTools Protocol (CDP) that we use to trigger touch events is only
  // available in Chromium browsers.
  test.skip(browserName !== &quot;chromium&quot;);

  // Replace `playwright.dev` with the actual page under test.
  await page.goto(&quot;https://playwright.dev/&quot;);
  await pinchToZoomGesture(page);

  const scaleFactor = await page.evaluate(() =&amp;gt; {
    return visualViewport?.scale;
  });

  expect(scaleFactor).toBeGreaterThan(1);
});
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item></channel></rss>