Repository: cs01/termpair (language: python)

Issue:
convert to TypeScript
**How would this feature be useful?**
<!-- Describe any use cases this solves or frustrations it alleviates.   -->
The codebase will be easier to work with, and bugs will be caught earlier



Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 41afe56..50135d3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,7 @@
 ## 0.1.2.0
 * Add ability to copy+paste using keystrokes
 * Improve error messages, in particular if there is no server running
+* [dev] migrate codebase to typescript
 
 ## 0.1.1.1
 * Fix server bug when using SSL certs (#44)
diff --git a/termpair/frontend_src/package.json b/termpair/frontend_src/package.json
index 6129352..b95175e 100644
--- a/termpair/frontend_src/package.json
+++ b/termpair/frontend_src/package.json
@@ -3,10 +3,15 @@
   "version": "0.1.0",
   "private": true,
   "dependencies": {
+    "@types/jest": "^26.0.24",
+    "@types/node": "^16.3.1",
+    "@types/react": "^17.0.14",
+    "@types/react-dom": "^17.0.9",
     "moment": "^2.24.0",
     "react": "^16.8.4",
     "react-dom": "^16.8.4",
     "react-scripts": "3.3.0",
+    "typescript": "^4.3.5",
     "xterm": "^4.4.0"
   },
   "scripts": {
diff --git a/termpair/frontend_src/src/App.js b/termpair/frontend_src/src/App.tsx
similarity index 86%
rename from termpair/frontend_src/src/App.js
rename to termpair/frontend_src/src/App.tsx
index 58b0b3c..a0252cf 100644
--- a/termpair/frontend_src/src/App.js
+++ b/termpair/frontend_src/src/App.tsx
@@ -5,7 +5,7 @@ import logo from "./logo.png"; // logomakr.com/4N54oK
 import { Terminal as Xterm } from "xterm";
 import moment from "moment";
 import { getSecretKey, decrypt, encrypt } from "./encryption";
-function Led(props) {
+function Led(props: any) {
   return (
     <div className="flexnowrap">
       <div className={`led led-${props.color}`} />
@@ -14,7 +14,7 @@ function Led(props) {
   );
 }
 
-function TopBar(props) {
+function TopBar(props: any) {
   return (
     <div id="top">
       <a href="https://github.com/cs01/termpair">
@@ -24,7 +24,7 @@ function TopBar(props) {
   );
 }
 
-function StatusBar(props) {
+function StatusBar(props: any) {
   return (
     <div id="statusbar">
       {" "}
@@ -70,8 +70,15 @@ function BottomBar() {
   );
 }
 
-class App extends Component {
-  constructor(props) {
+type AppState = any;
+
+class App extends Component<{}, AppState> {
+  props: any;
+  setState: any;
+  state: any;
+  terminalRef: any;
+  xterm: Xterm;
+  constructor(props: {}) {
     super(props);
     const terminalId = new URLSearchParams(window.location.search).get(
       "terminal_id"
@@ -115,12 +122,19 @@ class App extends Component {
 
   async componentDidMount() {
     const xterm = this.xterm;
-    xterm.open(document.getElementById("terminal"));
+    const el = document.getElementById("terminal");
+    if (!el) {
+      console.error("no xterm element found");
+      return;
+    }
+
+    xterm.open(el);
     xterm.attachCustomKeyEventHandler(
       getCustomKeyEventHandler(
         xterm,
         this.props?.terminalData?.allow_browser_control,
-        async (newInput) => {
+        async (newInput: any) => {
+          // @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'Nullable<CryptoKey>' is not assi... Remove this comment to see the full error message
           webSocket.send(await encrypt(secretEncryptionKey, newInput));
         }
       )
@@ -157,7 +171,7 @@ class App extends Component {
       `${ws_protocol}://${window.location.hostname}:${window.location.port}${window.location.pathname}connect_browser_to_terminal?terminal_id=${this.state.terminalId}`
     );
 
-    xterm.onData(async (data) => {
+    xterm.onData(async (data: any) => {
       webSocket.send(await encrypt(secretEncryptionKey, data));
     });
 
@@ -187,11 +201,12 @@ class App extends Component {
       this.setState({ num_clients: 0 });
     });
 
-    async function handleWebsocketMessage(message) {
+    async function handleWebsocketMessage(this: any, message: any) {
       const data = JSON.parse(message.data);
       if (data.event === "new_output") {
         const encryptedBase64Payload = data.payload;
         const decryptedPayload = await decrypt(
+          // @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'Nullable<CryptoKey>' is not assi... Remove this comment to see the full error message
           secretEncryptionKey,
           encryptedBase64Payload
         );
@@ -212,15 +227,17 @@ class App extends Component {
   }
 }
 
-function writeInstructions(xterm) {
+function writeInstructions(xterm: any) {
   xterm.writeln("To broadcast a terminal, run");
   const host = `${window.location.protocol}//${window.location.hostname}${window.location.pathname}`;
   xterm.writeln("");
   let port = window.location.port;
   if (!window.location.port) {
     if (window.location.protocol === "https:") {
+      // @ts-expect-error ts-migrate(2322) FIXME: Type 'number' is not assignable to type 'string'.
       port = 443;
     } else {
+      // @ts-expect-error ts-migrate(2322) FIXME: Type 'number' is not assignable to type 'string'.
       port = 80;
     }
   }
@@ -245,7 +262,11 @@ function writeInstructions(xterm) {
  * @param {*} sendInputToTerminal - function to encode and send input over the websocket
  * @returns nothing
  */
-function getCustomKeyEventHandler(terminal, canType, sendInputToTerminal) {
+function getCustomKeyEventHandler(
+  terminal: any,
+  canType: any,
+  sendInputToTerminal: any
+) {
   /**
    * Custom key event handler which is run before keys are
    * processed, giving consumers of xterm.js ultimate control as to what keys
@@ -255,7 +276,7 @@ function getCustomKeyEventHandler(terminal, canType, sendInputToTerminal) {
    * propagation and/or prevent the default action. The function returns
    * whether the event should be processed by xterm.js.
    */
-  async function customKeyEventHandler(e) {
+  function customKeyEventHandler(e: any) {
     if (e.type !== "keydown") {
       return true;
     }
@@ -265,8 +286,9 @@ function getCustomKeyEventHandler(terminal, canType, sendInputToTerminal) {
         if (!canType) {
           return false;
         }
-        const toPaste = await navigator.clipboard.readText();
-        sendInputToTerminal(toPaste);
+        navigator.clipboard.readText().then((toPaste) => {
+          sendInputToTerminal(toPaste);
+        });
         return false;
       } else if (key === "c" || key === "x") {
         // 'x' is used as an alternate to 'c' because ctrl+c is taken
diff --git a/termpair/frontend_src/src/encryption.js b/termpair/frontend_src/src/encryption.tsx
similarity index 83%
rename from termpair/frontend_src/src/encryption.js
rename to termpair/frontend_src/src/encryption.tsx
index 3f5cd7a..a78797f 100644
--- a/termpair/frontend_src/src/encryption.js
+++ b/termpair/frontend_src/src/encryption.tsx
@@ -3,7 +3,7 @@
 
 const IV_LENGTH = 12;
 
-export async function getSecretKey() {
+export async function getSecretKey(): Promise<Nullable<CryptoKey>> {
   try {
     const b64EncodedKey = window.location.hash.substring(
       1, // skip the '#' symbol
@@ -14,7 +14,7 @@ export async function getSecretKey() {
       "raw",
       keyData,
       {
-        name: "AES-GCM"
+        name: "AES-GCM",
       },
       false, // extractable
       ["encrypt", "decrypt"]
@@ -25,7 +25,10 @@ export async function getSecretKey() {
   }
 }
 
-export async function decrypt(secretKey, encryptedPayloadB64) {
+export async function decrypt(
+  secretKey: CryptoKey,
+  encryptedPayloadB64: string
+) {
   // decode base64 data to unencrypted iv and encrypted data
   const ivAndPayload = Buffer.from(encryptedPayloadB64, "base64");
 
@@ -39,7 +42,7 @@ export async function decrypt(secretKey, encryptedPayloadB64) {
     await window.crypto.subtle.decrypt(
       {
         name: "AES-GCM",
-        iv: iv
+        iv: iv,
       },
       secretKey,
       encryptedTerminalOutput
@@ -48,13 +51,13 @@ export async function decrypt(secretKey, encryptedPayloadB64) {
   return decryptedTerminalOutput;
 }
 
-export async function encrypt(secretKey, utf8Payload) {
+export async function encrypt(secretKey: CryptoKey, utf8Payload: string) {
   // The same iv must never be reused with a given key
   const iv = window.crypto.getRandomValues(new Uint8Array(IV_LENGTH));
   const encryptedArrayBuffer = await window.crypto.subtle.encrypt(
     {
       name: "AES-GCM",
-      iv: iv
+      iv: iv,
     },
     secretKey,
     new TextEncoder().encode(utf8Payload)
@@ -68,14 +71,17 @@ export async function encrypt(secretKey, utf8Payload) {
   return base64EncryptedString;
 }
 
-function _combineBuffers(buffer1, buffer2) {
+function _combineBuffers(
+  buffer1: Uint8Array,
+  buffer2: ArrayBuffer
+): ArrayBufferLike {
   const tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
   tmp.set(new Uint8Array(buffer1), 0);
   tmp.set(new Uint8Array(buffer2), buffer1.byteLength);
   return tmp.buffer;
 }
 
-function _arrayBufferToBase64(buffer) {
+function _arrayBufferToBase64(buffer: ArrayBuffer) {
   const bytes = new Uint8Array(buffer);
   let binary = "";
   const len = bytes.byteLength;
diff --git a/termpair/frontend_src/src/global.d.ts b/termpair/frontend_src/src/global.d.ts
new file mode 100644
index 0000000..0ca4e55
--- /dev/null
+++ b/termpair/frontend_src/src/global.d.ts
@@ -0,0 +1,1 @@
+type Nullable<T> = T | null;
diff --git a/termpair/frontend_src/src/index.js b/termpair/frontend_src/src/index.tsx
similarity index 100%
rename from termpair/frontend_src/src/index.js
rename to termpair/frontend_src/src/index.tsx
diff --git a/termpair/frontend_src/src/react-app-env.d.ts b/termpair/frontend_src/src/react-app-env.d.ts
new file mode 100644
index 0000000..6431bc5
--- /dev/null
+++ b/termpair/frontend_src/src/react-app-env.d.ts
@@ -0,0 +1,1 @@
+/// <reference types="react-scripts" />
diff --git a/termpair/frontend_src/src/serviceWorker.js b/termpair/frontend_src/src/serviceWorker.ts
similarity index 96%
rename from termpair/frontend_src/src/serviceWorker.js
rename to termpair/frontend_src/src/serviceWorker.ts
index f8c7e50..2848e22 100644
--- a/termpair/frontend_src/src/serviceWorker.js
+++ b/termpair/frontend_src/src/serviceWorker.ts
@@ -20,7 +20,7 @@ const isLocalhost = Boolean(
     )
 );
 
-export function register(config) {
+export function register(config: any) {
   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);
@@ -54,7 +54,7 @@ export function register(config) {
   }
 }
 
-function registerValidSW(swUrl, config) {
+function registerValidSW(swUrl: any, config: any) {
   navigator.serviceWorker
     .register(swUrl)
     .then(registration => {
@@ -98,7 +98,7 @@ function registerValidSW(swUrl, config) {
     });
 }
 
-function checkValidServiceWorker(swUrl, config) {
+function checkValidServiceWorker(swUrl: any, config: any) {
   // Check if the service worker can be found. If it can't reload the page.
   fetch(swUrl)
     .then(response => {
diff --git a/termpair/frontend_src/tsconfig.json b/termpair/frontend_src/tsconfig.json
new file mode 100644
index 0000000..f2850b7
--- /dev/null
+++ b/termpair/frontend_src/tsconfig.json
@@ -0,0 +1,25 @@
+{
+  "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"
+  ]
+}
diff --git a/termpair/frontend_src/yarn.lock b/termpair/frontend_src/yarn.lock
index ea3eb78..ee616af 100644
--- a/termpair/frontend_src/yarn.lock
+++ b/termpair/frontend_src/yarn.lock
@@ -1377,6 +1377,17 @@
     "@types/istanbul-reports" "^1.1.1"
     "@types/yargs" "^13.0.0"
 
+"@jest/types@^26.6.2":
+  version "26.6.2"
+  resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e"
+  integrity sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==
+  dependencies:
+    "@types/istanbul-lib-coverage" "^2.0.0"
+    "@types/istanbul-reports" "^3.0.0"
+    "@types/node" "*"
+    "@types/yargs" "^15.0.0"
+    chalk "^4.0.0"
+
 "@mrmlnc/readdir-enhanced@^2.2.1":
   version "2.2.1"
   resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde"
@@ -1559,6 +1570,21 @@
     "@types/istanbul-lib-coverage" "*"
     "@types/istanbul-lib-report" "*"
 
+"@types/istanbul-reports@^3.0.0":
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz#9153fe98bba2bd565a63add9436d6f0d7f8468ff"
+  integrity sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==
+  dependencies:
+    "@types/istanbul-lib-report" "*"
+
+"@types/jest@^26.0.24":
+  version "26.0.24"
+  resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.24.tgz#943d11976b16739185913a1936e0de0c4a7d595a"
+  integrity sha512-E/X5Vib8BWqZNRlDxj9vYXhsDwPYbPINqKF9BsnSoon4RQ0D9moEuLD8txgyypFLH7J4+Lho9Nr/c8H0Fi+17w==
+  dependencies:
+    jest-diff "^26.0.0"
+    pretty-format "^26.0.0"
+
 "@types/json-schema@^7.0.3", "@types/json-schema@^7.0.5":
   version "7.0.8"
   resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.8.tgz#edf1bf1dbf4e04413ca8e5b17b3b7d7d54b59818"
@@ -1569,7 +1595,7 @@
   resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40"
   integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==
 
-"@types/node@*":
+"@types/node@*", "@types/node@^16.3.1":
   version "16.3.1"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-16.3.1.tgz#24691fa2b0c3ec8c0d34bfcfd495edac5593ebb4"
   integrity sha512-N87VuQi7HEeRJkhzovao/JviiqKjDKMVKxKMfUvSKw+MbkbW8R0nA3fi/MQhhlxV2fQ+2ReM+/Nt4efdrJx3zA==
@@ -1579,11 +1605,37 @@
   resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
   integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
 
+"@types/prop-types@*":
+  version "15.7.4"
+  resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11"
+  integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==
+
 "@types/q@^1.5.1":
   version "1.5.5"
   resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.5.tgz#75a2a8e7d8ab4b230414505d92335d1dcb53a6df"
   integrity sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ==
 
+"@types/react-dom@^17.0.9":
+  version "17.0.9"
+  resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.9.tgz#441a981da9d7be117042e1a6fd3dac4b30f55add"
+  integrity sha512-wIvGxLfgpVDSAMH5utdL9Ngm5Owu0VsGmldro3ORLXV8CShrL8awVj06NuEXFQ5xyaYfdca7Sgbk/50Ri1GdPg==
+  dependencies:
+    "@types/react" "*"
+
+"@types/react@*", "@types/react@^17.0.14":
+  version "17.0.14"
+  resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.14.tgz#f0629761ca02945c4e8fea99b8177f4c5c61fb0f"
+  integrity sha512-0WwKHUbWuQWOce61UexYuWTGuGY/8JvtUe/dtQ6lR4sZ3UiylHotJeWpf3ArP9+DSGUoLY3wbU59VyMrJps5VQ==
+  dependencies:
+    "@types/prop-types" "*"
+    "@types/scheduler" "*"
+    csstype "^3.0.2"
+
+"@types/scheduler@*":
+  version "0.16.2"
+  resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
+  integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==
+
 "@types/stack-utils@^1.0.1":
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
@@ -1601,6 +1653,13 @@
   dependencies:
     "@types/yargs-parser" "*"
 
+"@types/yargs@^15.0.0":
+  version "15.0.14"
+  resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.14.tgz#26d821ddb89e70492160b66d10a0eb6df8f6fb06"
+  integrity sha512-yEJzHoxf6SyQGhBhIYGXQDSCkJjB6HohDShto7m8vaKg9Yp0Yn8+71J9eakh2bnPg6BfsH9PRMhiRTZnd4eXGQ==
+  dependencies:
+    "@types/yargs-parser" "*"
+
 "@typescript-eslint/eslint-plugin@^2.8.0":
   version "2.34.0"
   resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.34.0.tgz#6f8ce8a46c7dea4a6f1d171d2bb8fbae6dac2be9"
@@ -1949,7 +2008,7 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1:
   dependencies:
     color-convert "^1.9.0"
 
-ansi-styles@^4.1.0:
+ansi-styles@^4.0.0, ansi-styles@^4.1.0:
   version "4.3.0"
   resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
   integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
@@ -2793,7 +2852,7 @@ chalk@^1.1.3:
     strip-ansi "^3.0.0"
     supports-color "^2.0.0"
 
-chalk@^4.1.0:
+chalk@^4.0.0, chalk@^4.1.0:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad"
   integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==
@@ -3503,6 +3562,11 @@ cssstyle@^1.0.0, cssstyle@^1.1.1:
   dependencies:
     cssom "0.3.x"
 
+csstype@^3.0.2:
+  version "3.0.8"
+  resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.8.tgz#d2266a792729fb227cd216fb572f43728e1ad340"
+  integrity sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==
+
 cyclist@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
@@ -3681,6 +3745,11 @@ diff-sequences@^24.9.0:
   resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5"
   integrity sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew==
 
+diff-sequences@^26.6.2:
+  version "26.6.2"
+  resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1"
+  integrity sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==
+
 diffie-hellman@^5.0.0:
   version "5.0.3"
   resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
@@ -5860,6 +5929,16 @@ jest-diff@^24.9.0:
     jest-get-type "^24.9.0"
     pretty-format "^24.9.0"
 
+jest-diff@^26.0.0:
+  version "26.6.2"
+  resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.6.2.tgz#1aa7468b52c3a68d7d5c5fdcdfcd5e49bd164394"
+  integrity sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==
+  dependencies:
+    chalk "^4.0.0"
+    diff-sequences "^26.6.2"
+    jest-get-type "^26.3.0"
+    pretty-format "^26.6.2"
+
 jest-docblock@^24.3.0:
   version "24.9.0"
   resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-24.9.0.tgz#7970201802ba560e1c4092cc25cbedf5af5a8ce2"
@@ -5915,6 +5994,11 @@ jest-get-type@^24.9.0:
   resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-24.9.0.tgz#1684a0c8a50f2e4901b6644ae861f579eed2ef0e"
   integrity sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q==
 
+jest-get-type@^26.3.0:
+  version "26.3.0"
+  resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0"
+  integrity sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==
+
 jest-haste-map@^24.9.0:
   version "24.9.0"
   resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-24.9.0.tgz#b38a5d64274934e21fa417ae9a9fbeb77ceaac7d"
@@ -8391,6 +8475,16 @@ pretty-format@^24.9.0:
     ansi-styles "^3.2.0"
     react-is "^16.8.4"
 
+pretty-format@^26.0.0, pretty-format@^26.6.2:
+  version "26.6.2"
+  resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93"
+  integrity sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==
+  dependencies:
+    "@jest/types" "^26.6.2"
+    ansi-regex "^5.0.0"
+    ansi-styles "^4.0.0"
+    react-is "^17.0.1"
+
 process-nextick-args@~2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
@@ -8642,6 +8736,11 @@ react-is@^16.8.1, react-is@^16.8.4:
   resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
   integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
 
+react-is@^17.0.1:
+  version "17.0.2"
+  resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
+  integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
+
 react-scripts@3.3.0:
   version "3.3.0"
   resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-3.3.0.tgz#f26a21f208f20bd04770f43e50b5bbc151920c2a"
@@ -10151,6 +10250,11 @@ typedarray@^0.0.6:
   resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
   integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
 
+typescript@^4.3.5:
+  version "4.3.5"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4"
+  integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==
+
 uglify-js@3.4.x:
   version "3.4.10"
   resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.10.tgz#9ad9563d8eb3acdfb8d38597d2af1d815f6a755f"


Test patch:
diff --git a/termpair/frontend_src/src/App.test.js b/termpair/frontend_src/src/App.test.js
deleted file mode 100644
index a754b20..0000000
--- a/termpair/frontend_src/src/App.test.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import React from 'react';
-import ReactDOM from 'react-dom';
-import App from './App';
-
-it('renders without crashing', () => {
-  const div = document.createElement('div');
-  ReactDOM.render(<App />, div);
-  ReactDOM.unmountComponentAtNode(div);
-});
diff --git a/termpair/frontend_src/src/App.test.tsx b/termpair/frontend_src/src/App.test.tsx
new file mode 100644
index 0000000..23c181f
--- /dev/null
+++ b/termpair/frontend_src/src/App.test.tsx
@@ -0,0 +1,9 @@
+import React from "react";
+import ReactDOM from "react-dom";
+import App from "./App";
+
+it("renders without crashing", () => {
+  const div = document.createElement("div");
+  ReactDOM.render(<App />, div);
+  ReactDOM.unmountComponentAtNode(div);
+});