diff --git a/.github/workflows/branch.yml b/.github/workflows/branch.yml index 78b7e437..ad0cf0cb 100644 --- a/.github/workflows/branch.yml +++ b/.github/workflows/branch.yml @@ -28,6 +28,12 @@ jobs: - build uses: ./.github/workflows/unit-test.yml + type-check: + needs: + - install + - build + uses: ./.github/workflows/type-check.yml + e2e-test: needs: - install diff --git a/.github/workflows/type-check.yml b/.github/workflows/type-check.yml new file mode 100644 index 00000000..259aa491 --- /dev/null +++ b/.github/workflows/type-check.yml @@ -0,0 +1,36 @@ +name: Type Check + +on: + workflow_call: + +jobs: + type-check: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + + - name: Restore Build Cache + id: restore-build-cache + uses: actions/cache/restore@v4 + with: + path: | + ~/.cache/Cypress + common/temp + */.rush/temp + */node_modules + */dist + key: ${{ runner.os }}-build-${{ hashFiles('common/config/rush/repo-state.json') }} + fail-on-cache-miss: true + + - name: Configure Discord Secrets + run: (cd discord && ./client_config_ci.sh) + env: + # Any values will do, discord application does not run during this job + DISCORD_CLIENT_ID: fake_discord_client_id + DISCORD_CLIENT_SECRET: fake_discord_client_secret + DISCORD_GUILD_ID: fake_guild_id + DISCORD_SESSION_SECRET: fake_discord_session_secret + DISCORD_REDIRECT_URI: fake_discord_redirect_uri + + - name: Type Check + run: node common/scripts/install-run-rush.js type-check diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fa0e6320..6b1ae297 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,10 @@ Notes specifically for repository maintainers ## TODOs +* upload cypress screenshots, videos, and/or +rush *.log artifacts to github build artifacts for debugging * auth fails when switching discord accounts at login +* auth fails on first couple renders, refresh resolves * upgrade to eslint v9 * docs: updating/upgrading node * lint js/md on precommit? @@ -50,7 +53,7 @@ git commit -m 'build(deps): rush update --full' ### 2. Minor Updates -Use a custom rush command (see [command-line.json]) that calls +Use a custom rush command[^1] (see [command-line.json]) that calls [npm-check-updates] to bump all packages to the latest minor version. ```sh @@ -62,7 +65,7 @@ git commit -m 'build(deps): rush update-minor' ### 3. Major Upgrades -Use a custom rush command (see [command-line.json]) that calls +Use a custom rush command[^1] (see [command-line.json]) that calls [npm-check-updates] to bump all packages to the latest major version. ```sh @@ -79,6 +82,8 @@ corresponds with the expected major version of Node and this repository will continue to use Node 18 until Node 20 becomes the active Long Term Support (LTS) version * `eslint` is excluded because some plugins are not compatible with v9 +* `@testing-library/react` is excluded because of peer dependency conflcts +* `eslint-plugin-ava` is excluded because it expects `eslint` >= 9 ## Updating pnpm @@ -109,6 +114,11 @@ git add rush.json common/scripts git commit -m 'build(deps): update rush from x to y' ``` +--- + +[^1]: `update-minor` and `update-major` depend on [jq](https://stedolan.github.io/jq/) +(with Homebrew: `brew install jq`) + [--reject]: https://www.npmjs.com/package/npm-check-updates#reject [@trshcmpctr/scaffold]: ./scaffold [command-line.json]: ./common/config/rush/command-line.json diff --git a/client/.eslintrc.cjs b/client/.eslintrc.cjs index 489e179c..5be5ef22 100644 --- a/client/.eslintrc.cjs +++ b/client/.eslintrc.cjs @@ -87,6 +87,21 @@ module.exports = { extends: ['@trshcmpctr/eslint-config-jest'], }, + // Typescript Tests + { + files: [ + '*.test.ts', + '*.test.tsx', + ], + rules: { + // Add support for understanding when it's ok to pass an + // unbound method to jest expect calls: + // https://github.com/jest-community/eslint-plugin-jest/blob/c5819965e3e8c8dd8c938d2921b1e9629981bdb7/docs/rules/unbound-method.md + '@typescript-eslint/unbound-method': 'off', + 'jest/unbound-method': 'error', + }, + }, + // React tests using @testing-library/react. { files: [ diff --git a/client/.gitignore b/client/.gitignore index 1521c8b7..1f47a0c2 100644 --- a/client/.gitignore +++ b/client/.gitignore @@ -1 +1,2 @@ +cypress/screenshots dist diff --git a/client/README.md b/client/README.md index 04fea4c3..d7cedca6 100644 --- a/client/README.md +++ b/client/README.md @@ -47,7 +47,7 @@ npm run lint Run `Jest` tests. ```bash -npm run test:jest +npm run test ``` ## Type Check diff --git a/client/__snapshots__/webpack.config.test.js.snap b/client/__snapshots__/webpack.config.test.js.snap index d53df314..9b76e788 100644 --- a/client/__snapshots__/webpack.config.test.js.snap +++ b/client/__snapshots__/webpack.config.test.js.snap @@ -110,7 +110,7 @@ exports[`webpackConfig development mode matches snapshot 1`] = ` "chunksSortMode": "auto", "compile": true, "excludeChunks": [], - "favicon": false, + "favicon": "/src/favicon.ico", "filename": "index.html", "hash": false, "inject": "head", @@ -126,6 +126,7 @@ exports[`webpackConfig development mode matches snapshot 1`] = ` "xhtml": false, }, "userOptions": { + "favicon": "/src/favicon.ico", "template": "/src/index.html", "title": "trshcmpctr", }, @@ -253,7 +254,7 @@ exports[`webpackConfig production mode matches snapshot 1`] = ` "chunksSortMode": "auto", "compile": true, "excludeChunks": [], - "favicon": false, + "favicon": "/src/favicon.ico", "filename": "index.html", "hash": false, "inject": "head", @@ -269,6 +270,7 @@ exports[`webpackConfig production mode matches snapshot 1`] = ` "xhtml": false, }, "userOptions": { + "favicon": "/src/favicon.ico", "template": "/src/index.html", "title": "trshcmpctr", }, diff --git a/client/config/rush-project.json b/client/config/rush-project.json new file mode 100644 index 00000000..3edb9779 --- /dev/null +++ b/client/config/rush-project.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/rush-project.schema.json", + "operationSettings": [ + { + "operationName": "build", + "outputFolderNames": ["dist"] + } + ] +} diff --git a/client/cypress/e2e/spec.cy.ts b/client/cypress/e2e/spec.cy.ts index e9bd6413..a6a5fb42 100644 --- a/client/cypress/e2e/spec.cy.ts +++ b/client/cypress/e2e/spec.cy.ts @@ -1,7 +1,35 @@ -describe('client', () => { - it('renders', () => { +describe('app', () => { + it('visits home', () => { cy.visit('/'); - cy.get('#root > h1') - .should('have.text', 'trshcmpctr'); + + cy.get('#root h1').should('have.text', 'trshcmpctr'); + cy.get('#root p').should('have.text', + 'welcome to the trash compactor, '); + }); + + it('navigates to new', () => { + cy.visit('/'); + cy.get('a[href="/new"]').click(); + + cy.get('h2').should('have.text', 'new'); + cy.get('form').within(() => { + cy.get('select').should('have.text', '1.20.1'); + }); + }); + + it('navigates to worlds', () => { + cy.visit('/'); + cy.get('a[href="/worlds"]').click(); + + cy.get('h2').should('have.text', 'worlds'); + cy.get('thead > tr > td:first-child').should('have.text', 'name'); + }); + + it('navigates to a world', () => { + cy.visit('/'); + cy.get('a[href="/worlds"]').click(); + cy.get('a[href="/worlds/1"]').click(); + + cy.get('h3').should('have.text', '1'); }); }); diff --git a/client/package.json b/client/package.json index 893e4743..a76befdc 100644 --- a/client/package.json +++ b/client/package.json @@ -10,16 +10,19 @@ "scripts": { "build": "webpack", "build:production": "webpack --env production", + "build:watch-webpack": "nodemon --exec 'webpack serve' --watch 'webpack.config.js'", "clean": "rm -rf dist", "cy:open": "cypress open --config-file=cypress/cypress.config.ts --e2e --browser=chrome", "cy:run": "cypress run --config-file=cypress/cypress.config.ts", - "lint": "eslint --report-unused-disable-directives --ext .js,.jsx,.ts,.tsx,.cjs --max-warnings=0 .", + "lint": "eslint --report-unused-disable-directives --ext .js,.jsx,.ts,.tsx,.cjs --max-warnings=0 --cache .", "lint:md": "markdownlint-cli2 --config node_modules/@trshcmpctr/markdownlint-config/.markdownlint-cli2.jsonc", "serve": "webpack serve", "start": "npm run serve -- --open", - "test": "npm run test:jest && npm run type-check", + "start:production": "npm run serve -- --open --env production", + "test": "NODE_OPTIONS=--experimental-vm-modules jest --reporters=@trshcmpctr/jest-stdout-reporter --silent", + "test:debug": "NODE_OPTIONS=\"--experimental-vm-modules --inspect\" jest --runInBand --no-cache", + "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch", "test:cypress": "start-server-and-test serve http://localhost:8080 cy:run", - "test:jest": "NODE_OPTIONS=--experimental-vm-modules jest --no-cache --reporters=@trshcmpctr/jest-stdout-reporter --silent", "type-check": "tsc", "watch": "webpack --watch" }, @@ -54,35 +57,35 @@ "@trshcmpctr/markdownlint-config": "workspace:*", "@types/jest": "~29.5.2", "@types/node": "~18.19.31", - "@types/react-dom": "~18.2.6", - "@types/react-router-dom": "~5.3.3", - "@types/react": "~18.2.14", + "@types/react-dom": "~18.3.0", + "@types/react": "~18.3.3", "@types/testing-library__jest-dom": "~5.14.5", - "@typescript-eslint/eslint-plugin": "~7.7.1", - "@typescript-eslint/parser": "~7.7.1", + "@typescript-eslint/eslint-plugin": "~7.12.0", + "@typescript-eslint/parser": "~7.12.0", "babel-jest": "~29.7.0", "babel-loader": "~9.1.0", "babel-plugin-syntax-dynamic-import": "~6.18.0", "css-loader": "~7.1.1", - "cypress": "13.8.0", + "cypress": "13.11.0", "discord-api-types": "~0.37.46", "eslint-import-resolver-typescript": "~3.6.1", - "eslint-plugin-cypress": "~3.0.0", + "eslint-plugin-cypress": "~3.3.0", "eslint-plugin-eslint-comments": "~3.2.0", "eslint-plugin-import": "~2.29.1", - "eslint-plugin-jest": "~28.2.0", + "eslint-plugin-jest": "~28.5.0", "eslint-plugin-jsx-a11y": "~6.8.0", "eslint-plugin-node": "~11.1.0", "eslint-plugin-react-hooks": "~4.6.0", "eslint-plugin-react": "~7.34.1", "eslint-plugin-testing-library": "~6.2.2", - "eslint-webpack-plugin": "~4.1.0", + "eslint-webpack-plugin": "~4.2.0", "eslint": "~8.57.0", "html-webpack-plugin": "~5.6.0", "jest-environment-jsdom": "~29.7.0", "jest-serializer-path": "~0.1.15", "jest": "~29.7.0", "markdownlint-cli2": "~0.13.0", + "nodemon": "~3.1.0", "start-server-and-test": "~2.0.0", "style-loader": "~4.0.0", "terser-webpack-plugin": "~5.3.1", @@ -93,10 +96,10 @@ "webpack": "~5.91.0" }, "dependencies": { - "axios": "~1.6.8", + "axios": "~1.7.2", "core-js": "~3.37.0", - "react-dom": "~18.2.0", - "react-router-dom": "~6.22.3", - "react": "~18.2.0" + "react-dom": "~18.3.1", + "react-router-dom": "~6.23.1", + "react": "~18.3.1" } } diff --git a/client/src/App/App.css b/client/src/App/App.css index 3f5a02c9..a10f55d1 100644 --- a/client/src/App/App.css +++ b/client/src/App/App.css @@ -1,3 +1,16 @@ +a { + color: aqua; +} + +a:visited { + color: blueviolet; +} + .navigation-list { list-style-type: none; + display: inline-flex; +} + +.navigation-list li { + padding-right: 6px; } diff --git a/client/src/App/App.test.tsx b/client/src/App/App.test.tsx index 306eb715..37d7ef09 100644 --- a/client/src/App/App.test.tsx +++ b/client/src/App/App.test.tsx @@ -1,14 +1,17 @@ import { render, screen, + waitFor, } from '@testing-library/react'; import React from 'react'; -import App from './App'; +import { App } from './App'; describe('App', () => { - it('renders a heading', () => { + it('renders a heading', async () => { render(); - expect(screen.getByRole('heading')).toHaveTextContent('trshcmpctr'); + await waitFor( + () => expect(screen.getByRole('heading')).toHaveTextContent('trshcmpctr') + ); }); }); diff --git a/client/src/App/App.tsx b/client/src/App/App.tsx index 65d54158..0c5552d0 100644 --- a/client/src/App/App.tsx +++ b/client/src/App/App.tsx @@ -1,164 +1,25 @@ -import React, { StrictMode, useEffect, useState } from 'react'; -import { createBrowserRouter, RouterProvider, Link, useParams, Outlet } from 'react-router-dom'; +import React, { StrictMode } from 'react'; +import { createBrowserRouter, RouterProvider } from 'react-router-dom'; import './App.css'; -import { useGuildMemberData } from './use-guild-member-data'; -import { Welcome } from './Welcome'; -type GuildUserData = ReturnType; - -const getWelcomeMessage = (guildUser: GuildUserData) => { - const welcome = 'welcome to the trash compactor'; - - if (guildUser?.user?.username) { - return `${welcome}, ${guildUser.user.username}`; - } - - return `${welcome}, `; -}; - -const DelayedLoadingMessage = () => { - const [showLoading, setShowLoading] = useState(false); - - useEffect(() => { - // Showing a loading message right away contributes to worse perceived performance - const timeout = setTimeout(() => setShowLoading(true), 500); - - return () => clearTimeout(timeout); - }); - - if (showLoading) { - return (

loading...

); - } - - return null; -}; - -const Home = () => { - const guildUser = useGuildMemberData(); - - // FIXME: if not a guild member, user just sees 'loading...' forever - if (guildUser) { - return ( - <> -
    -
  • - new -
  • -
  • - worlds -
  • -
- - - ); - } - - return (); -}; - -const WorldsList = () => { - const fakeWorlds = [ - { - id: 1, - label: 'world one', - version: '1.16.5', - createdAt: '2023/06/28', - lastOnline: '2023/06/28', - createdBy: '@shaned.gg' - }, - { - id: 2, - label: 'world two', - version: '1.19.0', - createdAt: '2023/06/28', - lastOnline: '2023/06/28', - createdBy: '@shaned.gg' - }, - { - id: 3, - label: 'world three', - version: '1.20.1', - createdAt: '2023/06/28', - lastOnline: '2023/06/28', - createdBy: '@shaned.gg' - }, - ]; - - return ( - <> -
    -
  • - back -
  • -
- - - - - - - - - - - - {fakeWorlds.map(({ id, label, version, createdAt, createdBy, lastOnline }) => { - return ( - - - - - - - - ); - })} - -
nameversioncreatedlast onlinecreated by
{label}{version}{new Date(createdAt).toLocaleDateString()}{new Date(lastOnline).toLocaleDateString()}{createdBy}
- - - ); -}; - -const WorldDetail = () => { - const { worldId } = useParams(); - return ( - <> -

{worldId}

- - ); -}; - -const NewWorld = () => { - return ( - <> -
    -
  • - back -
  • -
-
- - - - - -
- - ); -}; +import { ErrorCard } from './components/ErrorCard'; +import { Footer } from './components/Footer'; +import { Header } from './components/Header'; +import { Home } from './components/Home'; +import { NewWorld } from './components/NewWorld'; +import { WorldDetail } from './components/WorldDetail'; +import { Worlds } from './components/Worlds'; const router = createBrowserRouter([ { path: '/', element: , + errorElement: , }, { path: '/worlds', - element: , + element: , children: [ { path: ':worldId', @@ -172,13 +33,25 @@ const router = createBrowserRouter([ }, ]); -const App = () => { +export const App = () => { return ( -

trshcmpctr

- +
+
+
+ +
+
+
); }; - -export default App; diff --git a/client/src/App/Nav.test.tsx b/client/src/App/Nav.test.tsx deleted file mode 100644 index cef802b0..00000000 --- a/client/src/App/Nav.test.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { - render, - screen, -} from '@testing-library/react'; -import React from 'react'; - -import { Nav } from './Nav'; - -describe('Nav', () => { - it('renders a list of links', () => { - render( -