Automerge backed Markdown editor for the Atmosphere
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628// src/plugin/loader/__tests__/plugin-api-global.test.ts/** * Surface contract for the `window.__REAMS_PLUGIN_API__` shim used by * external plugins loaded via blob URL. Plugins do * `require("obsidian").EditorSuggest` (etc.) and that name MUST resolve * to a real class — otherwise `class X extends obsidian.EditorSuggest` * throws "class heritage … is not an object or null" at module * evaluation time, before `Plugin.onload()` runs, and the install * silently fails (no row in the plugin list, no localStorage entry). * * This test guards the surface so regressions surface here at * commit time, not in production after a real third-party plugin * tries to use the API. * * Real-world repro: obsidian-emoji-shortcodes v2.2.0 by phibr0 * (Plugin Source: https://github.com/phibr0/obsidian-emoji-shortcodes) * imports `EditorSuggest`, `EditorSuggestContext`, `EditorSuggestTriggerInfo` * from 'obsidian'. The first two are CLASSES at runtime (the third is a * type-only export). Without `EditorSuggest` on the shim, install fails. */import { describe, it, expect, beforeAll } from 'vitest'import { installPluginApiGlobal } from '../plugin-api-global'import { WidgetType } from '@codemirror/view'import { EditorState, RangeSetBuilder, RangeValue } from '@codemirror/state'import { syntaxTree } from '@codemirror/language'import { reamsMarkdownLanguage, tokenClassNodeProp } from '../../../editor/markdown-language'
beforeAll(() => { installPluginApiGlobal()})
describe('window.__REAMS_PLUGIN_API__ surface', () => { it('exposes the EditorSuggest framework as runtime classes', () => { // EditorSuggest and PopoverSuggest MUST be present as classes // so plugins can `class X extends obsidian.EditorSuggest<T>`. // This was the missing piece that caused obsidian-emoji-shortcodes // (and any third-party plugin using EditorSuggest) to fail with // "class heritage obsidian.EditorSuggest is not an object or null". const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> expect(typeof api.EditorSuggest).toBe('function') expect(typeof api.PopoverSuggest).toBe('function') })
it('exposes Plugin / Component / Editor as runtime classes', () => { const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> // Sanity baseline — these are the absolute-minimum classes any // non-trivial plugin needs. If they ever go missing this whole // shim is broken, not just one feature. expect(typeof api.Plugin).toBe('function') expect(typeof api.Component).toBe('function') expect(typeof api.Editor).toBe('function') expect(typeof api.App).toBe('function') })
it('exposes Modal / SuggestModal / FuzzySuggestModal as runtime classes', () => { const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> // The non-EditorSuggest popover stack — used by QuickAdd, Periodic // Notes, Calendar, etc. Already present prior to this work but // covered here defensively to keep the contract test self-contained. expect(typeof api.Modal).toBe('function') expect(typeof api.SuggestModal).toBe('function') expect(typeof api.FuzzySuggestModal).toBe('function') })
it('exposes MarkdownRenderChild as a runtime class', () => { // Real-world repro: obsidian42-strange-new-worlds (SNW) does // `class extends Kt.MarkdownRenderChild` where Kt = require('obsidian'). // Without this, install fails at module evaluation with // "Class extends value undefined is not a constructor or null". const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> expect(typeof api.MarkdownRenderChild).toBe('function') })
it('does NOT expose ReamsRequestUrlError on the CJS surface (Reams-specific)', () => { // ReamsRequestUrlError is a Reams-specific error class, NOT part of // Obsidian's public API. Plugins needing it should import from 'reams' // module instead. This test confirms it is NOT exposed on the CJS shim // for require('obsidian'). See upstream contract alignment. const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> expect(api.ReamsRequestUrlError).toBeUndefined() })
it('exposes htmlToMarkdown as the same function reference as the ESM import', async () => { // Realm identity: plugins loaded via the CJS shim path call // `window.__REAMS_PLUGIN_API__.htmlToMarkdown(...)`. Plugins loaded via // the ESM virtual module call `import { htmlToMarkdown } from 'obsidian'` // which resolves through `obsidian-compat/index.ts`. Both must yield the // same function reference; otherwise behavior could silently drift // (different turndown configurations, different defensive guards) and // plugins would observe different output depending on load path. // See chainlink #339 + adversary review round 2 (M-R1). const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> const { htmlToMarkdown: esmFn } = await import('../../obsidian-stubs') expect(api.htmlToMarkdown).toBe(esmFn) })})
describe('window.CodeMirror legacy CM5 stub', () => { it('is installed as a no-op shim by installPluginApiGlobal', () => { // Real-world repro: cm-typewriter-scroll-obsidian touches // `CodeMirror.commands.scrollSelectionToCenter = ...` and // `CodeMirror.defineOption(...)` at module-load time. Without this stub, // those lines throw ReferenceError before the plugin's CM6 path // (registerEditorExtension) ever runs. The stub allows module // evaluation to complete; we never invoke CM5 callbacks. const cm = (window as unknown as { CodeMirror?: Record<string, unknown> }).CodeMirror expect(cm).toBeDefined() expect(typeof cm?.commands).toBe('object') expect(typeof cm?.defineOption).toBe('function') expect(typeof cm?.defineMode).toBe('function') expect(cm?.Init).toBeDefined() })
it('lets plugin module-level CM5 setup execute without throwing', () => { // Mirrors lines 78-89 of cm-typewriter-scroll-obsidian/main.js. expect(() => { const CM = (window as unknown as { CodeMirror: { commands: Record<string, unknown>; defineOption: (...args: unknown[]) => void; defineMode: (...args: unknown[]) => void; Init: unknown } }).CodeMirror CM.commands.scrollSelectionToCenter = function (_cm: unknown) { /* noop */ } CM.defineOption('typewriterScrolling', false, function (_cm: unknown, _val: unknown, old: unknown) { if (old && old !== CM.Init) { /* noop */ } }) CM.defineMode('dataview', () => ({})) }).not.toThrow() })
it('exposes CM6 primitives from @codemirror/state', () => { // Plugins using registerEditorExtension() need CM6 primitives. // Obsidian bundles CM6 and re-exports these via `import { X } from 'obsidian'`. // Real-world example: Typewriter Scroll imports Extension, Transaction. const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> // EditorState is NOT in Obsidian's public API — plugins use require('@codemirror/state') directly expect(api.EditorState).toBeUndefined() // StateField is NOT in Obsidian's public API — plugins use require('@codemirror/state') directly expect(api.StateField).toBeUndefined() // StateEffect is NOT in Obsidian's public API — plugins use require('@codemirror/state') directly expect(api.StateEffect).toBeUndefined() // Facet is NOT in Obsidian's public API — plugins use require('@codemirror/state') directly expect(api.Facet).toBeUndefined() // Transaction is NOT in Obsidian's public API — plugins use require('@codemirror/state') directly expect(api.Transaction).toBeUndefined() // Compartment is NOT in Obsidian's public API — plugins use require('@codemirror/state') directly expect(api.Compartment).toBeUndefined() // Prec is NOT in Obsidian's public API — plugins use require('@codemirror/state') directly expect(api.Prec).toBeUndefined() // RangeSetBuilder and RangeSet are NOT in Obsidian's public API expect(api.RangeSetBuilder).toBeUndefined() expect(api.RangeSet).toBeUndefined() // Text is NOT in Obsidian's public API — plugins use require('@codemirror/state') directly expect(api.Text).toBeUndefined() })
it('exposes CM6 primitives from @codemirror/view', () => { // EditorView is NOT in Obsidian's public API — plugins use require('@codemirror/view') directly const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> expect(api.EditorView).toBeUndefined() // ViewPlugin is NOT part of Obsidian's public API — intentionally excluded // It's available via require('@codemirror/view') for plugins that need it expect(api.ViewPlugin).toBeUndefined() // ViewUpdate is NOT part of Obsidian's public API — intentionally excluded expect(api.ViewUpdate).toBeUndefined() // Decoration is NOT part of Obsidian's public API — intentionally excluded. Plugins use require('@codemirror/view') directly. expect(api.Decoration).toBeUndefined() // WidgetType is NOT part of Obsidian's public API — intentionally excluded expect(api.WidgetType).toBeUndefined() // MatchDecorator is NOT part of Obsidian's public API — intentionally excluded expect(api.MatchDecorator).toBeUndefined() // keymap is NOT part of Obsidian's public API — intentionally excluded // It's available via require('@codemirror/view') for plugins that need it expect(api.keymap).toBeUndefined() // lineNumbers is NOT part of Obsidian's public API — intentionally excluded // It's available via require('@codemirror/view') for plugins that need it expect(api.lineNumbers).toBeUndefined() })})
describe('global DOM helpers', () => { it('creates a detached fragment and populates it through the callback', () => { const createFragment = (window as unknown as { createFragment?: (callback?: (fragment: DocumentFragment) => void) => DocumentFragment }).createFragment expect(typeof createFragment).toBe('function')
const fragment = createFragment!((root) => { const span = document.createElement('span') span.textContent = 'Dataview' root.appendChild(span) }) expect(fragment).toBeInstanceOf(DocumentFragment) expect(fragment.firstElementChild?.textContent).toBe('Dataview') expect(fragment.isConnected).toBe(false) })})
describe('window.__REAMS_CM6_MODULES__', () => { it('is installed by installPluginApiGlobal', () => { // CM6 modules are installed alongside the plugin API expect(window.__REAMS_CM6_MODULES__).toBeDefined() expect(typeof window.__REAMS_CM6_MODULES__).toBe('object') })
it('exposes @codemirror/state module for require()', () => { // Plugins do `require('@codemirror/state')` — this is the runtime object const stateModule = window.__REAMS_CM6_MODULES__?.['@codemirror/state'] expect(stateModule).toBeDefined() expect(stateModule?.EditorState).toBeDefined() expect(stateModule?.StateField).toBeDefined() expect(stateModule?.Facet).toBeDefined() expect(stateModule?.Transaction).toBeDefined() // Compartment is NOT in Obsidian's 'obsidian' module, but IS available via require('@codemirror/state') expect(stateModule?.Compartment).toBeDefined() expect(stateModule?.RangeValue).toBeDefined() expect(stateModule?.RangeSetBuilder).toBeDefined() expect(stateModule?.RangeValue).toBe(RangeValue) expect(stateModule?.RangeSetBuilder).toBe(RangeSetBuilder) const PluginRangeValue = class extends (stateModule?.RangeValue as typeof RangeValue) { eq() { return true } } const Builder = stateModule?.RangeSetBuilder as typeof RangeSetBuilder const builder = new Builder() builder.add(0, 1, new PluginRangeValue()) expect(builder.finish().size).toBe(1) })
it('exposes @codemirror/view module for require()', () => { // Plugins do `require('@codemirror/view')` — Typewriter Scroll uses this const viewModule = window.__REAMS_CM6_MODULES__?.['@codemirror/view'] expect(viewModule).toBeDefined() expect(viewModule?.EditorView).toBeDefined() expect(viewModule?.ViewPlugin).toBeDefined() expect(viewModule?.Decoration).toBeDefined() expect(viewModule?.WidgetType).toBeDefined() expect(viewModule?.WidgetType).toBe(WidgetType) const PluginWidget = class extends (viewModule?.WidgetType as typeof WidgetType) { toDOM() { return document.createElement('span') } } expect(new PluginWidget()).toBeInstanceOf(WidgetType) })
it('exposes @codemirror/language module for require()', () => { const langModule = window.__REAMS_CM6_MODULES__?.['@codemirror/language'] expect(langModule).toBeDefined() expect(typeof langModule?.syntaxTree).toBe('function') expect(langModule?.syntaxTree).toBe(syntaxTree) expect(typeof langModule?.Language).toBe('function') expect(typeof langModule?.HighlightStyle).toBe('function') // class })
it('preserves native Lezer iterate callback values for both access styles', () => { const state = EditorState.create({ doc: '`= this.priority`', extensions: [reamsMarkdownLanguage], }) const names: string[] = [] const nestedNames: string[] = [] syntaxTree(state).iterate({ enter: cursor => { names.push(cursor.name) }, }) syntaxTree(state).iterate({ enter: ({ node }) => { nestedNames.push(node.name) }, }) expect(names).toContain('InlineCode') expect(nestedNames).toContain('InlineCode')
const inlineCode: Array<{ from: number; to: number; text: string; tokenClass?: string }> = [] syntaxTree(state).iterate({ enter: ({ node }) => { if (node.name === 'InlineCode') { inlineCode.push({ from: node.from, to: node.to, text: state.doc.sliceString(node.from, node.to), tokenClass: node.type.prop(tokenClassNodeProp), }) } }, }) expect(inlineCode).toEqual([{ from: 1, to: 16, text: '= this.priority', tokenClass: 'inline-code' }]) })
it('exposes @codemirror/commands module for require()', () => { const cmdsModule = window.__REAMS_CM6_MODULES__?.['@codemirror/commands'] expect(cmdsModule).toBeDefined() expect(typeof cmdsModule?.defaultKeymap).toBe('object') // array of keymaps // NOTE: history is NOT part of Obsidian's public API expect(cmdsModule?.history).toBeUndefined() expect(typeof cmdsModule?.undo).toBe('function') expect(typeof cmdsModule?.redo).toBe('function') })
// @lezer/highlight is NOT part of Obsidian's public API and is not exposed. // classHighlighter, Tag, tags, highlightTree, and tagHighlighter should be // imported directly from @lezer/highlight, not through the CJS shim. it('@lezer/highlight module is NOT exposed (not in Obsidian public API)', () => { const hlModule = window.__REAMS_CM6_MODULES__?.['@lezer/highlight'] expect(hlModule).toBeUndefined() })
it('classHighlighter is NOT on the CJS shim (not in Obsidian public API)', () => { const api = (window as { __REAMS_PLUGIN_API__?: Record<string, unknown> }) .__REAMS_PLUGIN_API__ expect(api).toBeDefined() expect(api!['classHighlighter']).toBeUndefined() })
it('highlightTree is NOT on the CJS shim (not in Obsidian public API)', () => { const api = (window as { __REAMS_PLUGIN_API__?: Record<string, unknown> }) .__REAMS_PLUGIN_API__ expect(api).toBeDefined() expect(api!['highlightTree']).toBeUndefined() })
it('exposes @lezer/common module for require()', () => { const commonModule = window.__REAMS_CM6_MODULES__?.['@lezer/common'] expect(commonModule).toBeDefined() // NOTE: Tree is NOT part of Obsidian's public API. Plugins needing Tree should // import directly from @lezer/common. expect(commonModule?.Tree).toBeUndefined() expect(typeof commonModule?.NodeType).toBe('function') // NodeType is a class expect(typeof commonModule?.NodeProp).toBe('function') })
it('exposes loadMathJax as the same function reference as the ESM import', async () => { // Realm identity: CJS and ESM paths must expose the same function // reference so plugins using either path get consistent behavior. const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> expect(typeof api.loadMathJax).toBe('function')
const { loadMathJax: esmFn } = await import('../../obsidian-stubs') expect(api.loadMathJax).toBe(esmFn) })})
describe('Bases Value types (Obsidian 1.10.0+)', () => { it('exposes Value and NullValue as runtime classes', () => { // Value is the abstract base class for the Bases value system. // NullValue represents null values in Bases views. // Plugins may extend Value or use NullValue.value singleton. const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> expect(typeof api.Value).toBe('function') expect(typeof api.NullValue).toBe('function') })
it('exposes Value and NullValue as the same identity as ESM import', async () => { // Realm identity: CJS and ESM paths must expose the same class // reference so plugins using either path get consistent instanceof behavior. const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> const { Value: esmValue, NullValue: esmNullValue } = await import('../../obsidian-stubs') expect(api.Value).toBe(esmValue) expect(api.NullValue).toBe(esmNullValue) })
it('NullValue.value is a singleton instance of NullValue', () => { const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> const NullValueClass = api.NullValue as { value: { isTruthy: () => boolean; toString: () => string } } expect(NullValueClass.value).toBeDefined() expect(NullValueClass.value.toString()).toBe('') expect(NullValueClass.value.isTruthy()).toBe(false) })
it('Value.equals handles null values correctly', () => { const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> const ValueClass = api.Value as { equals: (a: unknown, b: unknown) => boolean } const NullValueClass = api.NullValue as { value: unknown }
// null == null is true expect(ValueClass.equals(null, null)).toBe(true) // null != NullValue.value (NullValue is a Value, not null) expect(ValueClass.equals(null, NullValueClass.value)).toBe(false) expect(ValueClass.equals(NullValueClass.value, null)).toBe(false) })
it('exposes NotNullValue and RegExpValue as runtime classes', () => { // NotNullValue is the abstract base class for non-null values. // RegExpValue wraps a RegExp pattern. const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> expect(typeof api.NotNullValue).toBe('function') expect(typeof api.RegExpValue).toBe('function') })
it('exposes NotNullValue and RegExpValue as the same identity as ESM import', async () => { // Realm identity: CJS and ESM paths must expose the same class // reference so plugins using either path get consistent instanceof behavior. const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> const { NotNullValue: esmNotNull, RegExpValue: esmRegExp } = await import('../../obsidian-stubs') expect(api.NotNullValue).toBe(esmNotNull) expect(api.RegExpValue).toBe(esmRegExp) })
it('RegExpValue extends NotNullValue extends Value', () => { const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> // eslint-disable-next-line @typescript-eslint/no-explicit-any const RegExpValueClass = api.RegExpValue as new () => any const instance = new RegExpValueClass() expect(instance instanceof (api.NotNullValue as Function)).toBe(true) expect(instance instanceof (api.Value as Function)).toBe(true) })
it('RegExpValue.isTruthy() returns true and toString() returns empty string', () => { const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> const RegExpValueClass = api.RegExpValue as new () => { isTruthy: () => boolean; toString: () => string } const instance = new RegExpValueClass() // RegExpValue is always truthy (even if it wraps an empty pattern) expect(instance.isTruthy()).toBe(true) // toString returns empty string (stub implementation) expect(instance.toString()).toBe('') })
it('exposes FileValue as a runtime class', () => { // FileValue wraps a file reference in Obsidian. // Used by Bases for file property values. const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> expect(typeof api.FileValue).toBe('function') })
it('exposes FileValue as the same identity as ESM import', async () => { // Realm identity: CJS and ESM paths must expose the same class // reference so plugins using either path get consistent instanceof behavior. const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> const { FileValue: esmFileValue } = await import('../../obsidian-stubs') expect(api.FileValue).toBe(esmFileValue) })
it('FileValue extends NotNullValue extends Value', () => { const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> // eslint-disable-next-line @typescript-eslint/no-explicit-any const FileValueClass = api.FileValue as new () => any const instance = new FileValueClass() expect(instance instanceof (api.NotNullValue as Function)).toBe(true) expect(instance instanceof (api.Value as Function)).toBe(true) })
it('FileValue.isTruthy() returns true and toString() returns empty string', () => { const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> const FileValueClass = api.FileValue as new () => { isTruthy: () => boolean; toString: () => string } const instance = new FileValueClass() // FileValue is always truthy (file references are non-null) expect(instance.isTruthy()).toBe(true) // toString returns empty string (stub implementation) expect(instance.toString()).toBe('') })
it('exposes BooleanValue as a runtime class', () => { // BooleanValue wraps a boolean primitive. // Used by Bases for checkbox/toggle values. const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> expect(typeof api.BooleanValue).toBe('function') })
it('exposes BooleanValue as the same identity as ESM import', async () => { // Realm identity: CJS and ESM paths must expose the same class // reference so plugins using either path get consistent instanceof behavior. const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> const { BooleanValue: esmBooleanValue } = await import('../../obsidian-stubs') expect(api.BooleanValue).toBe(esmBooleanValue) })
it('BooleanValue extends PrimitiveValue extends NotNullValue extends Value', () => { const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> // eslint-disable-next-line @typescript-eslint/no-explicit-any const BooleanValueClass = api.BooleanValue as new (value: boolean) => any const instance = new BooleanValueClass(true) expect(instance instanceof (api.PrimitiveValue as Function)).toBe(true) expect(instance instanceof (api.NotNullValue as Function)).toBe(true) expect(instance instanceof (api.Value as Function)).toBe(true) })
it('BooleanValue.type is "boolean"', () => { const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> const BooleanValueClass = api.BooleanValue as { type: string } expect(BooleanValueClass.type).toBe('boolean') })
it('exposes NumberValue as a runtime class', () => { // NumberValue wraps a number primitive. // Used by Bases for numeric values. const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> expect(typeof api.NumberValue).toBe('function') })
it('exposes NumberValue as the same identity as ESM import', async () => { // Realm identity: CJS and ESM paths must expose the same class // reference so plugins using either path get consistent instanceof behavior. const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> const { NumberValue: esmNumberValue } = await import('../../obsidian-stubs') expect(api.NumberValue).toBe(esmNumberValue) })
it('NumberValue extends PrimitiveValue extends NotNullValue extends Value', () => { const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> // eslint-disable-next-line @typescript-eslint/no-explicit-any const NumberValueClass = api.NumberValue as new (value: number) => any const instance = new NumberValueClass(42) expect(instance instanceof (api.PrimitiveValue as Function)).toBe(true) expect(instance instanceof (api.NotNullValue as Function)).toBe(true) expect(instance instanceof (api.Value as Function)).toBe(true) })
it('NumberValue.type is "number"', () => { const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> const NumberValueClass = api.NumberValue as { type: string } expect(NumberValueClass.type).toBe('number') })})
describe('BasesEntry and BasesEntryGroup (Obsidian 1.10.0+)', () => { it('exposes BasesEntry as a runtime class', () => { // BasesEntry represents a single "row" or file in a Base. // Plugins use it to access file data and property values. const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> expect(typeof api.BasesEntry).toBe('function') })
it('exposes BasesEntry as the same identity as ESM import', async () => { // Realm identity: CJS and ESM paths must expose the same class // reference so plugins using either path get consistent instanceof behavior. const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> const { BasesEntry: esmBasesEntry } = await import('../../obsidian-stubs') expect(api.BasesEntry).toBe(esmBasesEntry) })
it('BasesEntry.file throws (stub implementation)', () => { const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> // eslint-disable-next-line @typescript-eslint/no-explicit-any const BasesEntryClass = api.BasesEntry as new () => any const entry = new BasesEntryClass() expect(() => entry.file).toThrow('BasesEntry.file: Not implemented in Reams') })
it('BasesEntry.getValue() throws (stub implementation)', () => { const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> // eslint-disable-next-line @typescript-eslint/no-explicit-any const BasesEntryClass = api.BasesEntry as new () => any const entry = new BasesEntryClass() expect(() => entry.getValue('note.test')).toThrow('BasesEntry.getValue: Not implemented in Reams') })
it('exposes BasesEntryGroup as a runtime class', () => { // BasesEntryGroup groups BasesEntry objects by a groupBy key value. // Plugins use it to display grouped data in Base views. const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> expect(typeof api.BasesEntryGroup).toBe('function') })
it('exposes BasesEntryGroup as the same identity as ESM import', async () => { // Realm identity: CJS and ESM paths must expose the same class // reference so plugins using either path get consistent instanceof behavior. const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> const { BasesEntryGroup: esmBasesEntryGroup } = await import('../../obsidian-stubs') expect(api.BasesEntryGroup).toBe(esmBasesEntryGroup) })
it('BasesEntryGroup.entries is initialized to empty array', () => { const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> // eslint-disable-next-line @typescript-eslint/no-explicit-any const BasesEntryGroupClass = api.BasesEntryGroup as new () => any const group = new BasesEntryGroupClass() expect(group.entries).toEqual([]) })
it('BasesEntryGroup.key is initially undefined', () => { const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> // eslint-disable-next-line @typescript-eslint/no-explicit-any const BasesEntryGroupClass = api.BasesEntryGroup as new () => any const group = new BasesEntryGroupClass() expect(group.key).toBeUndefined() })
it('BasesEntryGroup.hasKey() returns false when key is undefined', () => { const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> // eslint-disable-next-line @typescript-eslint/no-explicit-any const BasesEntryGroupClass = api.BasesEntryGroup as new () => any const group = new BasesEntryGroupClass() expect(group.hasKey()).toBe(false) })
it('BasesEntryGroup.hasKey() returns false when key is NullValue', () => { const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> // eslint-disable-next-line @typescript-eslint/no-explicit-any const BasesEntryGroupClass = api.BasesEntryGroup as new () => any const NullValueClass = api.NullValue as { value: unknown } const group = new BasesEntryGroupClass() group.key = NullValueClass.value expect(group.hasKey()).toBe(false) })
it('BasesEntryGroup.hasKey() returns true when key is a non-null value', () => { const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> // eslint-disable-next-line @typescript-eslint/no-explicit-any const BasesEntryGroupClass = api.BasesEntryGroup as new () => any const StringValueClass = api.StringValue as new (value: string) => unknown const group = new BasesEntryGroupClass() group.key = new StringValueClass('test') expect(group.hasKey()).toBe(true) })})
describe('QueryController (Obsidian 1.10.0+)', () => { it('exposes QueryController as a runtime class', () => { // QueryController is responsible for executing Bases queries and // evaluating filters and formulas. Notifies views of updated results. const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> expect(typeof api.QueryController).toBe('function') })
it('exposes QueryController as the same identity as ESM import', async () => { // Realm identity: CJS and ESM paths must expose the same class // reference so plugins using either path get consistent instanceof behavior. const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> const { QueryController: esmQueryController } = await import('../../obsidian-stubs') expect(api.QueryController).toBe(esmQueryController) })
it('QueryController extends Component', () => { // QueryController extends Component to participate in the lifecycle system. const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> // eslint-disable-next-line @typescript-eslint/no-explicit-any const QueryControllerClass = api.QueryController as new () => any // eslint-disable-next-line @typescript-eslint/no-explicit-any const ComponentClass = api.Component as new () => any const controller = new QueryControllerClass() expect(controller).toBeInstanceOf(ComponentClass) })})