Skip to content

Commit

Permalink
fix(DHIS2-13294): add link to navigate to app after install (#555)
Browse files Browse the repository at this point in the history
* fix(DHIS2-13294): add link to navigate to app after install

* test: add tests for ManualInstall

* fix: ensure logic does not fail with empty response pre v40

* refactor: update label for redirect button
  • Loading branch information
kabaros authored Sep 11, 2024
1 parent af39ed1 commit 27e157a
Show file tree
Hide file tree
Showing 7 changed files with 691 additions and 628 deletions.
12 changes: 6 additions & 6 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ const config = {
],
coverageThreshold: {
global: {
// TODO: The following should be 50
branches: 0,
// TODO: The following should be ~50%
branches: 10,

// TODO: The following should be 75
functions: 0,
lines: 0,
statements: 0,
// TODO: The following should be ~75%
functions: 20,
lines: 20,
statements: 20,
},
},
}
Expand Down
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,15 @@
"@dhis2/cli-style": "^10.4.1",
"@testing-library/jest-dom": "^6.1.5",
"@testing-library/react": "^12",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^29.5.11",
"jest": "^29.7.0",
"react-dom": "^16"
},
"dependencies": {
"@dhis2/app-runtime": "^3.2.0",
"@dhis2/d2-i18n": "^1.1.0",
"@dhis2/ui": "^9.0.1",
"@dhis2/app-runtime": "^3.2.1",
"@dhis2/d2-i18n": "^1.1.3",
"@dhis2/ui": "^9.11.3",
"moment": "^2.29.0",
"prop-types": "^15.8.1",
"query-string": "^6.14.1",
Expand All @@ -48,6 +49,6 @@
"use-query-params": "^1.2.0"
},
"resolutions": {
"@dhis2/ui": "^9.0.1"
"@dhis2/ui": "^9.11.3"
}
}
37 changes: 31 additions & 6 deletions src/pages/ManualInstall/ManualInstall.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,34 @@ import { useAlert } from '@dhis2/app-runtime'
import i18n from '@dhis2/d2-i18n'
import { Button, CircularLoader } from '@dhis2/ui'
import React, { useState, useRef } from 'react'
import { useHistory } from 'react-router-dom'
import { useApi } from '../../api.js'
import styles from './ManualInstall.module.css'

const UploadButton = () => {
const history = useHistory()
const { uploadApp } = useApi()
const [isUploading, setIsUploading] = useState(false)
const successAlert = useAlert(i18n.t('App installed successfully'), {
success: true,
})
const successAlert = useAlert(
i18n.t('App installed successfully'),
(options) => ({
success: true,
actions: options?.id
? [
{
label: i18n.t('View app details'),
onClick: () => {
history.push(`/app/${options.id}`)
},
},
]
: [],
})
)
const errorAlert = useAlert(
({ error }) =>
i18n.t('Failed to install app: {{errorMessage}}', {
errorMessage: error.message,
errorMessage: error?.message,
nsSeparator: '-:-',
}),
{ critical: true }
Expand All @@ -27,9 +42,18 @@ const UploadButton = () => {
const handleUpload = async (event) => {
setIsUploading(true)
try {
await uploadApp(event.target.files[0])
const response = await uploadApp(event.target.files[0])

// using response.text() rather .json() to avoid breaking in <v40
// where the API returned empty response which throws with .json()
const responseText = await response.text()
const appHubId = responseText
? JSON.parse(responseText)?.app_hub_id
: null

formEl.current.reset()
successAlert.show()

successAlert.show({ id: appHubId })
} catch (error) {
errorAlert.show({ error })
}
Expand All @@ -40,6 +64,7 @@ const UploadButton = () => {
<>
<form className={styles.hiddenForm} ref={formEl}>
<input
data-test="file-upload"
type="file"
accept="application/zip"
ref={inputEl}
Expand Down
92 changes: 92 additions & 0 deletions src/pages/ManualInstall/ManualInstall.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { Provider } from '@dhis2/app-runtime'
import { render } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import React from 'react'
import '@testing-library/jest-dom'
import { useHistory } from 'react-router-dom'
import { MockAlertStack } from '../../test-utils/index.js'
import { ManualInstall } from './ManualInstall.js'

jest.mock('react-router-dom', () => ({
useHistory: jest.fn(() => ({
push: jest.fn(),
})),
}))

const renderWithProvider = (component) => {
return render(component, {
wrapper: ({ children }) => {
return (
<Provider config={{}}>
{children}
<MockAlertStack />
</Provider>
)
},
})
}
describe('Manual Install', () => {
const historyPush = jest.fn()

beforeEach(() => {
global.fetch = jest.fn()
useHistory.mockImplementation(() => ({ push: historyPush }))
})

afterEach(() => {
delete global.fetch
jest.resetAllMocks()
})

it('should allow navigating to the app', async () => {
jest.spyOn(global, 'fetch').mockResolvedValueOnce({
text: () =>
Promise.resolve(
JSON.stringify({ app_hub_id: 'some_apphub_id' })
),
})

const { getByTestId, getByText, findByText } = renderWithProvider(
<ManualInstall />
)

const fileInput = getByTestId('file-upload')
userEvent.upload(fileInput, 'testfile')

await findByText('App installed successfully')
await userEvent.click(getByText('View app details'))
expect(historyPush).toHaveBeenCalledWith('/app/some_apphub_id')
})

it('should work with an empty response (pre v41)', async () => {
jest.spyOn(global, 'fetch').mockResolvedValueOnce({
text: () => null,
})

const { getByTestId, findByText, queryByText } = renderWithProvider(
<ManualInstall />
)

const fileInput = getByTestId('file-upload')
userEvent.upload(fileInput, 'testfile')

await findByText('App installed successfully')
expect(queryByText('View app details')).not.toBeInTheDocument()
expect(historyPush).not.toHaveBeenCalled()
})

it('should show an error if it fails', async () => {
jest.spyOn(global, 'fetch').mockRejectedValue('upload failed')

const { getByTestId, findByText, queryByText } = renderWithProvider(
<ManualInstall />
)

const fileInput = getByTestId('file-upload')
userEvent.upload(fileInput, 'testfile')

await findByText('Failed to install app:')
expect(queryByText('View app details')).not.toBeInTheDocument()
expect(historyPush).not.toHaveBeenCalled()
})
})
49 changes: 49 additions & 0 deletions src/test-utils/MockAlertStack.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { useAlerts } from '@dhis2/app-runtime'
import PropTypes from 'prop-types'
import React, { useEffect } from 'react'

const MockAlert = ({ alert }) => {
useEffect(() => {
if (alert.options?.duration) {
setTimeout(() => alert.remove(), alert.options?.duration)
}
}, [alert])
return (
<div style={{ backgroundColor: '#CCC', padding: 8 }}>
{alert.message}
{alert?.options?.actions?.map((action, i) => (
<button key={i} onClick={action.onClick}>
{action.label}
</button>
))}
</div>
)
}

MockAlert.propTypes = {
alert: PropTypes.shape({
message: PropTypes.string,
options: PropTypes.shape({
actions: PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.string,
onClick: PropTypes.func,
})
),
duration: PropTypes.number,
}),
remove: PropTypes.func,
}),
}

export const MockAlertStack = () => {
const alerts = useAlerts()

return (
<div style={{ position: 'absolute', bottom: 16, right: 16 }}>
{alerts.map((alert) => (
<MockAlert key={alert.id} alert={alert} />
))}
</div>
)
}
1 change: 1 addition & 0 deletions src/test-utils/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './MockAlertStack.js'
Loading

1 comment on commit 27e157a

@dhis2-bot
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.