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'],