From 931d23192587eac0565b1a33da48910c5f52d589 Mon Sep 17 00:00:00 2001
From: Rainer Hahnekamp <rainer.hahnekamp@gmail.com>
Date: Sun, 5 Jan 2025 17:42:27 +0100
Subject: [PATCH 1/2] test(devtools): add e2e tests for devtools

---
 apps/demo/e2e/devtools.spec.ts                | 62 +++++++++++++++++++
 apps/demo/eslint.config.cjs                   | 10 ++-
 apps/demo/playwright.config.ts                | 35 +++++++++++
 apps/demo/project.json                        | 28 ++++-----
 .../src/app/devtools/todo-detail.component.ts | 23 ++++---
 package.json                                  |  2 +-
 pnpm-lock.yaml                                | 14 +++--
 7 files changed, 138 insertions(+), 36 deletions(-)
 create mode 100644 apps/demo/e2e/devtools.spec.ts
 create mode 100644 apps/demo/playwright.config.ts

diff --git a/apps/demo/e2e/devtools.spec.ts b/apps/demo/e2e/devtools.spec.ts
new file mode 100644
index 0000000..0dc3cbe
--- /dev/null
+++ b/apps/demo/e2e/devtools.spec.ts
@@ -0,0 +1,62 @@
+import { test, expect } from '@playwright/test';
+import { Action } from '@ngrx/store';
+
+test('has title', async ({ page }) => {
+  await page.goto('');
+
+  await page.evaluate(() => {
+    window['devtoolsSpy'] = [];
+
+    window['__REDUX_DEVTOOLS_EXTENSION__'] = {
+      connect: () => {
+        return {
+          send: (data: Action) => {
+            window['devtoolsSpy'].push(data);
+          },
+        };
+      },
+    };
+  });
+  await page.getByRole('link', { name: 'DevTools' }).click();
+  await page
+    .getByRole('row', { name: 'Go for a walk' })
+    .getByRole('checkbox')
+    .click();
+  await page
+    .getByRole('row', { name: 'Exercise' })
+    .getByRole('checkbox')
+    .click();
+
+  await expect(
+    page.getByRole('region', { name: 'Go for a walk' })
+  ).toBeVisible();
+  await expect(page.getByRole('region', { name: 'Exercise' })).toBeVisible();
+
+  await page
+    .getByRole('row', { name: 'Go for a walk' })
+    .getByRole('checkbox')
+    .click();
+  await page
+    .getByRole('row', { name: 'Exercise' })
+    .getByRole('checkbox')
+    .click();
+
+  await expect(
+    page.getByRole('region', { name: 'Go for a walk' })
+  ).toBeHidden();
+  await expect(page.getByRole('region', { name: 'Exercise' })).toBeHidden();
+
+  const devtoolsActions = await page.evaluate(() => window['devtoolsSpy']);
+
+  expect(devtoolsActions).toEqual([
+    { type: 'add todo' },
+    { type: 'select todo 1' },
+    { type: 'Store Update' },
+    { type: 'select todo 4' },
+    { type: 'Store Update' },
+    { type: 'select todo 1' },
+    { type: 'Store Update' },
+    { type: 'select todo 4' },
+    { type: 'Store Update' },
+  ]);
+});
diff --git a/apps/demo/eslint.config.cjs b/apps/demo/eslint.config.cjs
index 8831209..0d9ee19 100644
--- a/apps/demo/eslint.config.cjs
+++ b/apps/demo/eslint.config.cjs
@@ -1,7 +1,10 @@
+const playwright = require('eslint-plugin-playwright');
 const nx = require('@nx/eslint-plugin');
 const baseConfig = require('../../eslint.config.cjs');
 
 module.exports = [
+  playwright.configs['flat/recommended'],
+
   ...baseConfig,
   ...nx.configs['flat/angular'],
   ...nx.configs['flat/angular-template'],
@@ -25,5 +28,10 @@ module.exports = [
         },
       ],
     },
-  }
+  },
+  {
+    files: ['**/*.ts', '**/*.js'],
+    // Override or add rules here
+    rules: {},
+  },
 ];
diff --git a/apps/demo/playwright.config.ts b/apps/demo/playwright.config.ts
new file mode 100644
index 0000000..d1911d9
--- /dev/null
+++ b/apps/demo/playwright.config.ts
@@ -0,0 +1,35 @@
+import { defineConfig, devices } from '@playwright/test';
+import * as path from 'node:path';
+
+// For CI, you may want to set BASE_URL to the deployed application.
+const baseURL = process.env['BASE_URL'] || 'http://localhost:4200';
+
+/**
+ * Read environment variables from file.
+ * https://github.com/motdotla/dotenv
+ */
+// require('dotenv').config();
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+  use: {
+    baseURL,
+    /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
+    trace: 'on-first-retry',
+  },
+  /* Run your local dev server before starting the tests */
+  webServer: {
+    command: 'pnpm start',
+    url: 'http://localhost:4200',
+    reuseExistingServer: true,
+    cwd: path.join(__dirname, '../..'),
+  },
+  projects: [
+    {
+      name: 'chromium',
+      use: { ...devices['Desktop Chrome'] },
+    },
+  ],
+});
diff --git a/apps/demo/project.json b/apps/demo/project.json
index 2f19bc0..dc10fbb 100644
--- a/apps/demo/project.json
+++ b/apps/demo/project.json
@@ -8,21 +8,14 @@
   "targets": {
     "build": {
       "executor": "@angular-devkit/build-angular:application",
-      "outputs": [
-        "{options.outputPath}"
-      ],
+      "outputs": ["{options.outputPath}"],
       "options": {
         "outputPath": "dist/apps/demo",
         "index": "apps/demo/src/index.html",
         "browser": "apps/demo/src/main.ts",
-        "polyfills": [
-          "zone.js"
-        ],
+        "polyfills": ["zone.js"],
         "tsConfig": "apps/demo/tsconfig.app.json",
-        "assets": [
-          "apps/demo/src/favicon.ico",
-          "apps/demo/src/assets"
-        ],
+        "assets": ["apps/demo/src/favicon.ico", "apps/demo/src/assets"],
         "styles": [
           "@angular/material/prebuilt-themes/deeppurple-amber.css",
           "apps/demo/src/styles.css"
@@ -73,18 +66,21 @@
     },
     "lint": {
       "executor": "@nx/eslint:lint",
-      "outputs": [
-        "{options.outputFile}"
-      ]
+      "outputs": ["{options.outputFile}"]
     },
     "test": {
       "executor": "@nx/jest:jest",
-      "outputs": [
-        "{workspaceRoot}/coverage/{projectRoot}"
-      ],
+      "outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
       "options": {
         "jestConfig": "apps/demo/jest.config.ts"
       }
+    },
+    "e2e": {
+      "executor": "@nx/playwright:playwright",
+      "outputs": ["{workspaceRoot}/dist/.playwright/apps/demo"],
+      "options": {
+        "config": "apps/demo/playwright.config.ts"
+      }
     }
   }
 }
diff --git a/apps/demo/src/app/devtools/todo-detail.component.ts b/apps/demo/src/app/devtools/todo-detail.component.ts
index 6d819b7..91ec342 100644
--- a/apps/demo/src/app/devtools/todo-detail.component.ts
+++ b/apps/demo/src/app/devtools/todo-detail.component.ts
@@ -23,12 +23,14 @@ const TodoDetailStore = signalStore(
 
 @Component({
   selector: 'demo-todo-detail',
-  template: ` <mat-card>
-    <mat-card-title>{{ todo().name }}</mat-card-title>
-    <mat-card-content>
-      <textarea>{{ todo().description }}</textarea>
-    </mat-card-content>
-  </mat-card>`,
+  template: ` <section [attr.aria-label]="todo().name">
+    <mat-card>
+      <mat-card-title>{{ todo().name }}</mat-card-title>
+      <mat-card-content>
+        <textarea>{{ todo().description }}</textarea>
+      </mat-card-content>
+    </mat-card>
+  </section>`,
   imports: [MatCardModule],
   providers: [TodoDetailStore],
   styles: `
@@ -42,11 +44,8 @@ export class TodoDetailComponent {
   todo = input.required<Todo>();
 
   constructor() {
-    effect(
-      () => {
-        renameDevtoolsName(this.#todoDetailStore, `todo-${this.todo().id}`);
-      },
-      { allowSignalWrites: true }
-    );
+    effect(() => {
+      renameDevtoolsName(this.#todoDetailStore, `todo-${this.todo().id}`);
+    });
   }
 }
diff --git a/package.json b/package.json
index 4de3b37..2f0ff86 100644
--- a/package.json
+++ b/package.json
@@ -63,7 +63,7 @@
     "autoprefixer": "^10.4.19",
     "eslint": "^9.8.0",
     "eslint-config-prettier": "^9.0.0",
-    "eslint-plugin-playwright": "^0.15.3",
+    "eslint-plugin-playwright": "^1.6.2",
     "eslint-plugin-unused-imports": "^4.1.4",
     "husky": "^9.0.11",
     "jest": "^29.7.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6ea275e..8da9140 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -148,8 +148,8 @@ importers:
         specifier: ^9.0.0
         version: 9.1.0(eslint@9.17.0(jiti@1.21.6))
       eslint-plugin-playwright:
-        specifier: ^0.15.3
-        version: 0.15.3(eslint@9.17.0(jiti@1.21.6))
+        specifier: ^1.6.2
+        version: 1.8.3(eslint@9.17.0(jiti@1.21.6))
       eslint-plugin-unused-imports:
         specifier: ^4.1.4
         version: 4.1.4(@typescript-eslint/eslint-plugin@8.19.0(@typescript-eslint/parser@8.19.0(eslint@9.17.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.17.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.17.0(jiti@1.21.6))
@@ -5178,10 +5178,11 @@ packages:
     peerDependencies:
       eslint: '>=7.0.0'
 
-  eslint-plugin-playwright@0.15.3:
-    resolution: {integrity: sha512-LQMW5y0DLK5Fnpya7JR1oAYL2/7Y9wDiYw6VZqlKqcRGSgjbVKNqxraphk7ra1U3Bb5EK444xMgUlQPbMg2M1g==}
+  eslint-plugin-playwright@1.8.3:
+    resolution: {integrity: sha512-h87JPFHkz8a6oPhn8GRGGhSQoAJjx0AkOv1jME6NoMk2FpEsfvfJJNaQDxLSqSALkCr0IJXPGTnp6SIRVu5Nqg==}
+    engines: {node: '>=16.6.0'}
     peerDependencies:
-      eslint: '>=7'
+      eslint: '>=8.40.0'
       eslint-plugin-jest: '>=25'
     peerDependenciesMeta:
       eslint-plugin-jest:
@@ -14593,9 +14594,10 @@ snapshots:
     dependencies:
       eslint: 9.17.0(jiti@1.21.6)
 
-  eslint-plugin-playwright@0.15.3(eslint@9.17.0(jiti@1.21.6)):
+  eslint-plugin-playwright@1.8.3(eslint@9.17.0(jiti@1.21.6)):
     dependencies:
       eslint: 9.17.0(jiti@1.21.6)
+      globals: 13.24.0
 
   eslint-plugin-unused-imports@4.1.4(@typescript-eslint/eslint-plugin@8.19.0(@typescript-eslint/parser@8.19.0(eslint@9.17.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.17.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.17.0(jiti@1.21.6)):
     dependencies:

From 93c9440577817f03a210e4a6902b004723c63cdb Mon Sep 17 00:00:00 2001
From: Rainer Hahnekamp <rainer.hahnekamp@gmail.com>
Date: Sun, 5 Jan 2025 18:05:31 +0100
Subject: [PATCH 2/2] test(devtools): add e2e tests for devtools

---
 .github/workflows/build.yml | 1 +
 apps/demo/jest.config.ts    | 2 +-
 package.json                | 1 +
 3 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 3facedb..450daa0 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -21,6 +21,7 @@ jobs:
       - run: pnpm install --frozen-lockfile
       - run: pnpm run lint:all
       - run: pnpm run test:all
+      - run: pnpm run test:e2e
       - run: pnpm run build:all
       - run: ./integration-tests.sh
 
diff --git a/apps/demo/jest.config.ts b/apps/demo/jest.config.ts
index 8623305..94d9334 100644
--- a/apps/demo/jest.config.ts
+++ b/apps/demo/jest.config.ts
@@ -1,4 +1,3 @@
- 
 export default {
   displayName: 'demo',
   preset: '../../jest.preset.js',
@@ -13,6 +12,7 @@ export default {
       },
     ],
   },
+  testPathIgnorePatterns: ['e2e'],
   transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
   snapshotSerializers: [
     'jest-preset-angular/build/serializers/no-ng-attributes',
diff --git a/package.json b/package.json
index 2f0ff86..be23442 100644
--- a/package.json
+++ b/package.json
@@ -6,6 +6,7 @@
     "start": "nx serve demo",
     "lint:all": "nx run-many --targets=lint",
     "test:all": "nx run-many --targets=test",
+    "test:e2e": "nx e2e demo",
     "build:all": "nx run-many --targets=build",
     "verify:all": "nx run-many --targets=lint,test,build"
   },