From 38757d9df69689bee55b02233711b227138b7e00 Mon Sep 17 00:00:00 2001 From: chrisjwalk-bot <268224883+chrisjwalk-bot@users.noreply.github.com> Date: Sun, 15 Mar 2026 14:10:01 -0400 Subject: [PATCH 1/8] docs: update readme with current tech stack and github actions badge Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 79 +-------------------------------- apps/web-app/src/assets/home.md | 79 ++++++++++++++++++--------------- docs/github-readme.md | 2 +- 3 files changed, 45 insertions(+), 115 deletions(-) diff --git a/README.md b/README.md index b2275957..c8d48fb0 100644 --- a/README.md +++ b/README.md @@ -1,78 +1 @@ -[![Build status](https://freshpondmedia.visualstudio.com/FreshPondMediaGit/_apis/build/status/chrisjwalk.angular-cli-netcore-ngrx-starter)](https://freshpondmedia.visualstudio.com/FreshPondMediaGit/_build/latest?definitionId=43) - -# Nx + Angular + .NET 10.0 - -This is basic demo of how to use a full stack [Nx monorepo](https://nx.dev/getting-started/tutorials/angular-monorepo-tutorial) with [Angular](https://angular.dev) and .NET 10.0 with [Microsoft.AspNetCore.SpaServices.Extensions](https://docs.microsoft.com/en-us/aspnet/core/client-side/spa/angular) and a demo Azure pipeline for Azure DevOps. - -## Demo - -See a live demo here: [https://angularclinetcorengrxstarter.azurewebsites.net/](https://angularclinetcorengrxstarter.azurewebsites.net/) - -## Getting Started? - -- **Make sure you have at least Node 24.x or higher (w/ pnpm 10+) installed!** -- **This repository uses ASP.NET 10.0, which has a hard requirement on .NET SDK 10.0.x. Please install these items from [https://dotnet.microsoft.com/download](https://dotnet.microsoft.com/download)** - -## Visual Studio 2022 - -Make sure you have .NET 10.0 installed and/or the latest VS2026. - -## Visual Studio Code - -> Note: Make sure you have the C# extension & .NET Debugger installed. - - pnpm install - -## Serve Development App - -``` -pnpm start -``` - -Both the api (dotnet) and web app (Angular) will build and run in dev mode. Open your browser on http://localhost:4200/ to see the Angular app, or https://localhost:60254/swagger to see the api documentation generated by Swagger. - -## Serve Production App (PWA enabled) - -``` -pnpm serve:prod -``` - -## Lint - -``` -pnpm lint -``` - -## Unit Tests - -Run unit tests by executing: - -``` -pnpm test -``` - -Please note that for test coverage you need dotnet-coverage to be installed: - -``` -dotnet tool install --global dotnet-coverage -``` - -More information is available on https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-test - -## End-to-end Tests - -Run e2e tests by executing: - -``` -pnpm e2e -``` - -## Build Production App - -Build the production Angular app and Publish the release .NET app, run: - -``` -pnpm build:prod -``` - -The contents of the. `/dist` folder should now contain something that can be deployed to and Azure web service or IIS instance. +[![CI](https://github.com/chrisjwalk/angular-cli-netcore-ngrx-starter/actions/workflows/ci.yml/badge.svg)](https://github.com/chrisjwalk/angular-cli-netcore-ngrx-starter/actions/workflows/ci.yml) diff --git a/apps/web-app/src/assets/home.md b/apps/web-app/src/assets/home.md index 652c2058..6a777f6a 100644 --- a/apps/web-app/src/assets/home.md +++ b/apps/web-app/src/assets/home.md @@ -1,76 +1,83 @@ # Nx + Angular + .NET 10.0 -This is basic demo of how to use a full stack [Nx monorepo](https://nx.dev/getting-started/tutorials/angular-monorepo-tutorial) with [Angular](https://angular.dev) and .NET 10.0 with [Microsoft.AspNetCore.SpaServices.Extensions](https://docs.microsoft.com/en-us/aspnet/core/client-side/spa/angular) and a demo Azure pipeline for Azure DevOps. +A full-stack demo using an [Nx monorepo](https://nx.dev) with [Angular](https://angular.dev) (zoneless, signals) and a .NET 10.0 Web API backend. Deployed to Azure App Service with automated PR preview deployments via Azure Static Web Apps. -## Demo +## Tech stack -See a live demo here: [https://angularclinetcorengrxstarter.azurewebsites.net/](https://angularclinetcorengrxstarter.azurewebsites.net/) +**Frontend** -## Getting Started? +- [Angular 21](https://angular.dev) — zoneless change detection, standalone components, signals +- [NgRx Signal Store](https://ngrx.io/guide/signals) — reactive state management +- [Angular Material](https://material.angular.io) — UI component library +- [Tailwind CSS v4](https://tailwindcss.com) — utility-first styling +- [Angular PWA](https://angular.dev/ecosystem/service-workers) — service worker & offline support -- **Make sure you have at least Node 24.x or higher (w/ pnpm 10+) installed!** -- **This repository uses ASP.NET 10.0, which has a hard requirement on .NET SDK 10.0.x. Please install these items from [https://dotnet.microsoft.com/download](https://dotnet.microsoft.com/download)** +**Backend** -## Visual Studio 2022 +- [.NET 10.0](https://dotnet.microsoft.com) Web API +- [ASP.NET Core Identity](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/identity) — bearer token authentication +- [Entity Framework Core](https://learn.microsoft.com/en-us/ef/core/) with Azure SQL -Make sure you have .NET 10.0 installed and/or the latest VS2026. +**Tooling** -## Visual Studio Code +- [Nx](https://nx.dev) — monorepo build system with affected commands +- [Vitest](https://vitest.dev) — unit tests +- [Playwright](https://playwright.dev) — end-to-end tests +- [pnpm](https://pnpm.io) — package manager -> Note: Make sure you have the C# extension & .NET Debugger installed. +## Demo - pnpm install +Live demo: [https://angularclinetcorengrxstarter.azurewebsites.net/](https://angularclinetcorengrxstarter.azurewebsites.net/) -## Serve Development App +## Getting started -``` -pnpm start -``` +**Prerequisites** -Both the api (dotnet) and web app (Angular) will build and run in dev mode. Open your browser on http://localhost:4200/ to see the Angular app, or https://localhost:60254/swagger to see the api documentation generated by Swagger. +- Node 24.x+ with pnpm 10+ +- .NET SDK 10.0.x — [download](https://dotnet.microsoft.com/download) -## Serve Production App (PWA enabled) +**Install dependencies** +```bash +pnpm install ``` -pnpm serve:prod + +## Serve development app + +```bash +pnpm start ``` +Starts both the .NET API and Angular app in dev mode. Open [http://localhost:4200](http://localhost:4200) for the app, or [https://localhost:60254/swagger](https://localhost:60254/swagger) for the API docs. + ## Lint -``` +```bash pnpm lint ``` -## Unit Tests - -Run unit tests by executing: +## Unit tests -``` +```bash pnpm test ``` -Please note that for test coverage you need dotnet-coverage to be installed: +Coverage requires `dotnet-coverage`: -``` +```bash dotnet tool install --global dotnet-coverage ``` -More information is available on https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-test - -## End-to-end Tests +## End-to-end tests -Run e2e tests by executing: - -``` +```bash pnpm e2e ``` -## Build Production App +## Build for production -Build the production Angular app and Publish the release .NET app, run: - -``` +```bash pnpm build:prod ``` -The contents of the. `/dist` folder should now contain something that can be deployed to and Azure web service or IIS instance. +Builds the Angular app and publishes the .NET project to `/dist`, ready to deploy to Azure App Service. diff --git a/docs/github-readme.md b/docs/github-readme.md index e646d5af..c8d48fb0 100644 --- a/docs/github-readme.md +++ b/docs/github-readme.md @@ -1 +1 @@ -[![Build status](https://freshpondmedia.visualstudio.com/FreshPondMediaGit/_apis/build/status/chrisjwalk.angular-cli-netcore-ngrx-starter)](https://freshpondmedia.visualstudio.com/FreshPondMediaGit/_build/latest?definitionId=43) +[![CI](https://github.com/chrisjwalk/angular-cli-netcore-ngrx-starter/actions/workflows/ci.yml/badge.svg)](https://github.com/chrisjwalk/angular-cli-netcore-ngrx-starter/actions/workflows/ci.yml) From 09fcb420610726ac947b4868b73fccf5bc930384 Mon Sep 17 00:00:00 2001 From: chrisjwalk-bot <268224883+chrisjwalk-bot@users.noreply.github.com> Date: Sun, 15 Mar 2026 14:18:39 -0400 Subject: [PATCH 2/8] docs: make readme.md the source of truth, derive home.md from it - README.md is now the single source of truth (edit this directly) - update-readme target now generates home.md by stripping the badge line - Remove docs/github-readme.md (no longer needed) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 84 +++++++++++++++++++++++++++++++++++++++ apps/web-app/project.json | 2 +- docs/github-readme.md | 1 - 3 files changed, 85 insertions(+), 2 deletions(-) delete mode 100644 docs/github-readme.md diff --git a/README.md b/README.md index c8d48fb0..276c4ba3 100644 --- a/README.md +++ b/README.md @@ -1 +1,85 @@ [![CI](https://github.com/chrisjwalk/angular-cli-netcore-ngrx-starter/actions/workflows/ci.yml/badge.svg)](https://github.com/chrisjwalk/angular-cli-netcore-ngrx-starter/actions/workflows/ci.yml) + +# Nx + Angular + .NET 10.0 + +A full-stack demo using an [Nx monorepo](https://nx.dev) with [Angular](https://angular.dev) (zoneless, signals) and a .NET 10.0 Web API backend. Deployed to Azure App Service with automated PR preview deployments via Azure Static Web Apps. + +## Tech stack + +**Frontend** + +- [Angular 21](https://angular.dev) — zoneless change detection, standalone components, signals +- [NgRx Signal Store](https://ngrx.io/guide/signals) — reactive state management +- [Angular Material](https://material.angular.io) — UI component library +- [Tailwind CSS v4](https://tailwindcss.com) — utility-first styling +- [Angular PWA](https://angular.dev/ecosystem/service-workers) — service worker & offline support + +**Backend** + +- [.NET 10.0](https://dotnet.microsoft.com) Web API +- [ASP.NET Core Identity](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/identity) — bearer token authentication +- [Entity Framework Core](https://learn.microsoft.com/en-us/ef/core/) with Azure SQL + +**Tooling** + +- [Nx](https://nx.dev) — monorepo build system with affected commands +- [Vitest](https://vitest.dev) — unit tests +- [Playwright](https://playwright.dev) — end-to-end tests +- [pnpm](https://pnpm.io) — package manager + +## Demo + +Live demo: [https://angularclinetcorengrxstarter.azurewebsites.net/](https://angularclinetcorengrxstarter.azurewebsites.net/) + +## Getting started + +**Prerequisites** + +- Node 24.x+ with pnpm 10+ +- .NET SDK 10.0.x — [download](https://dotnet.microsoft.com/download) + +**Install dependencies** + +```bash +pnpm install +``` + +## Serve development app + +```bash +pnpm start +``` + +Starts both the .NET API and Angular app in dev mode. Open [http://localhost:4200](http://localhost:4200) for the app, or [https://localhost:60254/swagger](https://localhost:60254/swagger) for the API docs. + +## Lint + +```bash +pnpm lint +``` + +## Unit tests + +```bash +pnpm test +``` + +Coverage requires `dotnet-coverage`: + +```bash +dotnet tool install --global dotnet-coverage +``` + +## End-to-end tests + +```bash +pnpm e2e +``` + +## Build for production + +```bash +pnpm build:prod +``` + +Builds the Angular app and publishes the .NET project to `/dist`, ready to deploy to Azure App Service. diff --git a/apps/web-app/project.json b/apps/web-app/project.json index b12854e1..8c5934d1 100644 --- a/apps/web-app/project.json +++ b/apps/web-app/project.json @@ -116,7 +116,7 @@ "executor": "nx:run-commands", "outputs": [], "options": { - "command": "cat docs/github-readme.md > README.md && cat apps/web-app/src/assets/home.md >> README.md" + "command": "sed -n '/^# /,$p' README.md > apps/web-app/src/assets/home.md" } } } diff --git a/docs/github-readme.md b/docs/github-readme.md deleted file mode 100644 index c8d48fb0..00000000 --- a/docs/github-readme.md +++ /dev/null @@ -1 +0,0 @@ -[![CI](https://github.com/chrisjwalk/angular-cli-netcore-ngrx-starter/actions/workflows/ci.yml/badge.svg)](https://github.com/chrisjwalk/angular-cli-netcore-ngrx-starter/actions/workflows/ci.yml) From 3c85ecebb357d13b418bbf2500b1ef0944e26715 Mon Sep 17 00:00:00 2001 From: chrisjwalk-bot <268224883+chrisjwalk-bot@users.noreply.github.com> Date: Sun, 15 Mar 2026 14:24:50 -0400 Subject: [PATCH 3/8] dx: auto-sync home.md from readme.md via lint-staged - Add .lintstagedrc.cjs with function syntax to run update-readme whenever README.md is committed, then auto-stage home.md - Move lint-staged config out of package.json (functions require .cjs) - Remove update-readme from postinstall (no longer needed there) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .lintstagedrc.cjs | 8 ++++++++ package.json | 6 +----- 2 files changed, 9 insertions(+), 5 deletions(-) create mode 100644 .lintstagedrc.cjs diff --git a/.lintstagedrc.cjs b/.lintstagedrc.cjs new file mode 100644 index 00000000..b12084b8 --- /dev/null +++ b/.lintstagedrc.cjs @@ -0,0 +1,8 @@ +module.exports = { + '*.{ts,js}': 'eslint --cache --cache-location=.husky/_ --fix', + '*.{ts,js,css,scss,md,mdx}': 'prettier --write', + 'README.md': () => [ + 'pnpm nx run web-app:update-readme', + 'git add apps/web-app/src/assets/home.md', + ], +}; diff --git a/package.json b/package.json index afef7675..3fec749a 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "dotnet:build": "dotnet build apps/api", "dotnet:update": "dotnet outdated -u apps/api", "preinstall": "pnpm dotnet:restore", - "postinstall": "pnpm build:dotnet-builder && pnpm update-readme", + "postinstall": "pnpm build:dotnet-builder", "prepare": "husky", "update-packages": "ts-node ./tools/update-packages/src/main.ts" }, @@ -138,9 +138,5 @@ "typescript-eslint": "8.57.0", "vite": "8.0.0", "vitest": "4.1.0" - }, - "lint-staged": { - "*.{ts,js}": "eslint --cache --cache-location=.husky/_ --fix", - "*.{ts,js,css,scss,md,mdx}": "prettier --write" } } From fb4f96ba04f81638a6bacb08c4eb0b710c607356 Mon Sep 17 00:00:00 2001 From: chrisjwalk-bot <268224883+chrisjwalk-bot@users.noreply.github.com> Date: Sun, 15 Mar 2026 14:27:40 -0400 Subject: [PATCH 4/8] docs: add features section and contributing note to readme Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 12 ++++++++++++ apps/web-app/src/assets/home.md | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/README.md b/README.md index 276c4ba3..4ef75550 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,14 @@ A full-stack demo using an [Nx monorepo](https://nx.dev) with [Angular](https://angular.dev) (zoneless, signals) and a .NET 10.0 Web API backend. Deployed to Azure App Service with automated PR preview deployments via Azure Static Web Apps. +## Features + +- **Authentication** — register, login, and logout with JWT bearer tokens backed by ASP.NET Core Identity +- **Notification centre** — persistent notification panel with unread count, mark-as-read, dismiss, and action support (e.g. one-click reload on SW update) +- **PWA / service worker** — offline support; notifies users when a new app version is available with an in-app prompt to reload +- **Debug page** (`/debug`) — trigger test notifications and inspect service worker update state during development +- **PR preview deployments** — every pull request gets a live preview URL via Azure Static Web Apps + ## Tech stack **Frontend** @@ -83,3 +91,7 @@ pnpm build:prod ``` Builds the Angular app and publishes the .NET project to `/dist`, ready to deploy to Azure App Service. + +## Contributing + +`apps/web-app/src/assets/home.md` is auto-generated from this file — **edit `README.md` only**. The lint-staged hook regenerates `home.md` automatically whenever `README.md` is committed. diff --git a/apps/web-app/src/assets/home.md b/apps/web-app/src/assets/home.md index 6a777f6a..98f0afe7 100644 --- a/apps/web-app/src/assets/home.md +++ b/apps/web-app/src/assets/home.md @@ -2,6 +2,14 @@ A full-stack demo using an [Nx monorepo](https://nx.dev) with [Angular](https://angular.dev) (zoneless, signals) and a .NET 10.0 Web API backend. Deployed to Azure App Service with automated PR preview deployments via Azure Static Web Apps. +## Features + +- **Authentication** — register, login, and logout with JWT bearer tokens backed by ASP.NET Core Identity +- **Notification centre** — persistent notification panel with unread count, mark-as-read, dismiss, and action support (e.g. one-click reload on SW update) +- **PWA / service worker** — offline support; notifies users when a new app version is available with an in-app prompt to reload +- **Debug page** (`/debug`) — trigger test notifications and inspect service worker update state during development +- **PR preview deployments** — every pull request gets a live preview URL via Azure Static Web Apps + ## Tech stack **Frontend** @@ -81,3 +89,7 @@ pnpm build:prod ``` Builds the Angular app and publishes the .NET project to `/dist`, ready to deploy to Azure App Service. + +## Contributing + +`apps/web-app/src/assets/home.md` is auto-generated from this file — **edit `README.md` only**. The lint-staged hook regenerates `home.md` automatically whenever `README.md` is committed. From 978cc1cbec95b2a09415063b488aa46d7f2a9290 Mon Sep 17 00:00:00 2001 From: chrisjwalk-bot <268224883+chrisjwalk-bot@users.noreply.github.com> Date: Sun, 15 Mar 2026 14:39:03 -0400 Subject: [PATCH 5/8] ci: add coverage summary to job summary and pr comment Uses irongut/CodeCoverageSummary to parse cobertura XMLs and: - Write a coverage table + badge to the GitHub Actions job summary - Post a sticky PR comment with the coverage report Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae27205d..fe671218 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,3 +80,25 @@ jobs: name: test-results path: junit/ if-no-files-found: ignore + + - name: Coverage summary + if: always() + uses: irongut/CodeCoverageSummary@v1.3.0 + with: + filename: coverage/**/cobertura-coverage.xml + badge: true + fail_below_min: false + format: markdown + output: both + thresholds: '60 80' + + - name: Write coverage to job summary + if: always() + run: cat code-coverage-results.md >> $GITHUB_STEP_SUMMARY + + - name: Add coverage PR comment + if: always() && github.event_name == 'pull_request' + uses: marocchino/sticky-pull-request-comment@v2 + with: + recreate: true + path: code-coverage-results.md From fe447227aec9cd8b22d377457c80fd07d7db01e7 Mon Sep 17 00:00:00 2001 From: chrisjwalk-bot <268224883+chrisjwalk-bot@users.noreply.github.com> Date: Sun, 15 Mar 2026 14:39:30 -0400 Subject: [PATCH 6/8] docs: add husky/lint-staged and coverage to tooling section Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 3 ++- apps/web-app/src/assets/home.md | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4ef75550..c7380b52 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,9 @@ A full-stack demo using an [Nx monorepo](https://nx.dev) with [Angular](https:// **Tooling** - [Nx](https://nx.dev) — monorepo build system with affected commands -- [Vitest](https://vitest.dev) — unit tests +- [Vitest](https://vitest.dev) — unit tests with ~93% line coverage - [Playwright](https://playwright.dev) — end-to-end tests +- [Husky](https://typicode.github.io/husky/) + [lint-staged](https://github.com/lint-staged/lint-staged) — pre-commit hooks for linting, formatting, and keeping `home.md` in sync - [pnpm](https://pnpm.io) — package manager ## Demo diff --git a/apps/web-app/src/assets/home.md b/apps/web-app/src/assets/home.md index 98f0afe7..1c64ec9b 100644 --- a/apps/web-app/src/assets/home.md +++ b/apps/web-app/src/assets/home.md @@ -29,8 +29,9 @@ A full-stack demo using an [Nx monorepo](https://nx.dev) with [Angular](https:// **Tooling** - [Nx](https://nx.dev) — monorepo build system with affected commands -- [Vitest](https://vitest.dev) — unit tests +- [Vitest](https://vitest.dev) — unit tests with ~93% line coverage - [Playwright](https://playwright.dev) — end-to-end tests +- [Husky](https://typicode.github.io/husky/) + [lint-staged](https://github.com/lint-staged/lint-staged) — pre-commit hooks for linting, formatting, and keeping `home.md` in sync - [pnpm](https://pnpm.io) — package manager ## Demo From 6745a111c76df7dbcaeac20e92cc3931a206f2f3 Mon Sep 17 00:00:00 2001 From: chrisjwalk-bot <268224883+chrisjwalk-bot@users.noreply.github.com> Date: Sun, 15 Mar 2026 14:46:04 -0400 Subject: [PATCH 7/8] test: add debug component tests and always run full test suite in ci - Add debug.spec.ts covering all 6 add methods, clearAll, and markAllRead - Change CI test step to always run test:all so coverage report includes all libs, not just affected projects Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 7 +-- apps/web-app/src/app/debug/debug.spec.ts | 77 ++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 6 deletions(-) create mode 100644 apps/web-app/src/app/debug/debug.spec.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fe671218..83511f19 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,12 +50,7 @@ jobs: fi - name: Test - run: | - if [ "${{ github.event_name }}" = "pull_request" ]; then - pnpm nx affected --target=test --base=$NX_BASE --configuration ci - else - pnpm test:all - fi + run: pnpm test:all - name: Build run: | diff --git a/apps/web-app/src/app/debug/debug.spec.ts b/apps/web-app/src/app/debug/debug.spec.ts new file mode 100644 index 00000000..6681045f --- /dev/null +++ b/apps/web-app/src/app/debug/debug.spec.ts @@ -0,0 +1,77 @@ +import { provideNoopAnimations } from '@angular/platform-browser/animations'; +import { NotificationStore } from '@myorg/shared'; +import { fireEvent, render, screen } from '@testing-library/angular'; + +import { Debug } from './debug'; + +async function setup() { + const { fixture } = await render(Debug, { + providers: [provideNoopAnimations(), NotificationStore], + }); + const store = fixture.componentInstance.store; + return { store }; +} + +describe('Debug', () => { + it('should render debug tools heading', async () => { + await setup(); + expect(screen.getByText(/debug tools/i)).toBeTruthy(); + }); + + it('addInfo should add an info notification', async () => { + const { store } = await setup(); + fireEvent.click(screen.getByText('Add info')); + expect(store.notifications().length).toBe(1); + expect(store.notifications()[0].kind).toBe('info'); + }); + + it('addError should add an error notification', async () => { + const { store } = await setup(); + fireEvent.click(screen.getByText('Add error')); + expect(store.notifications()[0].kind).toBe('error'); + }); + + it('addAuth should add an auth notification', async () => { + const { store } = await setup(); + fireEvent.click(screen.getByText('Add auth')); + expect(store.notifications()[0].kind).toBe('auth'); + }); + + it('addSwUpdate should add a sw-update notification with reload action', async () => { + const { store } = await setup(); + fireEvent.click(screen.getByText('Add sw-update')); + const notification = store.notifications()[0]; + expect(notification.kind).toBe('sw-update'); + expect(notification.action?.label).toBe('Reload'); + }); + + it('addWithAction should add a notification with an action', async () => { + const { store } = await setup(); + fireEvent.click(screen.getByText('Add with action')); + expect(store.notifications()[0].action?.label).toBe('Do it'); + }); + + it('addAutoDismiss should add a notification with autoDismissMs', async () => { + const { store } = await setup(); + fireEvent.click(screen.getByText('Add auto-dismiss (3s)')); + expect(store.notifications()[0].autoDismissMs).toBe(3000); + }); + + it('clearAll should dismiss all notifications', async () => { + const { store } = await setup(); + fireEvent.click(screen.getByText('Add info')); + fireEvent.click(screen.getByText('Add error')); + expect(store.notifications().length).toBe(2); + fireEvent.click(screen.getByText('Clear all')); + expect(store.notifications().length).toBe(0); + }); + + it('mark all read should mark all notifications as read', async () => { + const { store } = await setup(); + fireEvent.click(screen.getByText('Add info')); + fireEvent.click(screen.getByText('Add error')); + expect(store.unreadCount()).toBe(2); + fireEvent.click(screen.getByText('Mark all read')); + expect(store.unreadCount()).toBe(0); + }); +}); From e157ccf31a2c18876c51f4140234d70da497f801 Mon Sep 17 00:00:00 2001 From: chrisjwalk-bot <268224883+chrisjwalk-bot@users.noreply.github.com> Date: Sun, 15 Mar 2026 15:10:06 -0400 Subject: [PATCH 8/8] test: enforce per-file coverage thresholds, fix gaps, use affected tests in CI - Revert CI test step to nx affected (only test:all on main push) - Add annotation to coverage PR comment noting it's affected-only - Add vitest perFile coverage thresholds (80% all metrics) to catch new code added without tests - Exclude *.routes.ts, *.config.ts, main.ts, environments from coverage - Fix coverage gaps to meet thresholds: - notification.store: add markRead multi-notification test (covers ternary else branch) - notification-list: add read-state and iconFor fallback tests - notification-bell: add singular aria-label, handset branch, overlay toggle-close, and re-open tests - counter.store: add inputCount and event dispatch tests (covers reducer callbacks via withCounterReducer) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 16 +++++++- .../src/lib/state/counter.store.spec.ts | 40 +++++++++++++++++++ .../lib/components/notification-bell.spec.ts | 38 ++++++++++++++++++ .../lib/components/notification-list.spec.ts | 17 ++++++++ .../src/lib/state/notification.store.spec.ts | 10 +++++ vite.config.mts | 13 ++++++ 6 files changed, 133 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 83511f19..c04fb83c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,7 +50,12 @@ jobs: fi - name: Test - run: pnpm test:all + run: | + if [ "${{ github.event_name }}" = "pull_request" ]; then + pnpm nx affected --target=test --base=$NX_BASE + else + pnpm test:all + fi - name: Build run: | @@ -87,6 +92,15 @@ jobs: output: both thresholds: '60 80' + - name: Annotate coverage report + if: always() && github.event_name == 'pull_request' + run: | + echo "> [!NOTE]" > coverage-note.md + echo "> Coverage shown for **affected projects only**. Per-file thresholds (80%) are enforced by vitest." >> coverage-note.md + echo "" >> coverage-note.md + cat code-coverage-results.md >> coverage-note.md + mv coverage-note.md code-coverage-results.md + - name: Write coverage to job summary if: always() run: cat code-coverage-results.md >> $GITHUB_STEP_SUMMARY diff --git a/libs/counter/src/lib/state/counter.store.spec.ts b/libs/counter/src/lib/state/counter.store.spec.ts index 21ea5385..1ca71bb7 100644 --- a/libs/counter/src/lib/state/counter.store.spec.ts +++ b/libs/counter/src/lib/state/counter.store.spec.ts @@ -1,9 +1,11 @@ import { TestBed } from '@angular/core/testing'; import { patchState } from '@ngrx/signals'; import { unprotected } from '@ngrx/signals/testing'; +import { injectDispatch } from '@ngrx/signals/events'; import { counterInitialState, CounterStore, + counterEvents, decrementCount, incrementCount, setCount, @@ -37,4 +39,42 @@ describe('CounterStore', () => { count--; expect(store.count()).toBe(count); }); + + it('inputCount should set count from a valid numeric string', () => { + store.inputCount('42'); + expect(store.count()).toBe(42); + }); + + it('inputCount should ignore non-numeric input', () => { + store.inputCount('abc'); + expect(store.count()).toBe(0); + }); + + it('should handle setCount event via reducer', () => { + const dispatcher = TestBed.runInInjectionContext(() => + injectDispatch(counterEvents), + ); + dispatcher.setCount(7); + TestBed.flushEffects(); + expect(store.count()).toBe(7); + }); + + it('should handle incrementCount event via reducer', () => { + const dispatcher = TestBed.runInInjectionContext(() => + injectDispatch(counterEvents), + ); + dispatcher.incrementCount(); + TestBed.flushEffects(); + expect(store.count()).toBe(1); + }); + + it('should handle decrementCount event via reducer', () => { + const dispatcher = TestBed.runInInjectionContext(() => + injectDispatch(counterEvents), + ); + patchState(unprotected(store), setCount(5)); + dispatcher.decrementCount(); + TestBed.flushEffects(); + expect(store.count()).toBe(4); + }); }); diff --git a/libs/shared/src/lib/components/notification-bell.spec.ts b/libs/shared/src/lib/components/notification-bell.spec.ts index 671de2ea..b231fe97 100644 --- a/libs/shared/src/lib/components/notification-bell.spec.ts +++ b/libs/shared/src/lib/components/notification-bell.spec.ts @@ -48,4 +48,42 @@ describe('NotificationBell', () => { const button = screen.getByRole('button'); expect(button.getAttribute('aria-label')).toContain('2 unread'); }); + + it('should use singular form in aria-label when exactly 1 notification is unread', async () => { + const { fixture } = await setup(); + const store = fixture.debugElement.injector.get(NotificationStore); + store.add({ kind: 'info', title: 'One' }); + fixture.detectChanges(); + expect(screen.getByRole('button').getAttribute('aria-label')).toBe( + '1 unread notification', + ); + }); + + it('should open bottom sheet on handset devices', async () => { + const { fixture } = await setup(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const component = fixture.debugElement.componentInstance as any; + const breakpointObserver = fixture.debugElement.injector.get( + component['breakpointObserver'].constructor, + ); + vi.spyOn(breakpointObserver, 'isMatched').mockReturnValue(true); + const bottomSheet = fixture.debugElement.injector.get( + component['bottomSheet'].constructor, + ); + vi.spyOn(bottomSheet, 'open').mockImplementation( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + () => null as any, + ); + fireEvent.click(screen.getByRole('button')); + expect(bottomSheet.open).toHaveBeenCalled(); + }); + + it('should close the overlay when bell is clicked while panel is open', async () => { + const { component } = await setup(); + fireEvent.click(screen.getByRole('button')); // open + const markAllReadSpy = vi.spyOn(component.store, 'markAllRead'); + fireEvent.click(screen.getByRole('button')); // close + expect(markAllReadSpy).toHaveBeenCalled(); + fireEvent.click(screen.getByRole('button')); // re-open — reuses existing overlayRef + }); }); diff --git a/libs/shared/src/lib/components/notification-list.spec.ts b/libs/shared/src/lib/components/notification-list.spec.ts index c112d267..a0094249 100644 --- a/libs/shared/src/lib/components/notification-list.spec.ts +++ b/libs/shared/src/lib/components/notification-list.spec.ts @@ -97,4 +97,21 @@ describe('NotificationList', () => { screen.queryByRole('button', { name: /mark all read/i }), ).toBeFalsy(); }); + + it('should not show unread indicator for already-read notifications', async () => { + const { store, fixture } = await setup(); + store.add({ kind: 'info', title: 'Read item' }); + store.markRead(store.notifications()[0].id); + fixture.detectChanges(); + await screen.findByText('Read item'); + expect(document.querySelectorAll('[aria-label="Unread"]')).toHaveLength(0); + }); + + it('iconFor should return notifications icon for unknown kind', async () => { + const { fixture } = await setup(); + const component = fixture.debugElement + .componentInstance as NotificationList; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(component.iconFor('unknown' as any)).toBe('notifications'); + }); }); diff --git a/libs/shared/src/lib/state/notification.store.spec.ts b/libs/shared/src/lib/state/notification.store.spec.ts index 917df5e4..a205d6bf 100644 --- a/libs/shared/src/lib/state/notification.store.spec.ts +++ b/libs/shared/src/lib/state/notification.store.spec.ts @@ -49,6 +49,16 @@ describe('NotificationStore', () => { expect(store.unreadCount()).toBe(0); }); + it('should only mark the targeted notification as read (not others)', () => { + store.add({ kind: 'info', title: 'A' }); + store.add({ kind: 'info', title: 'B' }); + const idA = store.notifications()[0].id; + store.markRead(idA); + expect(store.notifications()[0].read).toBe(true); + expect(store.notifications()[1].read).toBe(false); + expect(store.unreadCount()).toBe(1); + }); + it('should mark all notifications as read', () => { store.add({ kind: 'info', title: 'A' }); store.add({ kind: 'error', title: 'B' }); diff --git a/vite.config.mts b/vite.config.mts index d28acf26..d88a3e24 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -19,6 +19,19 @@ export const baseConfig = { provider: 'v8', reporter: ['text', 'cobertura'], include: ['src/**/*.{ts,tsx}'], + exclude: [ + 'src/**/*.routes.ts', + 'src/**/*.config.ts', + 'src/main.ts', + 'src/environments/**', + ], + thresholds: { + perFile: true, + statements: 80, + branches: 80, + functions: 80, + lines: 80, + }, }, server: { deps: {