You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
560 lines
20 KiB
560 lines
20 KiB
// @vitest-environment jsdom
|
|
|
|
/**
|
|
* Tests for active-uix's two boot paths:
|
|
* - `createActiveUix(options)` — STANDALONE: active-uix instantiates
|
|
* its own services. No `active-app` involved. Owns lifecycles.
|
|
* - `attachActiveUix(app)` — ATTACH: external active-app injects the
|
|
* UIX services. active-uix doesn't own anything.
|
|
*
|
|
* Both produce an `ActiveUix` with the same observable surface.
|
|
* Components (soma / morfo / sema / eidos) consume that surface and
|
|
* have no way to tell which boot path was used — that decoupling is
|
|
* the whole point of active-uix.
|
|
*/
|
|
|
|
import { describe, expect, it, vi } from 'vitest';
|
|
|
|
import { createActiveApp, type ActiveApp } from '$active-app';
|
|
import {
|
|
ActiveUixMissingServiceError,
|
|
attachActiveUix,
|
|
createActiveUix
|
|
} from './active-uix.svelte';
|
|
import { defineUixServices } from './services';
|
|
import type { LangNode } from '$libs/langs';
|
|
import { standardPrefsDimensions } from '$prefs';
|
|
import { _resetMorfoRegistryForTesting, registerMorfo, v, type Morfo } from '$uix/morfo';
|
|
import { alertDialogMorfo } from '$uix/morfo/components/alert-dialog';
|
|
import { announceMorfo } from '$uix/morfo/components/announce';
|
|
import { breadcrumbMorfo } from '$uix/morfo/components/breadcrumb';
|
|
import { carouselMorfo } from '$uix/morfo/components/carousel';
|
|
import { clipboardMorfo } from '$uix/morfo/components/clipboard';
|
|
import { colorFieldMorfo } from '$uix/morfo/components/color-field';
|
|
import { colorPickerMorfo } from '$uix/morfo/components/color-picker';
|
|
import { comboboxMorfo } from '$uix/morfo/components/combobox';
|
|
import { commandMorfo } from '$uix/morfo/components/command';
|
|
import { dragDropMorfo } from '$uix/morfo/components/drag-drop';
|
|
import { drawerMorfo } from '$uix/morfo/components/drawer';
|
|
import { feedMorfo } from '$uix/morfo/components/feed';
|
|
import { fileUploadMorfo } from '$uix/morfo/components/file-upload';
|
|
import { formMorfo } from '$uix/morfo/components/form';
|
|
import { gridListMorfo } from '$uix/morfo/components/grid-list';
|
|
import { listboxMorfo } from '$uix/morfo/components/listbox';
|
|
import { meterMorfo } from '$uix/morfo/components/meter';
|
|
import { navigationMenuMorfo } from '$uix/morfo/components/navigation-menu';
|
|
import { paginationMorfo } from '$uix/morfo/components/pagination';
|
|
import { pinInputMorfo } from '$uix/morfo/components/pin-input';
|
|
import { progressMorfo } from '$uix/morfo/components/progress';
|
|
import { ratingGroupMorfo } from '$uix/morfo/components/rating-group';
|
|
import { scrollAreaMorfo } from '$uix/morfo/components/scroll-area';
|
|
import { searchFieldMorfo } from '$uix/morfo/components/search-field';
|
|
import { splitterMorfo } from '$uix/morfo/components/splitter';
|
|
import { tagGroupMorfo } from '$uix/morfo/components/tag-group';
|
|
import { tagsInputMorfo } from '$uix/morfo/components/tags-input';
|
|
import { stepperMorfo } from '$uix/morfo/components/stepper';
|
|
import { tableMorfo } from '$uix/morfo/components/table';
|
|
import { treeGridMorfo } from '$uix/morfo/components/tree-grid';
|
|
import { treeViewMorfo } from '$uix/morfo/components/tree-view';
|
|
|
|
const minimalLang = { schema: {} satisfies LangNode, defaultLocale: 'es' as const };
|
|
const bilingualPrefs = standardPrefsDimensions({
|
|
languages: ['es', 'en'],
|
|
locales: ['es-ES', 'en-US'],
|
|
currencies: ['EUR', 'USD'],
|
|
defaults: {
|
|
language: 'es',
|
|
locale: 'es-ES',
|
|
currency: 'EUR'
|
|
}
|
|
});
|
|
|
|
// ── createActiveUix (standalone — no active-app) ─────────────────────────────
|
|
|
|
describe('createActiveUix (standalone)', () => {
|
|
it('exposes the canonical UIX surface', () => {
|
|
const uix = createActiveUix({ langs: minimalLang });
|
|
try {
|
|
expect(uix.langs).toBeDefined();
|
|
expect(uix.format).toBeDefined();
|
|
expect(uix.dom).toBeDefined();
|
|
expect(uix.events).toBeDefined();
|
|
expect(uix.logger).toBeDefined();
|
|
expect(uix.bus).toBeDefined();
|
|
expect(uix.timers).toBeDefined();
|
|
expect(uix.prefs).toBeDefined();
|
|
expect(uix.portal).toBeUndefined();
|
|
expect((uix.prefs.effective() as Record<string, unknown>).language).toBe('es');
|
|
expect((uix.prefs.effective() as Record<string, unknown>).sound).toBe('allow');
|
|
expect(uix.ownsApp).toBe(true);
|
|
} finally {
|
|
uix.dispose();
|
|
}
|
|
});
|
|
|
|
it('does NOT create an active-app — uix.app is undefined in standalone mode', () => {
|
|
const uix = createActiveUix({ langs: minimalLang });
|
|
try {
|
|
expect(uix.app).toBeUndefined();
|
|
} finally {
|
|
uix.dispose();
|
|
}
|
|
});
|
|
|
|
it('opts out of format when format=false', () => {
|
|
const uix = createActiveUix({ langs: minimalLang, format: false });
|
|
try {
|
|
expect(uix.format).toBeUndefined();
|
|
} finally {
|
|
uix.dispose();
|
|
}
|
|
});
|
|
|
|
it('exposes the generic UI portal target when configured', () => {
|
|
const uix = createActiveUix({ langs: minimalLang, portal: '#portal-root' });
|
|
try {
|
|
expect(uix.portal).toBe('#portal-root');
|
|
} finally {
|
|
uix.dispose();
|
|
}
|
|
});
|
|
|
|
it('does not auto-project prefs attrs; composition roots wire prefs projection explicitly', () => {
|
|
const root = document.documentElement;
|
|
root.removeAttribute('dir');
|
|
root.removeAttribute('data-motion');
|
|
root.removeAttribute('data-sound');
|
|
root.removeAttribute('data-haptic');
|
|
root.removeAttribute('data-mode');
|
|
root.removeAttribute('data-density');
|
|
root.removeAttribute('data-theme');
|
|
const uix = createActiveUix({ langs: minimalLang });
|
|
try {
|
|
expect(root.hasAttribute('dir')).toBe(false);
|
|
expect(root.hasAttribute('data-motion')).toBe(false);
|
|
expect(root.hasAttribute('data-sound')).toBe(false);
|
|
expect(root.hasAttribute('data-haptic')).toBe(false);
|
|
expect(root.hasAttribute('data-mode')).toBe(false);
|
|
expect(root.hasAttribute('data-density')).toBe(false);
|
|
expect(root.hasAttribute('data-theme')).toBe(false);
|
|
} finally {
|
|
uix.dispose();
|
|
}
|
|
});
|
|
|
|
it('registers componentLangs by default — t() resolves a soma component path', () => {
|
|
const uix = createActiveUix({ langs: minimalLang });
|
|
try {
|
|
const out = uix.langs.t('components.dialog.content.roledescription');
|
|
expect(typeof out).toBe('string');
|
|
expect(out.length).toBeGreaterThan(0);
|
|
expect(uix.langs.t('components.calendar.prev-month')).toBe('Mes anterior');
|
|
expect(uix.langs.t('components.range-calendar.range-start')).toBe('inicio del rango');
|
|
} finally {
|
|
uix.dispose();
|
|
}
|
|
});
|
|
|
|
it('registers commonLangs by default without requiring app schema entries', () => {
|
|
const uix = createActiveUix({ langs: minimalLang });
|
|
try {
|
|
expect(uix.langs.t('common.buttons.close')).toBe('Cerrar');
|
|
expect(uix.langs.ts('#?common.buttons.close|Close')).toBe('Cerrar');
|
|
} finally {
|
|
uix.dispose();
|
|
}
|
|
});
|
|
|
|
it('skips componentLangs when registerComponentLangs: false', () => {
|
|
const uix = createActiveUix({
|
|
langs: minimalLang,
|
|
registerComponentLangs: false
|
|
});
|
|
try {
|
|
const out = uix.langs.t('components.dialog.content.roledescription');
|
|
expect(out).not.toMatch(/dialog window/i);
|
|
expect(uix.langs.t('common.buttons.close')).toBe('common.buttons.close');
|
|
} finally {
|
|
uix.dispose();
|
|
}
|
|
});
|
|
|
|
it('registers morfo translations that arrive after boot', () => {
|
|
_resetMorfoRegistryForTesting();
|
|
const uix = createActiveUix({
|
|
langs: minimalLang
|
|
});
|
|
try {
|
|
registerMorfo({
|
|
name: 'LazySample',
|
|
kebab: 'lazy-sample',
|
|
scope: ['soma'],
|
|
translations: {
|
|
label: { es: 'Muestra diferida', en: 'Lazy sample' }
|
|
},
|
|
parts: [
|
|
{
|
|
name: 'Provider',
|
|
kebab: 'provider',
|
|
kind: 'public',
|
|
defaultElement: 'div',
|
|
optional: false,
|
|
data: [],
|
|
aria: [
|
|
{
|
|
attr: 'aria-label',
|
|
value: v.translationRef('label', 'Lazy sample')
|
|
}
|
|
]
|
|
}
|
|
]
|
|
} satisfies Morfo);
|
|
|
|
expect(uix.langs.t('components.lazy-sample.label')).toBe('Muestra diferida');
|
|
registerMorfo(fileUploadMorfo);
|
|
expect(uix.langs.t('components.file-upload.dropzone')).toBe('Subir archivos');
|
|
expect(uix.langs.t('components.file-upload.list')).toBe('Archivos seleccionados');
|
|
expect(uix.langs.ts('#?common.buttons.clear|Clear all')).toBe('Limpiar todo');
|
|
registerMorfo(progressMorfo);
|
|
registerMorfo(meterMorfo);
|
|
expect(uix.langs.t('components.progress.label')).toBe('Cargando');
|
|
expect(uix.langs.t('components.meter.label')).toBe('Medidor');
|
|
registerMorfo(searchFieldMorfo);
|
|
registerMorfo(comboboxMorfo);
|
|
registerMorfo(commandMorfo);
|
|
registerMorfo(drawerMorfo);
|
|
registerMorfo(feedMorfo);
|
|
expect(uix.langs.t('components.search-field.clear')).toBe('Borrar búsqueda');
|
|
expect(uix.langs.t('components.combobox.toggle')).toBe('Alternar');
|
|
expect(uix.langs.t('components.command.list')).toBe('Paleta de comandos');
|
|
expect(uix.langs.t('components.drawer.trigger')).toBe('Abrir cajón');
|
|
expect(uix.langs.t('components.feed.label')).toBe('Feed');
|
|
registerMorfo(listboxMorfo);
|
|
registerMorfo(navigationMenuMorfo);
|
|
registerMorfo(treeViewMorfo);
|
|
registerMorfo(stepperMorfo);
|
|
registerMorfo(splitterMorfo);
|
|
registerMorfo(scrollAreaMorfo);
|
|
expect(uix.langs.t('components.listbox.label')).toBe('Opciones');
|
|
expect(uix.langs.t('components.navigation-menu.label')).toBe('Principal');
|
|
expect(uix.langs.t('components.tree-view.label')).toBe('Árbol');
|
|
expect(uix.langs.t('components.stepper.list')).toBe('Pasos');
|
|
expect(uix.langs.t('components.splitter.resize')).toBe('Redimensionar paneles');
|
|
expect(uix.langs.t('components.scroll-area.scrollbar-horizontal')).toBe(
|
|
'Barra de desplazamiento horizontal'
|
|
);
|
|
registerMorfo(gridListMorfo);
|
|
registerMorfo(pinInputMorfo);
|
|
registerMorfo(ratingGroupMorfo);
|
|
expect(uix.langs.t('components.grid-list.label')).toBe('Lista en cuadrícula');
|
|
expect(uix.langs.t('components.grid-list.select-row')).toBe('Seleccionar fila');
|
|
expect(uix.langs.t('components.pin-input.input')).toBe('Código de verificación');
|
|
expect(uix.langs.t('components.rating-group.label')).toBe('Valoración');
|
|
registerMorfo(tagGroupMorfo);
|
|
registerMorfo(tagsInputMorfo);
|
|
expect(uix.langs.t('components.tag-group.remove', { tag: 'UIX' })).toBe('Eliminar UIX');
|
|
expect(uix.langs.t('components.tags-input.remove')).toBe('Eliminar etiqueta');
|
|
expect(uix.langs.t('components.tags-input.clear')).toBe('Limpiar todas las etiquetas');
|
|
registerMorfo(treeGridMorfo);
|
|
registerMorfo(tableMorfo);
|
|
registerMorfo(paginationMorfo);
|
|
expect(uix.langs.t('components.tree-grid.expand')).toBe('Expandir fila');
|
|
expect(uix.langs.t('components.tree-grid.toggle')).toBe('Alternar fila');
|
|
expect(uix.langs.t('components.table.row-detail-collapse')).toBe('Ocultar detalles');
|
|
expect(uix.langs.t('components.table.toggle-detail')).toBe('Alternar detalle');
|
|
expect(uix.langs.t('components.pagination.page', { value: 4 })).toBe('Página 4');
|
|
registerMorfo(alertDialogMorfo);
|
|
registerMorfo(announceMorfo);
|
|
registerMorfo(breadcrumbMorfo);
|
|
registerMorfo(clipboardMorfo);
|
|
registerMorfo(carouselMorfo);
|
|
expect(uix.langs.t('components.alert-dialog.action')).toBe('Confirmar');
|
|
expect(uix.langs.t('components.announce.label')).toBe('Anuncios');
|
|
expect(uix.langs.t('components.breadcrumb.ellipsis')).toBe('Más');
|
|
expect(uix.langs.t('components.clipboard.copy')).toBe('Copiar al portapapeles');
|
|
expect(uix.langs.t('components.carousel.indicator', { index: 2 })).toBe(
|
|
'Ir a la diapositiva 2'
|
|
);
|
|
registerMorfo(dragDropMorfo);
|
|
registerMorfo(formMorfo);
|
|
expect(uix.langs.t('components.drag-drop.drag-over', { target: 'Lista' })).toBe(
|
|
'Sobre zona de destino Lista'
|
|
);
|
|
expect(uix.langs.t('components.drag-drop.droppable')).toBe('zona de destino');
|
|
expect(uix.langs.t('components.form.error-summary-single')).toBe(
|
|
'1 error del formulario necesita tu atención'
|
|
);
|
|
expect(uix.langs.t('common.buttons.submit')).toBe('Enviar');
|
|
registerMorfo(colorFieldMorfo);
|
|
registerMorfo(colorPickerMorfo);
|
|
expect(uix.langs.t('components.color-field.channel-r')).toBe('Rojo');
|
|
expect(uix.langs.t('components.color-field.format-select')).toBe('Formato de color');
|
|
expect(uix.langs.t('components.color-picker.trigger')).toBe('Abrir selector de color');
|
|
expect(uix.langs.t('components.color-picker.area-description')).toBe(
|
|
'Usa las flechas para ajustar saturación (horizontal) y brillo (vertical)'
|
|
);
|
|
} finally {
|
|
uix.dispose();
|
|
_resetMorfoRegistryForTesting();
|
|
}
|
|
});
|
|
|
|
it('does not re-register morfo translations already covered by componentLangs', () => {
|
|
_resetMorfoRegistryForTesting();
|
|
const uix = createActiveUix({
|
|
langs: minimalLang
|
|
});
|
|
try {
|
|
registerMorfo({
|
|
name: 'Dialog',
|
|
kebab: 'dialog',
|
|
scope: ['soma'],
|
|
translations: {
|
|
content: {
|
|
roledescription: { es: 'otra ventana', en: 'other window' }
|
|
}
|
|
},
|
|
parts: [
|
|
{
|
|
name: 'Provider',
|
|
kebab: 'provider',
|
|
kind: 'public',
|
|
defaultElement: 'div',
|
|
optional: false,
|
|
data: [],
|
|
aria: []
|
|
}
|
|
]
|
|
} satisfies Morfo);
|
|
|
|
expect(uix.langs.t('components.dialog.content.roledescription')).toBe('ventana de diálogo');
|
|
} finally {
|
|
uix.dispose();
|
|
_resetMorfoRegistryForTesting();
|
|
}
|
|
});
|
|
|
|
it('dispose tears down every owned service (standalone owns lifecycle)', () => {
|
|
const uix = createActiveUix({ langs: minimalLang });
|
|
const langSpy = vi.spyOn(uix.langs, 'dispose');
|
|
const formatSpy = vi.spyOn(uix.format!, 'dispose');
|
|
const eventsSpy = vi.spyOn(uix.events!, 'dispose');
|
|
uix.dispose();
|
|
expect(langSpy).toHaveBeenCalledTimes(1);
|
|
expect(formatSpy).toHaveBeenCalledTimes(1);
|
|
expect(eventsSpy).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('dispose is idempotent', () => {
|
|
const uix = createActiveUix({ langs: minimalLang });
|
|
const langSpy = vi.spyOn(uix.langs, 'dispose');
|
|
uix.dispose();
|
|
uix.dispose();
|
|
uix.dispose();
|
|
expect(langSpy).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
// ── attachActiveUix (integration with externally composed active-app) ────────
|
|
|
|
describe('attachActiveUix (integration mode)', () => {
|
|
it('attaches to an app composed via defineUixServices', () => {
|
|
const app = createActiveApp({
|
|
services: defineUixServices({ langs: minimalLang })
|
|
});
|
|
const uix = attachActiveUix(app as unknown as ActiveApp);
|
|
try {
|
|
// Identity-equal: uix proxies to the same service instances.
|
|
expect(uix.langs).toBe(app.langs);
|
|
expect(uix.events).toBe(app.events);
|
|
expect(uix.dom).toBe(app.dom);
|
|
expect(uix.ownsApp).toBe(false);
|
|
expect(uix.app).toBe(app);
|
|
} finally {
|
|
uix.dispose();
|
|
app.dispose();
|
|
}
|
|
});
|
|
|
|
it('coexists with extra services in the same app schema', () => {
|
|
// Real-world case: app composes UIX services + its own (cache,
|
|
// session, http, ...). attachActiveUix consumes the UIX slice;
|
|
// the rest stays on app for advanced consumers via uix.app.
|
|
const customFactory = {
|
|
name: 'custom' as const,
|
|
coreDependencies: [] as const,
|
|
initMode: 'lazy' as const,
|
|
create() {
|
|
return { tag: 'extra' };
|
|
}
|
|
};
|
|
|
|
const app = createActiveApp({
|
|
services: {
|
|
...defineUixServices({ langs: minimalLang }),
|
|
custom: customFactory
|
|
} as never
|
|
}) as unknown as ActiveApp;
|
|
const uix = attachActiveUix(app);
|
|
try {
|
|
expect(uix.langs).toBeDefined();
|
|
expect((app as unknown as { custom: { tag: string } }).custom.tag).toBe('extra');
|
|
} finally {
|
|
uix.dispose();
|
|
app.dispose();
|
|
}
|
|
});
|
|
|
|
it('registers componentLangs on attach (default true)', () => {
|
|
const app = createActiveApp({
|
|
services: defineUixServices({ langs: minimalLang })
|
|
});
|
|
const uix = attachActiveUix(app as unknown as ActiveApp);
|
|
try {
|
|
const out = uix.langs.t('components.dialog.content.roledescription');
|
|
expect(typeof out).toBe('string');
|
|
expect(out.length).toBeGreaterThan(0);
|
|
} finally {
|
|
uix.dispose();
|
|
app.dispose();
|
|
}
|
|
});
|
|
|
|
it('throws ActiveUixMissingServiceError when required services are missing', () => {
|
|
const app = createActiveApp({ services: {} });
|
|
try {
|
|
expect(() => attachActiveUix(app as unknown as ActiveApp)).toThrow(
|
|
ActiveUixMissingServiceError
|
|
);
|
|
} finally {
|
|
app.dispose();
|
|
}
|
|
});
|
|
|
|
it('the error reports every missing service so the integrator can fix the schema', () => {
|
|
const app = createActiveApp({ services: {} });
|
|
try {
|
|
attachActiveUix(app as unknown as ActiveApp);
|
|
expect.fail('should have thrown');
|
|
} catch (err) {
|
|
if (!(err instanceof ActiveUixMissingServiceError)) throw err;
|
|
expect(err.missing).toContain('langs');
|
|
expect(err.missing).toContain('dom');
|
|
expect(err.missing).not.toContain('events');
|
|
expect(err.message).toContain('defineUixServices');
|
|
} finally {
|
|
app.dispose();
|
|
}
|
|
});
|
|
|
|
it('rejects attach mode when the external app omits dom', () => {
|
|
const app = createActiveApp({
|
|
services: defineUixServices({ langs: minimalLang, dom: false })
|
|
});
|
|
try {
|
|
expect(() => attachActiveUix(app as unknown as ActiveApp)).toThrow(
|
|
ActiveUixMissingServiceError
|
|
);
|
|
} finally {
|
|
app.dispose();
|
|
}
|
|
});
|
|
|
|
it('does not subscribe langs to prefs.language twice in attach mode', () => {
|
|
const app = createActiveApp({
|
|
prefs: { schema: bilingualPrefs },
|
|
services: defineUixServices({
|
|
langs: {
|
|
schema: {
|
|
hello: { es: 'Hola', en: 'Hello' }
|
|
},
|
|
defaultLocale: 'es'
|
|
}
|
|
})
|
|
});
|
|
const uix = attachActiveUix(app as unknown as ActiveApp);
|
|
try {
|
|
const spy = vi.spyOn(uix.langs, 'setLocale');
|
|
(app.prefs as unknown as { language: { set(value: 'en'): void } }).language.set('en');
|
|
|
|
expect(spy).toHaveBeenCalledTimes(1);
|
|
expect(uix.langs.t('hello')).toBe('Hello');
|
|
} finally {
|
|
uix.dispose();
|
|
app.dispose();
|
|
}
|
|
});
|
|
|
|
it('dispose does NOT tear down the underlying app (ownsApp=false)', () => {
|
|
const app = createActiveApp({
|
|
services: defineUixServices({ langs: minimalLang })
|
|
});
|
|
const spy = vi.spyOn(app, 'dispose');
|
|
const uix = attachActiveUix(app as unknown as ActiveApp);
|
|
uix.dispose();
|
|
expect(spy).not.toHaveBeenCalled();
|
|
// External owner is responsible for disposal.
|
|
app.dispose();
|
|
expect(spy).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
describe('active-uix degraded DOM mode', () => {
|
|
it('dom=false keeps event projection inside the disabled no-op dom', async () => {
|
|
const uix = createActiveUix({ langs: minimalLang, dom: false });
|
|
const target = document.createElement('div');
|
|
try {
|
|
const promise = uix.events!.emit({
|
|
target,
|
|
name: 'announce',
|
|
family: 'signal',
|
|
hold: 0
|
|
});
|
|
|
|
expect(target.hasAttribute('data-event')).toBe(false);
|
|
await promise;
|
|
expect(target.hasAttribute('data-event')).toBe(false);
|
|
} finally {
|
|
uix.dispose();
|
|
}
|
|
});
|
|
});
|
|
|
|
// ── Cross-mode parity ───────────────────────────────────────────────────────
|
|
|
|
describe('cross-mode parity', () => {
|
|
it('both modes produce an ActiveUix with the same surface keys', () => {
|
|
const standalone = createActiveUix({ langs: minimalLang });
|
|
const externalApp = createActiveApp({
|
|
services: defineUixServices({ langs: minimalLang })
|
|
});
|
|
const attached = attachActiveUix(externalApp as unknown as ActiveApp);
|
|
try {
|
|
const surface = (uix: typeof standalone) =>
|
|
[
|
|
uix.langs !== undefined,
|
|
uix.format !== undefined,
|
|
uix.dom !== undefined,
|
|
uix.events !== undefined,
|
|
uix.logger !== undefined,
|
|
uix.bus !== undefined,
|
|
uix.timers !== undefined,
|
|
uix.prefs !== undefined,
|
|
'portal' in uix
|
|
].every(Boolean);
|
|
|
|
expect(surface(standalone)).toBe(true);
|
|
expect(surface(attached)).toBe(true);
|
|
expect(standalone.ownsApp).toBe(true);
|
|
expect(attached.ownsApp).toBe(false);
|
|
// app is the canonical asymmetry: only present in attach mode.
|
|
expect(standalone.app).toBeUndefined();
|
|
expect(attached.app).toBe(externalApp);
|
|
} finally {
|
|
standalone.dispose();
|
|
attached.dispose();
|
|
externalApp.dispose();
|
|
}
|
|
});
|
|
});
|