From c80e52112a8d2a147116b1895e83eaec2d7f2b7c Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Wed, 17 Jan 2024 01:53:09 -0500 Subject: [PATCH] websocket update works --- package-lock.json | 121 ++++++++++++++++++- package.json | 5 +- server/src/express_app.ts | 9 +- server/src/modules/jam.tsx | 49 +++++++- tests-e2e/playwright.config.ts | 2 +- tests-e2e/support/components/jam_page_pom.ts | 9 +- tests-e2e/tests/jam_page.spec.ts | 48 ++++++-- 7 files changed, 223 insertions(+), 20 deletions(-) diff --git a/package-lock.json b/package-lock.json index 882c5e4..09ec96b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,11 +10,14 @@ "license": "ISC", "dependencies": { "@kitajs/html": "^3.0.10", - "express": "^4.18.2" + "express": "^4.18.2", + "express-ws": "^5.0.2", + "ws": "^8.16.0" }, "devDependencies": { "@kitajs/ts-html-plugin": "^1.3.3", "@types/express": "^4.17.21", + "@types/express-ws": "^3.0.4", "@types/node": "^20.10.6", "esbuild": "^0.19.11", "esbuild-plugin-istanbul": "^0.3.0", @@ -1148,6 +1151,17 @@ "@types/send": "*" } }, + "node_modules/@types/express-ws": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/express-ws/-/express-ws-3.0.4.tgz", + "integrity": "sha512-Yjj18CaivG5KndgcvzttWe8mPFinPCHJC2wvyQqVzA7hqeufM8EtWMj6mpp5omg3s8XALUexhOu8aXAyi/DyJQ==", + "dev": true, + "dependencies": { + "@types/express": "*", + "@types/express-serve-static-core": "*", + "@types/ws": "*" + } + }, "node_modules/@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", @@ -1214,6 +1228,15 @@ "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", "dev": true }, + "node_modules/@types/ws": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -1925,6 +1948,40 @@ "node": ">= 0.10.0" } }, + "node_modules/express-ws": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/express-ws/-/express-ws-5.0.2.tgz", + "integrity": "sha512-0uvmuk61O9HXgLhGl3QhNSEtRsQevtmbL94/eILaliEADZBHZOQUAiHFrGPrgsjikohyrmSG5g+sCfASTt0lkQ==", + "dependencies": { + "ws": "^7.4.6" + }, + "engines": { + "node": ">=4.5.0" + }, + "peerDependencies": { + "express": "^4.0.0 || ^5.0.0-alpha.1" + } + }, + "node_modules/express-ws/node_modules/ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -3928,6 +3985,26 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/ws": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -4743,6 +4820,17 @@ "@types/send": "*" } }, + "@types/express-ws": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/express-ws/-/express-ws-3.0.4.tgz", + "integrity": "sha512-Yjj18CaivG5KndgcvzttWe8mPFinPCHJC2wvyQqVzA7hqeufM8EtWMj6mpp5omg3s8XALUexhOu8aXAyi/DyJQ==", + "dev": true, + "requires": { + "@types/express": "*", + "@types/express-serve-static-core": "*", + "@types/ws": "*" + } + }, "@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", @@ -4809,6 +4897,15 @@ "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", "dev": true }, + "@types/ws": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -5337,6 +5434,22 @@ "vary": "~1.1.2" } }, + "express-ws": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/express-ws/-/express-ws-5.0.2.tgz", + "integrity": "sha512-0uvmuk61O9HXgLhGl3QhNSEtRsQevtmbL94/eILaliEADZBHZOQUAiHFrGPrgsjikohyrmSG5g+sCfASTt0lkQ==", + "requires": { + "ws": "^7.4.6" + }, + "dependencies": { + "ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "requires": {} + } + } + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -6776,6 +6889,12 @@ } } }, + "ws": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "requires": {} + }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 3e1143d..9cafbe6 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "devDependencies": { "@kitajs/ts-html-plugin": "^1.3.3", "@types/express": "^4.17.21", + "@types/express-ws": "^3.0.4", "@types/node": "^20.10.6", "esbuild": "^0.19.11", "esbuild-plugin-istanbul": "^0.3.0", @@ -16,7 +17,9 @@ }, "dependencies": { "@kitajs/html": "^3.0.10", - "express": "^4.18.2" + "express": "^4.18.2", + "express-ws": "^5.0.2", + "ws": "^8.16.0" }, "scripts": { "start": "node dist/server/index.js", diff --git a/server/src/express_app.ts b/server/src/express_app.ts index abae1f3..3d4c134 100644 --- a/server/src/express_app.ts +++ b/server/src/express_app.ts @@ -1,9 +1,10 @@ -import express from 'express'; +import express, {Router} from 'express'; +import expressWs from 'express-ws'; import {AppDependencies} from './types/app_dependencies'; import {renderLayout} from './views/layout'; import {loginRouter, renderLoginPage} from './modules/login'; -import {jamRouter, renderJamPage} from './modules/jam'; +import {initJamRouterWebsocket, jamRouter, renderJamPage} from './modules/jam'; const handlePage = (renderer: () => string | Promise): express.Handler => async (req, res, next) => { const page = await Promise.resolve(renderer()); @@ -14,7 +15,7 @@ const handlePage = (renderer: () => string | Promise): express.Handler = }; export const initApp = (deps: AppDependencies) => { - const app = express(); + const app = expressWs(express()).app; app.get('/', handlePage(renderLoginPage)); @@ -24,5 +25,7 @@ export const initApp = (deps: AppDependencies) => { app.use(jamRouter); app.use(loginRouter); + initJamRouterWebsocket(); + return app; }; diff --git a/server/src/modules/jam.tsx b/server/src/modules/jam.tsx index e4fe3b9..ae7a05b 100644 --- a/server/src/modules/jam.tsx +++ b/server/src/modules/jam.tsx @@ -3,12 +3,14 @@ import Html from '@kitajs/html'; import express from 'express'; +import {WebSocket} from 'ws'; export const jamRouter = express.Router(); enum ROUTES { JAM_ACTIONS_ADD_CHORD = '/jam/actions/add_chord', JAM_ACTIONS_NEW_JAM = '/jam/actions/new_jam', + JAM_ROOM_WEBSOCKET = '/jam/ws', } type JamState = { @@ -30,29 +32,68 @@ const jamState: JamState = { ], }; + +const connectedSockets: WebSocket[] = []; + +export const initJamRouterWebsocket = () => { + jamRouter.ws(ROUTES.JAM_ROOM_WEBSOCKET, (ws, req) => { + connectedSockets.push(ws); + console.log('ws connected'); + ws.send(JamView().toString()); + + ws.on('message', (msg) => { + console.log(msg); + }); + + ws.on('close', () => { + console.log('ws closed'); + connectedSockets.splice(connectedSockets.indexOf(ws), 1); + }); + + }); +}; + +const refreshAll = () => { + connectedSockets.forEach((ws) => { + ws.send(JamView().toString()); + }); +} + jamRouter.post(ROUTES.JAM_ACTIONS_ADD_CHORD, (req, res) => { const {chord} = req.query; - jamState.selectedChords.push(chord); + res.send(JamView()); + refreshAll(); }); jamRouter.post(ROUTES.JAM_ACTIONS_NEW_JAM, (req, res) => { jamState.selectedChords = []; + res.send(JamView()); + refreshAll(); }); export const renderJamPage = () => { - return JamView(); + return JamPage(); }; +export const JamPage = () => { + return ( + <> +
+ + + ) +} + export const JamView = () => { const chordNames = jamState.availableChords; const selectedChords = jamState.selectedChords; return ( -
- +
+
diff --git a/tests-e2e/playwright.config.ts b/tests-e2e/playwright.config.ts index 79558fe..9994d92 100644 --- a/tests-e2e/playwright.config.ts +++ b/tests-e2e/playwright.config.ts @@ -12,7 +12,7 @@ import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ testDir: './tests', /* Run tests in files in parallel */ - fullyParallel: true, + // fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, /* Retry on CI only */ diff --git a/tests-e2e/support/components/jam_page_pom.ts b/tests-e2e/support/components/jam_page_pom.ts index 112f9fa..0108343 100644 --- a/tests-e2e/support/components/jam_page_pom.ts +++ b/tests-e2e/support/components/jam_page_pom.ts @@ -1,3 +1,5 @@ +import {Page} from '@playwright/test'; + export class JamPage { constructor(private readonly page: Page) {} @@ -5,7 +7,7 @@ export class JamPage { chordButtons = this.page.locator('.chord-button'); clickButton = async (name: string) => { - await this.page.getByRole('button', {name}).click(); + await this.page.getByRole('button', {name, exact: true}).click(); return new Promise((resolve) => setTimeout(resolve, 10)); } @@ -14,4 +16,9 @@ export class JamPage { await this.clickButton(name); } } + + clickNewJam = async () => { + await this.page.getByRole('button', {name: 'New Jam', exact: true}).click(); + return new Promise((resolve) => setTimeout(resolve, 10)); + } } diff --git a/tests-e2e/tests/jam_page.spec.ts b/tests-e2e/tests/jam_page.spec.ts index 7785ddd..e8a4090 100644 --- a/tests-e2e/tests/jam_page.spec.ts +++ b/tests-e2e/tests/jam_page.spec.ts @@ -4,18 +4,48 @@ import {JamPage} from '../support/components/jam_page_pom'; const wait = (ms = 10) => new Promise((resolve) => setTimeout(resolve, ms)); -test('test', async ({page}) => { - await page.goto('/'); +test.describe('Jam page', () => { - await page.locator('#firstname').fill('Michael'); - await page.getByRole('button', {name: 'Submit'}).click(); + test('basic usage', async ({page}) => { + await page.goto('/'); - await wait(100); + await page.locator('#firstname').fill('Michael'); + await page.getByRole('button', {name: 'Submit'}).click(); - expect(page).toHaveURL('/jam'); + await wait(100); - const jamPage = new JamPage(page); - await jamPage.clickButtons(['C', 'Dm', 'F', 'Em', 'Am', 'G']); + expect(page).toHaveURL('/jam'); - await expect(jamPage.draftedChords).toHaveText('CDmFEmAmG'); + const jamPage = new JamPage(page); + await jamPage.clickNewJam(); + await jamPage.clickButtons(['C', 'Dm', 'F', 'Em', 'Am', 'G']); + + await expect(jamPage.draftedChords).toHaveText('CDmFEmAmG'); + }); + + test('two users', async ({browser}) => { + const mainBrowser = await browser.newContext(); + const otherBrowser = await browser.newContext(); + + const mainUser = await mainBrowser.newPage(); + const otherUser = await otherBrowser.newPage(); + + await mainUser.goto('/jam'); + await otherUser.goto('/jam'); + + await wait(100); + + const jamPage = new JamPage(mainUser); + await jamPage.clickNewJam(); + await jamPage.clickButtons(['C', 'Dm', 'F', 'Em', 'Am', 'G']); + + await expect(jamPage.draftedChords).toHaveText('CDmFEmAmG'); + + const jamPage2 = new JamPage(otherUser); + await expect(jamPage2.draftedChords).toHaveText('CDmFEmAmG'); + + await jamPage2.clickNewJam(); + await expect(jamPage2.draftedChords).toHaveText(''); + await expect(jamPage.draftedChords).toHaveText(''); + }); });