From f4fd77c96bb8e7526d2ab84dd57040e49c7b1025 Mon Sep 17 00:00:00 2001 From: Hendrik de Graaf Date: Wed, 19 Aug 2026 15:16:02 +0200 Subject: [PATCH 1/2] fix: stabilize config object --- .../src/components/ServerVersionProvider.js | 45 +++++--- .../__tests__/ServerVersionProvider.test.js | 102 ++++++++++++++++++ 2 files changed, 133 insertions(+), 14 deletions(-) create mode 100644 adapter/src/components/__tests__/ServerVersionProvider.test.js diff --git a/adapter/src/components/ServerVersionProvider.js b/adapter/src/components/ServerVersionProvider.js index 439ba832f..cf8fdab79 100644 --- a/adapter/src/components/ServerVersionProvider.js +++ b/adapter/src/components/ServerVersionProvider.js @@ -1,7 +1,7 @@ import { Provider } from '@dhis2/app-runtime' import { getBaseUrlByAppName, setBaseUrlByAppName } from '@dhis2/pwa' import PropTypes from 'prop-types' -import React, { useEffect, useState } from 'react' +import React, { useEffect, useMemo, useState } from 'react' import { get } from '../utils/api.js' import { parseDHIS2ServerVersion, parseVersion } from '../utils/parseVersion.js' import { LoadingMask } from './LoadingMask.js' @@ -43,6 +43,35 @@ export const ServerVersionProvider = ({ const { systemInfo } = systemInfoState const { userInfo } = userInfoState const { baseUrl } = baseUrlState + const serverVersion = useMemo( + () => + systemInfo?.version + ? parseDHIS2ServerVersion(systemInfo.version) + : undefined, + [systemInfo?.version] + ) + const config = useMemo( + () => ({ + appName, + appUrlSlug, + appVersion: parseVersion(appVersion), + baseUrl, + apiVersion: apiVersion || serverVersion?.minor, + serverVersion, + systemInfo, + pwaEnabled, + }), + [ + appName, + appUrlSlug, + appVersion, + baseUrl, + apiVersion, + serverVersion, + systemInfo, + pwaEnabled, + ] + ) useEffect(() => { // if URL prop is not set, set state to error to show login modal. @@ -204,21 +233,9 @@ export const ServerVersionProvider = ({ return } - const serverVersion = parseDHIS2ServerVersion(systemInfo.version) - const realApiVersion = serverVersion.minor - return ( ({ + Provider: jest.fn(({ children }) => children), +})) + +jest.mock('@dhis2/pwa', () => ({ + getBaseUrlByAppName: jest.fn(() => Promise.resolve(undefined)), + setBaseUrlByAppName: jest.fn(() => Promise.resolve()), + OfflineInterface: jest.fn(), +})) + +jest.mock('../../utils/api.js', () => ({ + get: jest.fn(), +})) + +const abortableResolve = (value) => { + const promise = Promise.resolve(value) + promise.abort = jest.fn() + return promise +} + +const props = { + appName: 'test-app', + appUrlSlug: 'test-app', + appVersion: '1.2.3', + url: 'http://localhost:8080', + pwaEnabled: false, +} + +const lastConfig = () => + Provider.mock.calls[Provider.mock.calls.length - 1][0].config + +describe('ServerVersionProvider', () => { + beforeEach(() => { + jest.clearAllMocks() + get.mockImplementation((url) => + url.endsWith('/api/me') + ? abortableResolve({ id: 'user-id' }) + : abortableResolve({ version: '2.41.1' }) + ) + }) + + it('provides the same config object across re-renders', async () => { + const { rerender } = render( + +
+ + ) + + await waitFor(() => expect(Provider).toHaveBeenCalled()) + const config = lastConfig() + const callCount = Provider.mock.calls.length + + rerender( + +
+ + ) + + // The re-render must actually reach Provider, or the test proves nothing + expect(Provider.mock.calls.length).toBeGreaterThan(callCount) + expect(lastConfig()).toBe(config) + }) + + it('parses the server version into the config', async () => { + render( + +
+ + ) + + await waitFor(() => expect(Provider).toHaveBeenCalled()) + expect(lastConfig()).toMatchObject({ + apiVersion: 41, + appVersion: { major: 1, minor: 2, patch: 3 }, + serverVersion: { major: 2, minor: 41, patch: 1 }, + }) + }) + + it('does not warn about the server version while system info loads', async () => { + const warn = jest.spyOn(console, 'warn').mockImplementation(() => {}) + + render( + +
+ + ) + + // Nothing to parse yet — must not warn about an undefined version + expect(warn).not.toHaveBeenCalled() + + await waitFor(() => expect(Provider).toHaveBeenCalled()) + expect(warn).not.toHaveBeenCalled() + + warn.mockRestore() + }) +}) From 78582191eaa10b14561851978f7ce56a306b8a2a Mon Sep 17 00:00:00 2001 From: Hendrik de Graaf Date: Wed, 19 Aug 2026 15:29:37 +0200 Subject: [PATCH 2/2] chore: fix failing test --- .../src/components/__tests__/ServerVersionProvider.test.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/adapter/src/components/__tests__/ServerVersionProvider.test.js b/adapter/src/components/__tests__/ServerVersionProvider.test.js index 07c05ee6f..b11fafac5 100644 --- a/adapter/src/components/__tests__/ServerVersionProvider.test.js +++ b/adapter/src/components/__tests__/ServerVersionProvider.test.js @@ -18,6 +18,12 @@ jest.mock('../../utils/api.js', () => ({ get: jest.fn(), })) +// LoginModal imports the generated `src/locales` module, which is gitignored and +// absent in CI. It is not exercised here, so keep it out of the module graph. +jest.mock('../LoginModal.js', () => ({ + LoginModal: () => null, +})) + const abortableResolve = (value) => { const promise = Promise.resolve(value) promise.abort = jest.fn()