Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions zeppelin-web-angular/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ limitations under the License.

# AGENTS.md

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.
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-notebook-core` and `zeppelin-sdk`, which are 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.

Expand All @@ -26,8 +26,9 @@ The repository root `AGENTS.md` asks every change to include unit tests. This fi
## Layout

- 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.
- The shell 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.
- `projects/zeppelin-notebook-core` has a dedicated Vitest config (`vitest.notebook-core.config.mts`) using Node without Angular test setup. It covers the package's contract specs and the dependency-boundary checks under `test/notebook-core`. These specs are not part of the shell or React suites. The core compiler excludes DOM libraries and application path aliases.

## Running

Expand All @@ -36,22 +37,26 @@ The repository root `AGENTS.md` asks every change to include unit tests. This fi
| `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 |
| `npm run test:notebook-core` | Run the dedicated notebook-core Node suite |
| `npm run typecheck:notebook-core` | Check core source and specs, rebuild the package, and check the React type-only contract against built declarations and the same core source |

`test:shell` is bound to the Maven `test` phase (`pom.xml`), so a spec added here starts running in CI the day it merges. It does not run where you would expect. `frontend.yml` builds this module with `-DskipTests`, which frontend-maven-plugin honours by skipping `test`-phase executions, so the run that counts is `mvnw verify -Pweb-e2e` inside the `run-playwright-e2e-tests` job. A failing spec surfaces there, under an e2e job name. Giving the unit tests a step of their own is [ZEPPELIN-6566](https://issues.apache.org/jira/browse/ZEPPELIN-6566).
`test:shell`, `test:notebook-core`, and `typecheck:notebook-core` are bound to the Maven `test` phase (`pom.xml`), so a spec added here starts running in CI the day it merges. It does not run where you would expect. `frontend.yml` builds this module with `-DskipTests`, which frontend-maven-plugin honours by skipping `test`-phase executions, so the run that counts is `mvnw verify -Pweb-e2e` inside the `run-playwright-e2e-tests` job. A failing spec surfaces there, under an e2e job name. Giving the unit tests a step of their own is [ZEPPELIN-6566](https://issues.apache.org/jira/browse/ZEPPELIN-6566).

## Where a test belongs

The frontend has two test layers, not three. There is no integration tier.

| Layer | Runner | Answers |
| --- | --- | --- |
| Unit | Vitest + jsdom | Is the judgement we wrote correct? |
| Unit | Vitest + jsdom (shell/React) or Node (notebook-core) | Is the judgement we wrote correct? |
| E2E | Playwright | Does the page actually work in a browser? |

Prefer a unit test when the question can be answered without a browser. Reach for e2e when the answer depends on wiring: routing, mounting a federated remote, authentication, or anything a user would have to click.

The two layers do not substitute for each other, and neither replaces the cross-framework parity checks the React migration needs.

The notebook-core host/remote contract is currently a type-only scaffold; it does not implement a notebook runtime.

A third kind is planned but does not exist yet: contract specs that replay captured WebSocket traffic against the notebook runtime, arriving with [ZEPPELIN-6627](https://issues.apache.org/jira/browse/ZEPPELIN-6627). Those will run on Vitest as well and live under `test/contract/`, with the captured traffic beside them. Conventions for them are added once they exist.

## What to test
Expand Down Expand Up @@ -139,4 +144,4 @@ This is a different measurement from `e2e/reporter.coverage.ts`, which counts an
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 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.
5. Run `npm run test:shell` for shell/SDK/visualization changes. For notebook-core changes, run `npm run test:notebook-core` and `npm run typecheck:notebook-core`. Confirm the relevant checks pass before opening a PR.
4 changes: 4 additions & 0 deletions zeppelin-web-angular/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ Run `npm run build` to build the project. The build artifacts will be stored in

Run `npm run test:shell` to execute the Angular shell unit tests via [Vitest](https://vitest.dev).

Run `npm run test:notebook-core` for the dedicated Node-based Vitest suite. Contract specs live in `projects/zeppelin-notebook-core`; dependency-boundary checks live in `test/notebook-core`. Run `npm run typecheck:notebook-core` to check the core and its test infrastructure, rebuild the core package, and check the React contract against both built declarations and the same core source. The React contract is a type-only scaffold; it does not mount a notebook or implement a runtime.

The React remote has a separate jsdom suite: run `npm test --prefix projects/zeppelin-react`. The shell and React suites do not include the notebook-core suite.

## Implementation Progress

### Pages
Expand Down
21 changes: 21 additions & 0 deletions zeppelin-web-angular/angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,27 @@
}
}
},
"zeppelin-notebook-core": {
"projectType": "library",
"root": "projects/zeppelin-notebook-core",
"sourceRoot": "projects/zeppelin-notebook-core/src",
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"tsConfig": "projects/zeppelin-notebook-core/tsconfig.json",
"project": "projects/zeppelin-notebook-core/ng-package.json"
}
},
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": ["projects/zeppelin-notebook-core/**/*.ts"]
}
}
}
},
"zeppelin-sdk": {
"projectType": "library",
"root": "projects/zeppelin-sdk",
Expand Down
10 changes: 8 additions & 2 deletions zeppelin-web-angular/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,11 @@ module.exports = tseslint.config(
// *.spec.ts. Point type-aware linting at the spec program explicitly.
files: [
'src/**/*.spec.ts',
'projects/zeppelin-{sdk,visualization}/**/*.spec.ts',
'projects/zeppelin-{notebook-core,sdk,visualization}/**/*.spec.ts',
'test/**/*.spec.ts',
'test/test-setup.ts',
'test/notebook-core/**/*.ts',
'vitest.notebook-core.config.mts',
'vitest.shell.config.mts'
],
languageOptions: {
Expand All @@ -177,7 +179,11 @@ 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', 'test/**/*.spec.ts'],
files: [
'src/**/*.spec.ts',
'projects/zeppelin-{notebook-core,sdk,visualization}/**/*.spec.ts',
'test/**/*.spec.ts'
],
plugins: { vitest },
rules: {
'vitest/expect-expect': 'error',
Expand Down
5 changes: 4 additions & 1 deletion zeppelin-web-angular/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"build": "npm run build:projects && npm run build:react && npm run build:angular",
"build:angular": "ng build --configuration production",
"build:react": "cd projects/zeppelin-react && npm run build",
"build:projects": "npm run build-project:sdk && npm run build-project:vis",
"build:projects": "npm run build-project:sdk && npm run build-project:notebook-core && npm run build-project:vis",
"build-project:notebook-core": "ng build --project zeppelin-notebook-core",
"build-project:sdk": "ng build --project zeppelin-sdk",
"check:websocket-contract": "node --test scripts/check-websocket-contract.test.js && node scripts/check-websocket-contract.js",
"generate:notebook-parity-scenarios": "node scripts/generate-notebook-parity-scenarios.mjs",
Expand All @@ -21,6 +22,8 @@
"lint:fix": "cross-env NODE_OPTIONS='--max-old-space-size=8192' ng lint --fix && npm run lint:fix:react && prettier --write \"**/*.{ts,tsx,mts,js,json,css,html}\"",
"lint:react": "cd projects/zeppelin-react && npm run lint",
"lint:fix:react": "cd projects/zeppelin-react && npm run lint:fix",
"typecheck:notebook-core": "tsc -p projects/zeppelin-notebook-core/tsconfig.json --noEmit && tsc -p projects/zeppelin-notebook-core/tsconfig.spec.json --noEmit && npm run build-project:notebook-core && tsc -p projects/zeppelin-react/tsconfig.notebook-core.dist.json --noEmit && tsc -p projects/zeppelin-react/tsconfig.notebook-core.json --noEmit",
"test:notebook-core": "vitest run --config vitest.notebook-core.config.mts",
"test:shell": "vitest run --config vitest.shell.config.mts",
"test:eslint-rules": "node --test eslint-rules/",
"e2e": "playwright test",
Expand Down
22 changes: 22 additions & 0 deletions zeppelin-web-angular/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,28 @@
</configuration>
</execution>

<execution>
<id>npm typecheck notebook core</id>
<goals>
<goal>npm</goal>
</goals>
<phase>test</phase>
<configuration>
<arguments>run typecheck:notebook-core</arguments>
</configuration>
</execution>

<execution>
<id>npm test notebook core</id>
<goals>
<goal>npm</goal>
</goals>
<phase>test</phase>
<configuration>
<arguments>run test:notebook-core</arguments>
</configuration>
</execution>

<execution>
<id>npm e2e</id>
<goals>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../dist/zeppelin-notebook-core",
"lib": {
"entryFile": "src/public-api.ts"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "@zeppelin/notebook-core",
"version": "0.0.1",
"sideEffects": false,
"dependencies": {
"tslib": "^2.0.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// @vitest-environment node

/*
* 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 { assertType, describe, expect, it } from 'vitest';

import type {
NotebookCorePort,
NotebookCoreRemoteProps,
NotebookCoreSnapshot,
NotebookCoreSnapshotListener
} from './public-api';

// assertType does not invoke this callback; tsc checks the rejected mutations.
assertType<(props: NotebookCoreRemoteProps) => void>(props => {
const snapshot = props.core.getSnapshot();

// @ts-expect-error Snapshot note IDs are readonly.
snapshot.noteId = 'another-note';
// @ts-expect-error Snapshot revision IDs are readonly.
snapshot.revisionId = 'another-revision';
// @ts-expect-error The remote cannot replace the shared core port.
props.core = { ...props.core };
// @ts-expect-error The core snapshot reader is readonly.
props.core.getSnapshot = () => snapshot;
// @ts-expect-error The core subscription method is readonly.
props.core.subscribe = () => () => undefined;
});

const fakeCorePort = (initialSnapshot: NotebookCoreSnapshot) => {
let snapshot = initialSnapshot;
const listeners = new Set<NotebookCoreSnapshotListener>();
const core: NotebookCorePort = {
getSnapshot: () => snapshot,
subscribe: listener => {
listeners.add(listener);
return () => listeners.delete(listener);
}
};

return {
core,
publish: (nextSnapshot: NotebookCoreSnapshot) => {
snapshot = nextSnapshot;
for (const listener of listeners) {
listener();
}
}
};
};

describe('notebook core host and remote contract', () => {
it('demonstrates snapshot subscription and cleanup with a fake host-owned port', () => {
const host = fakeCorePort({ noteId: '2A94M5J1Z', revisionId: null });
const remoteProps: NotebookCoreRemoteProps = { core: host.core };
const snapshots: unknown[] = [];

expect(remoteProps.core).toBe(host.core);

const unsubscribe = remoteProps.core.subscribe(() => snapshots.push(remoteProps.core.getSnapshot()));
host.publish({ noteId: '2A94M5J1Z', revisionId: 'rev-1' });
unsubscribe();
host.publish({ noteId: '2A94M5J1Z', revisionId: 'rev-2' });

expect(snapshots).toEqual([{ noteId: '2A94M5J1Z', revisionId: 'rev-1' }]);
expect(remoteProps.core.getSnapshot()).toEqual({ noteId: '2A94M5J1Z', revisionId: 'rev-2' });
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* 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.
*/

export type NotebookCoreSnapshot = Readonly<{
noteId: string;
revisionId: string | null;
}>;

export type NotebookCoreUnsubscribe = () => void;

export type NotebookCoreSnapshotListener = () => void;

export type NotebookCorePort = Readonly<{
getSnapshot: () => NotebookCoreSnapshot;
subscribe: (listener: NotebookCoreSnapshotListener) => NotebookCoreUnsubscribe;
}>;

export type NotebookCoreRemoteProps = Readonly<{
core: NotebookCorePort;
}>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* 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.
*/

export * from './host-remote-contract';
13 changes: 13 additions & 0 deletions zeppelin-web-angular/projects/zeppelin-notebook-core/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "../../out-tsc/lib",
"target": "es2020",
"lib": ["es2020"],
"paths": {},
"declaration": true,
"inlineSources": true,
"types": []
},
"exclude": ["**/*.spec.ts"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/spec/notebook-core",
"types": ["node"]
},
"include": ["src/**/*.spec.ts", "../../test/notebook-core/**/*.ts"],
"exclude": []
}
12 changes: 12 additions & 0 deletions zeppelin-web-angular/projects/zeppelin-react/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions zeppelin-web-angular/projects/zeppelin-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
},
"dependencies": {
"@ant-design/icons": "5.4.0",
"@zeppelin/notebook-core": "file:../zeppelin-notebook-core",
"@zeppelin/sdk": "file:../zeppelin-sdk",
"ansi-to-react": "6.2.6",
"highlight.js": "^10.7.3",
Expand Down
1 change: 1 addition & 0 deletions zeppelin-web-angular/projects/zeppelin-react/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@
export { ConfigurationTable, mount as mountConfigurationTable } from './pages/ConfigurationTable';
export { PublishedParagraph, mount } from './pages/PublishedParagraph';
export { ParagraphFooter, mount as mountParagraphFooter } from './components/paragraph/ParagraphFooter';
export type { NotebookCoreRemoteProps } from './notebookCoreContract';
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* 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.
*/

export type { NotebookCoreRemoteProps } from '@zeppelin/notebook-core';
1 change: 1 addition & 0 deletions zeppelin-web-angular/projects/zeppelin-react/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"baseUrl": "src",
"paths": {
"@/*": ["./*"],
"@zeppelin/notebook-core": ["../../zeppelin-notebook-core/src/public-api.ts"],
"@zeppelin/sdk": ["../../zeppelin-sdk/src"],
"@zeppelin/sdk/*": ["../../zeppelin-sdk/src/*"]
}
Expand Down
Loading
Loading