This is an automated email from the ASF dual-hosted git repository.

tbonelee pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zeppelin.git


The following commit(s) were added to refs/heads/master by this push:
     new 57566734da [ZEPPELIN-6686] Align E2E coverage with Angular component 
targets
57566734da is described below

commit 57566734da0c706513529cf1024bc746dcde4976
Author: YONGJAE LEE (이용재) <[email protected]>
AuthorDate: Fri Sep 4 23:41:59 2026 +0900

    [ZEPPELIN-6686] Align E2E coverage with Angular component targets
    
    ### What is this PR for?
    
    `CoverageReporter` ignored the configured `transform` targets and scanned 
the Angular source tree again with `scandirectory`. This kept structural 
components in the report denominator and allowed `PAGES` to drift from the 
reported targets.
    
    This PR uses `cfg.transform` as the reporter denominator. 
`getCoverageTransformPaths()` discovers `src/app/**/*.component.ts`, removes 
four explicit structural components, and sorts the result. A Vitest spec 
requires `PAGES` to match the discovered targets.
    
    Node.js 22 provides `fs.globSync`, so this PR removes `scandirectory`. It 
also includes package-level reporter specs in the shell Vitest, TypeScript and 
ESLint paths.
    
    The metric covers the current Angular-hosted routes. A later route cutover 
must define how React-owned routes are counted.
    
    This PR does not change runtime behavior, add E2E coverage, or introduce a 
coverage threshold. The current source tree contains 57 Angular components and 
four exclusions, resulting in 53 targets.
    
    The affected `AGENTS.md` files document the new rules. The package guidance 
also reflects the TestBed setup merged through ZEPPELIN-6637.
    
    ### What type of PR is it?
    
    Bug Fix
    
    ### Todos
    
    * [x] Use `transform` as the reporter denominator
    * [x] Discover Angular component targets
    * [x] Check `PAGES` and exclusion alignment
    * [x] Remove `scandirectory`
    * [x] Include package-level reporter specs in test and lint configuration
    * [x] Update the applicable `AGENTS.md` files
    
    ### What is the Jira issue?
    
    ZEPPELIN-6686
    
    ### How should this be tested?
    
    ```bash
    cd zeppelin-web-angular
    npm run test:shell
    npx tsc -p src/tsconfig.spec.json --noEmit
    npx eslint \
      e2e/reporter.coverage.ts \
      e2e/reporter.coverage.config.ts \
      e2e/utils.ts \
      test/reporter.coverage.spec.ts \
      vitest.shell.config.mts \
      eslint.config.js
    ```
    
    Results:
    
    * Shell tests: 7 files and 32 tests passed
    * Reporter tests: 3 tests passed
    * TypeScript check: passed
    * ESLint: 0 errors and 5 existing warnings in unchanged wait helpers
    * Current target alignment: 57 components, four exclusions and 53 targets
    
    ### Screenshots (if appropriate)
    
    N/A
    
    ### Questions:
    
    * Does the license files need to update? No
    * Is there breaking changes for older versions? No
    * Does this needs documentation? The affected `AGENTS.md` files are updated.
    
    
    Closes #5445 from voidmatcha/fix/e2e-coverage-targets.
    
    Signed-off-by: ChanHo Lee <[email protected]>
---
 zeppelin-web-angular/AGENTS.md                     | 22 +++----
 zeppelin-web-angular/angular.json                  |  2 +-
 zeppelin-web-angular/e2e/AGENTS.md                 |  6 +-
 .../e2e/reporter.coverage.config.ts                |  6 +-
 zeppelin-web-angular/e2e/reporter.coverage.ts      | 44 +-------------
 zeppelin-web-angular/e2e/utils.ts                  | 26 ++++++--
 zeppelin-web-angular/eslint.config.js              |  3 +-
 zeppelin-web-angular/package-lock.json             | 71 ----------------------
 zeppelin-web-angular/package.json                  |  1 -
 zeppelin-web-angular/src/tsconfig.spec.json        |  1 +
 .../test/reporter.coverage.spec.ts                 | 71 ++++++++++++++++++++++
 zeppelin-web-angular/vitest.shell.config.mts       |  9 ++-
 12 files changed, 121 insertions(+), 141 deletions(-)

diff --git a/zeppelin-web-angular/AGENTS.md b/zeppelin-web-angular/AGENTS.md
index c6fbaf21ba..ddec79bf2b 100644
--- a/zeppelin-web-angular/AGENTS.md
+++ b/zeppelin-web-angular/AGENTS.md
@@ -17,7 +17,7 @@ limitations under the License.
 
 # AGENTS.md
 
-Unit test conventions for this package. They apply to the Angular shell in 
`src/` and to the libraries under `projects/` that have no file of their own: 
`zeppelin-sdk`, which is framework-neutral, and `zeppelin-visualization`, which 
is mostly so apart from one `@Component` base class.
+Unit test conventions for this package. They apply to the Angular shell in 
`src/`, package-level infrastructure specs under `test/`, and the libraries 
under `projects/` that have no file of their own: `zeppelin-sdk`, which is 
framework-neutral, and `zeppelin-visualization`, which is mostly so apart from 
one `@Component` base class.
 
 Two subtrees override this file: [`e2e/AGENTS.md`](e2e/AGENTS.md) for the 
Playwright suite, and 
[`projects/zeppelin-react/AGENTS.md`](projects/zeppelin-react/AGENTS.md) for 
the React remote, which has a different CI status and one exception of its own.
 
@@ -25,15 +25,15 @@ The repository root `AGENTS.md` asks every change to 
include unit tests. This fi
 
 ## Layout
 
-- A spec lives next to its source: `foo.ts` / `foo.spec.ts`.
-- The runner is Vitest on jsdom. There is no Karma and no `TestBed` bootstrap 
in the setup file; `test/test-setup.ts` loads `zone.js` and nothing else.
-- `npm run test:shell` covers `src/`, `projects/zeppelin-sdk` and 
`projects/zeppelin-visualization`. The two libraries have no runner of their 
own; they ride on the shell config because their code needs nothing extra. 
`projects/zeppelin-react` is separate. It has its own Vitest config and its own 
file here.
+- A product-code spec lives next to its source: `foo.ts` / `foo.spec.ts`. 
Specs for package-level test and reporting infrastructure live under `test/`.
+- The runner is Vitest on jsdom. There is no Karma. `test/test-setup.ts` loads 
`zone.js` and reflection metadata, initializes Angular `TestBed`, and resets 
the test environment after each spec.
+- `npm run test:shell` covers `src/`, package-level specs under `test/`, 
`projects/zeppelin-sdk` and `projects/zeppelin-visualization`. The two 
libraries have no runner of their own; they ride on the shell config because 
their code needs nothing extra. `projects/zeppelin-react` is separate. It has 
its own Vitest config and its own file here.
 
 ## Running
 
 | Command | Purpose |
 | --- | --- |
-| `npm run test:shell` | Run the unit tests for `src/` and the two libraries |
+| `npm run test:shell` | Run the unit tests for `src/`, `test/` and the two 
libraries |
 | `npm run test:shell -- --coverage` | Same, with a coverage report |
 | `npm run test:shell -- foo.spec.ts` | Run one file |
 
@@ -97,9 +97,9 @@ it('renders a dash for null and undefined', ...)     // good
 it('handles input', ...)                             // not a test name
 ```
 
-## Angular classes without TestBed
+## Angular classes with and without TestBed
 
-Directives, pipes and services are plain classes. Construct them directly and 
pass mocks to the constructor. `TestBed` is not needed and is slower.
+Construct directly testable directives, pipes and services as plain classes 
when Angular framework wiring is not the subject of the spec. Pass mocks to the 
constructor instead of starting `TestBed` unnecessarily.
 
 ```ts
 const loader = { loadModule } as Pick<ReactRemoteLoaderService, 'loadModule'>;
@@ -108,7 +108,7 @@ const directive = new ReactMountDirective(host, ngZone, 
loader as ReactRemoteLoa
 
 See `src/app/share/react-mount/react-mount.directive.spec.ts` for a worked 
example, and `src/app/share/pipes/humanize-bytes.pipe.spec.ts` for a pipe.
 
-**A spec cannot declare a decorator of its own.** Importing a decorated class 
from source works (the pipe and directive specs here do exactly that), but 
writing `@Injectable()` inside a spec file fails with `SyntaxError: Invalid or 
unexpected token`. Spec files are excluded from the nearest `tsconfig.json` 
(`src/tsconfig.json` for the shell, each library's own under `projects/`), so 
the transform never picks up the decorator settings; that is 
[ZEPPELIN-6637](https://issues.apache.org/jir [...]
+Use `TestBed` when the behavior depends on template bindings, dependency 
injection, change detection, or Angular lifecycle wiring. 
`src/app/share/react-mount/react-mount.directive.testbed.spec.ts` shows that 
path with a decorated host component. Keep the direct-construction spec beside 
it for behavior that does not need Angular wiring.
 
 ## Migration (Angular to React)
 
@@ -122,9 +122,9 @@ A spec written after a surface moves to React pins the new 
implementation's beha
 
 `--coverage` produces a v8 report under `coverage/`. It is measured, not 
gated. There are no thresholds.
 
-**Read the percentage carefully: it is not whole-tree coverage.** The 
denominator is only the files the specs actually load, because `include` is 
left unset while ZEPPELIN-6637 is open (see `vitest.shell.config.mts`). Most of 
the tree is absent from the report rather than counted as zero, so the figure 
reads far better than the real state, and it can *fall* as specs are added, 
since each new spec pulls more files into the denominator. Expect a large drop 
when `include` is eventually turned on.
+**Read the percentage carefully: it is not whole-tree coverage.** The 
denominator is only the files the specs actually load, because `include` is 
left unset (see `vitest.shell.config.mts`). Coverage parsing is separate from 
the test transform, so the whole tree must be deliberately remeasured before 
that setting is widened. Most of the tree is absent from the report rather than 
counted as zero, so the figure reads far better than the real state, and it can 
*fall* as specs are added, sinc [...]
 
-That is deliberate. `src/` currently holds a few specs against 222 source 
files, so any threshold set today is either meaningless or permanently red. The 
intended progression is: measure only, then ratchet so the number cannot fall, 
then require coverage on changed files. A whole-tree percentage is the wrong 
target during a migration, because much of the tree is going to be rewritten 
anyway.
+That is deliberate. `src/` currently holds a few specs against more than 200 
source files, so any threshold set today is either meaningless or permanently 
red. The intended progression is: measure only, then ratchet so the number 
cannot fall, then require coverage on changed files. A whole-tree percentage is 
the wrong target during a migration, because much of the tree is going to be 
rewritten anyway.
 
 This is a different measurement from `e2e/reporter.coverage.ts`, which counts 
annotated component pages rather than lines. The two numbers are not comparable 
and are not merged.
 
@@ -134,5 +134,5 @@ This is a different measurement from 
`e2e/reporter.coverage.ts`, which counts an
 2. Create `foo.spec.ts` next to `foo.ts`.
 3. Import from `vitest` (`describe`, `expect`, `it`), not from Jasmine or Jest.
    Check the target has callers before you invest in it. 
`get-keyword-positions.spec.ts` is a worked example of a function that turned 
out to have none.
-4. Construct the class directly; do not reach for `TestBed`.
+4. Construct the class directly unless the behavior depends on Angular wiring; 
use `TestBed` when it does.
 5. Run `npm run test:shell` and confirm it passes before opening a PR.
diff --git a/zeppelin-web-angular/angular.json 
b/zeppelin-web-angular/angular.json
index fa7d20ec48..0561cd77b2 100644
--- a/zeppelin-web-angular/angular.json
+++ b/zeppelin-web-angular/angular.json
@@ -151,7 +151,7 @@
         "lint": {
           "builder": "@angular-eslint/builder:lint",
           "options": {
-            "lintFilePatterns": ["src/**/*.ts", "src/**/*.html", "e2e/**/*.ts"]
+            "lintFilePatterns": ["src/**/*.ts", "src/**/*.html", 
"test/**/*.ts", "e2e/**/*.ts"]
           }
         }
       }
diff --git a/zeppelin-web-angular/e2e/AGENTS.md 
b/zeppelin-web-angular/e2e/AGENTS.md
index 6360429976..9b76c1f387 100644
--- a/zeppelin-web-angular/e2e/AGENTS.md
+++ b/zeppelin-web-angular/e2e/AGENTS.md
@@ -88,7 +88,7 @@ test.describe('Home Page - Core Elements', () => {
 });
 ```
 
-Use an existing key from the `PAGES` object in `e2e/utils.ts`; add a new one 
there if the page is missing. `PAGES` is also the coverage-instrumentation set 
(`getCoverageTransformPaths`), so it defines the coverage denominator. Purely 
structural / non-page components (lifecycle hooks, shared UI primitives like 
the spinner or resize handle) are intentionally omitted from `PAGES`. They are 
exercised transitively and are not counted.
+Use an existing key from the `PAGES` object in `e2e/utils.ts`; add a new one 
there if the page is missing. The reporter discovers 
`src/app/**/*.component.ts` automatically and removes only the entries in 
`COVERAGE_EXCLUDED_COMPONENTS`, so component additions, deletions and moves 
update the denominator automatically. `PAGES` separately supplies the 
annotation names and must match those discovered targets. 
`test/reporter.coverage.spec.ts` enforces that match, rejects duplicate entries 
and  [...]
 
 ## Running
 
@@ -136,7 +136,7 @@ Pages are moving from Angular to React fragments 
incrementally. Today this is na
 
 ### Coverage
 
-- Coverage is tracked by `PAGES` key, not source file. The key is the stable 
identity; the path behind it is an implementation detail. When a page moves to 
React, update its path in `PAGES` rather than deleting the key (deleting drops 
it from the coverage denominator). Specs keep the same 
`addPageAnnotationBeforeEach(PAGES.KEY)` call across the migration.
+- Coverage attribution is tracked by `PAGES` key while the denominator is 
discovered from the Angular component tree. The key is the stable identity; the 
path behind it is an implementation detail. While Angular still hosts the 
route, keep the key mapped to that host component and cover fragment-only 
behavior in the React package tests. When the Angular host component is 
removed, revise the composed E2E target policy in the same change instead of 
silently deleting the key. Specs keep the [...]
 
 ### Suite Shape
 
@@ -148,7 +148,7 @@ Pages are moving from Angular to React fragments 
incrementally. Today this is na
 
 - **Locators (classic exception):** the classic templates predate roles and 
`data-testid`, so the role/label/text-first rule cannot apply. Sanctioned here: 
element ids (`#findInput`), `ng-click="..."` / `ng-controller="..."` attribute 
selectors, class selectors the legacy templates already expose (`.username`, 
`.interpreterHead`), and Ace/Select2 internals. Do not add `data-testid` to the 
frozen `zeppelin-web` sources.
 - **Readiness:** `waitForZeppelinReady` is Angular-specific (`[ng-version]`) 
and does not resolve on `/classic`; gate on a classic-visible signal instead 
(e.g. the first `ParagraphCtrl` paragraph, or `.ace_text-input` attached).
-- **Coverage:** `PAGES` is the Angular coverage denominator; classic pages are 
intentionally outside it, so `addPageAnnotationBeforeEach` is not used here.
+- **Coverage:** classic pages are outside the discovered Angular component 
target set, so `addPageAnnotationBeforeEach` is not used here.
 - **Running:** the classic suite has its own config, 
`playwright.classic.config.js` (Desktop Chrome only, targets 
`http://localhost:8080`), and needs a Zeppelin server built with 
`-Pweb-classic`. The `:4200` dev server does not serve `/classic`, so a plain 
`npm run e2e` never includes it. Run it with `npm run e2e:classic` (single 
spec: `npm run e2e:classic -- tests/classic/<spec>`). In CI the workflow 
enables it on the anonymous matrix leg only 
(`-Dweb.e2e.classic.disabled=false`), match [...]
 - **POM:** inlining locators/helpers is acceptable while the suite is this 
small; if it grows, move them behind `models/classic-*.ts` / `*.util.ts`.
 - The React-migration / framework-neutral-spec guidance does not apply to 
`tests/classic/`.
diff --git a/zeppelin-web-angular/e2e/reporter.coverage.config.ts 
b/zeppelin-web-angular/e2e/reporter.coverage.config.ts
index 7e8f6d69f6..0068e5c373 100644
--- a/zeppelin-web-angular/e2e/reporter.coverage.config.ts
+++ b/zeppelin-web-angular/e2e/reporter.coverage.config.ts
@@ -16,11 +16,7 @@ import { getCoverageTransformPaths } from './utils';
 const outputPath = join(__dirname, '..', 'playwright-coverage');
 
 export default {
-  rootPath: join(__dirname, '..'),
   outputPath,
-  testMatch: [/\.component$/],
-  excludes: [/\.spec\.ts$/, /\.module\.ts$/, /\.guard\.ts$/, /\.routing\.ts$/, 
/\.html$/, /\.less$/, /\.css$/],
-  // Transform configuration for coverage instrumentation
-  // Specifies which component files to instrument for coverage tracking
+  // Automatically discovered component targets are the reporter denominator.
   transform: getCoverageTransformPaths()
 };
diff --git a/zeppelin-web-angular/e2e/reporter.coverage.ts 
b/zeppelin-web-angular/e2e/reporter.coverage.ts
index 452bde90cd..08865c521e 100644
--- a/zeppelin-web-angular/e2e/reporter.coverage.ts
+++ b/zeppelin-web-angular/e2e/reporter.coverage.ts
@@ -15,7 +15,6 @@ import { promises as fs } from 'fs';
 import { join } from 'path';
 import { FullResult, Reporter, TestCase, TestResult } from 
'@playwright/test/reporter';
 import { flatMap, sortBy } from 'lodash';
-import { scanDirectory, Results } from 'scandirectory';
 import cfg from './reporter.coverage.config';
 
 const TEST_STATUS = {
@@ -47,15 +46,9 @@ class CoverageReporter implements Reporter {
   testedIds = new Map<string, TestStatusType>();
   targetPaths: string[] = [];
 
-  async onBegin() {
+  onBegin() {
     console.log('Coverage reporter starting...');
-    console.log('Root path:', cfg.rootPath);
-
-    const results = await scanDirectory({
-      directory: cfg.rootPath
-    });
-
-    this.targetPaths = this.processScannedFiles(results);
+    this.targetPaths = [...cfg.transform];
     console.log('Target paths:', this.targetPaths.length);
   }
 
@@ -85,39 +78,6 @@ class CoverageReporter implements Reporter {
     await this.saveResultsToFile(results, result.status);
   }
 
-  processScannedFiles(results: Results): string[] {
-    return Object.keys(results)
-      .filter(key => !results[key].directory)
-      .map(key => this.normalizeFilePath(key, results))
-      .filter(key => key !== '.')
-      .filter(key => this.shouldIncludeFile(key));
-  }
-
-  normalizeFilePath(key: string, results: Results): string {
-    if (/index\.tsx?$/.test(key)) {
-      return results[key].parent?.relativePath || '.';
-    }
-    return key.replace(/\.tsx?$/, '');
-  }
-
-  shouldIncludeFile(key: string): boolean {
-    if (cfg.testMatch?.length) {
-      const matchesTest = cfg.testMatch.some(rule => (rule instanceof RegExp ? 
rule.test(key) : rule === key));
-      if (!matchesTest) {
-        return false;
-      }
-    }
-
-    if (cfg.excludes?.length) {
-      const isExcluded = cfg.excludes.some(rule => (rule instanceof RegExp ? 
rule.test(key) : rule === key));
-      if (isExcluded) {
-        return false;
-      }
-    }
-
-    return true;
-  }
-
   extractPageAnnotations(test: TestCase): string[] {
     const annotations = test.annotations
       .filter(({ type }) => type === 'page')
diff --git a/zeppelin-web-angular/e2e/utils.ts 
b/zeppelin-web-angular/e2e/utils.ts
index d4500d3f6e..39784d825f 100644
--- a/zeppelin-web-angular/e2e/utils.ts
+++ b/zeppelin-web-angular/e2e/utils.ts
@@ -10,6 +10,8 @@
  * limitations under the License.
  */
 
+import { globSync } from 'fs';
+import { join, sep } from 'path';
 import { test, expect, Page, TestInfo } from '@playwright/test';
 import { LoginTestUtil } from './models/login-page.util';
 import { E2E_TEST_FOLDER } from './models/base-page';
@@ -20,9 +22,7 @@ export const NOTEBOOK_PATTERNS = {
   LINK_SELECTOR: 'a[href*="/notebook/"]'
 } as const;
 
-// Coverage denominator. Structural/shared components
-// (lifecycle hooks, spin, resize-handle, page-header) are intentionally 
omitted;
-// they have no page-level behavior and are exercised transitively.
+// Annotation registry for page-level coverage.
 export const PAGES = {
   // Main App
   APP: 'src/app/app.component',
@@ -99,6 +99,14 @@ export const PAGES = {
   }
 } as const;
 
+// These structural/shared components have no page-level behavior and are 
exercised transitively rather than counted as separate E2E targets.
+export const COVERAGE_EXCLUDED_COMPONENTS = [
+  'src/app/core/destroy-hook/destroy-hook.component',
+  'src/app/share/page-header/page-header.component',
+  'src/app/share/resize-handle/resize-handle.component',
+  'src/app/share/spin/spin.component'
+] as const;
+
 export const addPageAnnotation = (pageName: string, testInfo: TestInfo) => {
   testInfo.annotations.push({
     type: 'page',
@@ -133,7 +141,17 @@ export const flattenPageComponents = (pages: 
PageStructureType): string[] => {
   return result.sort();
 };
 
-export const getCoverageTransformPaths = (): string[] => 
flattenPageComponents(PAGES);
+export const getCoverageTransformPaths = (
+  rootPath = join(__dirname, '..'),
+  excludedComponents: readonly string[] = COVERAGE_EXCLUDED_COMPONENTS
+): string[] => {
+  const excluded = new Set(excludedComponents);
+
+  return globSync('src/app/**/*.component.ts', { cwd: rootPath })
+    .map(componentPath => componentPath.split(sep).join('/').replace(/\.ts$/, 
''))
+    .filter(componentPath => !excluded.has(componentPath))
+    .sort();
+};
 
 export const waitForUrlNotContaining = async (page: Page, fragment: string) => 
{
   await page.waitForLoadState('domcontentloaded', { timeout: 10000 });
diff --git a/zeppelin-web-angular/eslint.config.js 
b/zeppelin-web-angular/eslint.config.js
index 23b87ff73a..fa0e046c59 100644
--- a/zeppelin-web-angular/eslint.config.js
+++ b/zeppelin-web-angular/eslint.config.js
@@ -164,6 +164,7 @@ module.exports = tseslint.config(
     files: [
       'src/**/*.spec.ts',
       'projects/zeppelin-{sdk,visualization}/**/*.spec.ts',
+      'test/**/*.spec.ts',
       'test/test-setup.ts',
       'vitest.shell.config.mts'
     ],
@@ -176,7 +177,7 @@ module.exports = tseslint.config(
   },
   {
     // Catch specs that cannot fail, as eslint-plugin-playwright does for e2e.
-    files: ['src/**/*.spec.ts', 
'projects/zeppelin-{sdk,visualization}/**/*.spec.ts'],
+    files: ['src/**/*.spec.ts', 
'projects/zeppelin-{sdk,visualization}/**/*.spec.ts', 'test/**/*.spec.ts'],
     plugins: { vitest },
     rules: {
       'vitest/expect-expect': 'error',
diff --git a/zeppelin-web-angular/package-lock.json 
b/zeppelin-web-angular/package-lock.json
index 4725357f04..68ed893fab 100644
--- a/zeppelin-web-angular/package-lock.json
+++ b/zeppelin-web-angular/package-lock.json
@@ -80,7 +80,6 @@
         "monaco-editor-webpack-plugin": "7.1.1",
         "ng-packagr": "^21.2.3",
         "prettier": "^3.6.2",
-        "scandirectory": "8.1.1",
         "style-loader": "^4.0.0",
         "ts-node": "~7.0.0",
         "typescript": "~5.9.3",
@@ -10299,21 +10298,6 @@
         "node": ">= 0.4"
       }
     },
-    "node_modules/editions": {
-      "version": "6.22.0",
-      "dev": true,
-      "license": "Artistic-2.0",
-      "dependencies": {
-        "version-range": "^4.15.0"
-      },
-      "engines": {
-        "ecmascript": ">= es5",
-        "node": ">=4"
-      },
-      "funding": {
-        "url": "https://bevry.me/fund";
-      }
-    },
     "node_modules/ee-first": {
       "version": "1.1.1",
       "dev": true,
@@ -12416,32 +12400,6 @@
         "node": "^20.17.0 || >=22.9.0"
       }
     },
-    "node_modules/ignorefs": {
-      "version": "5.0.4",
-      "dev": true,
-      "license": "Artistic-2.0",
-      "dependencies": {
-        "editions": "^6.21.0",
-        "ignorepatterns": "^5.6.0"
-      },
-      "engines": {
-        "node": ">=4"
-      },
-      "funding": {
-        "url": "https://bevry.me/fund";
-      }
-    },
-    "node_modules/ignorepatterns": {
-      "version": "5.6.0",
-      "dev": true,
-      "license": "Artistic-2.0",
-      "engines": {
-        "node": ">=4"
-      },
-      "funding": {
-        "url": "https://bevry.me/fund";
-      }
-    },
     "node_modules/image-size": {
       "version": "0.5.5",
       "dev": true,
@@ -16656,24 +16614,6 @@
         "node": ">=v12.22.7"
       }
     },
-    "node_modules/scandirectory": {
-      "version": "8.1.1",
-      "dev": true,
-      "license": "Artistic-2.0",
-      "dependencies": {
-        "editions": "^6.21.0",
-        "ignorefs": "^5.0.4"
-      },
-      "bin": {
-        "scandirectory": "bin.cjs"
-      },
-      "engines": {
-        "node": ">=4"
-      },
-      "funding": {
-        "url": "https://bevry.me/fund";
-      }
-    },
     "node_modules/schema-utils": {
       "version": "4.3.0",
       "dev": true,
@@ -18282,17 +18222,6 @@
         "fmin": "0.0.2"
       }
     },
-    "node_modules/version-range": {
-      "version": "4.15.0",
-      "dev": true,
-      "license": "Artistic-2.0",
-      "engines": {
-        "node": ">=4"
-      },
-      "funding": {
-        "url": "https://bevry.me/fund";
-      }
-    },
     "node_modules/vite": {
       "version": "8.2.1",
       "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz";,
diff --git a/zeppelin-web-angular/package.json 
b/zeppelin-web-angular/package.json
index 8acdda44a6..f40fadc5e8 100644
--- a/zeppelin-web-angular/package.json
+++ b/zeppelin-web-angular/package.json
@@ -109,7 +109,6 @@
     "monaco-editor-webpack-plugin": "7.1.1",
     "ng-packagr": "^21.2.3",
     "prettier": "^3.6.2",
-    "scandirectory": "8.1.1",
     "style-loader": "^4.0.0",
     "ts-node": "~7.0.0",
     "typescript": "~5.9.3",
diff --git a/zeppelin-web-angular/src/tsconfig.spec.json 
b/zeppelin-web-angular/src/tsconfig.spec.json
index e4233dd1d4..409fd8b367 100644
--- a/zeppelin-web-angular/src/tsconfig.spec.json
+++ b/zeppelin-web-angular/src/tsconfig.spec.json
@@ -8,6 +8,7 @@
     "**/*.spec.ts",
     "../projects/zeppelin-sdk/**/*.spec.ts",
     "../projects/zeppelin-visualization/**/*.spec.ts",
+    "../test/**/*.spec.ts",
     "../test/test-setup.ts",
     "../vitest.shell.config.mts"
   ],
diff --git a/zeppelin-web-angular/test/reporter.coverage.spec.ts 
b/zeppelin-web-angular/test/reporter.coverage.spec.ts
new file mode 100644
index 0000000000..a443194b59
--- /dev/null
+++ b/zeppelin-web-angular/test/reporter.coverage.spec.ts
@@ -0,0 +1,71 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs';
+import { tmpdir } from 'os';
+import { dirname, join } from 'path';
+import { afterEach, describe, expect, it } from 'vitest';
+import CoverageReporter from '../e2e/reporter.coverage';
+import cfg from '../e2e/reporter.coverage.config';
+import { COVERAGE_EXCLUDED_COMPONENTS, flattenPageComponents, 
getCoverageTransformPaths, PAGES } from '../e2e/utils';
+
+const fixtureRoots: string[] = [];
+
+const writeFixture = (rootPath: string, relativePath: string) => {
+  const filePath = join(rootPath, relativePath);
+  mkdirSync(dirname(filePath), { recursive: true });
+  writeFileSync(filePath, '');
+};
+
+afterEach(() => {
+  fixtureRoots.splice(0).forEach(rootPath => rmSync(rootPath, { recursive: 
true, force: true }));
+});
+
+describe('CoverageReporter', () => {
+  it('uses the configured coverage targets as its denominator', async () => {
+    const reporter = new CoverageReporter();
+
+    await reporter.onBegin();
+
+    expect(reporter.targetPaths).toEqual(cfg.transform);
+  });
+
+  it('discovers component additions and deletions while honoring exclusions', 
() => {
+    const rootPath = mkdtempSync(join(tmpdir(), 'zeppelin-e2e-coverage-'));
+    fixtureRoots.push(rootPath);
+    writeFixture(rootPath, 'src/app/included/included.component.ts');
+    writeFixture(rootPath, 'src/app/excluded/excluded.component.ts');
+    writeFixture(rootPath, 'src/app/ignored/ignored.service.ts');
+
+    expect(getCoverageTransformPaths(rootPath, 
['src/app/excluded/excluded.component'])).toEqual([
+      'src/app/included/included.component'
+    ]);
+
+    rmSync(join(rootPath, 'src/app/included/included.component.ts'));
+
+    expect(getCoverageTransformPaths(rootPath, 
['src/app/excluded/excluded.component'])).toEqual([]);
+  });
+
+  it('keeps annotations and exclusions aligned with existing component files', 
() => {
+    const pagePaths = flattenPageComponents(PAGES);
+    const packageRoot = join(__dirname, '..');
+
+    expect(pagePaths).toEqual(cfg.transform);
+    expect(new Set(pagePaths).size).toBe(pagePaths.length);
+    expect(new 
Set(COVERAGE_EXCLUDED_COMPONENTS).size).toBe(COVERAGE_EXCLUDED_COMPONENTS.length);
+    COVERAGE_EXCLUDED_COMPONENTS.forEach(componentPath => {
+      expect(existsSync(join(packageRoot, `${componentPath}.ts`))).toBe(true);
+    });
+  });
+});
diff --git a/zeppelin-web-angular/vitest.shell.config.mts 
b/zeppelin-web-angular/vitest.shell.config.mts
index 0c035c2b2b..da9eab60a6 100644
--- a/zeppelin-web-angular/vitest.shell.config.mts
+++ b/zeppelin-web-angular/vitest.shell.config.mts
@@ -25,13 +25,18 @@ export default defineConfig({
   },
   test: {
     environment: 'jsdom',
-    include: ['src/**/*.spec.ts', 'projects/zeppelin-sdk/**/*.spec.ts', 
'projects/zeppelin-visualization/**/*.spec.ts'],
+    include: [
+      'src/**/*.spec.ts',
+      'projects/zeppelin-sdk/**/*.spec.ts',
+      'projects/zeppelin-visualization/**/*.spec.ts',
+      'test/**/*.spec.ts'
+    ],
     setupFiles: ['./test/test-setup.ts'],
     coverage: {
       provider: 'v8',
       reporter: ['text', 'lcov'],
       reportsDirectory: './coverage',
-      // No `include`: v4 then reports only the files the specs load. Naming a 
directory makes the provider parse every source under it, which fails on 
decorator syntax while ZEPPELIN-6637 is open. Re-measure before widening, since 
coverage parses separately from the test transform. Note the reported 
percentage is therefore not whole-tree coverage; see AGENTS.md.
+      // No `include`: v4 then reports only files loaded by specs. Naming a 
directory makes the coverage provider parse the whole tree separately from the 
test transform, so re-measure before widening. The percentage is therefore not 
whole-tree coverage; see AGENTS.md.
       // TS exclusions are aligned with the e2e coverage reporter's where they 
overlap. The two measurements stay separate and are not comparable.
       exclude: ['**/*.spec.ts', '**/*.module.ts', '**/*.guard.ts', 
'**/*.routing.ts', '**/public-api.ts', '**/index.ts']
       // No thresholds on purpose: see zeppelin-web-angular/AGENTS.md.

Reply via email to