Merge pull request 'Improve testing ux' (#3) from two-instances into master

Reviewed-on: https://codeberg.org/pzp/zooboard/pulls/3
This commit is contained in:
Powersource 2024-04-20 13:48:24 +00:00
commit dad3f4a2ec
8 changed files with 243 additions and 27 deletions

View File

@ -1,8 +1,21 @@
# Zooboard # Zooboard
To develop: To run it yourself:
1. `npm install` 1. `npm install`
2. `npm start` in one terminal, `npm run electron` in another 2. Download, install, and run [pzp-hub][pzp-hub].
3. Edit JS code 3. In the zooboard repo, `npm run start-all`
4. Sometimes you may need to re-run step 2
To run with a friend:
1. Both `npm install`
2. One of you downloads, installs, and runs [pzp-hub][pzp-hub] on a machine with a public IP.
3. You both, in the zooboard repo, run `npm start` in one terminal and `npm run electron` in another.
To use:
1. One peer goes to the hub website (default on localhost is [0.0.0.0:3000](http://0.0.0.0:3000)), gets the admin invite, and uses it in the app by clicking "Join".
2. That peer clicks "Invite" in the app and gives that invite to the other peer.
3. The other peer uses that invite by clicking "Join".
4. Draw together and be happy!
5. [Discuss bugs here](https://codeberg.org/pzp/pzp-hub/issues) or pzp in general on the #pzp hashtag on your preferred social network.
[pzp-hub]: https://codeberg.org/pzp/pzp-hub

39
main.js
View File

@ -20,7 +20,7 @@ na.sodium_free = function sodium_free_monkey_patched() {}
process.env.ZOOBOARD_DATA ??= Path.join(app.getPath('appData'), 'zooboard') process.env.ZOOBOARD_DATA ??= Path.join(app.getPath('appData'), 'zooboard')
app.setPath('userData', process.env.ZOOBOARD_DATA) app.setPath('userData', process.env.ZOOBOARD_DATA)
const path = Path.resolve(app.getPath('userData'), 'ppppp') const path = process.env.PZP_DIR ?? Path.resolve(app.getPath('userData'), 'ppppp')
const keypairPath = Path.join(path, 'keypair.json') const keypairPath = Path.join(path, 'keypair.json')
const keypair = Keypair.loadOrCreateSync(keypairPath) const keypair = Keypair.loadOrCreateSync(keypairPath)
@ -135,10 +135,12 @@ async function writeElements(ev, elements) {
async function createInvite() { async function createInvite() {
if (globalAccountID === null) throw new Error('account not loaded') if (globalAccountID === null) throw new Error('account not loaded')
const { url } = await p(peer.invite.createForFriend)({ let { url } = await p(peer.invite.createForFriend)({
hubs: 1, hubs: 1,
id: globalAccountID, id: globalAccountID,
}) })
// if the hub is on localhost, it's probably on the default port of 3000, so let's make things a bit easier for the user
if (url.indexOf('0.0.0.0') !== -1) url = url.replace("0.0.0.0", "0.0.0.0:3000")
return url return url
} }
@ -228,7 +230,10 @@ async function handlePPPPPUri(ev, uri) {
setTimeout(handlePPPPPUri, 100, null, uri) setTimeout(handlePPPPPUri, 100, null, uri)
return return
} }
if (!uri.startsWith('ppppp://')) return console.log('Not a ppppp:// URI', uri) if (uri.startsWith("http:") || uri.startsWith("https://")) {
uri = decodeURIComponent(uri.split('/invite#')[1])
}
if (!uri.startsWith('ppppp://')) return console.log('Not a ppppp invite URI', uri)
const commands = peer.invite.parse(uri) const commands = peer.invite.parse(uri)
for (const command of commands) { for (const command of commands) {
console.log('Executing command', JSON.stringify(command)) console.log('Executing command', JSON.stringify(command))
@ -277,20 +282,20 @@ if (process.defaultApp) {
app.setAsDefaultProtocolClient('ppppp') app.setAsDefaultProtocolClient('ppppp')
} }
const hasLock = app.requestSingleInstanceLock() //const hasLock = app.requestSingleInstanceLock()
if (!hasLock) { //if (!hasLock) {
app.quit() // app.quit()
} else { //} else {
app.on('second-instance', (ev, argv, cwd, extraData) => { // app.on('second-instance', (ev, argv, cwd, extraData) => {
if (mainWindow) { // if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore() // if (mainWindow.isMinimized()) mainWindow.restore()
mainWindow.focus() // mainWindow.focus()
if (argv.length > 1) { // if (argv.length > 1) {
handlePPPPPUri(null, argv[argv.length - 1]) // handlePPPPPUri(null, argv[argv.length - 1])
} // }
} // }
}) // })
app.whenReady().then(() => { app.whenReady().then(() => {
ipcMain.handle('loadAccount', loadAccount) ipcMain.handle('loadAccount', loadAccount)
@ -313,4 +318,4 @@ if (!hasLock) {
if (process.platform !== 'darwin') app.quit() if (process.platform !== 'darwin') app.quit()
}) })
}) })
} //}

179
package-lock.json generated
View File

@ -12,6 +12,7 @@
"@testing-library/jest-dom": "^5.17.0", "@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0", "@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"concurrently": "^8.2.2",
"debounce": "2.0", "debounce": "2.0",
"ppppp-caps": "github:staltz/ppppp-caps#93fa810b9a40b78aef4872d4c2a8412cccb52929", "ppppp-caps": "github:staltz/ppppp-caps#93fa810b9a40b78aef4872d4c2a8412cccb52929",
"ppppp-conductor": "github:staltz/ppppp-conductor#8ebeb0fb12de766fac21f8292121307cf7d1bd1e", "ppppp-conductor": "github:staltz/ppppp-conductor#8ebeb0fb12de766fac21f8292121307cf7d1bd1e",
@ -6777,6 +6778,148 @@
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
}, },
"node_modules/concurrently": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz",
"integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==",
"dependencies": {
"chalk": "^4.1.2",
"date-fns": "^2.30.0",
"lodash": "^4.17.21",
"rxjs": "^7.8.1",
"shell-quote": "^1.8.1",
"spawn-command": "0.0.2",
"supports-color": "^8.1.1",
"tree-kill": "^1.2.2",
"yargs": "^17.7.2"
},
"bin": {
"conc": "dist/bin/concurrently.js",
"concurrently": "dist/bin/concurrently.js"
},
"engines": {
"node": "^14.13.0 || >=16.0.0"
},
"funding": {
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
}
},
"node_modules/concurrently/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/concurrently/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/concurrently/node_modules/chalk/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/concurrently/node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.1",
"wrap-ansi": "^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/concurrently/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/concurrently/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/concurrently/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"engines": {
"node": ">=8"
}
},
"node_modules/concurrently/node_modules/supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/concurrently/node_modules/yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
"dependencies": {
"cliui": "^8.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.3",
"y18n": "^5.0.5",
"yargs-parser": "^21.1.1"
},
"engines": {
"node": ">=12"
}
},
"node_modules/concurrently/node_modules/yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"engines": {
"node": ">=12"
}
},
"node_modules/confusing-browser-globals": { "node_modules/confusing-browser-globals": {
"version": "1.0.11", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz",
@ -7323,6 +7466,21 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/date-fns": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
"dependencies": {
"@babel/runtime": "^7.21.0"
},
"engines": {
"node": ">=0.11"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/date-fns"
}
},
"node_modules/debounce": { "node_modules/debounce": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/debounce/-/debounce-2.0.0.tgz", "resolved": "https://registry.npmjs.org/debounce/-/debounce-2.0.0.tgz",
@ -17303,6 +17461,14 @@
"queue-microtask": "^1.2.2" "queue-microtask": "^1.2.2"
} }
}, },
"node_modules/rxjs": {
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/safe-array-concat": { "node_modules/safe-array-concat": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz",
@ -18083,6 +18249,11 @@
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
"deprecated": "Please use @jridgewell/sourcemap-codec instead" "deprecated": "Please use @jridgewell/sourcemap-codec instead"
}, },
"node_modules/spawn-command": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz",
"integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ=="
},
"node_modules/spdy": { "node_modules/spdy": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz",
@ -19032,6 +19203,14 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/tree-kill": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
"bin": {
"tree-kill": "cli.js"
}
},
"node_modules/tryer": { "node_modules/tryer": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz",

View File

@ -5,17 +5,21 @@
"main": "main.js", "main": "main.js",
"homepage": "./", "homepage": "./",
"scripts": { "scripts": {
"start": "BROWSER=none react-scripts start", "start": "PORT=3001 BROWSER=none react-scripts start",
"start2": "PZP_DIR=/tmp/zooboard-test PORT=3002 BROWSER=none react-scripts start",
"build": "react-scripts build", "build": "react-scripts build",
"test": "react-scripts test", "test": "react-scripts test",
"eject": "react-scripts eject", "eject": "react-scripts eject",
"electron": "ELECTRON_START_URL=http://localhost:3000 DEBUG=*,-shse,-pull-secretstream electron ." "electron": "ELECTRON_START_URL=http://localhost:3001 DEBUG=*,-shse,-pull-secretstream electron .",
"electron2": "PZP_DIR=/tmp/zooboard-test ELECTRON_START_URL=http://localhost:3002 DEBUG=*,-shse,-pull-secretstream electron .",
"start-all": "concurrently --kill-others \"npm start\" \"./scripts/wait.sh 3001 && npm run electron\" \"npm run start2\" \"./scripts/wait.sh 3002 && npm run electron2\""
}, },
"dependencies": { "dependencies": {
"@excalidraw/excalidraw": "0.17.2", "@excalidraw/excalidraw": "0.17.2",
"@testing-library/jest-dom": "^5.17.0", "@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0", "@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"concurrently": "^8.2.2",
"debounce": "2.0", "debounce": "2.0",
"ppppp-caps": "github:staltz/ppppp-caps#93fa810b9a40b78aef4872d4c2a8412cccb52929", "ppppp-caps": "github:staltz/ppppp-caps#93fa810b9a40b78aef4872d4c2a8412cccb52929",
"ppppp-db": "github:staltz/ppppp-db#cf1532965ea1d16929ed2291a9b737a4ce74caac", "ppppp-db": "github:staltz/ppppp-db#cf1532965ea1d16929ed2291a9b737a4ce74caac",

4
scripts/wait.sh Executable file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env bash
# waits until a certain port is reachable
WAIT_PORT=$1
bash -c 'until printf "" 2>>/dev/null >>/dev/tcp/$0/$1; do sleep 1; done' localhost $WAIT_PORT

View File

@ -16,6 +16,7 @@ function App() {
const [excalidrawAPI, setExcalidrawAPI] = useState(null) const [excalidrawAPI, setExcalidrawAPI] = useState(null)
const [inviteCode, setInviteCode] = useState(null) const [inviteCode, setInviteCode] = useState(null)
const [inviteErr, setInviteErr] = useState(null)
const [inviteModalOpen, setInviteModalOpen] = useState(false) const [inviteModalOpen, setInviteModalOpen] = useState(false)
const openInviteModal = () => setInviteModalOpen(true) const openInviteModal = () => setInviteModalOpen(true)
@ -45,7 +46,11 @@ function App() {
setInviteCode(null) setInviteCode(null)
openInviteModal() openInviteModal()
window.electronAPI.createInvite().then((invite) => { window.electronAPI.createInvite().then((invite) => {
setInviteErr(null)
setInviteCode(invite) setInviteCode(invite)
}).catch(err => {
setInviteCode(null)
setInviteErr(err)
}) })
} }
@ -102,6 +107,7 @@ function App() {
isOpen={inviteModalOpen} isOpen={inviteModalOpen}
onClose={closeInviteModal} onClose={closeInviteModal}
inviteCode={inviteCode} inviteCode={inviteCode}
inviteErr={inviteErr}
/> />
<JoinModal isOpen={joinModalOpen} onClose={closeJoinModal} /> <JoinModal isOpen={joinModalOpen} onClose={closeJoinModal} />
</div> </div>

View File

@ -1,7 +1,7 @@
import Button from './Button' import Button from './Button'
import Modal from './Modal' import Modal from './Modal'
function CreateInviteModal({ isOpen, onClose, inviteCode }) { function CreateInviteModal({ isOpen, onClose, inviteCode, inviteErr }) {
const copyInviteToClipboard = () => { const copyInviteToClipboard = () => {
window.electronAPI.copyToClipboard(inviteCode).then(() => { window.electronAPI.copyToClipboard(inviteCode).then(() => {
onClose() onClose()
@ -20,7 +20,12 @@ function CreateInviteModal({ isOpen, onClose, inviteCode }) {
return ( return (
<Modal isOpen={isOpen} onClose={onClose}> <Modal isOpen={isOpen} onClose={onClose}>
{inviteCode ? ( {inviteErr ? (
<>
<div className="text-slate-500 h-14">Got an error when creating the invite:</div>
<div className="text-slate-500 h-14">{inviteErr.toString()}</div>
</>
) : inviteCode ? (
<> <>
This is an invite code that you can send to a friend to collaborate on This is an invite code that you can send to a friend to collaborate on
this drawing with you. this drawing with you.

View File

@ -16,10 +16,10 @@ function JoinModal({ isOpen, onClose }) {
return ( return (
<Modal isOpen={isOpen} onClose={onClose}> <Modal isOpen={isOpen} onClose={onClose}>
Insert here the ppppp:// invite code you received from your friend. Insert here the ppppp:// or http(s):// invite code you received from your friend.
<textarea <textarea
key="input" key="input"
placeholder={'ppppp://...'} placeholder={'ppppp://... or http(s)://...'}
className="border font-mono border-gray-400 resize-none rounded px-1 text-wrap break-all outline-offset-3 outline-2 outline-green-500 my-4 h-64" className="border font-mono border-gray-400 resize-none rounded px-1 text-wrap break-all outline-offset-3 outline-2 outline-green-500 my-4 h-64"
onChange={updateCode} onChange={updateCode}
/> />