From af62d94c7d5f8136a14ccdd3db69143c3a9fbde2 Mon Sep 17 00:00:00 2001 From: "Eric L. Goldstein" Date: Fri, 5 Jan 2024 16:50:23 -0500 Subject: [PATCH] completed React Typescript tests --- CHANGELOG.md | 3 +- lib/eslintReactTypescriptConfig.json | 4 +- package-lock.json | 40 +++++++++++ package.json | 2 + .../exampleReactConfigEntrypoint.jsx | 2 +- .../.eslintrc.json | 8 ++- .../components/Board.tsx | 72 +++++++++++++++++++ .../components/Game.tsx | 53 ++++++++++++++ .../components/Square.tsx | 20 ++++++ ...exampleReactTypescriptConfigEntrypoint.tsx | 16 +++++ tsconfig.json | 1 + 11 files changed, 217 insertions(+), 4 deletions(-) create mode 100644 test/eslintReactTypescriptConfig/components/Board.tsx create mode 100644 test/eslintReactTypescriptConfig/components/Game.tsx create mode 100644 test/eslintReactTypescriptConfig/components/Square.tsx create mode 100644 test/eslintReactTypescriptConfig/exampleReactTypescriptConfigEntrypoint.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ca2c30..08f2bd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,8 @@ ## 1.2.0 -- Add example code for each configuration to verify correctness during continuous integration +- Testing added using example code for each configuration to verify correctness during continuous integration +- React and Preact TypeScript configs now allow JSX in files with `.tsx` file extensions - Remove dev dependency `npm-run-all` because it was only used during the `reinstall` NPM script and would cause an error if dependencies weren't installed prior to execution ## 1.1.0 diff --git a/lib/eslintReactTypescriptConfig.json b/lib/eslintReactTypescriptConfig.json index 3d7297e..8c223d5 100644 --- a/lib/eslintReactTypescriptConfig.json +++ b/lib/eslintReactTypescriptConfig.json @@ -9,5 +9,7 @@ "browser": false, "node": false }, - "rules": {} + "rules": { + "react/jsx-filename-extension": ["error", { "extensions": [".jsx", ".tsx"] }] + } } diff --git a/package-lock.json b/package-lock.json index 8111a84..4943f98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,8 @@ "@playwright/test": "1.40.1", "@types/aws-lambda": "8.10.131", "@types/node": "20.10.6", + "@types/react": "18.2.46", + "@types/react-dom": "18.2.18", "eslint": "8.56.0", "jest": "29.7.0", "prettier": "3.1.0", @@ -1418,6 +1420,38 @@ "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==" }, + "node_modules/@types/prop-types": { + "version": "15.7.11", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", + "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", + "dev": true + }, + "node_modules/@types/react": { + "version": "18.2.46", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.46.tgz", + "integrity": "sha512-nNCvVBcZlvX4NU1nRRNV/mFl1nNRuTuslAJglQsq+8ldXe5Xv0Wd2f7WTE3jOxhLH2BFfiZGC6GCp+kHQbgG+w==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.2.18", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.18.tgz", + "integrity": "sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", + "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", + "dev": true + }, "node_modules/@types/semver": { "version": "7.5.6", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", @@ -2332,6 +2366,12 @@ "node": ">= 8" } }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", diff --git a/package.json b/package.json index fa490ab..9e62c6a 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,8 @@ "@playwright/test": "1.40.1", "@types/aws-lambda": "8.10.131", "@types/node": "20.10.6", + "@types/react": "18.2.46", + "@types/react-dom": "18.2.18", "eslint": "8.56.0", "jest": "29.7.0", "prettier": "3.1.0", diff --git a/test/eslintReactConfig/exampleReactConfigEntrypoint.jsx b/test/eslintReactConfig/exampleReactConfigEntrypoint.jsx index 1abea70..f57c837 100644 --- a/test/eslintReactConfig/exampleReactConfigEntrypoint.jsx +++ b/test/eslintReactConfig/exampleReactConfigEntrypoint.jsx @@ -1,4 +1,4 @@ -// Code adapted from here: https://react.dev/learn/tutorial-tic-tac-toe#what-are-you-building +// Code based on this example: https://react.dev/learn/tutorial-tic-tac-toe#what-are-you-building // External Imports import { createRoot } from 'react-dom/client'; diff --git a/test/eslintReactTypescriptConfig/.eslintrc.json b/test/eslintReactTypescriptConfig/.eslintrc.json index c99d64f..3105c36 100644 --- a/test/eslintReactTypescriptConfig/.eslintrc.json +++ b/test/eslintReactTypescriptConfig/.eslintrc.json @@ -1,4 +1,10 @@ { "root": true, - "extends": ["../../lib/eslintReactTypescriptConfig.json"] + "extends": ["../../lib/eslintReactTypescriptConfig.json", "../../lib/eslintBrowserConfig.json"], + "overrides": [ + { + "files": "./components/*.tsx", + "rules": { "unicorn/filename-case": ["error", { "case": "pascalCase" }] } + } + ] } diff --git a/test/eslintReactTypescriptConfig/components/Board.tsx b/test/eslintReactTypescriptConfig/components/Board.tsx new file mode 100644 index 0000000..d1dc3a9 --- /dev/null +++ b/test/eslintReactTypescriptConfig/components/Board.tsx @@ -0,0 +1,72 @@ +// External Imports +import React from 'react'; + +// Internal Imports +import { Square } from './Square.tsx'; + +// Local Functions +function calculateWinner(squares: string[]) { + const lines = [ + [0, 1, 2], + [3, 4, 5], + [6, 7, 8], + [0, 3, 6], + [1, 4, 7], + [2, 5, 8], + [0, 4, 8], + [2, 4, 6], + ]; + for (const [a, b, c] of lines) { + if (squares[a!] && squares[a!] === squares[b!] && squares[a!] === squares[c!]) { + return squares[a!]; + } + } + // eslint-disable-next-line unicorn/no-null -- return an explicit null value + return null; +} + +// Prop Types +interface BoardProps { + onPlay: (s: string[]) => void; + squares: string[]; + xIsNext: boolean; +} + +// Component Definition +function Board({ onPlay, squares, xIsNext }: BoardProps) { + function handleClick(index: number) { + if (calculateWinner(squares) ?? squares[index]) { + return; + } + const nextSquares = [...squares]; + nextSquares[index] = xIsNext ? 'X' : 'O'; + onPlay(nextSquares); + } + + const winner = calculateWinner(squares); + const status = winner ? `Winner: ${winner}` : `Next player: ${xIsNext ? 'X' : 'O'}`; + + return ( + <> +
{status}
+
+ handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
+
+ handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
+
+ handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
+ + ); +} + +// Module Exports +export { Board }; diff --git a/test/eslintReactTypescriptConfig/components/Game.tsx b/test/eslintReactTypescriptConfig/components/Game.tsx new file mode 100644 index 0000000..56c43da --- /dev/null +++ b/test/eslintReactTypescriptConfig/components/Game.tsx @@ -0,0 +1,53 @@ +// External Imports +import React, { useCallback, useState } from 'react'; + +// Internal Imports +import { Board } from './Board.tsx'; + +// Component Definition +function Game() { + // eslint-disable-next-line unicorn/no-null -- the game expects explicit null values + const [history, setHistory] = useState([Array.from({ length: 9 }).fill(null) as string[]]); + const [currentMove, setCurrentMove] = useState(0); + const handlePlay = useCallback( + (nextSquares: string[]) => { + const nextHistory = [...history.slice(0, currentMove + 1), nextSquares]; + setHistory(nextHistory); + setCurrentMove(nextHistory.length - 1); + }, + [currentMove, history], + ); + + function jumpTo(nextMove: number) { + setCurrentMove(nextMove); + } + + const xIsNext = currentMove % 2 === 0; + const currentSquares = history[currentMove]!; + + const moves = history.map((_squares, move) => { + const description = move > 0 ? `Go to move #${move}` : 'Go to game start'; + return ( + // eslint-disable-next-line react/no-array-index-key -- this is the best unique value +
  • + +
  • + ); + }); + + return ( +
    +
    + +
    +
    +
      {moves}
    +
    +
    + ); +} + +// Module Exports +export { Game }; diff --git a/test/eslintReactTypescriptConfig/components/Square.tsx b/test/eslintReactTypescriptConfig/components/Square.tsx new file mode 100644 index 0000000..5c075bc --- /dev/null +++ b/test/eslintReactTypescriptConfig/components/Square.tsx @@ -0,0 +1,20 @@ +// External Imports +import React from 'react'; + +// Prop Types +interface SquareProps { + onSquareClick: () => void; + value: string; +} + +// Component Definition +function Square({ onSquareClick, value }: SquareProps) { + return ( + + ); +} + +// Module Export +export { Square }; diff --git a/test/eslintReactTypescriptConfig/exampleReactTypescriptConfigEntrypoint.tsx b/test/eslintReactTypescriptConfig/exampleReactTypescriptConfigEntrypoint.tsx new file mode 100644 index 0000000..42ca6dc --- /dev/null +++ b/test/eslintReactTypescriptConfig/exampleReactTypescriptConfigEntrypoint.tsx @@ -0,0 +1,16 @@ +// Code based on this example: https://react.dev/learn/tutorial-tic-tac-toe#what-are-you-building + +// External Imports +import { createRoot } from 'react-dom/client'; +import React from 'react'; + +// Internal Imports +import { Game } from './components/Game.tsx'; + +// Render the Application +const rootElement = document.querySelector('#react-root'); +if (!rootElement) { + throw new TypeError('React root element not found'); +} +const reactRoot = createRoot(rootElement); +reactRoot.render(); diff --git a/tsconfig.json b/tsconfig.json index 5701de6..f3c362c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "compilerOptions": { "allowImportingTsExtensions": true, "verbatimModuleSyntax": true, + "jsx": "preserve", // ASTRO BASE TSCONFIG // Enable top-level await, and other modern ESM features.