<?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: Denis Skvortsov</title>
    <description>The latest articles on DEV Community by Denis Skvortsov (@denis_skvortsov).</description>
    <link>https://dev.to/denis_skvortsov</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3079093%2F4fd4f610-1231-4ddd-9960-c1c424b41151.jpg</url>
      <title>DEV Community: Denis Skvortsov</title>
      <link>https://dev.to/denis_skvortsov</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/denis_skvortsov"/>
    <language>en</language>
    <item>
      <title>Selective test execution mechanism with Playwright using GitHub Actions</title>
      <dc:creator>Denis Skvortsov</dc:creator>
      <pubDate>Sat, 24 May 2025 09:49:04 +0000</pubDate>
      <link>https://dev.to/denis_skvortsov/selective-test-execution-mechanism-with-playwright-using-github-actions-862</link>
      <guid>https://dev.to/denis_skvortsov/selective-test-execution-mechanism-with-playwright-using-github-actions-862</guid>
      <description>&lt;h2&gt;
  
  
  &lt;strong&gt;TL;DR&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Selective test execution:&lt;/strong&gt; run only the tests related to the actual code changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Saves time and resources:&lt;/strong&gt; speeds up the process and reduces CI/CD load, especially in cloud environments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Works for both monorepos and split repositories:&lt;/strong&gt; the solution fits projects with either a monorepo or separate frontend/backend repositories.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Uses GitHub Actions and Playwright:&lt;/strong&gt; configures CI/CD to filter tests by tags and run only the relevant ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example implementation:&lt;/strong&gt; available in a public GitHub &lt;a href="https://github.com/skvortsov-den/playwright-selective-tests-github-actions" rel="noopener noreferrer"&gt;repository&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;When I first faced the task of setting up testing, I realized that running the entire test suite after every code change is not just slow - it's wasteful. Especially in large projects where microservices, frontend, and backend each have their own test sets. In such setups, running all tests for every commit or PR is unnecessary and significantly slows down the development cycle.&lt;/p&gt;

&lt;p&gt;For example, if a frontend developer changes a button or a small UI component, why would we run all backend tests? Or if a backend developer updates a single endpoint, there’s no point in triggering all UI tests - they’re completely unrelated. In those cases, tests become unnecessary overhead that slows down progress.&lt;/p&gt;

&lt;p&gt;In some teams, test success is a hard requirement before merging code. If you can’t merge until all tests pass - but your change affects only a small part of the system - triggering all tests becomes a bottleneck.&lt;/p&gt;

&lt;p&gt;To solve this, I implemented selective test execution - running only the tests that are actually affected by code changes. This approach helps save both time and infrastructure resources, making the testing and release process faster. In this article, I’ll share my experience and show how to set up such a mechanism using Playwright and GitHub Actions - whether you’re working in a monorepo or with separate frontend/backend repositories.&lt;/p&gt;

&lt;h2&gt;
  
  
  What problem are we solving?
&lt;/h2&gt;

&lt;p&gt;Running the full e2e test suite on every code change is not always justified. In projects that include frontend, backend and shared modules, this often leads to problems like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Developers waiting for CI feedback longer than it takes to write the fix itself&lt;/li&gt;
&lt;li&gt;CI infrastructure usage skyrockets (and if you’re in the cloud - so do the costs)&lt;/li&gt;
&lt;li&gt;Tests are triggered that have nothing to do with the actual changes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Typical scenarios:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A frontend developer changes a visual component, but all backend test chains are executed&lt;/li&gt;
&lt;li&gt;A backend developer tweaks business logic, and the entire UI test suite runs too&lt;/li&gt;
&lt;li&gt;A shared utility is updated, and suddenly no one knows whether to run a full regression or not&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It gets even worse when tests are a mandatory check before merging. Even a minor change can block the entire process - while dozens or even hundreds of unrelated tests are waiting to complete.&lt;/p&gt;

&lt;p&gt;My goal wasn’t just to speed up testing. I wanted CI to run only what truly matters. Automatically. Without manual rules or exception lists.&lt;/p&gt;

&lt;h2&gt;
  
  
  How i implemented it
&lt;/h2&gt;

&lt;p&gt;To demonstrate how you can run only the relevant e2e tests, I put together a small monorepository with a simple but flexible architecture. This isn’t a production-ready setup - it’s a demo project, intentionally simplified to make the mechanism easy to understand and adapt to any real-world structure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My main goals were:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Everything should work transparently - no magic config files&lt;/li&gt;
&lt;li&gt;The implementation should be reusable - something you can easily apply to another project&lt;/li&gt;
&lt;li&gt;The structure should remain flexible - easy to extend with new services without rewriting the pipeline&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Project structure:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;├── frontend/                    # Frontend applications
│   └── apps/                    # Frontend microservices
│       ├── microservice1/
│       └── microservice2/
│       └── shared/             # Common frontend components

├── backend/                     # Backend services
│   └── apps/                    # Backend microservices
│       ├── microservice3/
│       ├── microservice4/
│       └── microservice5/

├── .github/                     # GitHub Actions
│   ├── workflows/               # CI/CD configuration
│   │   └── e2e-runner.yml       # Selective test runner
│   └── preconditions/           # Reusable actions
│       └── e2e/                 # E2E tests environment setup
│           └── action.yml       # Composite action for environment setup

└── tests/                       # Tests
    └── e2e/                     # E2E tests 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything is split into logical areas - just like in real projects. There's frontend, backend, a shared area and a dedicated folder for e2e tests. The entire pipeline runs on GitHub Actions. Dependencies are installed using a reusable &lt;code&gt;precondition&lt;/code&gt; action. The core logic is tag-based.&lt;/p&gt;

&lt;p&gt;The tests themselves are intentionally primitive - because this article isn’t about test coverage or scenarios, it’s about the mechanism for &lt;strong&gt;running only what matters&lt;/strong&gt;. Everything else is just context.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Tagging Tests
&lt;/h3&gt;

&lt;p&gt;To determine which tests should run, I use tags directly in the test definitions. The logic is simple: if a PR changes &lt;code&gt;apps/microservice3&lt;/code&gt;, the CI system looks for e2e tests tagged with &lt;code&gt;@apps/microservice3&lt;/code&gt; and runs only those.&lt;/p&gt;

&lt;p&gt;Each tag is tied to a specific microservice or module. For example:&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;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Test 3&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;tag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@apps/microservice3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;ui&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;await&lt;/span&gt; &lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&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;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;openAppsMenu&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;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertServiceVisible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Maps&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;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertServiceVisible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Gmail&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;microservice3&lt;/code&gt; is affected, this test will be included. If another service is changed, it will be skipped-even if it's in the same file.&lt;/p&gt;

&lt;p&gt;I chose the format &lt;code&gt;@apps/&amp;lt;service-name&amp;gt;&lt;/code&gt; for two reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It matches the actual project structure.&lt;/li&gt;
&lt;li&gt;It’s easy to extract from file paths using &lt;code&gt;grep&lt;/code&gt; and &lt;code&gt;sed&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Another example:&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;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Test 1&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;tag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@apps/microservice1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;ui&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;await&lt;/span&gt; &lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&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;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;openAppsMenu&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;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertServiceVisible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;YouTube&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;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertServiceVisible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;YouTube Music&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a test is &lt;strong&gt;not tagged&lt;/strong&gt;, it won’t be picked up during selective test runs. For example:&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;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Test 5&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;ui&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;await&lt;/span&gt; &lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&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;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;openAppsMenu&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;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertServiceVisible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Calendar&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This test will only run during a full test execution (for example, when &lt;code&gt;shared&lt;/code&gt; or &lt;code&gt;tests/e2e&lt;/code&gt; changes are detected).&lt;/p&gt;

&lt;p&gt;Tagging is a core part of this mechanism. It doesn’t require additional tooling and can be maintained manually if needed.&lt;/p&gt;

&lt;p&gt;Playwright docs on tags: &lt;a href="https://playwright.dev/docs/test-annotations" rel="noopener noreferrer"&gt;Test annotations&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Analyzing сhanges and mapping to tags
&lt;/h3&gt;

&lt;p&gt;The next step is to determine what exactly has changed in the PR and which tests should be triggered. This is done in GitHub Actions using a simple &lt;code&gt;git diff&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What we look for:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Changes in paths like &lt;code&gt;apps/microserviceX&lt;/code&gt; → means a specific service was affected&lt;/li&gt;
&lt;li&gt;Changes in &lt;code&gt;shared&lt;/code&gt; → means potentially all services are affected&lt;/li&gt;
&lt;li&gt;Changes in &lt;code&gt;tests/e2e&lt;/code&gt; → likely the tests themselves were modified, so the full suite should be executed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s a simplified example from the &lt;code&gt;Find changes&lt;/code&gt; step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;changed_apps&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git diff &lt;span class="nt"&gt;--name-only&lt;/span&gt; origin/main HEAD | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"/apps/[^/]+/"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;changed_test_files&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git diff &lt;span class="nt"&gt;--name-only&lt;/span&gt; origin/main HEAD | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"^tests/e2e/"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we normalize the paths. For example, &lt;code&gt;frontend/apps/microservice1/pages/page.tsx&lt;/code&gt; becomes &lt;code&gt;apps/microservice1&lt;/code&gt;, which maps directly to the tag &lt;code&gt;@apps/microservice1&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;full_paths&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$changed_apps&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'s#^(.*/apps/[^/]+)/.*#\1#'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; | &lt;span class="nb"&gt;paste&lt;/span&gt; &lt;span class="nt"&gt;-sd&lt;/span&gt; &lt;span class="s2"&gt;"|"&lt;/span&gt; -&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;test_paths&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$full_paths&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;'|'&lt;/span&gt; &lt;span class="s1"&gt;'\n'&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'s#.*(apps/[^/]+)#\1#'&lt;/span&gt; | &lt;span class="nb"&gt;paste&lt;/span&gt; &lt;span class="nt"&gt;-sd&lt;/span&gt; &lt;span class="s2"&gt;"|"&lt;/span&gt; -&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Then what happens:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the changes include &lt;code&gt;shared&lt;/code&gt; or &lt;code&gt;tests/e2e&lt;/code&gt;, we skip filtering and run &lt;strong&gt;all tests&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;If only specific services were changed, we convert the paths into tags, which are passed to Playwright via &lt;code&gt;--grep&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result is saved into a GitHub Actions output variable called &lt;code&gt;test_scope&lt;/code&gt;, which is used in later steps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"test_scope=&lt;/span&gt;&lt;span class="nv"&gt;$test_paths&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$GITHUB_OUTPUT&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;test_scope&lt;/code&gt; ends up empty, it means either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No relevant changes were found&lt;/li&gt;
&lt;li&gt;Or no tests are tagged to match the changes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In such cases, you can either skip test execution or fall back to running the full suite - depending on your project’s policy.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Running tests with &lt;code&gt;grep&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Once we’ve determined which parts of the code have changed and built a list of tags, the next step is simply passing those tags to Playwright. For that, we use the built-in &lt;code&gt;--grep&lt;/code&gt; flag, which filters the tests by tags.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example run:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the PR affects both &lt;code&gt;apps/microservice1&lt;/code&gt; and &lt;code&gt;apps/microservice4&lt;/code&gt;, the resulting scope looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;test_scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;apps/microservice1|apps/microservice4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the tests are run like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx playwright &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--grep&lt;/span&gt; &lt;span class="s2"&gt;"@apps/microservice1|@apps/microservice4"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;|| true&lt;/code&gt;?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Because &lt;code&gt;grep&lt;/code&gt; might return no matches - for example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The service is new and doesn’t have tests yet&lt;/li&gt;
&lt;li&gt;The change is minor and not yet covered&lt;/li&gt;
&lt;li&gt;Tests exist but lack proper tags&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In these cases, we don’t want the CI to fail. It’s okay to skip test execution if no relevant tests were found. Failure should only happen when tests exist and they fail - not when there are simply no tests to run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When do we run all tests?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the changes include any of the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A shared directory (frontend or backend)&lt;/li&gt;
&lt;li&gt;Files in &lt;code&gt;tests/e2e&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Or if &lt;code&gt;test_scope&lt;/code&gt; is completely empty (depending on your policy)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We simply skip filtering and run the full suite:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;needs.detect-changes.outputs.test_scope == ''&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx playwright &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All of this is wrapped inside a pipeline with two jobs: &lt;code&gt;selective-tests&lt;/code&gt; and &lt;code&gt;all-tests&lt;/code&gt;. Based on the content of &lt;code&gt;test_scope&lt;/code&gt;, only one of them runs.&lt;/p&gt;

&lt;h3&gt;
  
  
  What we get in the end
&lt;/h3&gt;

&lt;p&gt;After implementing selective test execution on GitHub Actions, the benefits were immediately obvious - both in terms of human effort and infrastructure usage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Faster builds&lt;/strong&gt;&lt;br&gt;
CI no longer runs the entire test suite for every little change. If only one microservice is affected, only its e2e tests are triggered. As a result:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tests run significantly faster&lt;/li&gt;
&lt;li&gt;You get feedback almost immediately&lt;/li&gt;
&lt;li&gt;Releases are no longer blocked by irrelevant checks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Resource savings&lt;/strong&gt;&lt;br&gt;
If you're running tests in the cloud, CI/CD can get expensive - especially when all tests are triggered for every pull request. Selective execution helps save actual money by avoiding unnecessary resource usage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Easy to maintain&lt;/strong&gt;&lt;br&gt;
You don’t need to manually build complex test mappings. The entire system is driven by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A simple tag in each test (&lt;code&gt;@apps/xxx&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;git diff&lt;/code&gt; and &lt;code&gt;bash&lt;/code&gt; (2 lines of code)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;grep&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The approach is flexible and adaptable to any structure. You can filter by modules, features, user flows, folders, roles - whatever makes sense in your project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Works out of the box&lt;/strong&gt;&lt;br&gt;
This mechanism performs well in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Monorepos&lt;/li&gt;
&lt;li&gt;Projects with split frontend and backend&lt;/li&gt;
&lt;li&gt;Any project where e2e tests are a mandatory merge requirement&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Want real-world scenarios?&lt;/strong&gt;&lt;br&gt;
You can explore common situations - shared modules, new services, test changes, combinations - in the &lt;a href="https://github.com/skvortsov-den/playwright-selective-tests-github-actions/blob/main/README.md" rel="noopener noreferrer"&gt;project’s README&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Example GitHub Actions workflow
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;E2E Tests&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;main&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;detect-changes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;outputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test_scope&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.scope.outputs.test_scope }}&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Find changes&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;changes&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;echo "🔄  Analyzing changes in PR..."&lt;/span&gt;
          &lt;span class="s"&gt;changed_apps=$(git diff --name-only origin/main HEAD | grep -E "/apps/[^/]+/" || true)&lt;/span&gt;
          &lt;span class="s"&gt;changed_test_files=$(git diff --name-only origin/main HEAD | grep -E "^tests/e2e/" || true)&lt;/span&gt;

          &lt;span class="s"&gt;echo "📦 Files changed:"&lt;/span&gt;
          &lt;span class="s"&gt;[ -z "$changed_apps" ] &amp;amp;&amp;amp; echo "  No changes in files" || echo "$changed_apps" | sed 's/^/  /'&lt;/span&gt;

          &lt;span class="s"&gt;echo "🧪 E2E tests:"&lt;/span&gt;
          &lt;span class="s"&gt;[ -z "$changed_test_files" ] &amp;amp;&amp;amp; echo "  No changes in e2e tests" || echo "$changed_test_files" | sed 's/^/  /'&lt;/span&gt;

          &lt;span class="s"&gt;echo "✨ Affected services:"&lt;/span&gt;
          &lt;span class="s"&gt;full_paths=$(echo "$changed_apps" | sed -E 's#^(.*/apps/[^/]+)/.*#\1#' | sort -u | paste -sd "|" -)&lt;/span&gt;
          &lt;span class="s"&gt;[ -z "$full_paths" ] &amp;amp;&amp;amp; echo " No changes in services" || echo "$full_paths" | tr '|' '\n' | sed 's/^/  /'&lt;/span&gt;

          &lt;span class="s"&gt;test_paths=$(echo "$full_paths" | tr '|' '\n' | sed -E 's#.*(apps/[^/]+)#\1#' | paste -sd "|" -)&lt;/span&gt;
          &lt;span class="s"&gt;test_files=$(echo "$changed_test_files" | paste -sd "|" -)&lt;/span&gt;

          &lt;span class="s"&gt;echo "test_paths=${test_paths}" &amp;gt;&amp;gt; $GITHUB_OUTPUT&lt;/span&gt;
          &lt;span class="s"&gt;echo "changed_test=${test_files}" &amp;gt;&amp;gt; $GITHUB_OUTPUT&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check shared modules and modified e2e tests&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shared&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;test_paths="${{ steps.changes.outputs.test_paths }}"&lt;/span&gt;
          &lt;span class="s"&gt;changed_test="${{ steps.changes.outputs.changed_test }}"&lt;/span&gt;

          &lt;span class="s"&gt;echo "🔄 Checking shared modules and e2e tests:"&lt;/span&gt;

          &lt;span class="s"&gt;has_shared=$(echo "$test_paths" | tr '|' '\n' | grep -q "shared" &amp;amp;&amp;amp; echo "true" || echo "false")&lt;/span&gt;
          &lt;span class="s"&gt;has_e2e_changes=$([ ! -z "$changed_test" ] &amp;amp;&amp;amp; echo "true" || echo "false")&lt;/span&gt;

          &lt;span class="s"&gt;$has_shared &amp;amp;&amp;amp; echo "⚠️ Changes in shared modules detected" &amp;amp;&amp;amp; echo "$test_paths" | tr '|' '\n' | grep "shared" | sed 's/^/  /'&lt;/span&gt;
          &lt;span class="s"&gt;$has_e2e_changes &amp;amp;&amp;amp; echo "⚠️ Changes in e2e tests detected" &amp;amp;&amp;amp; echo "$changed_test" | tr '|' '\n' | sed 's/^/  /'&lt;/span&gt;

          &lt;span class="s"&gt;{ $has_shared || $has_e2e_changes; } &amp;amp;&amp;amp; test_paths="" || echo "✅ No changes in shared modules or e2e tests"&lt;/span&gt;

          &lt;span class="s"&gt;echo "test_paths=$test_paths" &amp;gt;&amp;gt; $GITHUB_OUTPUT&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set final scope&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;scope&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;test_paths="${{ steps.shared.outputs.test_paths }}"&lt;/span&gt;
          &lt;span class="s"&gt;echo "🔄 Result:"&lt;/span&gt;
          &lt;span class="s"&gt;[ -z "$test_paths" ] &amp;amp;&amp;amp; echo " ✅ All tests will be run" || {&lt;/span&gt;
            &lt;span class="s"&gt;echo " ✅ Running tests for:"&lt;/span&gt;
            &lt;span class="s"&gt;echo "$test_paths" | tr '|' '\n' | sed 's/^/    /'&lt;/span&gt;
          &lt;span class="s"&gt;}&lt;/span&gt;
          &lt;span class="s"&gt;echo "test_scope=$test_paths" &amp;gt;&amp;gt; $GITHUB_OUTPUT&lt;/span&gt;

  &lt;span class="na"&gt;selective-tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;detect-changes&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;needs.detect-changes.outputs.test_scope != ''&lt;/span&gt;
    &lt;span class="na"&gt;timeout-minutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;container&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mcr.microsoft.com/playwright:v1.52.0&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./.github/preconditions/e2e&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run selective tests&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx playwright test --grep "${{ needs.detect-changes.outputs.test_scope }}" || &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="na"&gt;all-tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;detect-changes&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;needs.detect-changes.outputs.test_scope == ''&lt;/span&gt;
    &lt;span class="na"&gt;timeout-minutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;container&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mcr.microsoft.com/playwright:v1.52.0&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./.github/preconditions/e2e&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run all tests&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx playwright test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Can you run only the tests that were changed?
&lt;/h3&gt;

&lt;p&gt;This is a common question that usually comes up first:&lt;br&gt;
&lt;strong&gt;"If we already know which files have changed - why not run only the tests that were also modified?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At first glance, it sounds reasonable. But in practice it’s not always a good idea.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why I didn’t go down that path
&lt;/h3&gt;

&lt;p&gt;In my setup, I run &lt;strong&gt;all tests related to the changed components&lt;/strong&gt;, not just the &lt;code&gt;.spec.ts&lt;/code&gt; files that were edited in the PR. The reason is simple - in some projects, tests are not properly isolated.&lt;/p&gt;

&lt;p&gt;A typical example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one test modifies some data,&lt;/li&gt;
&lt;li&gt;another test relies on the state left behind by the first one,&lt;/li&gt;
&lt;li&gt;or there’s a shared &lt;code&gt;setUp&lt;/code&gt; that affects the behavior of all tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is an anti-pattern, of course, but it's still common - especially in older or fast-growing projects.&lt;/p&gt;

&lt;p&gt;If you run only the modified test files, you can easily end up with a green build - even though the logic is actually broken.&lt;br&gt;
That’s why I went with a safer approach:&lt;br&gt;
&lt;strong&gt;we filter by the affected areas, but within that area, we run all the tests - even if the test files themselves weren't changed.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What about &lt;code&gt;--only-changed&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;Playwright does support a &lt;code&gt;--only-changed&lt;/code&gt; flag that runs only the &lt;code&gt;.spec.ts&lt;/code&gt; files which were changed.&lt;br&gt;
It can be helpful as a temporary solution or for small PRs.&lt;/p&gt;

&lt;p&gt;But it’s important to understand: this flag only works at the file level.&lt;br&gt;
It doesn’t track which modules or helpers were changed, nor does it understand which tests depend on them.&lt;/p&gt;

&lt;p&gt;So if you modify something like &lt;code&gt;auth.ts&lt;/code&gt;, which is used across all tests - &lt;code&gt;--only-changed&lt;/code&gt; won’t pick that up, because the test files themselves didn’t change.&lt;/p&gt;

&lt;h3&gt;
  
  
  What you can do if you want to take it further
&lt;/h3&gt;

&lt;p&gt;If your tests are well-isolated and your project architecture is clean, it’s possible to run only those tests that are truly affected by a change.&lt;/p&gt;

&lt;p&gt;At the &lt;code&gt;git diff&lt;/code&gt; stage, you can track not only changes in app code, but also updates to shared modules or e2e utilities. To identify which tests depend on those changes, you can build a &lt;strong&gt;dependency graph&lt;/strong&gt; between source files and test files - using tools like &lt;code&gt;ts-morph&lt;/code&gt; or &lt;code&gt;dependency-cruiser&lt;/code&gt;. This graph reveals which &lt;code&gt;.spec.ts&lt;/code&gt; files import or transitively rely on the modified code.&lt;/p&gt;

&lt;p&gt;This approach works best when your test structure is modular and the dependency graph is accurate and regularly maintained. Without that, the risk of silently skipping important tests increases.&lt;/p&gt;

&lt;p&gt;That’s why in many cases, it's safer and more predictable to run all tests within the affected scope - even if only a small change was made.&lt;br&gt;
It keeps things simple and reduces the chance of hidden regressions in CI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;Selective execution of e2e tests isn’t a silver bullet - but it’s an effective way to reduce build time, lower CI load, and speed up the development cycle. It’s especially valuable in projects where tests are a required condition for merging, and every minute of CI time matters.&lt;/p&gt;

&lt;p&gt;All you need is to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;tag your tests appropriately&lt;/li&gt;
&lt;li&gt;analyze changes in the PR&lt;/li&gt;
&lt;li&gt;and run tests using the &lt;code&gt;--grep&lt;/code&gt; filter&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The solution is simple, flexible and reusable. You can apply it in monorepos, in projects with separate frontend and backend repos or even as a shared internal standard within your team.&lt;/p&gt;

&lt;p&gt;If you want to take this further - like running only the truly modified tests - that’s also possible if architecture makes it achievable.&lt;/p&gt;

&lt;p&gt;If you’ve solved a similar problem differently - I’d love to hear how you approached it. Feel free to share your solutions.&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>playwright</category>
      <category>automation</category>
      <category>testing</category>
    </item>
    <item>
      <title>Practical use of Cursor and MCP Playwright in test automation</title>
      <dc:creator>Denis Skvortsov</dc:creator>
      <pubDate>Wed, 23 Apr 2025 11:12:02 +0000</pubDate>
      <link>https://dev.to/denis_skvortsov/practical-use-of-cursor-and-mcp-playwright-in-test-automation-ac4</link>
      <guid>https://dev.to/denis_skvortsov/practical-use-of-cursor-and-mcp-playwright-in-test-automation-ac4</guid>
      <description>&lt;p&gt;&lt;strong&gt;Introduction&lt;/strong&gt;&lt;br&gt;
This article is not a documentation review. It's my personal experience working with the Cursor and Playwright MCP tools for frontend test automation (JavaScript/TypeScript). I want to share how these tools truly help in everyday work, especially when it comes to writing or improving automated tests.&lt;br&gt;
The topic of automation is actively developing, and, perhaps in the near future, we will be able to automate tests almost by sheer willpower. But for now, I'll share what I've already found. Many developers are actively using AI for code generation, such as GitHub Copilot, but for me, Cursor in combination with Playwright MCP provides much more context. While these are far from perfect tools-sometimes they fix unnecessary places or don't work exactly as expected-they are still a powerful addition to the process.&lt;br&gt;
In this article, I will show how to use Cursor and Playwright MCP in practice, with test examples and explanations of where and how these tools help, as well as situations where they are better avoided.&lt;br&gt;
Here are the key points I would like to highlight in this article that may be useful when working with Cursor and MCP Playwright:&lt;br&gt;
Cursor can automatically add data-testid or getByRole to the code using a screenshot of the screen and highlighting the desired area where it should be inserted. This is especially useful for beginner automation engineers.&lt;br&gt;
MCP Playwright is useful for labeled pages, where elements have accessible attributes (e.g., data-testid, aria-label, etc.).&lt;br&gt;
MCP Playwright is ideal for writing simple scripts that do not require complex preconditions and works well for basic E2E testing.&lt;br&gt;
If you're using Cursor and MCP Playwright together, the best solution is to organize a monorepository, where both the frontend and automated tests are located in one repository.&lt;/p&gt;

&lt;p&gt;Additionally, it's important to remember that while these tools speed up the process, they do not replace engineering work, especially for more complex scenarios and integration tests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test Architecture: Basic principles&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before implementing MCP Playwright, it's important to revisit your testing pyramid and define what e2e tests mean in your context. The testing pyramid can vary for different teams and projects. E2E and integration tests can overlap, but MCP Playwright is useful only for simple scenarios.&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.amazonaws.com%2Fuploads%2Farticles%2Fbp2p23djoevsdo2eeweq.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.amazonaws.com%2Fuploads%2Farticles%2Fbp2p23djoevsdo2eeweq.png" alt="Image description" width="800" height="801"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;My tests and examples will be based on the website google.com, and I will use TypeScript for writing the tests. Google provides good accessibility for element recognition through MCP Playwright, which allows easy creation of stable tests for basic operations, such as searching on the page. I will also use the test architecture from the article &lt;a href="https://www.linkedin.com/pulse/simple-effective-e2e-test-architecture-playwright-denis-skvortsov-hv5pf" rel="noopener noreferrer"&gt;Simple and Effective E2E Test Architecture for Playwright&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How MCP Playwright and Cursor fit different types of tests:&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;E2E Tests:&lt;/em&gt; MCP Playwright is ideal for simple e2e tests where you need to check UI interactions, especially if the page is properly labeled. For example, checking a button or an input field. Here, MCP automatically generates code with the correct locators, making test writing easier. However, it's important to remember that MCP Playwright works best with simple scenarios.&lt;br&gt;
&lt;em&gt;Integration Tests:&lt;/em&gt; For integration tests where you need to interact with both the UI and the API, traditional automation methods are better. Cursor can help with generating templates and structuring mocks, but the logic of interacting between multiple modules and handling errors requires an engineering approach.&lt;br&gt;
&lt;em&gt;Unit Tests:&lt;/em&gt; Unit tests remain the responsibility of developers, but for automation engineers, Cursor can be a useful tool for speeding up test creation, especially for typical cases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example of using MCP Playwright in e2e tests&lt;/strong&gt;&lt;br&gt;
Open Google&lt;br&gt;
Search for "Playwright MCP"&lt;br&gt;
Verify that the search results contain the text "Model Context Protocol"&lt;br&gt;
Open Google again&lt;br&gt;
Search for "Playwright automation"&lt;br&gt;
Verify that the search results contain the word "testing"&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.amazonaws.com%2Fuploads%2Farticles%2Fic08fffk9ceddqnx44xy.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.amazonaws.com%2Fuploads%2Farticles%2Fic08fffk9ceddqnx44xy.png" alt="Image description" width="800" height="346"&gt;&lt;/a&gt;&lt;br&gt;
This test interacts solely with the UI. For such simple scenarios, MCP Playwright will generate the necessary code, ensuring stable and fast test execution.&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.amazonaws.com%2Fuploads%2Farticles%2Fd5qdcnb6gnvxny13uapy.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.amazonaws.com%2Fuploads%2Farticles%2Fd5qdcnb6gnvxny13uapy.png" alt="Image description" width="800" height="1064"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Integration Tests: An AQA Engineer's perspective&lt;/strong&gt;&lt;br&gt;
When it comes to more complex integration tests, Cursor can provide a test template and a structure for mocks, but for complex logic, such as error handling or interaction between multiple modules, you need to apply engineering thinking.&lt;br&gt;
&lt;strong&gt;Example&lt;/strong&gt;&lt;br&gt;
Cursor generated a test for a successful scenario, but it didn't account for all the negative cases related to errors. I had to refine it manually, adding error handling:&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.amazonaws.com%2Fuploads%2Farticles%2F4rqmn9bdra68b16ck8an.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.amazonaws.com%2Fuploads%2Farticles%2F4rqmn9bdra68b16ck8an.png" alt="Image description" width="800" height="100"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is an example of how Cursor helps with templates, but you still need to tweak it for more complex scenarios. Also, Cursor tends to overcomplicate solutions, so it's important to monitor what it generates and double-check its work.&lt;br&gt;
Cursor works well in monorepositories and can reuse existing UI and API methods. To limit the complexity of the generated code, you can use Cursor rules. &lt;a href="https://docs.cursor.com/context/rules" rel="noopener noreferrer"&gt;Cursor Rules Documentation&lt;/a&gt;.&lt;br&gt;
Of course, this is not a solution to all problems, but it can significantly simplify your work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unit tests: How Cursor helps speed up test creation&lt;/strong&gt;&lt;br&gt;
Unit tests are primarily the responsibility of developers, but as an automation engineer, you can also get involved, especially if you actively participate in writing code and understand its logic. Cursor helps generate tests quickly, saving time, especially for common cases.&lt;br&gt;
&lt;strong&gt;Example from Practice&lt;/strong&gt;&lt;br&gt;
With Cursor, I generated several tests for functions like formatCurrency and parseQuery. Here's an example of how it might look:&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.amazonaws.com%2Fuploads%2Farticles%2F6o90occrh7rfnrxcsnc9.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.amazonaws.com%2Fuploads%2Farticles%2F6o90occrh7rfnrxcsnc9.png" alt="Image description" width="800" height="146"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Cursor suggested edge cases like null, NaN, and empty strings, which are often forgotten in tests. This saves time, eliminates human errors, and helps make tests more comprehensive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to use Cursor to add data-testid or modify buttons on the frontend&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Cursor has a great feature for adding a screenshot of the page to a request, which is especially useful if you have a monorepository for both frontend and e2e tests. This is a great opportunity to immediately fix a component and label it in an accessible way while writing a script. Of course after making changes, you should verify the display, but this significantly simplifies the process for beginner automation engineers or those just getting familiar with the frontend.&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.amazonaws.com%2Fuploads%2Farticles%2Fn4spneuc2b6jgqseif6n.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.amazonaws.com%2Fuploads%2Farticles%2Fn4spneuc2b6jgqseif6n.png" alt="Image description" width="742" height="244"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monorepositories - A great solution for these tools&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Although I haven't had the opportunity to work with these tools in a single monorepository where both frontend and backend are located, in practice, when all the automated tests are in the same repository as the frontend, this solution has proven to be excellent. Firstly, you can immediately label the necessary elements on the frontend, and with Cursor, you can quickly pull the required API request or examine how the frontend is structured overall.&lt;br&gt;
Using a monorepository in combination with these tools gives you flexibility and improves collaboration between teams, especially in terms of writing and maintaining tests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When to use MCP Playwright sensibly&lt;/strong&gt;&lt;br&gt;
MCP Playwright is really good for testing interfaces when the page is properly labeled, and the scenarios are simple. It speeds up testing and eliminates the need to manually write locators.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use MCP Playwright if:&lt;/strong&gt;&lt;br&gt;
The UI is properly labeled with accessibility attributes.&lt;br&gt;
The page being tested is simple: a form, a button, or a basic navigation.&lt;br&gt;
The test logic is as simple as possible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't Use MCP Playwright if:&lt;/strong&gt;&lt;br&gt;
The interface is custom, and the elements do not have accessible attributes.&lt;br&gt;
The test logic is complex, involving multiple interactions or APIs.&lt;br&gt;
You need to check internal values, not just the visual state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;br&gt;
MCP Playwright and Cursor are powerful tools that can significantly speed up test creation and help with routine tasks. However, they cannot replace the work of an engineer, especially in more complex scenarios.&lt;br&gt;
Cursor is excellent for generating test templates and working with common cases. It helps quickly set up the structure of tests but requires refinement for more complex scenarios, such as error handling or complex logic.&lt;br&gt;
MCP works perfectly when the page is properly labeled with accessibility attributes. It eliminates the need to manually write locators, speeding up the testing process. But if the elements on the page are dynamic or not properly labeled, MCP may not be as reliable, and you should consider using other methods for element search.&lt;/p&gt;

&lt;p&gt;In any case, while these tools significantly speed up testing, they do not replace full engineering work. It's important to remember that for complex scenarios or integration tests, despite the convenience of these tools, you will still need manual intervention for correct logic setup.&lt;br&gt;
If you have already used MCP and Cursor in your projects, I'd be interested to hear how these tools helped you in real-world cases.&lt;/p&gt;

</description>
      <category>cursor</category>
      <category>mcp</category>
      <category>playwright</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
