Skip to content

Commit

Permalink
Merge pull request #725 from sebgroup/develop
Browse files Browse the repository at this point in the history
minor release: error boundary and translation context
  • Loading branch information
eweseong authored Mar 22, 2022
2 parents ef5b1f4 + 4b61042 commit 56054dc
Show file tree
Hide file tree
Showing 14 changed files with 465 additions and 31 deletions.
36 changes: 36 additions & 0 deletions docs/src/pages/docs/error-boundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import Docs from "@common/Docs";
import { ErrorBoundary } from "@sebgroup/react-components/ErrorBoundary";
import { useDynamicForm } from "@sebgroup/react-components/hooks/useDynamicForm";
import React from "react";

const importString: string = require("!raw-loader!@sebgroup/react-components/ErrorBoundary/ErrorBoundary");
const code: string = `<ErrorBoundary errorView={<>error view</>}>
<div>lorem ipsum</div>
</ErrorBoundary>`;

const ErrorBoundaryPage: React.FC = (): React.ReactElement<void> => {
const {
renderForm: renderControls,
state: { controls },
} = useDynamicForm([
{
key: "controls",
items: [{ key: "hasError", label: "hasError", controlType: "Checkbox" }],
},
]);

return (
<Docs
mainFile={importString}
example={
<div className="w-100 d-flex justify-content-center">
<ErrorBoundary errorView={<>error view</>}>{controls.hasError ? <div>{new Error()}</div> : <div>lorem ipsum</div>}</ErrorBoundary>
</div>
}
code={code}
controls={<>{renderControls()}</>}
/>
);
};

export default ErrorBoundaryPage;
5 changes: 5 additions & 0 deletions docs/static/components-list.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@
"path": "/docs/dropdown/",
"filePath": "./src/Dropdown/index.ts"
},
{
"name": "ErrorBoundary",
"path": "/docs/error-boundary/",
"filePath": "./src/ErrorBoundary/index.ts"
},
{
"name": "FeedbackIndicator",
"path": "/docs/feedbackindicator/",
Expand Down
2 changes: 1 addition & 1 deletion lib/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ module.exports = {
".+\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/test/file.mock.js",
},
testPathIgnorePatterns: ["node_modules", "\\.cache", "<rootDir>.*/public"],
transformIgnorePatterns: ["node_modules/(?!(@sebgroup|react|raf)/)"],
transformIgnorePatterns: ["<rootDir>/node_modules/(?!(@sebgroup|react|raf)/)"],
collectCoverage: true,
coveragePathIgnorePatterns: ["node_modules", "index.ts", "^.+\\.mock", "^.+\\.polyfills"],
testEnvironmentOptions: { resources: "usable" },
Expand Down
49 changes: 49 additions & 0 deletions lib/src/ErrorBoundary/ErrorBoundary.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act, Simulate } from "react-dom/test-utils";
import { ErrorBoundary } from ".";

describe("Component: ErrorBoundary", () => {
let container: HTMLDivElement = null;

beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
});

afterEach(() => {
unmountComponentAtNode(container);
container.remove();
container = null;
});

it("Should render correction", () => {
act(() => {
render(
<ErrorBoundary errorView={<div className="error-view">error view</div>}>
<div className="component">some component</div>
</ErrorBoundary>,
container
);
});

expect(container.firstElementChild.tagName.toLowerCase()).toEqual("div");
expect(container.firstElementChild.textContent).toEqual("some component");
expect(container.querySelector(".error-view")).toBeNull();
});

it("Should render error view when error occurs", () => {
act(() => {
render(
<ErrorBoundary errorView={<div className="error-view">error view</div>}>
<div className="component">{new Error()}</div>
</ErrorBoundary>,
container
);
});

expect(container.firstElementChild.tagName.toLowerCase()).toEqual("div");
expect(container.firstElementChild.textContent).toEqual("error view");
expect(container.querySelector(".component")).toBeNull();
});
});
29 changes: 29 additions & 0 deletions lib/src/ErrorBoundary/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from "react";

interface ErrorBoundaryProps {
/** The error view to be shown */
errorView: React.ReactNode;
}

interface ErrorBoundaryState {
hasError: boolean;
}

export class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false };
}

static getDerivedStateFromError() {
return { hasError: true };
}

render() {
if (this.state.hasError) {
return this.props.errorView;
}

return this.props.children;
}
}
1 change: 1 addition & 0 deletions lib/src/ErrorBoundary/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./ErrorBoundary";
13 changes: 9 additions & 4 deletions lib/src/Tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { randomId } from "@sebgroup/frontend-tools/randomId";
import classnames from "classnames";
import React from "react";
import { Overlay } from "./Overlay";
import classnames from "classnames";
import { ElementPosition } from "./useOverlay";
import "./tooltip.scss";
import { randomId } from "@sebgroup/frontend-tools/randomId";
import { ElementPosition } from "./useOverlay";

const InfoCircleIcon: JSX.Element = (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
Expand Down Expand Up @@ -81,6 +81,7 @@ export const Tooltip: React.FC<TooltipProps> = ({
const onTouchStartEvent = (e: React.TouchEvent<HTMLDivElement>) => onTouch(e, true);
const onTouchEndEvent = (e: React.TouchEvent<HTMLDivElement>) => onTouch(e, false);
const onFocusEvent = (e: React.FocusEvent<HTMLDivElement>) => onTooltipToggle(e, true);
const onBlurEvent = (e: React.FocusEvent<HTMLDivElement>) => onTooltipToggle(e, false);

React.useEffect(() => {
setTooltipId(id || randomId("rc-tooltip-"));
Expand All @@ -98,6 +99,7 @@ export const Tooltip: React.FC<TooltipProps> = ({
onTouchStart={trigger === "hover" && isMobile ? onTouchStartEvent : null}
onTouchEnd={trigger === "hover" && isMobile ? onTouchEndEvent : null}
onFocus={trigger === "focus" ? onFocusEvent : null}
onBlur={trigger === "focus" ? onBlurEvent : null}
>
{props.children ? (
React.Children.count(props.children) === 1 ? (
Expand Down Expand Up @@ -126,7 +128,10 @@ export const Tooltip: React.FC<TooltipProps> = ({
content={content}
disableAutoPosition={disableAutoPosition}
ref={contentRef}
onContentBlur={() => setShow(false)}
onContentBlur={(event: React.FocusEvent<HTMLDivElement>) => {
setShow(false);
onVisibleChange && onVisibleChange(event, false);
}}
show={show || forceShow}
tooltipReference={() => containerRef.current}
/>
Expand Down
1 change: 1 addition & 0 deletions lib/src/contexts/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./translationContext";
113 changes: 113 additions & 0 deletions lib/src/contexts/translationContext.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import fetchMock, { enableFetchMocks } from "jest-fetch-mock";
import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";
import { TranslationProvider, useTranslationContext } from "./translationContext";

enableFetchMocks();

const TestContext: React.FC<{ data?: any; translationKey: string }> = ({ data, translationKey }) => {
const { isLoading, t } = useTranslationContext();
return <div>{isLoading ? <span className="loading-screen">loading...</span> : <p className="translated-text">{t(translationKey, data)}</p>}</div>;
};

const MOCK_TRANSLATION_RESULT: any = { result: { content: { title_translation: "title_translation" } } };

describe("context: translationContext", () => {
let container: HTMLDivElement = null;

beforeEach(() => {
fetchMock.resetMocks();
container = document.createElement("div");
document.body.appendChild(container);
});

afterEach(() => {
unmountComponentAtNode(container);
container.remove();
container = null;
});

it("Should throw error when context used without provider", () => {
expect(() => render(<TestContext translationKey="title_translation" />, container)).toThrowError("useTranslationContext must be used within a TranslationProvider");
});

it("Should show loading screen when fetching translation", async () => {
fetchMock.mockResponseOnce(JSON.stringify(MOCK_TRANSLATION_RESULT));
await act(async () => {
render(
<TranslationProvider url="https://translation_url">
<TestContext translationKey="title_translation" />
</TranslationProvider>,
container
);
expect(container.querySelector(".loading-screen")).toBeDefined();
});
expect(container.querySelector(".loading-screen")).toBeNull();
});

it("Should get translation when key exist in translations", async () => {
fetchMock.mockResponseOnce(JSON.stringify(MOCK_TRANSLATION_RESULT));
await act(async () => {
render(
<TranslationProvider url="https://translation_url">
<TestContext translationKey="title_translation" />
</TranslationProvider>,
container
);
});
expect(container.querySelector(".translated-text").textContent).toEqual("title_translation");
});

it("Should interpolate translation when data exist", async () => {
fetchMock.mockResponseOnce(JSON.stringify({ result: { content: { text_hello: "today is {date}" } } }));
await act(async () => {
render(
<TranslationProvider url="https://translation_url">
<TestContext translationKey="text_hello" data={{ date: "2020-01-01" }} />
</TranslationProvider>,
container
);
});
expect(container.querySelector(".translated-text").textContent).toEqual("today is 2020-01-01");
});

it("Should get empty string when translation does not exist", async () => {
fetchMock.mockResponseOnce(JSON.stringify({}));
await act(async () => {
render(
<TranslationProvider url="https://translation_url">
<TestContext translationKey="key_unknown" />
</TranslationProvider>,
container
);
});
expect(container.querySelector(".translated-text").textContent).toEqual("");
});

it("Should map custom translation path when provided", async () => {
fetchMock.mockResponseOnce(JSON.stringify({ response: MOCK_TRANSLATION_RESULT }));
await act(async () => {
render(
<TranslationProvider url="https://translation_url" translationPath="response.result.content">
<TestContext translationKey="title_translation" />
</TranslationProvider>,
container
);
});
expect(container.querySelector(".translated-text").textContent).toEqual("title_translation");
});

it("Should get fallback translation when translation fetch fails", async () => {
fetchMock.mockRejectOnce();
await act(async () => {
render(
<TranslationProvider url="https://translation_url" fallbackTranslation={{ title_translation: "title_fallbacktranslation" }}>
<TestContext translationKey="title_translation" />
</TranslationProvider>,
container
);
});
expect(container.querySelector(".translated-text").textContent).toEqual("title_fallbacktranslation");
});
});
Loading

0 comments on commit 56054dc

Please sign in to comment.