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>
New file |
| | |
| | | [*] |
| | | tab_width = 4 |
| | | indent_size = 4 |
| | | insert_final_newline = true |
New file |
| | |
| | | { |
| | | "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 |
| | | } |
| | | } |
| | | ] |
| | | } |
New file |
| | |
| | | # 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 |
New file |
| | |
| | | { |
| | | "trailingComma": "all", |
| | | "tabWidth": 4 |
| | | } |
New file |
| | |
| | | # 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`). |
New file |
| | |
| | | { |
| | | "baseUrl": "http://localhost:3000" |
| | | } |
New file |
| | |
| | | { |
| | | "name": "Using fixtures to represent data", |
| | | "email": "hello@cypress.io", |
| | | "body": "Fixtures are a great way to mock data for responses to routes" |
| | | } |
New file |
| | |
| | | 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"); |
| | | }); |
| | | }); |
New file |
| | |
| | | /// <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 |
| | | } |
New file |
| | |
| | | // *********************************************** |
| | | // 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) => { ... }) |
New file |
| | |
| | | // *********************************************************** |
| | | // 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') |
New file |
| | |
| | | { |
| | | "compilerOptions": { |
| | | "target": "es5", |
| | | "lib": ["es5", "dom"], |
| | | "types": ["cypress"] |
| | | }, |
| | | "include": ["**/*.ts"] |
| | | } |
New file |
| | |
| | | { |
| | | "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" |
| | | ] |
| | | } |
| | | } |
New file |
| | |
| | | <!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> |
New file |
| | |
| | | { |
| | | "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" |
| | | } |
New file |
| | |
| | | # https://www.robotstxt.org/robotstxt.html |
| | | User-agent: * |
| | | Disallow: |
New file |
| | |
| | | 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(); |
| | | }); |
| | | }); |
New file |
| | |
| | | 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; |
New file |
| | |
| | | export interface Player { |
| | | name: string; |
| | | score: number; |
| | | } |
New file |
| | |
| | | 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); |
| | | }); |
| | | }); |
New file |
| | |
| | | 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> |
| | | ); |
| | | } |
New file |
| | |
| | | import React from "react"; |
| | | |
| | | export function ScoreDisplay() { |
| | | return <span>mocked score display</span>; |
| | | } |
New file |
| | |
| | | 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; |
| | | } |
New file |
| | |
| | | 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(); |
New file |
| | |
| | | /// <reference types="react-scripts" /> |
New file |
| | |
| | | // 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); |
| | | }); |
| | | } |
| | | } |
New file |
| | |
| | | // 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"; |
New file |
| | |
| | | { |
| | | "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"] |
| | | } |