Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
076fbec89f |
+84
@@ -0,0 +1,84 @@
|
||||
import { UmbConditionBase } from './condition-base.controller.js';
|
||||
import { expect, fixture } from '@open-wc/testing';
|
||||
import { UmbControllerHostElementMixin, type UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { customElement, html } from '@umbraco-cms/backoffice/external/lit';
|
||||
import type { UmbConditionConfigBase } from '@umbraco-cms/backoffice/extension-api';
|
||||
|
||||
@customElement('umb-test-condition-base-host')
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
class UmbTestConditionBaseHostElement extends UmbControllerHostElementMixin(HTMLElement) {}
|
||||
|
||||
class UmbTestCondition extends UmbConditionBase<UmbConditionConfigBase> {}
|
||||
|
||||
describe('UmbConditionBase', () => {
|
||||
let host: UmbControllerHostElement;
|
||||
const config: UmbConditionConfigBase = { alias: 'Umb.Test.Condition' };
|
||||
|
||||
beforeEach(async () => {
|
||||
host = await fixture(html`<umb-test-condition-base-host></umb-test-condition-base-host>`);
|
||||
});
|
||||
|
||||
it('exposes the config it was constructed with', () => {
|
||||
const condition = new UmbTestCondition(host, { config, onChange: () => {} });
|
||||
expect(condition.config).to.equal(config);
|
||||
});
|
||||
|
||||
it('initializes with permitted=false', () => {
|
||||
const condition = new UmbTestCondition(host, { config, onChange: () => {} });
|
||||
expect(condition.permitted).to.be.false;
|
||||
});
|
||||
|
||||
it('invokes onChange when permitted transitions to a new value', () => {
|
||||
let calls: Array<boolean> = [];
|
||||
const condition = new UmbTestCondition(host, {
|
||||
config,
|
||||
onChange: (permitted) => calls.push(permitted),
|
||||
});
|
||||
|
||||
condition.permitted = true;
|
||||
expect(calls).to.eql([true]);
|
||||
|
||||
condition.permitted = false;
|
||||
expect(calls).to.eql([true, false]);
|
||||
});
|
||||
|
||||
it('does NOT invoke onChange when permitted is set to the current value', () => {
|
||||
let callCount = 0;
|
||||
const condition = new UmbTestCondition(host, {
|
||||
config,
|
||||
onChange: () => callCount++,
|
||||
});
|
||||
|
||||
// Same as initial value (false) — should not fire.
|
||||
condition.permitted = false;
|
||||
expect(callCount).to.equal(0);
|
||||
|
||||
// Real transition — should fire once.
|
||||
condition.permitted = true;
|
||||
expect(callCount).to.equal(1);
|
||||
|
||||
// Same value again — should not fire.
|
||||
condition.permitted = true;
|
||||
expect(callCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('does not invoke onChange after destroy()', () => {
|
||||
let callCount = 0;
|
||||
const condition = new UmbTestCondition(host, {
|
||||
config,
|
||||
onChange: () => callCount++,
|
||||
});
|
||||
|
||||
condition.destroy();
|
||||
// After destroy the internal onChange reference is cleared, so further
|
||||
// permitted writes must not trigger any callback.
|
||||
condition.permitted = true;
|
||||
expect(callCount).to.equal(0);
|
||||
});
|
||||
|
||||
it('clears its config reference on destroy', () => {
|
||||
const condition = new UmbTestCondition(host, { config, onChange: () => {} });
|
||||
condition.destroy();
|
||||
expect(condition.config).to.be.undefined;
|
||||
});
|
||||
});
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
import { UmbDelayCondition, type DelayConditionConfig } from './delay.condition.js';
|
||||
import { aTimeout, expect, fixture } from '@open-wc/testing';
|
||||
import { UmbControllerHostElementMixin, type UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { customElement, html } from '@umbraco-cms/backoffice/external/lit';
|
||||
|
||||
@customElement('umb-test-delay-condition-host')
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
class UmbTestDelayConditionHostElement extends UmbControllerHostElementMixin(HTMLElement) {}
|
||||
|
||||
const baseConfig = (offset: string): DelayConditionConfig => ({
|
||||
alias: 'Umb.Condition.Delay',
|
||||
offset,
|
||||
});
|
||||
|
||||
describe('UmbDelayCondition', () => {
|
||||
let host: UmbControllerHostElement;
|
||||
|
||||
beforeEach(async () => {
|
||||
host = await fixture(html`<umb-test-delay-condition-host></umb-test-delay-condition-host>`);
|
||||
});
|
||||
|
||||
it('starts not permitted', () => {
|
||||
const condition = new UmbDelayCondition(host, {
|
||||
config: baseConfig('30'),
|
||||
onChange: () => {},
|
||||
});
|
||||
expect(condition.permitted).to.be.false;
|
||||
condition.destroy();
|
||||
});
|
||||
|
||||
it('becomes permitted after the configured offset has elapsed', async () => {
|
||||
const offsetMs = 30;
|
||||
const transitions: Array<boolean> = [];
|
||||
|
||||
const condition = new UmbDelayCondition(host, {
|
||||
config: baseConfig(String(offsetMs)),
|
||||
onChange: (permitted) => transitions.push(permitted),
|
||||
});
|
||||
|
||||
// Not yet — well before the timer fires.
|
||||
await aTimeout(5);
|
||||
expect(condition.permitted).to.be.false;
|
||||
expect(transitions).to.eql([]);
|
||||
|
||||
// Wait long enough for the timer (with a generous margin to keep the test stable).
|
||||
await aTimeout(offsetMs + 50);
|
||||
expect(condition.permitted).to.be.true;
|
||||
expect(transitions).to.eql([true]);
|
||||
|
||||
condition.destroy();
|
||||
});
|
||||
|
||||
it('does not fire onChange after destroy() when destroyed before the timer elapses', async () => {
|
||||
let callCount = 0;
|
||||
const condition = new UmbDelayCondition(host, {
|
||||
config: baseConfig('30'),
|
||||
onChange: () => callCount++,
|
||||
});
|
||||
|
||||
condition.destroy();
|
||||
// Wait past the original offset — no callback should run.
|
||||
await aTimeout(60);
|
||||
expect(callCount).to.equal(0);
|
||||
});
|
||||
|
||||
it('throws when offset is not a positive number', () => {
|
||||
expect(
|
||||
() => new UmbDelayCondition(host, { config: baseConfig('0'), onChange: () => {} }),
|
||||
).to.throw(/Offset must be a positive number/);
|
||||
|
||||
expect(
|
||||
() => new UmbDelayCondition(host, { config: baseConfig('-5'), onChange: () => {} }),
|
||||
).to.throw(/Offset must be a positive number/);
|
||||
|
||||
expect(
|
||||
() => new UmbDelayCondition(host, { config: baseConfig('not-a-number'), onChange: () => {} }),
|
||||
).to.throw(/Offset must be a positive number/);
|
||||
});
|
||||
});
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
import { UmbSwitchCondition, type SwitchConditionConfig } from './switch.condition.js';
|
||||
import { aTimeout, expect, fixture } from '@open-wc/testing';
|
||||
import { UmbControllerHostElementMixin, type UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { customElement, html } from '@umbraco-cms/backoffice/external/lit';
|
||||
|
||||
@customElement('umb-test-switch-condition-host')
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
class UmbTestSwitchConditionHostElement extends UmbControllerHostElementMixin(HTMLElement) {}
|
||||
|
||||
const baseConfig = (frequency: string): SwitchConditionConfig => ({
|
||||
alias: 'Umb.Condition.Switch',
|
||||
frequency,
|
||||
});
|
||||
|
||||
describe('UmbSwitchCondition', () => {
|
||||
let host: UmbControllerHostElement;
|
||||
|
||||
beforeEach(async () => {
|
||||
host = await fixture(html`<umb-test-switch-condition-host></umb-test-switch-condition-host>`);
|
||||
});
|
||||
|
||||
it('starts not permitted', () => {
|
||||
const condition = new UmbSwitchCondition(host, {
|
||||
config: baseConfig('30'),
|
||||
onChange: () => {},
|
||||
});
|
||||
expect(condition.permitted).to.be.false;
|
||||
condition.destroy();
|
||||
});
|
||||
|
||||
it('flips between permitted=true and permitted=false at the configured frequency', async () => {
|
||||
// Wait for two transitions (false→true→false) and assert the sequence.
|
||||
// We listen to onChange instead of polling `permitted` at fixed times,
|
||||
// since cumulative wait drift would otherwise make assertions racy.
|
||||
const frequencyMs = 30;
|
||||
const transitions: Array<boolean> = [];
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject(new Error('Switch condition did not flip twice in time')), 1000);
|
||||
|
||||
const condition = new UmbSwitchCondition(host, {
|
||||
config: baseConfig(String(frequencyMs)),
|
||||
onChange: (permitted) => {
|
||||
transitions.push(permitted);
|
||||
if (transitions.length === 2) {
|
||||
clearTimeout(timeout);
|
||||
condition.destroy();
|
||||
resolve();
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(transitions).to.eql([true, false]);
|
||||
});
|
||||
|
||||
it('does not flip after destroy()', async () => {
|
||||
const frequencyMs = 30;
|
||||
let callCount = 0;
|
||||
|
||||
const condition = new UmbSwitchCondition(host, {
|
||||
config: baseConfig(String(frequencyMs)),
|
||||
onChange: () => callCount++,
|
||||
});
|
||||
|
||||
// Destroy before the first transition fires.
|
||||
condition.destroy();
|
||||
await aTimeout(frequencyMs * 3 + 50);
|
||||
expect(callCount).to.equal(0);
|
||||
});
|
||||
|
||||
it('throws when frequency is not a positive number', () => {
|
||||
expect(
|
||||
() => new UmbSwitchCondition(host, { config: baseConfig('0'), onChange: () => {} }),
|
||||
).to.throw(/Frequency must be a positive number/);
|
||||
|
||||
expect(
|
||||
() => new UmbSwitchCondition(host, { config: baseConfig('-5'), onChange: () => {} }),
|
||||
).to.throw(/Frequency must be a positive number/);
|
||||
|
||||
expect(
|
||||
() => new UmbSwitchCondition(host, { config: baseConfig('NaN-string'), onChange: () => {} }),
|
||||
).to.throw(/Frequency must be a positive number/);
|
||||
});
|
||||
});
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
import { UmbAppEntryPointExtensionInitializer } from './app-entry-point-extension-initializer.js';
|
||||
import type { ManifestAppEntryPoint } from '../extensions/app-entry-point.extension.js';
|
||||
import { aTimeout, expect, fixture } from '@open-wc/testing';
|
||||
import { customElement, html } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbElementMixin, type UmbElement } from '@umbraco-cms/backoffice/element-api';
|
||||
import { UmbExtensionRegistry } from '@umbraco-cms/backoffice/extension-api';
|
||||
|
||||
@customElement('umb-test-app-entry-point-host')
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
class UmbTestAppEntryPointHostElement extends UmbElementMixin(HTMLElement) {}
|
||||
|
||||
const ALIAS = 'Umb.Test.AppEntryPoint';
|
||||
|
||||
const waitLoaded = (initializer: UmbAppEntryPointExtensionInitializer) =>
|
||||
new Promise<void>((resolve) => {
|
||||
const sub = initializer.loaded.subscribe((value) => {
|
||||
if (value === true) {
|
||||
resolve();
|
||||
queueMicrotask(() => sub.unsubscribe());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('UmbAppEntryPointExtensionInitializer', () => {
|
||||
let host: UmbElement;
|
||||
let registry: UmbExtensionRegistry<ManifestAppEntryPoint>;
|
||||
|
||||
beforeEach(async () => {
|
||||
host = (await fixture(html`<umb-test-app-entry-point-host></umb-test-app-entry-point-host>`)) as UmbElement;
|
||||
registry = new UmbExtensionRegistry();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
registry.clear();
|
||||
});
|
||||
|
||||
it('only observes manifests of type "appEntryPoint"', async () => {
|
||||
let called = false;
|
||||
const initializer = new UmbAppEntryPointExtensionInitializer(host, registry);
|
||||
|
||||
// Register a non-appEntryPoint manifest — must NOT trigger onInit.
|
||||
registry.register({
|
||||
type: 'backofficeEntryPoint',
|
||||
alias: 'Umb.Test.NotMe',
|
||||
name: 'should-not-fire',
|
||||
js: { onInit: () => (called = true) } as never,
|
||||
} as never);
|
||||
|
||||
await aTimeout(20);
|
||||
expect(called).to.be.false;
|
||||
initializer.destroy();
|
||||
});
|
||||
|
||||
it('calls onInit on the resolved module with (host, registry) when an appEntryPoint is registered', async () => {
|
||||
const initializer = new UmbAppEntryPointExtensionInitializer(host, registry);
|
||||
|
||||
const onInitArgs: Array<[unknown, unknown]> = [];
|
||||
const moduleInstance = {
|
||||
onInit: (h: unknown, r: unknown) => onInitArgs.push([h, r]),
|
||||
onUnload: () => {},
|
||||
};
|
||||
|
||||
registry.register({
|
||||
type: 'appEntryPoint',
|
||||
alias: ALIAS,
|
||||
name: 'test-app-entry-point',
|
||||
js: moduleInstance,
|
||||
} as ManifestAppEntryPoint);
|
||||
|
||||
await waitLoaded(initializer);
|
||||
expect(onInitArgs.length).to.equal(1);
|
||||
expect(onInitArgs[0][0]).to.equal(host);
|
||||
expect(onInitArgs[0][1]).to.equal(registry);
|
||||
|
||||
initializer.destroy();
|
||||
});
|
||||
|
||||
it('calls onUnload when an appEntryPoint manifest is unregistered', async () => {
|
||||
const initializer = new UmbAppEntryPointExtensionInitializer(host, registry);
|
||||
|
||||
let unloadCount = 0;
|
||||
const moduleInstance = {
|
||||
onInit: () => {},
|
||||
onUnload: () => unloadCount++,
|
||||
};
|
||||
|
||||
registry.register({
|
||||
type: 'appEntryPoint',
|
||||
alias: ALIAS,
|
||||
name: 'test-app-entry-point',
|
||||
js: moduleInstance,
|
||||
} as ManifestAppEntryPoint);
|
||||
|
||||
await waitLoaded(initializer);
|
||||
expect(unloadCount).to.equal(0);
|
||||
|
||||
registry.unregister(ALIAS);
|
||||
// Give the observer a tick to react.
|
||||
await aTimeout(20);
|
||||
expect(unloadCount).to.equal(1);
|
||||
|
||||
initializer.destroy();
|
||||
});
|
||||
|
||||
it('skips instantiation when the manifest has no js property', async () => {
|
||||
const initializer = new UmbAppEntryPointExtensionInitializer(host, registry);
|
||||
|
||||
// Without `js`, instantiateExtension is a no-op but the observer still
|
||||
// flips `loaded` to true.
|
||||
registry.register({
|
||||
type: 'appEntryPoint',
|
||||
alias: ALIAS,
|
||||
name: 'no-js-app-entry-point',
|
||||
} as ManifestAppEntryPoint);
|
||||
|
||||
await waitLoaded(initializer);
|
||||
// Reaching here without throwing is the whole assertion.
|
||||
initializer.destroy();
|
||||
});
|
||||
});
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
import { UmbBackofficeEntryPointExtensionInitializer } from './backoffice-entry-point-extension-initializer.js';
|
||||
import type { ManifestBackofficeEntryPoint } from '../extensions/backoffice-entry-point.extension.js';
|
||||
import { aTimeout, expect, fixture } from '@open-wc/testing';
|
||||
import { customElement, html } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbElementMixin, type UmbElement } from '@umbraco-cms/backoffice/element-api';
|
||||
import { UmbExtensionRegistry } from '@umbraco-cms/backoffice/extension-api';
|
||||
|
||||
@customElement('umb-test-backoffice-entry-point-host')
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
class UmbTestBackofficeEntryPointHostElement extends UmbElementMixin(HTMLElement) {}
|
||||
|
||||
const ALIAS = 'Umb.Test.BackofficeEntryPoint';
|
||||
|
||||
const waitLoaded = (initializer: UmbBackofficeEntryPointExtensionInitializer) =>
|
||||
new Promise<void>((resolve) => {
|
||||
const sub = initializer.loaded.subscribe((value) => {
|
||||
if (value === true) {
|
||||
resolve();
|
||||
queueMicrotask(() => sub.unsubscribe());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('UmbBackofficeEntryPointExtensionInitializer', () => {
|
||||
let host: UmbElement;
|
||||
let registry: UmbExtensionRegistry<ManifestBackofficeEntryPoint>;
|
||||
|
||||
beforeEach(async () => {
|
||||
host = (await fixture(
|
||||
html`<umb-test-backoffice-entry-point-host></umb-test-backoffice-entry-point-host>`,
|
||||
)) as UmbElement;
|
||||
registry = new UmbExtensionRegistry();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
registry.clear();
|
||||
});
|
||||
|
||||
it('only observes manifests of type "backofficeEntryPoint"', async () => {
|
||||
let called = false;
|
||||
const initializer = new UmbBackofficeEntryPointExtensionInitializer(host, registry);
|
||||
|
||||
registry.register({
|
||||
type: 'appEntryPoint',
|
||||
alias: 'Umb.Test.NotMe',
|
||||
name: 'should-not-fire',
|
||||
js: { onInit: () => (called = true) } as never,
|
||||
} as never);
|
||||
|
||||
await aTimeout(20);
|
||||
expect(called).to.be.false;
|
||||
initializer.destroy();
|
||||
});
|
||||
|
||||
it('calls onInit with (host, registry) when a backofficeEntryPoint is registered', async () => {
|
||||
const initializer = new UmbBackofficeEntryPointExtensionInitializer(host, registry);
|
||||
|
||||
const onInitArgs: Array<[unknown, unknown]> = [];
|
||||
const moduleInstance = {
|
||||
onInit: (h: unknown, r: unknown) => onInitArgs.push([h, r]),
|
||||
onUnload: () => {},
|
||||
};
|
||||
|
||||
registry.register({
|
||||
type: 'backofficeEntryPoint',
|
||||
alias: ALIAS,
|
||||
name: 'test-backoffice-entry-point',
|
||||
js: moduleInstance,
|
||||
} as ManifestBackofficeEntryPoint);
|
||||
|
||||
await waitLoaded(initializer);
|
||||
expect(onInitArgs.length).to.equal(1);
|
||||
expect(onInitArgs[0][0]).to.equal(host);
|
||||
expect(onInitArgs[0][1]).to.equal(registry);
|
||||
|
||||
initializer.destroy();
|
||||
});
|
||||
|
||||
it('calls onUnload when a backofficeEntryPoint manifest is unregistered', async () => {
|
||||
const initializer = new UmbBackofficeEntryPointExtensionInitializer(host, registry);
|
||||
|
||||
let unloadCount = 0;
|
||||
const moduleInstance = {
|
||||
onInit: () => {},
|
||||
onUnload: () => unloadCount++,
|
||||
};
|
||||
|
||||
registry.register({
|
||||
type: 'backofficeEntryPoint',
|
||||
alias: ALIAS,
|
||||
name: 'test-backoffice-entry-point',
|
||||
js: moduleInstance,
|
||||
} as ManifestBackofficeEntryPoint);
|
||||
|
||||
await waitLoaded(initializer);
|
||||
expect(unloadCount).to.equal(0);
|
||||
|
||||
registry.unregister(ALIAS);
|
||||
await aTimeout(20);
|
||||
expect(unloadCount).to.equal(1);
|
||||
|
||||
initializer.destroy();
|
||||
});
|
||||
|
||||
it('skips instantiation when the manifest has no js property', async () => {
|
||||
const initializer = new UmbBackofficeEntryPointExtensionInitializer(host, registry);
|
||||
|
||||
registry.register({
|
||||
type: 'backofficeEntryPoint',
|
||||
alias: ALIAS,
|
||||
name: 'no-js-backoffice-entry-point',
|
||||
} as ManifestBackofficeEntryPoint);
|
||||
|
||||
await waitLoaded(initializer);
|
||||
initializer.destroy();
|
||||
});
|
||||
});
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
import { UmbEntryPointExtensionInitializer } from './entry-point-extension-initializer.js';
|
||||
import type { ManifestEntryPoint } from '../extensions/entry-point.extension.js';
|
||||
import { aTimeout, expect, fixture } from '@open-wc/testing';
|
||||
import { customElement, html } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbElementMixin, type UmbElement } from '@umbraco-cms/backoffice/element-api';
|
||||
import { UmbExtensionRegistry } from '@umbraco-cms/backoffice/extension-api';
|
||||
|
||||
@customElement('umb-test-entry-point-host')
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
class UmbTestEntryPointHostElement extends UmbElementMixin(HTMLElement) {}
|
||||
|
||||
const ALIAS = 'Umb.Test.EntryPoint';
|
||||
|
||||
const waitLoaded = (initializer: UmbEntryPointExtensionInitializer) =>
|
||||
new Promise<void>((resolve) => {
|
||||
const sub = initializer.loaded.subscribe((value) => {
|
||||
if (value === true) {
|
||||
resolve();
|
||||
queueMicrotask(() => sub.unsubscribe());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// The (deprecated) entry-point initializer logs an error on every instantiation.
|
||||
// Suppress it for the duration of each test so the noise doesn't pollute output,
|
||||
// but capture the calls so we can also verify the deprecation warning fires.
|
||||
let originalConsoleError: typeof console.error;
|
||||
let consoleErrors: Array<unknown[]>;
|
||||
|
||||
describe('UmbEntryPointExtensionInitializer', () => {
|
||||
let host: UmbElement;
|
||||
let registry: UmbExtensionRegistry<ManifestEntryPoint>;
|
||||
|
||||
beforeEach(async () => {
|
||||
host = (await fixture(html`<umb-test-entry-point-host></umb-test-entry-point-host>`)) as UmbElement;
|
||||
registry = new UmbExtensionRegistry();
|
||||
|
||||
consoleErrors = [];
|
||||
originalConsoleError = console.error;
|
||||
console.error = (...args: unknown[]) => consoleErrors.push(args);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
console.error = originalConsoleError;
|
||||
registry.clear();
|
||||
});
|
||||
|
||||
it('logs a deprecation error when a manifest is instantiated', async () => {
|
||||
const initializer = new UmbEntryPointExtensionInitializer(host, registry);
|
||||
|
||||
registry.register({
|
||||
type: 'entryPoint',
|
||||
alias: ALIAS,
|
||||
name: 'test-entry-point',
|
||||
js: { onInit: () => {}, onUnload: () => {} } as never,
|
||||
} as ManifestEntryPoint);
|
||||
|
||||
await waitLoaded(initializer);
|
||||
|
||||
const deprecationLogged = consoleErrors.some((args) =>
|
||||
typeof args[0] === 'string' && args[0].includes('`entryPoint` extension-type is deprecated'),
|
||||
);
|
||||
expect(deprecationLogged).to.be.true;
|
||||
|
||||
initializer.destroy();
|
||||
});
|
||||
|
||||
it('calls onInit with (host, registry) when an entryPoint is registered', async () => {
|
||||
const initializer = new UmbEntryPointExtensionInitializer(host, registry);
|
||||
|
||||
const onInitArgs: Array<[unknown, unknown]> = [];
|
||||
const moduleInstance = {
|
||||
onInit: (h: unknown, r: unknown) => onInitArgs.push([h, r]),
|
||||
onUnload: () => {},
|
||||
};
|
||||
|
||||
registry.register({
|
||||
type: 'entryPoint',
|
||||
alias: ALIAS,
|
||||
name: 'test-entry-point',
|
||||
js: moduleInstance,
|
||||
} as ManifestEntryPoint);
|
||||
|
||||
await waitLoaded(initializer);
|
||||
expect(onInitArgs.length).to.equal(1);
|
||||
expect(onInitArgs[0][0]).to.equal(host);
|
||||
expect(onInitArgs[0][1]).to.equal(registry);
|
||||
|
||||
initializer.destroy();
|
||||
});
|
||||
|
||||
it('calls onUnload when an entryPoint manifest is unregistered', async () => {
|
||||
const initializer = new UmbEntryPointExtensionInitializer(host, registry);
|
||||
|
||||
let unloadCount = 0;
|
||||
const moduleInstance = {
|
||||
onInit: () => {},
|
||||
onUnload: () => unloadCount++,
|
||||
};
|
||||
|
||||
registry.register({
|
||||
type: 'entryPoint',
|
||||
alias: ALIAS,
|
||||
name: 'test-entry-point',
|
||||
js: moduleInstance,
|
||||
} as ManifestEntryPoint);
|
||||
|
||||
await waitLoaded(initializer);
|
||||
expect(unloadCount).to.equal(0);
|
||||
|
||||
registry.unregister(ALIAS);
|
||||
await aTimeout(20);
|
||||
expect(unloadCount).to.equal(1);
|
||||
|
||||
initializer.destroy();
|
||||
});
|
||||
|
||||
it('only observes manifests of type "entryPoint"', async () => {
|
||||
let called = false;
|
||||
const initializer = new UmbEntryPointExtensionInitializer(host, registry);
|
||||
|
||||
registry.register({
|
||||
type: 'backofficeEntryPoint',
|
||||
alias: 'Umb.Test.NotMe',
|
||||
name: 'should-not-fire',
|
||||
js: { onInit: () => (called = true) } as never,
|
||||
} as never);
|
||||
|
||||
await aTimeout(20);
|
||||
expect(called).to.be.false;
|
||||
initializer.destroy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
import { umbExtensionsRegistry } from './registry.js';
|
||||
import { expect } from '@open-wc/testing';
|
||||
import { UmbExtensionRegistry } from '@umbraco-cms/backoffice/extension-api';
|
||||
import type { ManifestKind } from '@umbraco-cms/backoffice/extension-api';
|
||||
|
||||
// Aliases scoped to this test file so we don't collide with anything else
|
||||
// that may already be registered in the shared singleton.
|
||||
const SECTION_ALIAS_A = 'Umb.Test.Registry.Section.A';
|
||||
const SECTION_ALIAS_B = 'Umb.Test.Registry.Section.B';
|
||||
const KIND_ALIAS = 'Umb.Test.Registry.Kind';
|
||||
|
||||
describe('umbExtensionsRegistry (singleton)', () => {
|
||||
afterEach(() => {
|
||||
// Conservative cleanup: remove only the aliases we added, so the singleton
|
||||
// is left in the same state the suite found it in.
|
||||
[SECTION_ALIAS_A, SECTION_ALIAS_B, KIND_ALIAS].forEach((alias) => {
|
||||
if (umbExtensionsRegistry.isRegistered(alias)) {
|
||||
umbExtensionsRegistry.unregister(alias);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('is an instance of UmbExtensionRegistry', () => {
|
||||
expect(umbExtensionsRegistry).to.be.instanceOf(UmbExtensionRegistry);
|
||||
});
|
||||
|
||||
it('registers and reports an extension as registered, then unregisters it', () => {
|
||||
expect(umbExtensionsRegistry.isRegistered(SECTION_ALIAS_A)).to.be.false;
|
||||
|
||||
umbExtensionsRegistry.register({
|
||||
type: 'section',
|
||||
name: 'Test Section A',
|
||||
alias: SECTION_ALIAS_A,
|
||||
meta: { label: 'Test Section A', pathname: 'test-a' },
|
||||
} as UmbExtensionManifest);
|
||||
|
||||
expect(umbExtensionsRegistry.isRegistered(SECTION_ALIAS_A)).to.be.true;
|
||||
|
||||
const ext = umbExtensionsRegistry.getByAlias(SECTION_ALIAS_A);
|
||||
expect(ext?.alias).to.equal(SECTION_ALIAS_A);
|
||||
|
||||
umbExtensionsRegistry.unregister(SECTION_ALIAS_A);
|
||||
expect(umbExtensionsRegistry.isRegistered(SECTION_ALIAS_A)).to.be.false;
|
||||
});
|
||||
|
||||
it('ignores duplicate registration of the same alias', () => {
|
||||
const manifest = {
|
||||
type: 'section',
|
||||
name: 'Test Section B (first)',
|
||||
alias: SECTION_ALIAS_B,
|
||||
meta: { label: 'first', pathname: 'b' },
|
||||
} as UmbExtensionManifest;
|
||||
|
||||
umbExtensionsRegistry.register(manifest);
|
||||
|
||||
// Suppress the expected console.error from the duplicate-registration check.
|
||||
const originalError = console.error;
|
||||
const errors: Array<unknown[]> = [];
|
||||
console.error = (...args) => errors.push(args);
|
||||
try {
|
||||
umbExtensionsRegistry.register({
|
||||
...manifest,
|
||||
name: 'Test Section B (second)',
|
||||
});
|
||||
} finally {
|
||||
console.error = originalError;
|
||||
}
|
||||
|
||||
// The first registration is preserved, the second is rejected.
|
||||
const ext = umbExtensionsRegistry.getByAlias(SECTION_ALIAS_B) as { name: string };
|
||||
expect(ext?.name).to.equal('Test Section B (first)');
|
||||
expect(errors.length).to.equal(1);
|
||||
});
|
||||
|
||||
it('merges a kind manifest into a registered extension that references it', () => {
|
||||
const kind: ManifestKind<UmbExtensionManifest> = {
|
||||
type: 'kind',
|
||||
alias: KIND_ALIAS,
|
||||
matchType: 'section',
|
||||
matchKind: 'test-kind',
|
||||
manifest: {
|
||||
type: 'section',
|
||||
name: 'kind-default-name',
|
||||
alias: 'kind-default-alias',
|
||||
meta: {
|
||||
label: 'label-from-kind',
|
||||
pathname: 'pathname-from-kind',
|
||||
},
|
||||
} as UmbExtensionManifest,
|
||||
};
|
||||
|
||||
umbExtensionsRegistry.register(kind);
|
||||
umbExtensionsRegistry.register({
|
||||
type: 'section',
|
||||
kind: 'test-kind',
|
||||
name: 'consumer',
|
||||
alias: SECTION_ALIAS_A,
|
||||
weight: 10,
|
||||
meta: {
|
||||
// `label` deliberately omitted — should fall back to the kind's value.
|
||||
pathname: 'pathname-from-extension',
|
||||
},
|
||||
} as unknown as UmbExtensionManifest);
|
||||
|
||||
const merged = umbExtensionsRegistry.getByAlias(SECTION_ALIAS_A) as {
|
||||
meta: { label: string; pathname: string };
|
||||
};
|
||||
|
||||
// Kind contributes the missing field; consumer wins where it sets a value.
|
||||
expect(merged.meta.label).to.equal('label-from-kind');
|
||||
expect(merged.meta.pathname).to.equal('pathname-from-extension');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user