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(),
+}))
+
+// 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()
+ 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()
+ })
+})