diff --git a/.gitignore b/.gitignore index a547bf3..5815b67 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* - +coverage node_modules dist dist-ssr diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100644 index 0000000..57e971a --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1 @@ +npm run coverage \ No newline at end of file diff --git a/coverage/App.tsx.html b/coverage/App.tsx.html new file mode 100644 index 0000000..8cf4d9a --- /dev/null +++ b/coverage/App.tsx.html @@ -0,0 +1,460 @@ + + + + +
++ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 | 1x +1x +1x +1x +1x +1x +1x +1x + + + + + + + + + + + + + + + + + + + + + + + + +1x +13x + +13x +14x +14x +14x +14x +14x +14x +14x +14x +14x + +13x + + + +13x +2x +2x + +13x +16x +16x +16x +16x +4x +12x +1x +1x +1x + + +1x +1x +16x + +13x +3x +3x +3x + +13x +13x +13x +13x +13x +13x + +13x +37x +37x + +13x +74x + +74x +4x +4x + +70x +70x +70x +70x +1x +69x +74x +74x +74x +74x +74x +45x + +25x +25x +4x + +21x +21x +11x +21x +21x + +25x + +74x +74x + +74x +13x + +1x + | import React from "react";
+import "./App.css";
+import ErrorButton from "./ErrorButton";
+import Button from "./Button";
+import Input from "./Input";
+import PlanetCard from "./PlanetCard";
+import Spinner from "./Spinner";
+import { PlanetApi } from "./PlanetFetch";
+interface PlanetProperties {
+ name?: string;
+ diameter: string;
+ rotation_period: string;
+ orbital_period: string;
+ population: string;
+ climate: string;
+ terrain: string;
+}
+
+interface Planet {
+ uid?: string;
+ name?: string;
+ properties: PlanetProperties;
+}
+
+interface AppState {
+ inputValue: string;
+ searchResults: Planet[];
+ isLoading: boolean;
+ error: string | null;
+ throwBoolean: boolean;
+}
+
+class App extends React.Component<object, AppState> {
+ localStorageKey: string = "starWarsQuery";
+
+ constructor(props: object) {
+ super(props);
+ this.state = {
+ inputValue: localStorage.getItem(this.localStorageKey) || "",
+ searchResults: [],
+ isLoading: false,
+ error: null,
+ throwBoolean: false,
+ };
+ }
+
+ resetErrorState = () => {
+ this.setState({ throwBoolean: false });
+ };
+
+ triggerError = () => {
+ this.setState({ throwBoolean: true });
+ };
+
+ fetchPlanets = async (searchQuery: string = "") => {
+ let errorMessage = "Unknown error";
+ this.setState({ isLoading: true, error: null });
+ try {
+ const planets = await PlanetApi.fetchPlanets(searchQuery);
+ this.setState({ searchResults: planets, isLoading: false });
+ } catch (error) {
+ if (error instanceof Error) {
+ errorMessage = error.message;
+ } else if (typeof error === "string") {
+ errorMessage = error;
+ }
+ this.setState({ error: errorMessage, isLoading: false });
+ }
+ };
+
+ handleSearch = () => {
+ localStorage.setItem(this.localStorageKey, this.state.inputValue.trim());
+ this.fetchPlanets(this.state.inputValue);
+ };
+
+ componentDidMount() {
+ this.setState({
+ inputValue: localStorage.getItem(this.localStorageKey) || "",
+ });
+ this.fetchPlanets(this.state.inputValue);
+ }
+
+ handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this.setState({ inputValue: e.target.value });
+ };
+
+ render() {
+ const { inputValue, searchResults, isLoading, throwBoolean } = this.state;
+
+ if (throwBoolean) {
+ throw new Error("Test error");
+ }
+
+ return (
+ <div className="app-container" data-testid="app">
+ <h1 className="title">Star Wars Planets</h1>
+ {this.state.error ? (
+ <div data-testid="error-message">{this.state.error}</div>
+ ) : null}
+ <div className="search-container">
+ <Input value={inputValue} onChange={this.handleInputChange} />
+ <Button onClick={this.handleSearch}>{"Search"}</Button>
+ </div>
+ {isLoading ? (
+ <Spinner />
+ ) : (
+ <div className="results-container">
+ {inputValue.trim() !== "" && searchResults.length === 0 ? (
+ <div className="nothing">Nothing</div>
+ ) : (
+ <div className="results-grid">
+ {searchResults.map((planet: Planet) => (
+ <PlanetCard key={planet.uid} planet={planet} />
+ ))}
+ </div>
+ )}
+ </div>
+ )}
+ <ErrorButton onClick={this.triggerError} />
+ </div>
+ );
+ }
+}
+
+export default App;
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 | 1x + + + + + + +1x +1x +70x +70x +1x + +1x + | import React from "react";
+
+interface ButtonProps {
+ onClick: () => void;
+ children: React.ReactNode;
+}
+
+class Button extends React.Component<ButtonProps> {
+ render() {
+ return <button onClick={this.props.onClick}>{this.props.children}</button>;
+ }
+}
+
+export default Button;
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 | 1x + + + + + + + + + + +1x +2x +3x +3x +3x +3x +3x +3x + +2x +4x +4x + +2x +2x +2x + +2x +1x +1x + +2x +9x +6x +6x +6x +6x + +6x +6x + +6x + +3x +9x +2x + +1x + | import React from "react";
+
+interface Props {
+ children: React.ReactNode;
+}
+
+interface State {
+ hasError: boolean;
+ error: Error | null;
+}
+
+class ErrorBoundary extends React.Component<Props, State> {
+ constructor(props: Props) {
+ super(props);
+ this.state = {
+ hasError: false,
+ error: null,
+ };
+ }
+
+ static getDerivedStateFromError(error: Error): State {
+ return { hasError: true, error: error };
+ }
+
+ componentDidCatch(error: Error) {
+ this.setState({ error });
+ }
+
+ resetError = () => {
+ this.setState({ hasError: false, error: null });
+ };
+
+ render() {
+ if (this.state.hasError) {
+ return (
+ <div className="error-boundary">
+ <p className="error-message">{this.state.error?.toString()}</p>
+ <button className="error-test-button" onClick={this.resetError}>
+ Reboot
+ </button>
+ </div>
+ );
+ }
+
+ return this.props.children;
+ }
+}
+
+export default ErrorBoundary;
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 | 1x + + + + + +1x +1x +70x +70x +70x + +70x + +70x +1x +1x + | import React from "react";
+
+interface ErrorButtonProps {
+ onClick: () => void;
+}
+
+class ErrorButton extends React.Component<ErrorButtonProps> {
+ render() {
+ const { onClick } = this.props;
+ return (
+ <button onClick={onClick} className="error-test-button">
+ Test Error
+ </button>
+ );
+ }
+}
+export default ErrorButton;
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 | 1x + + + + + +1x +1x +70x +70x +70x +70x +70x +70x + +70x +1x + +1x + | import React from "react";
+
+interface InputProps {
+ value: string;
+ onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
+}
+class Input extends React.Component<InputProps> {
+ render() {
+ return (
+ <input
+ type="text"
+ value={this.props.value}
+ onChange={this.props.onChange}
+ />
+ );
+ }
+}
+
+export default Input;
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 | 1x + + + + + + + + + + + + + + + + + +1x +1x +11x +11x +11x +11x +11x +11x +11x +11x +11x +11x +11x +11x +11x +11x +11x +11x +11x +11x +11x +11x +11x +11x +11x +11x +11x +11x +11x +11x +11x +11x +11x +11x +11x +11x +11x + +11x +1x + +1x + | import React from "react";
+
+interface Planet {
+ uid?: string;
+ name?: string;
+ properties: PlanetProperties;
+}
+
+interface PlanetProperties {
+ name?: string;
+ diameter: string;
+ rotation_period: string;
+ orbital_period: string;
+ population: string;
+ climate: string;
+ terrain: string;
+}
+
+class PlanetCard extends React.Component<{ planet: Planet }> {
+ render() {
+ const { planet } = this.props;
+ return (
+ <div className="planet-card" role="article">
+ <div className="planet-details">
+ <h3 className="planet-name">{planet.name}</h3>
+ <div className="detail-row">
+ <span className="detail-label">Diameter:</span>
+ <span className="prop_planet">{planet.properties.diameter}</span>
+ </div>
+ <div className="detail-row">
+ <span className="detail-label">Climate:</span>
+ <span className="prop_planet">{planet.properties.climate}</span>
+ </div>
+ <div className="detail-row">
+ <span className="detail-label">Terrain:</span>
+ <span className="prop_planet">{planet.properties.terrain}</span>
+ </div>
+ <div className="detail-row">
+ <span className="detail-label">Population:</span>
+ <span className="prop_planet">{planet.properties.population}</span>
+ </div>
+ <div className="detail-row">
+ <span className="detail-label">Rotation Period:</span>
+ <span className="prop_planet">
+ {planet.properties.rotation_period}
+ </span>
+ </div>
+ <div className="detail-row">
+ <span className="detail-label">Orbital Period:</span>
+ <span className="prop_planet">
+ {planet.properties.orbital_period}
+ </span>
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+
+export default PlanetCard;
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 | 1x + +1x +1x +45x +45x +45x +45x + +45x +1x + +1x + | import React from "react";
+
+class Spinner extends React.Component {
+ render() {
+ return (
+ <div className="spinner-container">
+ <div className="spinner"></div>
+ </div>
+ );
+ }
+}
+
+export default Spinner;
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| App.tsx | +
+
+ |
+ 95.29% | +81/85 | +95.83% | +23/24 | +88.88% | +8/9 | +95.29% | +81/85 | +
| Button.tsx | +
+
+ |
+ 100% | +7/7 | +100% | +1/1 | +100% | +1/1 | +100% | +7/7 | +
| ErrorBoundary.tsx | +
+
+ |
+ 100% | +31/31 | +100% | +8/8 | +100% | +6/6 | +100% | +31/31 | +
| ErrorButton.tsx | +
+
+ |
+ 100% | +10/10 | +100% | +1/1 | +100% | +1/1 | +100% | +10/10 | +
| Input.tsx | +
+
+ |
+ 100% | +12/12 | +100% | +1/1 | +100% | +1/1 | +100% | +12/12 | +
| PlanetCard.tsx | +
+
+ |
+ 100% | +41/41 | +100% | +1/1 | +100% | +1/1 | +100% | +41/41 | +
| PlanetFetch.ts | +
+
+ |
+ 90.9% | +40/44 | +87.5% | +14/16 | +100% | +3/3 | +90.9% | +40/44 | +
| Spinner.tsx | +
+
+ |
+ 100% | +10/10 | +100% | +1/1 | +100% | +1/1 | +100% | +10/10 | +
| main.tsx | +
+
+ |
+ 0% | +0/12 | +0% | +0/1 | +0% | +0/1 | +0% | +0/12 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 | + + + + + + + + + + + + + | import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import ErrorBoundary from "./ErrorBoundary"; +import "./index.css"; +import App from "./App.tsx"; + +createRoot(document.getElementById("root") as HTMLElement).render( + <StrictMode> + <ErrorBoundary> + <App /> + </ErrorBoundary> + </StrictMode>, +); + |