Guy Bianco IV
2020-10-23 ccdefeec7f141c77479c8830e7ed83494b09ea79
Scoreboard app unsolved (#8)

* add initial version of scoreboard app

* mock score display

* add cypress and tests

* add docker script and update readme

* remove loose type file

* remove hard-coded players ; add back generated loose type file

* yarn --> npm

* add cy:podman npm script

* add cross-env

* add max length test

* make changes for students to fix

* Update scoreboard/cypress/fixtures/example.json

Co-authored-by: Jaime Ramírez <jaime.ramirez@redhat.com>

Co-authored-by: Jaime Ramírez <jaime.ramirez@redhat.com>
33 files added
15676 ■■■■■ changed files
scoreboard/.editorconfig 4 ●●●● patch | view | raw | blame | history
scoreboard/.eslintrc.json 61 ●●●●● patch | view | raw | blame | history
scoreboard/.gitignore 24 ●●●●● patch | view | raw | blame | history
scoreboard/.prettierrc 4 ●●●● patch | view | raw | blame | history
scoreboard/README.md 89 ●●●●● patch | view | raw | blame | history
scoreboard/cypress.json 3 ●●●●● patch | view | raw | blame | history
scoreboard/cypress/fixtures/example.json 5 ●●●●● patch | view | raw | blame | history
scoreboard/cypress/integration/scoreboard.spec.ts 55 ●●●●● patch | view | raw | blame | history
scoreboard/cypress/plugins/index.js 21 ●●●●● patch | view | raw | blame | history
scoreboard/cypress/support/commands.js 25 ●●●●● patch | view | raw | blame | history
scoreboard/cypress/support/index.js 20 ●●●●● patch | view | raw | blame | history
scoreboard/cypress/tsconfig.json 8 ●●●●● patch | view | raw | blame | history
scoreboard/package-lock.json 14899 ●●●●● patch | view | raw | blame | history
scoreboard/package.json 48 ●●●●● patch | view | raw | blame | history
scoreboard/public/favicon.ico patch | view | raw | blame | history
scoreboard/public/index.html 42 ●●●●● patch | view | raw | blame | history
scoreboard/public/logo192.png patch | view | raw | blame | history
scoreboard/public/logo512.png patch | view | raw | blame | history
scoreboard/public/manifest.json 25 ●●●●● patch | view | raw | blame | history
scoreboard/public/robots.txt 3 ●●●●● patch | view | raw | blame | history
scoreboard/screenshot.png patch | view | raw | blame | history
scoreboard/src/App.test.tsx 12 ●●●●● patch | view | raw | blame | history
scoreboard/src/App.tsx 57 ●●●●● patch | view | raw | blame | history
scoreboard/src/Player.ts 4 ●●●● patch | view | raw | blame | history
scoreboard/src/ScoreDisplay.test.tsx 26 ●●●●● patch | view | raw | blame | history
scoreboard/src/ScoreDisplay.tsx 31 ●●●●● patch | view | raw | blame | history
scoreboard/src/__mocks__/ScoreDisplay.tsx 5 ●●●●● patch | view | raw | blame | history
scoreboard/src/index.css 16 ●●●●● patch | view | raw | blame | history
scoreboard/src/index.tsx 17 ●●●●● patch | view | raw | blame | history
scoreboard/src/react-app-env.d.ts 1 ●●●● patch | view | raw | blame | history
scoreboard/src/serviceWorker.ts 147 ●●●●● patch | view | raw | blame | history
scoreboard/src/setupTests.ts 5 ●●●●● patch | view | raw | blame | history
scoreboard/tsconfig.json 19 ●●●●● patch | view | raw | blame | history
scoreboard/.editorconfig
New file
@@ -0,0 +1,4 @@
[*]
tab_width = 4
indent_size = 4
insert_final_newline = true
scoreboard/.eslintrc.json
New file
@@ -0,0 +1,61 @@
{
    "env": {
        "browser": true,
        "node": true,
        "es6": true
    },
    "extends": [
        "eslint:recommended",
        "plugin:react/recommended",
        "plugin:@typescript-eslint/eslint-recommended",
        "prettier/@typescript-eslint",
        "plugin:prettier/recommended"
    ],
    "globals": {
        "Atomics": "readonly",
        "SharedArrayBuffer": "readonly",
        "process": "readonly"
    },
    "parser": "@typescript-eslint/parser",
    "parserOptions": {
        "ecmaFeatures": {
            "jsx": true
        },
        "ecmaVersion": 2018,
        "sourceType": "module"
    },
    "plugins": ["react", "@typescript-eslint"],
    "rules": {
        "max-len": ["error", 120],
        "indent": ["error", 4],
        "linebreak-style": ["error", "unix"],
        // Prettier determines quotes that are easiest to read
        "quotes": ["off", "double"],
        "semi": ["error", "always"],
        "eol-last": ["error", "always"]
    },
    "settings": {
        "react": {
            "version": "detect"
        }
    },
    "overrides": [
        {
            "files": ["*.ts", "*.tsx"],
            "rules": {
                "@typescript-eslint/no-unused-vars": [
                    2,
                    {
                        "args": "none"
                    }
                ]
            }
        },
        {
            "files": ["server.js", "src/Config.js"],
            "env": {
                "node": true
            }
        }
    ]
}
scoreboard/.gitignore
New file
@@ -0,0 +1,24 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
# cypress
*.mp4
scoreboard/.prettierrc
New file
@@ -0,0 +1,4 @@
{
    "trailingComma": "all",
    "tabWidth": 4
}
scoreboard/README.md
New file
@@ -0,0 +1,89 @@
# Scoreboard
A simple scoreboard app built using React.
![](screenshot.png)
## Pre-requisites
To run the application, you will need:
-   Node.js version 12 or higher
-   NPM version 6 or higher
Additionally, in order to run functional tests with Cypress, you will need either Chrome or Firefox installed.
You may instead use Docker to run Cypress in "headless mode" (see below under `npm run cy:docker`).
See more details about system requirements for Cypress here:
[https://docs.cypress.io/guides/getting-started/installing-cypress.html#System-requirements](https://docs.cypress.io/guides/getting-started/installing-cypress.html#System-requirements)
## Installation
Install all NPM dependencies by running:
`npm install`
Install NPM dependencies excluding develepment-only dependencies by running:
`npm install --prod`
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.<br />
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.<br />
You will also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.<br />
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.<br />
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.<br />
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run format`
Automatically formats all files using Prettier.
Learn more about Prettier at [prettier.io](https://prettier.io)
### `npm run cy:open`
Opens the Cypress GUI.
This allows you to select which browser Cypress should use, as well as select test files to run.
Requires Chrome or Firefox installation.
The development server must also be running (see `npm start`).
### `npm run cy:run`
Runs all Cypress tests in "headless mode" (doesn't open a browser window).
Requires Chrome or Firefox installation.
The development server must also be running (see `npm start`).
### `npm run cy:docker`
Runs all Cypress tests in "headless mode" withing a container using Docker.
Requires Docker installation.
The development server must also be running (see `npm start`).
### `npm run cy:podman`
Runs all Cypress tests in "headless mode" withing a container using podman.
Requires podman installation.
The development server must also be running (see `npm start`).
scoreboard/cypress.json
New file
@@ -0,0 +1,3 @@
{
    "baseUrl": "http://localhost:3000"
}
scoreboard/cypress/fixtures/example.json
New file
@@ -0,0 +1,5 @@
{
  "name": "Using fixtures to represent data",
  "email": "hello@cypress.io",
  "body": "Fixtures are a great way to mock data for responses to routes"
}
scoreboard/cypress/integration/scoreboard.spec.ts
New file
@@ -0,0 +1,55 @@
beforeEach(() => {
    // GIVEN the user is on the app
    cy.visit("/");
});
describe("add player", () => {
    it("should add a player", () => {
        // AND the user has entered 'bobby' into the 'Player Name' field
        cy.get("form").find('[placeholder="Player Name"]').type("bobby");
        // WHEN they submit the form
        cy.get("form").submit();
        // THEN 'bobby' should be added to the players
        cy.get("#player-scores").should("contain", "bobby");
    });
});
describe("update score", () => {
    beforeEach(() => {
        // AND the player 'tom' exists
        cy.get("form").find('[placeholder="Player Name"]').type("tom");
        cy.get("form").submit();
    });
    it("should increase a player's score", () => {
        // AND tom has a score of 0
        cy.get("#player-scores").should("contain", "tom: 0");
        // WHEN the user hits '+' next to 'tom'
        cy.get("#player-scores")
            .contains("tom")
            .siblings()
            .contains("+")
            .click();
        // THEN 'tom's score should be 1
        cy.get("#player-scores").should("contain", "tom: 1");
    });
    it.skip("should decrease a player's score", () => {
        // AND tom has a score of 0
        cy.get("#player-scores").should("contain", "tom: 0");
        // WHEN the user hits '-' next to 'tom'
        cy.get("#player-scores")
            .contains("tom")
            .siblings()
            .contains("-")
            .click();
        // THEN 'tom's score should be -1
        cy.get("#player-scores").should("contain", "tom: 0");
    });
});
scoreboard/cypress/plugins/index.js
New file
@@ -0,0 +1,21 @@
/// <reference types="cypress" />
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
/**
 * @type {Cypress.PluginConfig}
 */
module.exports = (on, config) => {
  // `on` is used to hook into various events Cypress emits
  // `config` is the resolved Cypress config
}
scoreboard/cypress/support/commands.js
New file
@@ -0,0 +1,25 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
scoreboard/cypress/support/index.js
New file
@@ -0,0 +1,20 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')
scoreboard/cypress/tsconfig.json
New file
@@ -0,0 +1,8 @@
{
    "compilerOptions": {
        "target": "es5",
        "lib": ["es5", "dom"],
        "types": ["cypress"]
    },
    "include": ["**/*.ts"]
}
scoreboard/package-lock.json
New file
Diff too large
scoreboard/package.json
New file
@@ -0,0 +1,48 @@
{
    "name": "scoreboard",
    "version": "0.1.0",
    "private": true,
    "dependencies": {
        "@testing-library/jest-dom": "^4.2.4",
        "@testing-library/react": "^9.3.2",
        "@testing-library/user-event": "^7.1.2",
        "@types/jest": "^24.0.0",
        "@types/node": "^12.0.0",
        "@types/react": "^16.9.0",
        "@types/react-dom": "^16.9.0",
        "react": "^16.14.0",
        "react-dom": "^16.14.0",
        "react-scripts": "^3.4.4",
        "typescript": "~3.7.2"
    },
    "devDependencies": {
        "cross-env": "^7.0.2",
        "cypress": "^5.4.0"
    },
    "scripts": {
        "start": "react-scripts start",
        "build": "react-scripts build",
        "format": "prettier --write .",
        "test": "react-scripts test",
        "test:coverage": "cross-env CI=true npm run test --ci --coverage --collectCoverageFrom='src'",
        "cy:open": "cross-env CYPRESS_CRASH_REPORTS=0 cypress open",
        "cy:run": "cross-env CYPRESS_CRASH_REPORTS=0 cypress run",
        "cy:docker": "docker run -it --rm -v $PWD:/e2e -w /e2e cypress/included:5.4.0 --config baseUrl=http://host.docker.internal:3000",
        "cy:podman": "podman run -it --rm -v $PWD:/e2e -w /e2e cypress/included:5.4.0 --config baseUrl=http://172.17.0.1:3000"
    },
    "eslintConfig": {
        "extends": "react-app"
    },
    "browserslist": {
        "production": [
            ">0.2%",
            "not dead",
            "not op_mini all"
        ],
        "development": [
            "last 1 chrome version",
            "last 1 firefox version",
            "last 1 safari version"
        ]
    }
}
scoreboard/public/favicon.ico
scoreboard/public/index.html
New file
@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <meta name="theme-color" content="#000000" />
        <meta
            name="description"
            content="Web site created using create-react-app"
        />
        <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
        <!--
      manifest.json provides metadata used when your web app is installed on a
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
    -->
        <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
        <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.
      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
        <title>Scoreboard</title>
    </head>
    <body>
        <noscript>You need to enable JavaScript to run this app.</noscript>
        <div id="root"></div>
        <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.
      You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.
      To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    --></body>
</html>
scoreboard/public/logo192.png
scoreboard/public/logo512.png
scoreboard/public/manifest.json
New file
@@ -0,0 +1,25 @@
{
    "short_name": "React App",
    "name": "Create React App Sample",
    "icons": [
        {
            "src": "favicon.ico",
            "sizes": "64x64 32x32 24x24 16x16",
            "type": "image/x-icon"
        },
        {
            "src": "logo192.png",
            "type": "image/png",
            "sizes": "192x192"
        },
        {
            "src": "logo512.png",
            "type": "image/png",
            "sizes": "512x512"
        }
    ],
    "start_url": ".",
    "display": "standalone",
    "theme_color": "#000000",
    "background_color": "#ffffff"
}
scoreboard/public/robots.txt
New file
@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
scoreboard/screenshot.png
scoreboard/src/App.test.tsx
New file
@@ -0,0 +1,12 @@
import React from "react";
import { render } from "@testing-library/react";
import App from "./App";
jest.mock("./ScoreDisplay");
describe("App Component", () => {
    test("renders the app component", () => {
        const { getByText, getAllByText } = render(<App />);
        expect(getByText(/Scoreboard/i)).toBeInTheDocument();
    });
});
scoreboard/src/App.tsx
New file
@@ -0,0 +1,57 @@
import React, { useState } from "react";
import { Player } from "./Player";
import { ScoreDisplay } from "./ScoreDisplay";
function App() {
    const [playerName, setPlayerName] = useState<string>("");
    const [players, setPlayers] = useState<Player[]>([]);
    function updateScore(playerName: string, newScore: number) {
        const updatedPlayers = players.map((player) => {
            if (player.name === playerName) {
                return { ...player, score: newScore };
            } else {
                return player;
            }
        });
        setPlayers(updatedPlayers);
    }
    function newPlayerSubmit(e: React.FormEvent<HTMLFormElement>) {
        e.preventDefault();
        setPlayers([...players, { name: playerName, score: 0 }]);
    }
    return (
        <>
            <h1>Scoreboard</h1>
            <div id="player-scores">
                {players.map((player, index) => (
                    <ScoreDisplay
                        key={index}
                        name={player.name}
                        score={player.score}
                        onUpdateScore={updateScore}
                    />
                ))}
            </div>
            <br />
            <form onSubmit={newPlayerSubmit}>
                <input
                    type="text"
                    placeholder="Player Name"
                    value={playerName}
                    maxLength={10}
                    onChange={(e) => setPlayerName(e.currentTarget.value)}
                />
                <button type="submit">Add Player</button>
            </form>
        </>
    );
}
export default App;
scoreboard/src/Player.ts
New file
@@ -0,0 +1,4 @@
export interface Player {
    name: string;
    score: number;
}
scoreboard/src/ScoreDisplay.test.tsx
New file
@@ -0,0 +1,26 @@
import React from "react";
import { fireEvent, render } from "@testing-library/react";
import { ScoreDisplay } from "./ScoreDisplay";
describe("ScoreDisplay Component", () => {
    test("renders a player's score", () => {
        const { getByText } = render(
            <ScoreDisplay name="foo" score={3} onUpdateScore={() => {}} />,
        );
        expect(getByText(/foo: 3/i)).toBeInTheDocument();
    });
    test("calls update score on + and -", () => {
        const updateScoreMock = jest.fn();
        const { getByText } = render(
            <ScoreDisplay
                name="foo"
                score={3}
                onUpdateScore={updateScoreMock}
            />,
        );
        fireEvent.click(getByText(/\+/));
        fireEvent.click(getByText(/\-/));
        expect(updateScoreMock).toBeCalledTimes(2);
    });
});
scoreboard/src/ScoreDisplay.tsx
New file
@@ -0,0 +1,31 @@
import React from "react";
export function ScoreDisplay(props: {
    name: string;
    score: number;
    onUpdateScore: (name: string, score: number) => void;
}) {
    const { name, score } = props;
    return (
        <div className="score-display">
            <span>
                {name}: {score}
            </span>
            <button
                aria-label={`increase ${name}'s score`}
                type="button"
                onClick={() => props.onUpdateScore(name, score + 1)}
            >
                +
            </button>
            <button
                aria-label={`decrease ${name}'s score`}
                type="button"
                onClick={() => props.onUpdateScore(name, score - 1)}
            >
                -
            </button>
        </div>
    );
}
scoreboard/src/__mocks__/ScoreDisplay.tsx
New file
@@ -0,0 +1,5 @@
import React from "react";
export function ScoreDisplay() {
    return <span>mocked score display</span>;
}
scoreboard/src/index.css
New file
@@ -0,0 +1,16 @@
body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
        "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
        "Helvetica Neue", sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    width: 15em;
}
.score-display {
    height: 2em;
}
.score-display button {
    float: right;
}
scoreboard/src/index.tsx
New file
@@ -0,0 +1,17 @@
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
ReactDOM.render(
    <React.StrictMode>
        <App />
    </React.StrictMode>,
    document.getElementById("root"),
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
scoreboard/src/react-app-env.d.ts
New file
@@ -0,0 +1 @@
/// <reference types="react-scripts" />
scoreboard/src/serviceWorker.ts
New file
@@ -0,0 +1,147 @@
// This optional code is used to register a service worker.
// register() is not called by default.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.
// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://bit.ly/CRA-PWA
const isLocalhost = Boolean(
    window.location.hostname === "localhost" ||
        // [::1] is the IPv6 localhost address.
        window.location.hostname === "[::1]" ||
        // 127.0.0.0/8 are considered localhost for IPv4.
        window.location.hostname.match(
            /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/,
        ),
);
type Config = {
    onSuccess?: (registration: ServiceWorkerRegistration) => void;
    onUpdate?: (registration: ServiceWorkerRegistration) => void;
};
export function register(config?: Config) {
    if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
        // The URL constructor is available in all browsers that support SW.
        const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
        if (publicUrl.origin !== window.location.origin) {
            // Our service worker won't work if PUBLIC_URL is on a different origin
            // from what our page is served on. This might happen if a CDN is used to
            // serve assets; see https://github.com/facebook/create-react-app/issues/2374
            return;
        }
        window.addEventListener("load", () => {
            const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
            if (isLocalhost) {
                // This is running on localhost. Let's check if a service worker still exists or not.
                checkValidServiceWorker(swUrl, config);
                // Add some additional logging to localhost, pointing developers to the
                // service worker/PWA documentation.
                navigator.serviceWorker.ready.then(() => {
                    console.log(
                        "This web app is being served cache-first by a service " +
                            "worker. To learn more, visit https://bit.ly/CRA-PWA",
                    );
                });
            } else {
                // Is not localhost. Just register service worker
                registerValidSW(swUrl, config);
            }
        });
    }
}
function registerValidSW(swUrl: string, config?: Config) {
    navigator.serviceWorker
        .register(swUrl)
        .then((registration) => {
            registration.onupdatefound = () => {
                const installingWorker = registration.installing;
                if (installingWorker == null) {
                    return;
                }
                installingWorker.onstatechange = () => {
                    if (installingWorker.state === "installed") {
                        if (navigator.serviceWorker.controller) {
                            // At this point, the updated precached content has been fetched,
                            // but the previous service worker will still serve the older
                            // content until all client tabs are closed.
                            console.log(
                                "New content is available and will be used when all " +
                                    "tabs for this page are closed. See https://bit.ly/CRA-PWA.",
                            );
                            // Execute callback
                            if (config && config.onUpdate) {
                                config.onUpdate(registration);
                            }
                        } else {
                            // At this point, everything has been precached.
                            // It's the perfect time to display a
                            // "Content is cached for offline use." message.
                            console.log("Content is cached for offline use.");
                            // Execute callback
                            if (config && config.onSuccess) {
                                config.onSuccess(registration);
                            }
                        }
                    }
                };
            };
        })
        .catch((error) => {
            console.error("Error during service worker registration:", error);
        });
}
function checkValidServiceWorker(swUrl: string, config?: Config) {
    // Check if the service worker can be found. If it can't reload the page.
    fetch(swUrl, {
        headers: { "Service-Worker": "script" },
    })
        .then((response) => {
            // Ensure service worker exists, and that we really are getting a JS file.
            const contentType = response.headers.get("content-type");
            if (
                response.status === 404 ||
                (contentType != null &&
                    contentType.indexOf("javascript") === -1)
            ) {
                // No service worker found. Probably a different app. Reload the page.
                navigator.serviceWorker.ready.then((registration) => {
                    registration.unregister().then(() => {
                        window.location.reload();
                    });
                });
            } else {
                // Service worker found. Proceed as normal.
                registerValidSW(swUrl, config);
            }
        })
        .catch(() => {
            console.log(
                "No internet connection found. App is running in offline mode.",
            );
        });
}
export function unregister() {
    if ("serviceWorker" in navigator) {
        navigator.serviceWorker.ready
            .then((registration) => {
                registration.unregister();
            })
            .catch((error) => {
                console.error(error.message);
            });
    }
}
scoreboard/src/setupTests.ts
New file
@@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import "@testing-library/jest-dom/extend-expect";
scoreboard/tsconfig.json
New file
@@ -0,0 +1,19 @@
{
    "compilerOptions": {
        "target": "es5",
        "lib": ["dom", "dom.iterable", "esnext"],
        "allowJs": true,
        "skipLibCheck": true,
        "esModuleInterop": true,
        "allowSyntheticDefaultImports": true,
        "strict": true,
        "forceConsistentCasingInFileNames": true,
        "module": "esnext",
        "moduleResolution": "node",
        "resolveJsonModule": true,
        "isolatedModules": true,
        "noEmit": true,
        "jsx": "react"
    },
    "include": ["src"]
}