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

voidmatcha 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 419d7029c0 [ZEPPELIN-6637] Compile Angular decorators in the shell 
unit test setup
419d7029c0 is described below

commit 419d7029c0247f7bd6dab5a0260ebf2e4accd504
Author: κΉ€μ˜ˆλ‚˜ <[email protected]>
AuthorDate: Wed Aug 26 23:06:39 2026 +0900

    [ZEPPELIN-6637] Compile Angular decorators in the shell unit test setup
    
    ### What is this PR for?
    
    ZEPPELIN-6567 gave the Angular shell a vitest setup, but only for specs 
that build their subject by hand. The spec it added is named "mounts React 
remotes outside the Angular zone without TestBed" and constructs the directive 
with `new ReactMountDirective(...)`. A spec that goes through TestBed does not 
run yet, for three separate reasons.
    
    Vitest 4 transforms specs with oxc, and that transform does not apply the 
decorator options the application build uses. They live in 
`tsconfig.base.json`, which `src/tsconfig.json` extends while excluding 
`**/*.spec.ts`. A decorated spec therefore fails to parse with `SyntaxError: 
Invalid or unexpected token`. Declaring `oxc.decorator` on the vitest config 
settles it independently of that lookup.
    
    `emitDecoratorMetadata` then compiles to a `__metadata` helper that is a 
silent no-op unless `Reflect.metadata` exists, so Angular JIT sees no 
`design:paramtypes` and constructor injection fails with `NG0202`. 
`src/polyfills.ts` pairs `zone.js` with `core-js/es7/reflect` for the 
application, and the setup already mirrors the first half, so mirroring the 
second needs no new dependency.
    
    Finally the setup never calls `TestBed.initTestEnvironment()`, and with 
vitest globals disabled Angular cannot install its per-test reset hook, so the 
second spec in a file hits `Cannot configure the test module when the test 
module has already been instantiated`.
    
    One note on the issue description. It attributes all of this to oxc not 
reading `experimentalDecorators` and `emitDecoratorMetadata`, and suggests 
importing `<at>angular/compiler`. The parse failure is real, but the missing 
polyfill and the missing TestBed initialization are separate causes, and 
`<at>angular/compiler` turns out not to be needed: `<at>angular/core/testing` 
already imports it, so the JIT facade is loaded either way.
    
    The TestBed spec for `ReactMountDirective` is the first consumer, and sits 
alongside the hand-driven one rather than replacing it. It renders the 
directive from a host template, so the decorator metadata, the `<at>Input` 
bindings and the constructor injection all have to resolve for it to run at 
all. It also provides zone change detection the way `main.ts` does, without 
which the zone assertions would pass vacuously against TestBed's zoneless 
default.
    
    None of the three files ships in the application bundle, so there is no 
runtime change.
    
    ### What type of PR is it?
    
    Improvement
    
    ### Todos
    
    None
    
    ### What is the Jira issue?
    
    https://issues.apache.org/jira/browse/ZEPPELIN-6637
    
    ### How should this be tested?
    
    `npm run test:shell` from `zeppelin-web-angular`, which is green at 5 specs 
across 2 files. Each piece of the setup was checked by removing it:
    
    * the `oxc` block: the TestBed spec fails to parse, `SyntaxError: Invalid 
or unexpected token`
    * `core-js/es7/reflect`: 3 failures, `NG0202: This constructor is not 
compatible with Angular Dependency Injection`
    * `initTestEnvironment`: `Need to call TestBed.initTestEnvironment() first`
    * `afterEach(resetTestingModule)`: the second spec onward fails with `test 
module has already been instantiated`
    * and to check the new spec is not asserting vacuously, changing the 
directive's `runOutsideAngular` to `run` makes its zone assertion fail
    
    Note that unit tests do not gate CI yet (ZEPPELIN-6566), so this suite has 
to be run locally for now.
    
    ### Screenshots (if appropriate)
    
    No, this changes test tooling only.
    
    ### Questions:
    
    * Does the license files need to update? No
    * Is there breaking changes for older versions? No
    * Does this needs documentation? No
    
    
    Closes #5435 from kimyenac/ZEPPELIN-6637.
    
    Signed-off-by: YONGJAE LEE <[email protected]>
---
 .../react-mount.directive.testbed.spec.ts          | 95 ++++++++++++++++++++++
 zeppelin-web-angular/test/test-setup.ts            | 13 +++
 zeppelin-web-angular/vitest.shell.config.mts       |  9 ++
 3 files changed, 117 insertions(+)

diff --git 
a/zeppelin-web-angular/src/app/share/react-mount/react-mount.directive.testbed.spec.ts
 
b/zeppelin-web-angular/src/app/share/react-mount/react-mount.directive.testbed.spec.ts
new file mode 100644
index 0000000000..17e621ab33
--- /dev/null
+++ 
b/zeppelin-web-angular/src/app/share/react-mount/react-mount.directive.testbed.spec.ts
@@ -0,0 +1,95 @@
+/*
+ * 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 { Component, NgZone, provideZoneChangeDetection } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { Mock, beforeEach, describe, expect, it, vi } from 'vitest';
+
+import { ReactExposedModule, ReactMountHandle, ReactProps } from 
'./react-mount-handle';
+import { ReactMountDirective } from './react-mount.directive';
+import { ReactRemoteLoaderService } from './react-remote-loader.service';
+
+@Component({
+  standalone: false,
+  template: `
+    <div [zeppelin-react-mount]="module" [reactProps]="reactProps"></div>
+  `
+})
+class HostComponent {
+  module = 'paragraph-footer';
+  reactProps: ReactProps = { paragraphId: 'p1' };
+}
+
+/**
+ * Companion to react-mount.directive.spec.ts, which drives the directive by
+ * hand. Going through TestBed puts the decorator metadata itself under test:
+ * the template bindings and constructor injection have to resolve to get here.
+ */
+describe('ReactMountDirective (TestBed)', () => {
+  let fixture: ComponentFixture<HostComponent>;
+  let handle: ReactMountHandle;
+  let mountedElements: HTMLElement[];
+  let mountZoneStates: boolean[];
+  let loadModule: Mock<(module: string) => Promise<ReactExposedModule>>;
+
+  beforeEach(() => {
+    mountedElements = [];
+    mountZoneStates = [];
+    handle = { update: vi.fn(), unmount: vi.fn() };
+    const remote: ReactExposedModule = {
+      mount: (element: HTMLElement) => {
+        mountedElements.push(element);
+        mountZoneStates.push(NgZone.isInAngularZone());
+        return handle;
+      }
+    };
+    loadModule = vi.fn(async () => remote);
+
+    TestBed.configureTestingModule({
+      declarations: [HostComponent, ReactMountDirective],
+      // TestBed defaults to zoneless, which would make the zone assertions
+      // below pass vacuously. main.ts bootstraps with zones, so mirror it.
+      providers: [provideZoneChangeDetection(), { provide: 
ReactRemoteLoaderService, useValue: { loadModule } }]
+    });
+
+    fixture = TestBed.createComponent(HostComponent);
+  });
+
+  it('mounts the remote on the host element outside the Angular zone', async 
() => {
+    fixture.detectChanges();
+    await fixture.whenStable();
+
+    expect(loadModule).toHaveBeenCalledWith('paragraph-footer');
+    
expect(mountedElements).toEqual([fixture.nativeElement.querySelector('div')]);
+    expect(mountZoneStates).toEqual([false]);
+  });
+
+  it('forwards later reactProps changes to the mount handle', async () => {
+    fixture.detectChanges();
+    await fixture.whenStable();
+
+    fixture.componentInstance.reactProps = { paragraphId: 'p2' };
+    fixture.detectChanges();
+
+    expect(handle.update).toHaveBeenCalledWith({ paragraphId: 'p2' });
+    expect(loadModule).toHaveBeenCalledOnce();
+  });
+
+  it('unmounts when the host component is destroyed', async () => {
+    fixture.detectChanges();
+    await fixture.whenStable();
+
+    fixture.destroy();
+
+    expect(handle.unmount).toHaveBeenCalledOnce();
+  });
+});
diff --git a/zeppelin-web-angular/test/test-setup.ts 
b/zeppelin-web-angular/test/test-setup.ts
index 7d0997c797..ae120021fd 100644
--- a/zeppelin-web-angular/test/test-setup.ts
+++ b/zeppelin-web-angular/test/test-setup.ts
@@ -11,3 +11,16 @@
  */
 
 import 'zone.js';
+// The pair src/polyfills.ts loads for the application. Without 
Reflect.metadata
+// the emitted `__metadata` helper is a silent no-op and injection fails 
NG0202.
+import 'core-js/es7/reflect';
+
+import { getTestBed } from '@angular/core/testing';
+import { BrowserTestingModule, platformBrowserTesting } from 
'@angular/platform-browser/testing';
+import { afterEach } from 'vitest';
+
+getTestBed().initTestEnvironment(BrowserTestingModule, 
platformBrowserTesting());
+
+// Vitest globals are disabled, so Angular cannot install its own reset hook 
and
+// the test module stays locked after the first spec instantiates it.
+afterEach(() => getTestBed().resetTestingModule());
diff --git a/zeppelin-web-angular/vitest.shell.config.mts 
b/zeppelin-web-angular/vitest.shell.config.mts
index 190081607b..0c035c2b2b 100644
--- a/zeppelin-web-angular/vitest.shell.config.mts
+++ b/zeppelin-web-angular/vitest.shell.config.mts
@@ -14,6 +14,15 @@
 import { defineConfig } from 'vitest/config';
 
 export default defineConfig({
+  // oxc does not apply the decorator options from tsconfig.base.json to specs,
+  // which src/tsconfig.json excludes. Undeclared, a decorated spec fails to
+  // parse with "Invalid or unexpected token".
+  oxc: {
+    decorator: {
+      emitDecoratorMetadata: true,
+      legacy: true
+    }
+  },
   test: {
     environment: 'jsdom',
     include: ['src/**/*.spec.ts', 'projects/zeppelin-sdk/**/*.spec.ts', 
'projects/zeppelin-visualization/**/*.spec.ts'],

Reply via email to