/* eslint-disable testing-library/no-node-access, testing-library/no-wait-for-multiple-assertions, testing-library/no-unnecessary-act, testing-library/no-manual-cleanup, jest-dom/prefer-empty, testing-library/prefer-presence-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 } from '../../../esm-react-utils/src';
import {
configInternalStore,
defineConfigSchema,
getExtensionSlotsConfigStore,
getExtensionsConfigStore,
provide,
registerModuleLoad,
resetConfigSystem,
temporaryConfigStore,
} from '../../../esm-config/src';
vi.mock('@openmrs/esm-api', async () => {
const original = await import('@openmrs/esm-api');
return {
...original,
sessionStore: mockSessionStore,
refetchCurrentUser: vi.fn(),
};
});
/**
* Expression evaluation tests
*
* These tests are in a separate file due to test isolation issues. When run with other
* extension/config tests, there is accumulation of state/subscriptions that causes infinite
* update loops. The tests pass when run individually or in this isolated file.
*
* Root cause: Even with subscription cleanup (resetConfigSystem) and deep equality checks,
* running these tests alongside other tests that manipulate the config system creates
* conditions where store updates don't settle. This is a test environment issue, not a
* production code bug - the functionality works correctly in isolation and in production.
*/
describe('Expression evaluation in extension display conditions', () => {
beforeEach(() => {
temporaryConfigStore.setState({ config: {} });
configInternalStore.setState({ providedConfigs: [], schemas: {}, moduleLoaded: {} });
mockSessionStore.setState({});
getExtensionSlotsConfigStore().setState({ slots: {} });
getExtensionsConfigStore().setState({ configs: {} });
updateInternalExtensionStore(() => ({ slots: {}, extensions: {} }));
resetConfigSystem();
});
afterEach(() => {
cleanup();
});
function RootComponent() {
return (
);
}
const App = openmrsComponentDecorator({
moduleName: 'esm-bedrock',
featureName: 'Bedrock',
disableTranslations: true,
})(RootComponent);
it('should show extension when the expression evalutes to true', async () => {
registerSimpleExtension('Schmoo', 'esm-bedrock', true);
attach('A slot', 'Schmoo');
defineConfigSchema('esm-bedrock', {});
registerModuleLoad('esm-bedrock');
provide({
'esm-bedrock': {
'Display conditions': {
expression: 'true',
},
},
});
act(() => {
render();
});
await screen.findByTestId(/slot/);
expect(screen.getByTestId('slot').firstChild).toHaveAttribute('data-extension-id', 'Schmoo');
});
it('should hide extension when the expression evaluates to false', async () => {
registerSimpleExtension('Schmoo', 'esm-bedrock', true);
attach('A slot', 'Schmoo');
defineConfigSchema('esm-bedrock', {});
registerModuleLoad('esm-bedrock');
provide({
'esm-bedrock': {
'Display conditions': {
expression: 'false',
},
},
});
act(() => {
render();
});
await screen.findByTestId(/slot/);
expect(screen.getByTestId('slot').firstChild).toBeNull();
});
it('should show extension using a complex expression', async () => {
registerSimpleExtension('Schmoo', 'esm-bedrock', true);
attach('A slot', 'Schmoo');
defineConfigSchema('esm-bedrock', {});
registerModuleLoad('esm-bedrock');
provide({
'esm-bedrock': {
'Display conditions': {
expression: 'session.user ? session.user.privileges.some(p => p.display === "YOWTCH!") : false',
},
},
});
render();
// Update session state after rendering so the component can react to the change
await act(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: 'YOWTCH!', display: 'YOWTCH!' }],
roles: [],
retired: false,
locale: 'en',
allowedLocales: ['en'],
},
},
});
});
await waitFor(() => {
const slot = screen.getByTestId('slot');
expect(slot.firstChild).toHaveAttribute('data-extension-id', 'Schmoo');
});
});
it('should hide extension using a complex expression', async () => {
registerSimpleExtension('Schmoo', 'esm-bedrock', true);
attach('A slot', 'Schmoo');
defineConfigSchema('esm-bedrock', {});
registerModuleLoad('esm-bedrock');
provide({
'esm-bedrock': {
'Display conditions': {
expression: 'session.user.privileges.every(p => p.display !== "YOWTCH!")',
},
},
});
render();
// Update session state after rendering so the component can react to the change
await act(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: 'YOWTCH!', display: 'YOWTCH!' }],
roles: [],
retired: false,
locale: 'en',
allowedLocales: ['en'],
},
},
});
});
await waitFor(() => {
const slot = screen.getByTestId('slot');
expect(slot.firstChild).toBeNull();
});
});
it('should hide extension if expression contains an error', async () => {
registerSimpleExtension('Schmoo', 'esm-bedrock', true);
attach('A slot', 'Schmoo');
defineConfigSchema('esm-bedrock', {});
registerModuleLoad('esm-bedrock');
render();
// Provide config with error expression after rendering so the component can react to the change
await act(async () => {
provide({
'esm-bedrock': {
'Display conditions': {
expression: 'NotDefined === true',
},
},
});
});
await waitFor(() => {
const slot = screen.getByTestId('slot');
expect(slot.firstChild).toBeNull();
});
}, 10000);
});
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,
});
}