<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged e2e</title>
<link>https://coderlegion.com/tag/e2e</link>
<description>Powered by Question2Answer</description>
<item>
<title>Angular-Aware E2E Testing: Query Components by @Input and Signals in Playwright</title>
<link>https://coderlegion.com/13831/angular-aware-e2e-testing-query-components-by-input-and-signals-in-playwright</link>
<description>&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=5952343712647183147&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;the-gap-between-angular-and-its-tests&quot;&gt;The Gap Between Angular and Its Tests&lt;/h3&gt;
&lt;p&gt;Angular applications are composed of typed components with declared &lt;code&gt;@Input()&lt;/code&gt; properties, &lt;code&gt;signal()&lt;/code&gt; state, and &lt;code&gt;@Output()&lt;/code&gt; events. Yet most E2E tests interact with these apps through raw CSS selectors — completely ignoring the component model.&lt;/p&gt;
&lt;p&gt;This leads to tests that know too much about the DOM and too little about the Angular contract. &lt;code&gt;@playwright-labs/selectors-angular&lt;/code&gt; fills that gap.&lt;/p&gt;
&lt;h3 id=&quot;installation&quot;&gt;Installation&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install -D @playwright-labs/selectors-angular
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;quick-setup&quot;&gt;Quick Setup&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// playwright.config.ts
import { selectors, defineConfig } from &quot;@playwright/test&quot;;
import { AngularEngine } from &quot;@playwright-labs/selectors-angular&quot;;

selectors.register(&quot;angular&quot;, AngularEngine);

export default defineConfig({
  use: { baseURL: &quot;http://localhost:4200&quot; },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or use the all-in-one import that registers the engine automatically:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { test, expect } from &quot;@playwright-labs/selectors-angular&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;the-angular-selector&quot;&gt;The &lt;code&gt;angular=&lt;/code&gt; Selector&lt;/h3&gt;
&lt;p&gt;Query components by their &lt;code&gt;@Input()&lt;/code&gt; values instead of DOM attributes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// By exact @Input value
page.locator('angular=app-button[label=&quot;Submit&quot;]')

// By nested property
page.locator('angular=app-user-card[user.role=&quot;admin&quot;]')

// By substring
page.locator('angular=app-button[type*=&quot;primary&quot;]')

// Multiple conditions (AND)
page.locator('angular=app-button[type=&quot;danger&quot;][disabled]')

// Regex
page.locator('angular=app-button[label=/^Sub/i]')

// Case-insensitive
page.locator('angular=app-button[label=&quot;submit&quot; i]')
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Supported operators mirror CSS attribute selectors:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Syntax &lt;/th&gt;&lt;th&gt;Meaning&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;[prop]&lt;/code&gt; &lt;/td&gt;&lt;td&gt;truthy&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;[prop=&quot;val&quot;]&lt;/code&gt; &lt;/td&gt;&lt;td&gt;exact match&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;[prop*=&quot;val&quot;]&lt;/code&gt; &lt;/td&gt;&lt;td&gt;contains&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;[prop^=&quot;val&quot;]&lt;/code&gt; &lt;/td&gt;&lt;td&gt;starts with&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;[prop$=&quot;val&quot;]&lt;/code&gt; &lt;/td&gt;&lt;td&gt;ends with&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;[prop~=&quot;val&quot;]&lt;/code&gt; &lt;/td&gt;&lt;td&gt;word in list&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;[prop\|=&quot;val&quot;]&lt;/code&gt; &lt;/td&gt;&lt;td&gt;exact or hyphen-prefix&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;the-ng-fixture&quot;&gt;The &lt;code&gt;$ng&lt;/code&gt; Fixture&lt;/h3&gt;
&lt;p&gt;Access component state directly from test code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;test(&quot;reads component properties&quot;, async ({ page, $ng }) =&amp;gt; {
  await page.goto(&quot;/&quot;);

  // @Input value
  const label = await $ng(&quot;app-button&quot;).first().input&amp;lt;string&amp;gt;(&quot;label&quot;);
  expect(label).toBe(&quot;Submit&quot;);

  // WritableSignal value
  const count = await $ng(&quot;app-counter&quot;).first().signal&amp;lt;number&amp;gt;(&quot;count&quot;);
  expect(count).toBe(0);

  // List inputs
  const inputs = await $ng(&quot;app-button&quot;).first().inputs();
  expect(inputs).toContain(&quot;disabled&quot;);

  // Check directives
  const dirs = await $ng(&quot;router-outlet&quot;).directives();
  expect(dirs.some((d) =&amp;gt; d.includes(&quot;RouterOutlet&quot;))).toBe(true);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;custom-matchers&quot;&gt;Custom Matchers&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { expect } from &quot;@playwright-labs/selectors-angular&quot;;

test(&quot;component matchers&quot;, async ({ page }) =&amp;gt; {
  const btn = page.locator(&quot;app-button&quot;).first();
  const counter = page.locator(&quot;app-counter&quot;).first();

  await expect(btn).toBeNgComponent();
  await expect(btn).toHaveNgInput(&quot;label&quot;);
  await expect(btn).toBeNgInput(&quot;label&quot;, &quot;Submit&quot;);
  await expect(btn).toHaveNgOutput(&quot;clicked&quot;);

  await expect(counter).toHaveNgSignal(&quot;count&quot;);
  await expect(counter).toBeNgSignal(&quot;count&quot;, 0);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;a-before-after-comparison&quot;&gt;A Before/After Comparison&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Depends on template class names — breaks on refactor
const deleteBtn = page.locator(&quot;button.danger:disabled&quot;);
await expect(deleteBtn).toHaveCount(1);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Queries the Angular component contract directly
const deleteBtn = page.locator('angular=app-button[type=&quot;danger&quot;][disabled]');
await expect(deleteBtn).toHaveCount(1);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The second version continues to work even if the template is rewritten to use different class names, a different element, or a different disabled mechanism — because it queries the component, not the DOM.&lt;/p&gt;
&lt;h3 id=&quot;signal-based-inputs-angular-17-&quot;&gt;Signal-Based Inputs (Angular 17+)&lt;/h3&gt;
&lt;p&gt;The engine transparently handles &lt;code&gt;input()&lt;/code&gt; signal-based inputs alongside traditional &lt;code&gt;@Input()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;@Component({ selector: &quot;app-badge&quot; })
export class BadgeComponent {
  count = input&amp;lt;number&amp;gt;(0);   // signal-based input
  @Input() label = &quot;&quot;;         // traditional input
}

// Both work identically in selectors:
page.locator('angular=app-badge[count=5]')
page.locator('angular=app-badge[label=&quot;New&quot;]')
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;requirements&quot;&gt;Requirements&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Angular 14+ in &lt;strong&gt;development mode&lt;/strong&gt; (&lt;code&gt;ng serve&lt;/code&gt; or &lt;code&gt;--configuration=development&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Playwright 1.40+&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;By teaching Playwright to speak Angular's language, &lt;code&gt;@playwright-labs/selectors-angular&lt;/code&gt; makes E2E tests that are more resilient, more readable, and more aligned with how the application is actually built.&lt;/p&gt;
&lt;p&gt;Source: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/vitalics/playwright-labs&quot;&gt;github.com/vitalics/playwright-labs&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/13831/angular-aware-e2e-testing-query-components-by-input-and-signals-in-playwright</guid>
<pubDate>Thu, 02 Apr 2026 12:12:00 +0000</pubDate>
</item>
</channel>
</rss>