diff --git a/zeppelin-web-angular/AGENTS.md b/zeppelin-web-angular/AGENTS.md index c6fbaf21ba1..fa2280ffeb3 100644 --- a/zeppelin-web-angular/AGENTS.md +++ b/zeppelin-web-angular/AGENTS.md @@ -84,6 +84,10 @@ A spec with no assertion, or one whose assertion sits inside an `if`, passes by The e2e suite gets the same protection from `eslint-plugin-playwright`. +## monaco-editor and path aliases in specs + +`vitest.shell.config.mts` mirrors the `paths` block in `tsconfig.base.json`, which Vite does not read; add an alias there when you add a path. Eleven files under `src/` import `monaco-editor`, two behind the `@zeppelin/services` barrel, so a spec that reaches the editor or notebook area loads it: a few seconds on first import and a `marked.umd.js.map` sourcemap warning, both monaco's, not ours. Mock it with `vi.mock('monaco-editor', ...)` when the spec only needs to assert the editor was called. The runner resolves monaco to `editor.api`, which skips the language registrations `editor.main` performs, so do not assert that a built-in language is registered. + ## Determinism No clock, no randomness, no network. A spec that reads `Date.now()` or fetches will eventually fail for reasons unrelated to the code under test. diff --git a/zeppelin-web-angular/src/app/services/array-ordering.service.spec.ts b/zeppelin-web-angular/src/app/services/array-ordering.service.spec.ts new file mode 100644 index 00000000000..0508814f40d --- /dev/null +++ b/zeppelin-web-angular/src/app/services/array-ordering.service.spec.ts @@ -0,0 +1,77 @@ +/* + * 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 { beforeEach, describe, expect, it } from 'vitest'; +import { NodeItem } from '@zeppelin/interfaces'; +// Through the barrel, not the neighbouring file: +// that is what walks the alias chain to monaco and @zeppelin/sdk, +// so this spec fails if either stops resolving. +import { ArrayOrderingService } from '@zeppelin/services'; + +const TRASH_FOLDER_ID = '~Trash'; + +const note = (id: string, title: string, children?: NodeItem[]): NodeItem => ({ id, title, children }) as NodeItem; + +describe('ArrayOrderingService', () => { + let service: ArrayOrderingService; + + beforeEach(() => { + service = new ArrayOrderingService(TRASH_FOLDER_ID); + }); + + describe('getNoteName', () => { + it('returns the title when it has one', () => { + expect(service.getNoteName(note('a1', 'My note'))).toBe('My note'); + }); + + it('falls back to the id when the title is blank', () => { + expect(service.getNoteName(note('a1', ' '))).toBe('Note a1'); + }); + }); + + describe('noteListOrdering', () => { + it('sorts the trash folder last by returning the highest code point', () => { + expect(service.noteListOrdering(note(TRASH_FOLDER_ID, 'Trash'))).toBe('￿'); + }); + + it('orders every other node by its display name', () => { + expect(service.noteListOrdering(note('a1', 'My note'))).toBe('My note'); + }); + }); + + describe('noteComparator', () => { + it('puts the trash folder after anything else, whichever side it is on', () => { + const trash = note(TRASH_FOLDER_ID, 'Trash'); + const other = note('a1', 'My note'); + + expect(service.noteComparator(trash, other)).toBe(1); + expect(service.noteComparator(other, trash)).toBe(-1); + }); + + it('puts folders before notes', () => { + const folder = note('f1', 'Folder', []); + const leaf = note('a1', 'Note'); + + expect(service.noteComparator(leaf, folder)).toBe(1); + expect(service.noteComparator(folder, leaf)).toBe(-1); + }); + + it('compares two nodes of the same kind by display name', () => { + expect(service.noteComparator(note('a1', 'Alpha'), note('a2', 'Beta'))).toBeLessThan(0); + }); + + it('uses the id fallback when a title is blank', () => { + // 'Note a1' sorts before 'Zebra', which the raw empty title would not. + expect(service.noteComparator(note('a1', ''), note('a2', 'Zebra'))).toBeLessThan(0); + }); + }); +}); diff --git a/zeppelin-web-angular/vitest.shell.config.mts b/zeppelin-web-angular/vitest.shell.config.mts index 0c035c2b2b6..a85648ced89 100644 --- a/zeppelin-web-angular/vitest.shell.config.mts +++ b/zeppelin-web-angular/vitest.shell.config.mts @@ -11,9 +11,42 @@ */ // vite is pinned in package.json: the React remote keeps its own lockfile and drifted to a different minor. +import { fileURLToPath } from 'node:url'; import { defineConfig } from 'vitest/config'; export default defineConfig({ + // Mirrors the `paths` block in tsconfig.base.json, which Vite does not read. + // The two library aliases resolve to source, not `dist/`, so a unit run does not wait on a build. + resolve: { + alias: [ + // monaco-editor ships no `main` and no `exports` (microsoft/monaco-editor#4848). + // `module` names editor.main, which boots the full editor and dies in jsdom. + // 0.55 restores resolution but still points there, so revisit rather than drop. + { + find: /^monaco-editor$/, + replacement: fileURLToPath(new URL('./node_modules/monaco-editor/esm/vs/editor/editor.api.js', import.meta.url)) + }, + { find: /^@zeppelin\/sdk$/, replacement: fileURLToPath(new URL('./projects/zeppelin-sdk/src', import.meta.url)) }, + { + find: /^@zeppelin\/sdk\/(.*)$/, + replacement: `${fileURLToPath(new URL('./projects/zeppelin-sdk/src', import.meta.url))}/$1` + }, + { + find: /^@zeppelin\/visualization$/, + replacement: fileURLToPath(new URL('./projects/zeppelin-visualization/src', import.meta.url)) + }, + { + find: /^@zeppelin\/visualization\/(.*)$/, + replacement: `${fileURLToPath(new URL('./projects/zeppelin-visualization/src', import.meta.url))}/$1` + }, + // `@zeppelin/*` falls back to src/environments in tsconfig; Vite aliases do not. + { + find: /^@zeppelin\/environment$/, + replacement: fileURLToPath(new URL('./src/environments/environment.ts', import.meta.url)) + }, + { find: /^@zeppelin\/(.*)$/, replacement: `${fileURLToPath(new URL('./src/app', import.meta.url))}/$1` } + ] + }, // 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".