/* eslint-disable testing-library/no-node-access, testing-library/no-wait-for-multiple-assertions, testing-library/no-unnecessary-act, testing-library/no-manual-cleanup, testing-library/await-async-queries */ import React from 'react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import '@testing-library/jest-dom/vitest'; import { act, cleanup, render, screen, waitFor } from '@testing-library/react'; import { type Person } from '@openmrs/esm-api'; import { mockSessionStore } from '@openmrs/esm-api/mock'; import { attach, registerExtension, updateInternalExtensionStore } from '../../../esm-extensions/src'; import { ExtensionSlot, getSyncLifecycle, openmrsComponentDecorator, useConfig, useExtensionStore, } from '../../../esm-react-utils/src'; import { configInternalStore, defineConfigSchema, getExtensionSlotsConfigStore, getExtensionsConfigStore, provide, registerModuleLoad, resetConfigSystem, temporaryConfigStore, Type, } from '../../../esm-config/src'; vi.mock('@openmrs/esm-api', async () => { const original = await import('@openmrs/esm-api'); return { ...original, sessionStore: mockSessionStore, refetchCurrentUser: vi.fn(), }; }); describe('Interaction between configuration and extension systems', () => { beforeEach(() => { temporaryConfigStore.setState({ config: {} }); configInternalStore.setState({ providedConfigs: [], schemas: {}, moduleLoaded: {} }); mockSessionStore.setState({}); getExtensionSlotsConfigStore().setState({ slots: {} }); getExtensionsConfigStore().setState({ configs: {} }); updateInternalExtensionStore(() => ({ slots: {}, extensions: {} })); resetConfigSystem(); }); afterEach(() => { cleanup(); }); it('Config should add, order, and remove extensions within slots', async () => { registerSimpleExtension('Fred', 'esm-flintstone'); registerSimpleExtension('Wilma', 'esm-flintstone'); registerSimpleExtension('Barney', 'esm-rubble'); registerSimpleExtension('Betty', 'esm-rubble'); attach('A slot', 'Fred'); attach('A slot', 'Wilma'); defineConfigSchema('esm-flintstone', {}); registerModuleLoad('esm-flintstone'); provide({ 'esm-flintstone': { extensionSlots: { 'A slot': { add: ['Barney', 'Betty'], order: ['Betty', 'Wilma'], remove: ['Fred'], }, }, }, }); const App = openmrsComponentDecorator({ moduleName: 'esm-flintstone', featureName: 'The Flintstones', disableTranslations: true, })(() => ); act(() => { render(); }); await waitFor(async () => { await screen.findByText('Betty'); const slot = screen.getByTestId('slot'); const extensions = slot.childNodes; expect(extensions[0]).toHaveTextContent('Betty'); expect(extensions[1]).toHaveTextContent('Wilma'); expect(extensions[2]).toHaveTextContent('Barney'); expect(screen.queryByText('Fred')).not.toBeInTheDocument(); }); }); it("Extensions should recieve config from module and from 'configure' key", async () => { registerSimpleExtension('Pebbles', 'esm-flintstone', true); defineConfigSchema('esm-flintstone', { town: { _type: Type.String, _default: 'Bedrock' }, }); registerModuleLoad('esm-flintstone'); attach('Flintstone slot', 'Pebbles'); attach('Future slot', 'Pebbles'); provide({ 'esm-flintstone': { town: 'Springfield', extensionSlots: { 'Future slot': { configure: { Pebbles: { town: 'New New York', }, }, }, }, }, }); const App = openmrsComponentDecorator({ moduleName: 'esm-flintstone', featureName: 'The Flintstones', disableTranslations: true, })(() => ( <> )); act(() => { render(); }); screen.findAllByText(/.*Pebbles.*/); const flintstonePebbles = screen.getByTestId('flintstone-slot'); const futurePebbles = screen.getByTestId('future-slot'); await waitFor(() => { expect(flintstonePebbles).toHaveTextContent(/Pebbles:.*Springfield/); expect(futurePebbles).toHaveTextContent(/Pebbles:.*New New York/); }); }); it('Should be possible to attach the same extension twice with different configurations', async () => { registerSimpleExtension('pet', 'esm-characters', true); defineConfigSchema('esm-characters', { name: { _type: Type.String, _default: '(no-name)' }, }); registerModuleLoad('esm-characters'); attach('Flintstone slot', 'pet#Dino'); attach('Flintstone slot', 'pet#BabyPuss'); provide({ 'esm-flintstone': { extensionSlots: { 'Flintstone slot': { configure: { 'pet#Dino': { name: 'Dino', }, 'pet#BabyPuss': { name: 'Baby Puss', }, }, }, }, }, }); const App = openmrsComponentDecorator({ moduleName: 'esm-flintstone', featureName: 'The Flintstones', disableTranslations: true, })(() => ); act(() => { render(); }); screen.findAllByText(/.*Dino.*/); const slot = screen.getByTestId('flintstone-slot'); await waitFor(() => { expect(slot.firstChild).toHaveTextContent(/Dino/); expect(slot.lastChild).toHaveTextContent(/Baby Puss/); }); }); it('Slot config should update with temporary config', async () => { registerSimpleExtension('Pearl', 'esm-slaghoople'); attach('A slot', 'Pearl'); defineConfigSchema('esm-slaghoople', {}); registerModuleLoad('esm-flintstone'); const App = openmrsComponentDecorator({ moduleName: 'esm-slaghoople', featureName: 'The Slaghooples', disableTranslations: true, })(() => ); act(() => { render(); }); await screen.findByText('Pearl'); act(() => { temporaryConfigStore.setState({ config: { 'esm-slaghoople': { extensionSlots: { 'A slot': { remove: ['Pearl'], }, }, }, }, }); }); await waitFor(() => expect(screen.queryByText('Pearl')).not.toBeInTheDocument()); }); it('Extension config should update with temporary config', async () => { registerSimpleExtension('Mr. Slate', 'esm-flintstone', true); attach('A slot', 'Mr. Slate'); defineConfigSchema('esm-flintstone', { tie: { _default: 'green' } }); registerModuleLoad('esm-flintstone'); const App = openmrsComponentDecorator({ moduleName: 'esm-quarry', featureName: 'The Flintstones', disableTranslations: true, })(() => ); act(() => { render(); }); await screen.findByText(/Mr. Slate/); expect(screen.getByTestId('slot')).toHaveTextContent(/green/); act(() => { temporaryConfigStore.setState({ config: { 'esm-quarry': { extensionSlots: { 'A slot': { configure: { 'Mr. Slate': { tie: 'black' }, }, }, }, }, }, }); }); expect(screen.getByTestId('slot')).toHaveTextContent(/black/); expect(screen.queryByText('green')).not.toBeInTheDocument(); }); // TODO restore this test it.skip('Extension config should be available in extension store', async () => { registerSimpleExtension('Bamm-Bamm', 'esm-flintstone', false); attach('A slot', 'Bamm-Bamm'); defineConfigSchema('esm-flintstone', { clothes: { _default: 'leopard' } }); registerModuleLoad('esm-flintstone'); function RootComponent() { const store = useExtensionStore(); return (
{store.slots['A slot'].assignedExtensions.map((e) => (
{JSON.stringify(e.config)}
))}
); } const App = openmrsComponentDecorator({ moduleName: 'esm-flintstone', featureName: 'The Flintstones', disableTranslations: true, })(RootComponent); act(() => { render(); }); await screen.findByTestId(/slot/); expect(screen.getByText(/clothes/)).toHaveTextContent(/leopard/); act(() => { temporaryConfigStore.setState({ config: { 'esm-flintstone': { extensionSlots: { 'A slot': { configure: { 'Bamm-Bamm': { clothes: 'tiger' }, }, }, }, }, }, }); }); expect(screen.getByText(/clothes/)).toHaveTextContent(/tiger/); }); it('should not show extension when user lacks configured privilege', async () => { mockSessionStore.setState({ loaded: true, session: { authenticated: true, sessionId: '1', user: { uuid: '1', display: 'Non-Admin', username: 'nonadmin', systemId: 'nonadmin', userProperties: {}, person: {} as Person, privileges: [], roles: [], retired: false, locale: 'en', allowedLocales: ['en'], }, }, }); registerSimpleExtension('Schmoo', 'esm-bedrock', true); registerSimpleExtension('Wilma', 'esm-flintstones', true); attach('A slot', 'Schmoo'); attach('A slot', 'Wilma'); defineConfigSchema('esm-bedrock', {}); defineConfigSchema('esm-flintstones', {}); registerModuleLoad('esm-bedrock'); registerModuleLoad('esm-flintstones'); provide({ 'esm-bedrock': { 'Display conditions': { privileges: ['Yabadabadoo!'], }, }, }); provide({ 'esm-flintstones': {}, }); const App = openmrsComponentDecorator({ moduleName: 'esm-bedrock', featureName: 'Bedrock', disableTranslations: true, })(() => ); act(() => { render(); }); await waitFor(() => { const slot = screen.getByTestId('slot'); expect(slot.firstChild).toHaveAttribute('data-extension-id', 'Wilma'); expect(screen.queryAllByText(/\bSchmoo\b/)).toHaveLength(0); }); }); it('should show extension when user has configured privilege', async () => { mockSessionStore.setState({ loaded: true, session: { authenticated: true, sessionId: '1', user: { uuid: '1', display: 'Non-Admin', username: 'nonadmin', systemId: 'nonadmin', userProperties: {}, person: {} as Person, privileges: [{ uuid: '1', name: 'Yabadabadoo!', display: 'Yabadabadoo!' }], roles: [], retired: false, locale: 'en', allowedLocales: ['en'], }, }, }); registerSimpleExtension('Schmoo', 'esm-bedrock', true); attach('A slot', 'Schmoo'); defineConfigSchema('esm-bedrock', {}); registerModuleLoad('esm-bedrock'); provide({ 'esm-bedrock': { 'Display conditions': { privileges: ['Yabadabadoo!'], }, }, }); const App = openmrsComponentDecorator({ moduleName: 'esm-bedrock', featureName: 'Bedrock', disableTranslations: true, })(() => ); act(() => { render(); }); await screen.findByTestId(/slot/); expect(screen.getByTestId('slot').firstChild).toHaveAttribute('data-extension-id', 'Schmoo'); }); it('should only show extensions users have default privilege for', async () => { // Set up initial session state before registering extensions mockSessionStore.setState({ loaded: true, session: { authenticated: true, sessionId: '1', user: { uuid: '1', display: 'Non-Admin', username: 'nonadmin', systemId: 'nonadmin', userProperties: {}, person: {} as Person, privileges: [{ uuid: '1', name: 'YOWTCH!', display: 'YOWTCH!' }], roles: [], retired: false, locale: 'en', allowedLocales: ['en'], }, }, }); registerSimpleExtension('Schmoo', 'esm-bedrock', true, 'Yabadabadoo!'); registerSimpleExtension('Wilma', 'esm-flintstones', true, 'YOWTCH!'); attach('A slot', 'Schmoo'); attach('A slot', 'Wilma'); defineConfigSchema('esm-bedrock', {}); defineConfigSchema('esm-flintstones', {}); registerModuleLoad('esm-bedrock'); registerModuleLoad('esm-flintstones'); provide({ 'esm-bedrock': {} }); provide({ 'esm-flintstones': {} }); function RootComponent() { return (
); } const App = openmrsComponentDecorator({ moduleName: 'esm-bedrock', featureName: 'Bedrock', disableTranslations: true, })(RootComponent); act(() => { render(); }); await waitFor(() => { const slot = screen.getByTestId('slot'); expect(slot.firstChild).toHaveAttribute('data-extension-id', 'Wilma'); expect(screen.queryAllByText(/\bSchmoo\b/)).toHaveLength(0); }); }); }); async function registerSimpleExtension( name: string, moduleName: string, takesConfig: boolean = false, privileges?: string | string[], ) { const SimpleComponent = () =>
{name}
; const ConfigurableComponent = () => { const config = useConfig(); return (
{name}: {JSON.stringify(config)}
); }; registerExtension({ name, moduleName, load: getSyncLifecycle(takesConfig ? ConfigurableComponent : SimpleComponent, { moduleName, featureName: moduleName, disableTranslations: true, }), meta: {}, privileges, }); }