Преглед изворни кода

Add Front End Testing:

1. Added JEST test support

2. Upgraded some dependencies (mainly typescript and eslint-typescript)

3. Replaced mocha with jest (as jest works better with TS)

4. Fixed .eslintrc to match the new eslint-typescript version

5. ADD two sample tests (footer.test.tsx and shortener.test.tsx)
dependabot[bot] пре 4 година
родитељ
комит
97ea40cb45

+ 2 - 1
.eslintrc

@@ -1,7 +1,7 @@
 {
   "extends": [
     "eslint:recommended",
-    "plugin:@typescript-eslint/recommended",
+    "plugin:@typescript-eslint/eslint-recommended",
     "plugin:prettier/recommended"
   ],
   "parser": "@typescript-eslint/parser",
@@ -14,6 +14,7 @@
     "no-useless-return": "warn",
     "no-var": "warn",
     "no-console": "warn",
+    "no-unused-vars": "off",
     "max-len": ["warn", { "comments": 80 }],
     "no-param-reassign": 0,
     "require-atomic-updates": 0,

+ 1 - 0
client/components/Input.tsx

@@ -1,3 +1,4 @@
+import React from 'react';
 import { Flex, BoxProps } from "reflexbox/styled-components";
 import styled, { css, keyframes } from "styled-components";
 import { withProp, prop, ifProp } from "styled-tools";

+ 1 - 0
client/components/Layout.tsx

@@ -1,3 +1,4 @@
+import React from "react";
 import { Flex } from "reflexbox/styled-components";
 import { FC } from "react";
 

+ 2 - 1
client/components/Shortener.tsx

@@ -204,6 +204,7 @@ const Shortener = () => {
           placeholder="Paste your long URL"
           placeholderSize={[16, 17, 18]}
           fontSize={[18, 20, 22]}
+          aria-label="target"
           width={1}
           height={[58, 64, 72]}
           px={0}
@@ -212,7 +213,7 @@ const Shortener = () => {
           autoFocus
           data-lpignore
         />
-        <SubmitIconWrapper onClick={onSubmit}>
+        <SubmitIconWrapper onClick={onSubmit} role="button" aria-label="submit">
           <Icon
             name={loading ? "spinner" : "send"}
             size={[22, 26, 28]}

+ 1 - 0
client/components/Text.tsx

@@ -1,3 +1,4 @@
+import React from "react";
 import { switchProp, ifNotProp, ifProp } from "styled-tools";
 import { Box, BoxProps } from "reflexbox/styled-components";
 import styled, { css } from "styled-components";

+ 52 - 0
client/components/__tests__/footer.test.tsx

@@ -0,0 +1,52 @@
+import React from "react";
+import { render } from "@testing-library/react";
+import { StoreProvider } from "easy-peasy";
+import { initializeStore } from "../../store";
+import Footer from "../Footer";
+import getConfig from "next/config";
+
+describe("<Footer /> component test", () => {
+  let app;
+
+  beforeEach(() => {
+    const store = initializeStore();
+    app = (
+      <StoreProvider store={store}>
+        <Footer />
+      </StoreProvider>
+    );
+  });
+
+  it("should contain a github link", () => {
+    const screen = render(app);
+    const githubLink = screen.getByRole("link", { name: "GitHub" });
+    expect(githubLink).toHaveAttribute("href", "https://github.com/thedevs-network/kutt");
+  });
+
+  it("should contain a TOS link", () => {
+    const config = getConfig();
+    const screen = render(app);
+    const tosLink = screen.getByRole("link", { name: "Terms of Service" });
+
+    expect(tosLink).toHaveAttribute("href", "/terms");
+  });
+
+  it("should show contact email if defined", () => {
+    const config = getConfig();
+    config.publicRuntimeConfig.CONTACT_EMAIL = 'foobar';
+    const screen = render(app);
+    const emailLink = screen.getByRole("link", { name: "Contact us" });
+
+    expect(emailLink).toHaveAttribute("href", "mailto:foobar");
+  });
+
+  it("should NOT show contact email if none is defined", () => {
+    const config = getConfig();
+    delete(config.publicRuntimeConfig.CONTACT_EMAIL);
+    const screen = render(app);
+    const emailLink= screen.queryByRole("link", { name: "Contact us" });
+
+    expect(emailLink).toBeNull();
+  });
+})
+

+ 59 - 0
client/components/__tests__/shortener.test.tsx

@@ -0,0 +1,59 @@
+import React from "react";
+import { render } from "@testing-library/react";
+import { StoreProvider, createStore, thunk } from "easy-peasy";
+import userEvent from "@testing-library/user-event"
+import { store } from "../../store";
+import Shortener from "../Shortener";
+
+describe("<Shortener /> component test", () => {
+  let app;
+
+  beforeEach(() => {
+    store.links = {
+      ...store.links,
+      submit: thunk(async (actions, payload) => {
+        return {
+          id: "0",
+          address: "localhost:3000/foobar",
+          banned: false,
+          created_at: "now",
+          link: "localhost:3000/foobar",
+          target: "",
+          updated_at: "now",
+          visit_count: 0
+        };
+      })
+    };
+    const testStore = createStore(store);
+    app = (
+      <StoreProvider store={testStore}>
+        <Shortener />
+      </StoreProvider>
+    );
+  });
+
+  it("Should show the short URL", async () => {
+    const screen = render(app);
+    const urlInput = screen.getByRole("textbox", { name: "target" });
+    userEvent.type(urlInput, "https://easy-peasy.now.sh/docs/api/thunk.html");
+    const submitButton = screen.getByRole("button", { name: "submit" });
+    userEvent.click(submitButton);
+    const msg = await screen.findByText(/localhost:3000\/foobar/i);
+    expect(msg).toBeInTheDocument();
+  });
+
+  it("Should empty target input", async () => {
+    const screen = render(app);
+    let urlInput: HTMLInputElement = screen.getByRole("textbox", {
+      name: "target"
+    }) as HTMLInputElement;
+    userEvent.type(urlInput, "https://easy-peasy.now.sh/docs/api/thunk.html");
+    const submitButton = screen.getByRole("button", { name: "submit" });
+    userEvent.click(submitButton);
+    await screen.findByText(/localhost:3000\/foobar/i);
+    urlInput = screen.getByRole("textbox", {
+      name: "target"
+    }) as HTMLInputElement;
+    expect(urlInput.value).toEqual("");
+  });
+});

+ 1 - 1
client/pages/stats.tsx

@@ -26,7 +26,7 @@ const StatsPage: NextPage<Props> = ({ id }) => {
   const { isAuthenticated } = useStoreState(s => s.auth);
   const [loading, setLoading] = useState(true);
   const [error, setError] = useState(false);
-  const [data, setData] = useState();
+  const [data, setData] = useState<Record<string, any> | undefined>();
   const [period, setPeriod] = useState("lastDay");
 
   const stats = data && data[period];

+ 1 - 1
client/store/store.ts

@@ -10,7 +10,7 @@ export interface StoreModel {
   links: Links;
   loading: Loading;
   settings: Settings;
-  reset: Action;
+  reset: Action<StoreModel>;
 }
 
 let initState: any = {};

+ 5 - 0
jest-setup.ts

@@ -0,0 +1,5 @@
+import "@testing-library/jest-dom";
+import nextConfig from "./next.config";
+
+jest.mock('next/config', () => () => nextConfig);
+

+ 13 - 0
jest.config.js

@@ -0,0 +1,13 @@
+module.exports = {
+  setupFilesAfterEnv: ['<rootDir>/jest-setup.ts'],
+  "preset": "ts-jest",
+  "transform": {
+    "^.+\\.js$": "babel-jest"
+  },
+  "testEnvironment": "jsdom",
+  "globals": {
+    "ts-jest": {
+      "tsconfig": "<rootDir>/tsconfig.test.json"
+    }
+  }
+};

Разлика између датотеке није приказан због своје велике величине
+ 618 - 282
package-lock.json


+ 20 - 10
package.json

@@ -4,10 +4,10 @@
   "description": "Modern URL shortener.",
   "main": "./production-server/server.js",
   "scripts": {
-    "test": "mocha --compilers js:@babel/register ./client/**/__test__/*.js",
+    "test": "jest",
     "docker:build": "docker build -t kutt .",
     "docker:run": "docker run -p 3000:3000 --env-file .env -d kutt:latest",
-    "dev": "npm run migrate && nodemon server/server.ts",
+    "dev": "npm run migrate && NODE_ENV=development nodemon server/server.ts",
     "build": "rimraf production-server && tsc --project tsconfig.json && copyfiles -f \"server/mail/*.html\" production-server/mail && next build client/ ",
     "start": "npm run migrate && cross-env NODE_ENV=production node production-server/server.js",
     "migrate": "knex migrate:latest --env production",
@@ -47,7 +47,7 @@
     "cross-env": "^7.0.2",
     "date-fns": "^2.9.0",
     "dotenv": "^8.2.0",
-    "easy-peasy": "^3.3.0",
+    "easy-peasy": "^5.0.3",
     "email-validator": "^1.2.3",
     "envalid": "^6.0.0",
     "express": "^4.17.1",
@@ -99,19 +99,24 @@
   },
   "devDependencies": {
     "@babel/cli": "^7.8.3",
-    "@babel/core": "^7.8.3",
+    "@babel/core": "^7.12.17",
     "@babel/node": "^7.8.3",
-    "@babel/preset-env": "^7.8.3",
+    "@babel/preset-env": "^7.12.17",
     "@babel/register": "^7.8.3",
+    "@testing-library/jest-dom": "^5.11.9",
+    "@testing-library/react": "^11.2.5",
+    "@testing-library/user-event": "^12.8.3",
     "@types/bcryptjs": "^2.4.2",
     "@types/body-parser": "^1.17.1",
     "@types/bull": "^3.12.0",
+    "@types/chai": "^4.2.15",
     "@types/cookie-parser": "^1.4.2",
     "@types/cors": "^2.8.6",
     "@types/date-fns": "^2.6.0",
     "@types/dotenv": "^4.0.3",
     "@types/express": "^4.17.2",
     "@types/helmet": "0.0.38",
+    "@types/jest": "^26.0.20",
     "@types/jsonwebtoken": "^7.2.8",
     "@types/jwt-decode": "^2.2.1",
     "@types/mongodb": "^3.3.14",
@@ -128,16 +133,18 @@
     "@types/react-tooltip": "^3.11.0",
     "@types/redis": "^2.8.14",
     "@types/reflexbox": "^4.0.0",
-    "@types/styled-components": "^4.1.8",
-    "@typescript-eslint/eslint-plugin": "^2.16.0",
-    "@typescript-eslint/parser": "^2.16.0",
+    "@types/sinon": "^9.0.10",
+    "@types/styled-components": "^5.1.7",
+    "@typescript-eslint/eslint-plugin": "^4.15.2",
+    "@typescript-eslint/parser": "^4.15.2",
     "babel": "^6.23.0",
     "babel-cli": "^6.26.0",
     "babel-core": "^6.26.3",
     "babel-eslint": "^8.2.6",
+    "babel-jest": "^26.6.3",
     "babel-plugin-styled-components": "^1.10.6",
     "babel-preset-env": "^1.7.0",
-    "chai": "^4.1.2",
+    "chai": "^4.3.0",
     "copyfiles": "^2.2.0",
     "deep-freeze": "^0.0.1",
     "eslint": "^5.16.0",
@@ -148,6 +155,7 @@
     "eslint-plugin-prettier": "^3.1.2",
     "eslint-plugin-react": "^7.18.0",
     "husky": "^0.15.0-rc.13",
+    "jest": "^26.6.3",
     "mocha": "^5.2.0",
     "nock": "^9.3.3",
     "nodemon": "^1.19.4",
@@ -155,6 +163,8 @@
     "redoc": "^2.0.0-rc.20",
     "rimraf": "^3.0.0",
     "sinon": "^6.0.0",
-    "typescript": "^3.7.5"
+    "ts-jest": "^26.5.1",
+    "ts-node": "^9.1.1",
+    "typescript": "^4.2.2"
   }
 }

+ 1 - 1
server/migration/04_links.ts

@@ -179,7 +179,7 @@ const postgres = knex({
                     );
                   });
                 }
-                resolve();
+                resolve(null);
               },
               onError(error) {
                 session.close();

+ 17 - 0
tsconfig.test.json

@@ -0,0 +1,17 @@
+{
+	"compilerOptions": {
+		"target": "es2019",
+		"module": "commonjs",
+		"sourceMap": true,
+		"outDir": "production-server",
+		"noUnusedLocals": false,
+		"resolveJsonModule": true,
+		"esModuleInterop": true,
+		"noEmit": false,
+		"emitDecoratorMetadata": true,
+		"experimentalDecorators": true,
+		"strict": false,
+    "jsx": "react",
+    "allowJs": true
+	}
+}

Неке датотеке нису приказане због велике количине промена