diff --git a/babel.config.js b/babel.config.js index fcf53b6..cb37526 100644 --- a/babel.config.js +++ b/babel.config.js @@ -14,4 +14,12 @@ module.exports = { }, }], ], + env: { + development: { + ignore: ["**/__tests__", "./scripts"], + }, + test: { + ignore: ["./scripts"], + }, + }, }; diff --git a/package-lock.json b/package-lock.json index 7f32ef6..2d3efca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "foris", - "version": "1.2.0", + "version": "1.3.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 19a00cd..c2e8123 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "foris", - "version": "1.2.0", + "version": "1.3.1", "description": "Set of components and utils for Foris and its plugins.", "author": "CZ.NIC, z.s.p.o.", "repository": { @@ -66,8 +66,8 @@ "webpack": "^4.41.0" }, "scripts": { - "build": "rm -rf dist; babel src --out-dir dist --ignore '**/__tests__' --source-maps inline --copy-files", - "build:watch": "babel src --verbose --watch --out-dir dist --ignore '**/__tests__' --source-maps inline --copy-files", + "build": "rm -rf dist; babel src --out-dir dist --source-maps inline --copy-files", + "build:watch": "babel src --verbose --watch --out-dir dist --source-maps inline --copy-files", "prepare": "rm -rf ./dist && npm run build", "lint": "eslint src", "test": "jest", diff --git a/scripts/publish.sh b/scripts/publish.sh index 6aa73cd..e18e977 100755 --- a/scripts/publish.sh +++ b/scripts/publish.sh @@ -5,7 +5,7 @@ then echo "\$NPM_TOKEN is not set" exit 1 else - # Need to replace "_" with "_" as GitLab CI won't accept secret vars with "-" + # Need to replace "_" with "-" as GitLab CI won't accept secret vars with "-" echo "//registry.npmjs.org/:_authToken=$(echo "$NPM_TOKEN" | tr _ -)" > .npmrc echo "unsafe-perm = true" >> ~/.npmrc if test "$1" = "beta" diff --git a/src/alertContext/AlertContext.js b/src/alertContext/AlertContext.js index bcdb347..48f64bd 100644 --- a/src/alertContext/AlertContext.js +++ b/src/alertContext/AlertContext.js @@ -5,10 +5,10 @@ * See /LICENSE for more information. */ -import React, { useState } from "react"; +import React, { useState, useContext, useCallback } from "react"; import PropTypes from "prop-types"; -import { Alert } from "bootstrap/Alert"; +import { Alert, ALERT_TYPES } from "bootstrap/Alert"; const AlertContext = React.createContext(); @@ -22,14 +22,28 @@ AlertContextProvider.propTypes = { function AlertContextProvider({ children }) { const [alert, setAlert] = useState(null); + const setAlertWrapper = useCallback((message, type = ALERT_TYPES.DANGER) => { + setAlert({ message, type }); + }, [setAlert]); + + const dismissAlert = useCallback(() => setAlert(null), [setAlert]); + return ( <> - {alert && setAlert(null)} />} - + {alert && ( + + {alert.message} + + )} + { children } ); } -export { AlertContext, AlertContextProvider }; +function useAlert() { + return useContext(AlertContext); +} + +export { AlertContext, AlertContextProvider, useAlert }; diff --git a/src/alertContext/__tests__/AlertContext.test.js b/src/alertContext/__tests__/AlertContext.test.js index 3c986a3..7801731 100644 --- a/src/alertContext/__tests__/AlertContext.test.js +++ b/src/alertContext/__tests__/AlertContext.test.js @@ -5,14 +5,19 @@ * See /LICENSE for more information. */ -import React, { useContext } from "react"; +import React from "react"; import { render, getByText, queryByText, fireEvent } from "customTestRender"; -import { AlertContext, AlertContextProvider } from "../AlertContext"; +import { useAlert, AlertContextProvider } from "../AlertContext"; function AlertTest() { - const setAlert = useContext(AlertContext); - return ; + const [setAlert, dismissAlert] = useAlert(); + return ( + <> + + + + ); }; describe("AlertContext", () => { @@ -36,7 +41,7 @@ describe("AlertContext", () => { expect(componentContainer).toMatchSnapshot(); }); - it("should dismiss alert", () => { + it("should dismiss alert with alert button", () => { fireEvent.click(getByText(componentContainer, "Set alert")); // Alert is present expect(getByText(componentContainer, "Alert content")).toBeDefined(); @@ -45,4 +50,14 @@ describe("AlertContext", () => { // Alert is gone expect(queryByText(componentContainer, "Alert content")).toBeNull(); }); + + it("should dismiss alert with external button", () => { + fireEvent.click(getByText(componentContainer, "Set alert")); + // Alert is present + expect(getByText(componentContainer, "Alert content")).toBeDefined(); + + fireEvent.click(getByText(componentContainer, "Dismiss alert")); + // Alert is gone + expect(queryByText(componentContainer, "Alert content")).toBeNull(); + }); }); diff --git a/src/alertContext/__tests__/__snapshots__/AlertContext.test.js.snap b/src/alertContext/__tests__/__snapshots__/AlertContext.test.js.snap index 97a4dc3..9f13a93 100644 --- a/src/alertContext/__tests__/__snapshots__/AlertContext.test.js.snap +++ b/src/alertContext/__tests__/__snapshots__/AlertContext.test.js.snap @@ -3,7 +3,7 @@ exports[`AlertContext should render alert 1`] = `
+
`; @@ -24,5 +27,8 @@ exports[`AlertContext should render component without alert 1`] = ` +
`; diff --git a/src/bootstrap/Alert.css b/src/bootstrap/Alert.css new file mode 100644 index 0000000..520d6fb --- /dev/null +++ b/src/bootstrap/Alert.css @@ -0,0 +1,17 @@ +.floating-alert { + /* Display alert above other components */ + z-index: 2000; +} + +@media(max-width: 768px) { + .floating-alert { + margin: 0.5rem; + } +} + +@media(min-width: 769px) { + .floating-alert { + width: 75%; + margin: 0.5rem auto; + } +} diff --git a/src/bootstrap/Alert.js b/src/bootstrap/Alert.js index 902bede..a63a6f7 100644 --- a/src/bootstrap/Alert.js +++ b/src/bootstrap/Alert.js @@ -8,11 +8,22 @@ import React from "react"; import PropTypes from "prop-types"; +import "./Alert.css"; + +export const ALERT_TYPES = Object.freeze({ + PRIMARY: "primary", + SECONDARY: "secondary", + SUCCESS: "success", + DANGER: "danger", + WARNING: "warning", + INFO: "info", + LIGHT: "light", + DARK: "dark", +}); + Alert.propTypes = { /** Type of the alert it adds as `alert-${type}` class. */ - type: PropTypes.string.isRequired, - /** Alert message. */ - message: PropTypes.string, + type: PropTypes.oneOf(Object.values(ALERT_TYPES)), /** Alert content. */ children: PropTypes.oneOfType([ PropTypes.arrayOf(PropTypes.node), @@ -20,15 +31,21 @@ Alert.propTypes = { ]), /** onDismiss handler. */ onDismiss: PropTypes.func, + /** Floating alerts stay on top of the page */ + floating: PropTypes.bool, +}; + +Alert.defaultProps = { + type: ALERT_TYPES.DANGER, + floating: false, }; export function Alert({ - type, message, onDismiss, children, + type, onDismiss, floating, children, }) { return ( -
+
{onDismiss ? : false} - {message} {children}
); diff --git a/src/bootstrap/Modal.js b/src/bootstrap/Modal.js index d0e02b0..4fc381f 100644 --- a/src/bootstrap/Modal.js +++ b/src/bootstrap/Modal.js @@ -41,7 +41,7 @@ export function Modal({ shown, setShown, children }) { return (
-
+
{children}
diff --git a/src/index.js b/src/index.js index 9561621..c412869 100644 --- a/src/index.js +++ b/src/index.js @@ -12,7 +12,7 @@ export { useAPIDelete } from "api/delete"; export { useAPIPatch } from "api/patch"; // Bootstrap -export { Alert } from "bootstrap/Alert"; +export { Alert, ALERT_TYPES } from "bootstrap/Alert"; export { Button } from "bootstrap/Button"; export { CheckBox } from "bootstrap/CheckBox"; export { DownloadButton } from "bootstrap/DownloadButton"; @@ -66,4 +66,8 @@ export { } from "validations"; // Alert context -export { AlertContext, AlertContextProvider } from "alertContext/AlertContext"; +export { AlertContext, AlertContextProvider, useAlert } from "alertContext/AlertContext"; + +// Testing utilities +export { mockJSONError } from "testUtils/network"; +export { mockSetAlert, mockDismissAlert } from "testUtils/alertContextMock"; diff --git a/src/testUtils/alertContextMock.js b/src/testUtils/alertContextMock.js new file mode 100644 index 0000000..22347e3 --- /dev/null +++ b/src/testUtils/alertContextMock.js @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/) + * + * This is free software, licensed under the GNU General Public License v3. + * See /LICENSE for more information. + */ + +import React from "react"; + +import { AlertContext } from "../alertContext/AlertContext"; + +const mockSetAlert = jest.fn(); +const mockDismissAlert = jest.fn(); + +function AlertContextMock({ children }) { + return ( + + { children } + + ); +} + +export { AlertContextMock, mockSetAlert, mockDismissAlert }; diff --git a/src/testUtils/customTestRender.js b/src/testUtils/customTestRender.js index ee927de..9092f72 100644 --- a/src/testUtils/customTestRender.js +++ b/src/testUtils/customTestRender.js @@ -7,31 +7,37 @@ /* eslint import/export: "off" */ -import React from 'react'; -import PropTypes from 'prop-types'; -import {UIDReset} from 'react-uid'; -import {StaticRouter} from 'react-router'; -import {render} from '@testing-library/react' +import React from "react"; +import { UIDReset } from "react-uid"; +import { StaticRouter } from "react-router"; +import { render } from "@testing-library/react"; +import PropTypes from "prop-types"; + +import { AlertContextMock } from "alertContextMock"; Wrapper.propTypes = { children: PropTypes.oneOfType([ PropTypes.arrayOf(PropTypes.node), - PropTypes.node - ]) + PropTypes.node, + ]), }; -function Wrapper({children}) { - return - - {children} - - +function Wrapper({ children }) { + return ( + + + + {children} + + + + ); } -const customTestRender = (ui, options) => render(ui, {wrapper: Wrapper, ...options}); +const customTestRender = (ui, options) => render(ui, { wrapper: Wrapper, ...options }); // re-export everything -export * from '@testing-library/react' +export * from "@testing-library/react"; // override render method -export {customTestRender as render} +export { customTestRender as render }; diff --git a/src/testUtils/network.js b/src/testUtils/network.js new file mode 100644 index 0000000..05efa59 --- /dev/null +++ b/src/testUtils/network.js @@ -0,0 +1,12 @@ +/* + * Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/) + * + * This is free software, licensed under the GNU General Public License v3. + * See /LICENSE for more information. + */ + +import mockAxios from 'jest-mock-axios'; + +export function mockJSONError(data) { + mockAxios.mockError({ response: { data, headers: { "content-type": "application/json" } } }); +} diff --git a/src/testUtils/setup.js b/src/testUtils/setup.js index af4800b..5c9ca17 100644 --- a/src/testUtils/setup.js +++ b/src/testUtils/setup.js @@ -15,12 +15,15 @@ global.afterEach(() => { // Mock babel (gettext) global._ = str => str; +global.ngettext = str => str; global.babel = {format: (str) => str}; global.ForisTranslations = {}; +// Mock web sockets +window.WebSocket = jest.fn(); + // Mock scrollIntoView -global.HTMLElement.prototype.scrollIntoView = () => { -}; +global.HTMLElement.prototype.scrollIntoView = () => {}; jest.doMock('moment', () => { moment.tz.setDefault('UTC');