test(vitest): amplia la suite con test unitari, integrazione, componenti e stress

- aggiunge test per gameState e utilita server
- aggiunge test di integrazione WebSocket
- aggiunge test componenti Vue (ControllerPage/DisplayPage)
- aggiunge test stress su carico WebSocket
- aggiorna configurazione Vitest per includere nuove cartelle e ambiente componenti
- aggiorna script npm e dipendenze di test
This commit is contained in:
2026-02-12 19:33:29 +01:00
parent 71119da727
commit 0b154d9e56
9 changed files with 2290 additions and 91 deletions

601
package-lock.json generated
View File

@@ -15,11 +15,14 @@
"ws": "^8.19.0" "ws": "^8.19.0"
}, },
"devDependencies": { "devDependencies": {
"@axe-core/playwright": "^4.11.1",
"@playwright/test": "^1.58.2", "@playwright/test": "^1.58.2",
"@types/node": "^25.2.3", "@types/node": "^25.2.3",
"@vitejs/plugin-vue": "^4.1.0", "@vitejs/plugin-vue": "^4.1.0",
"@vitest/ui": "^4.0.18", "@vitest/ui": "^4.0.18",
"@vue/test-utils": "^2.4.6",
"concurrently": "^9.2.1", "concurrently": "^9.2.1",
"happy-dom": "^20.6.1",
"jsdom": "^28.0.0", "jsdom": "^28.0.0",
"vite": "^4.3.9", "vite": "^4.3.9",
"vite-plugin-pwa": "^0.16.0", "vite-plugin-pwa": "^0.16.0",
@@ -112,6 +115,18 @@
"integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
"dev": true "dev": true
}, },
"node_modules/@axe-core/playwright": {
"version": "4.11.1",
"resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.11.1.tgz",
"integrity": "sha512-mKEfoUIB1MkVTht0BGZFXtSAEKXMJoDkyV5YZ9jbBmZCcWDz71tegNsdTkIN8zc/yMi5Gm2kx7Z5YQ9PfWNAWw==",
"dev": true,
"dependencies": {
"axe-core": "~4.11.1"
},
"peerDependencies": {
"playwright-core": ">= 1.0.0"
}
},
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
"version": "7.21.4", "version": "7.21.4",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz",
@@ -2336,6 +2351,102 @@
} }
} }
}, },
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
"dev": true,
"dependencies": {
"string-width": "^5.1.2",
"string-width-cjs": "npm:string-width@^4.2.0",
"strip-ansi": "^7.0.1",
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
"wrap-ansi": "^8.1.0",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@isaacs/cliui/node_modules/ansi-regex": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/@isaacs/cliui/node_modules/ansi-styles": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/@isaacs/cliui/node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true
},
"node_modules/@isaacs/cliui/node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"dev": true,
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@isaacs/cliui/node_modules/strip-ansi": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
"dev": true,
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"dev": true,
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/@jridgewell/gen-mapping": { "node_modules/@jridgewell/gen-mapping": {
"version": "0.3.3", "version": "0.3.3",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
@@ -2434,6 +2545,22 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/@one-ini/wasm": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz",
"integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==",
"dev": true
},
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
"dev": true,
"optional": true,
"engines": {
"node": ">=14"
}
},
"node_modules/@playwright/test": { "node_modules/@playwright/test": {
"version": "1.58.2", "version": "1.58.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
@@ -2941,6 +3068,21 @@
"integrity": "sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==", "integrity": "sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==",
"dev": true "dev": true
}, },
"node_modules/@types/whatwg-mimetype": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz",
"integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==",
"dev": true
},
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@vitejs/plugin-vue": { "node_modules/@vitejs/plugin-vue": {
"version": "4.2.3", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.2.3.tgz", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.2.3.tgz",
@@ -3149,6 +3291,25 @@
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.28.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.28.tgz",
"integrity": "sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ==" "integrity": "sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ=="
}, },
"node_modules/@vue/test-utils": {
"version": "2.4.6",
"resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.6.tgz",
"integrity": "sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==",
"dev": true,
"dependencies": {
"js-beautify": "^1.14.9",
"vue-component-type-helpers": "^2.0.0"
}
},
"node_modules/abbrev": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
"integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==",
"dev": true,
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
"node_modules/accepts": { "node_modules/accepts": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
@@ -3268,6 +3429,15 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/axe-core": {
"version": "4.11.1",
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz",
"integrity": "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/babel-plugin-polyfill-corejs2": { "node_modules/babel-plugin-polyfill-corejs2": {
"version": "0.4.3", "version": "0.4.3",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.3.tgz", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.3.tgz",
@@ -3666,6 +3836,16 @@
"url": "https://github.com/chalk/supports-color?sponsor=1" "url": "https://github.com/chalk/supports-color?sponsor=1"
} }
}, },
"node_modules/config-chain": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
"integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==",
"dev": true,
"dependencies": {
"ini": "^1.3.4",
"proto-list": "~1.2.1"
}
},
"node_modules/content-disposition": { "node_modules/content-disposition": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
@@ -3721,6 +3901,20 @@
"url": "https://opencollective.com/core-js" "url": "https://opencollective.com/core-js"
} }
}, },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/crypto-random-string": { "node_modules/crypto-random-string": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
@@ -3888,6 +4082,66 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"dev": true
},
"node_modules/editorconfig": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz",
"integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==",
"dev": true,
"dependencies": {
"@one-ini/wasm": "0.1.1",
"commander": "^10.0.0",
"minimatch": "9.0.1",
"semver": "^7.5.3"
},
"bin": {
"editorconfig": "bin/editorconfig"
},
"engines": {
"node": ">=14"
}
},
"node_modules/editorconfig/node_modules/commander": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
"integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
"dev": true,
"engines": {
"node": ">=14"
}
},
"node_modules/editorconfig/node_modules/minimatch": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz",
"integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==",
"dev": true,
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/editorconfig/node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"dev": true,
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/ee-first": { "node_modules/ee-first": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -4295,6 +4549,22 @@
"is-callable": "^1.1.3" "is-callable": "^1.1.3"
} }
}, },
"node_modules/foreground-child": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
"dev": true,
"dependencies": {
"cross-spawn": "^7.0.6",
"signal-exit": "^4.0.1"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/forwarded": { "node_modules/forwarded": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -4529,6 +4799,44 @@
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true "dev": true
}, },
"node_modules/happy-dom": {
"version": "20.6.1",
"resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.6.1.tgz",
"integrity": "sha512-+0vhESXXhFwkdjZnJ5DlmJIfUYGgIEEjzIjB+aKJbFuqlvvKyOi+XkI1fYbgYR9QCxG5T08koxsQ6HrQfa5gCQ==",
"dev": true,
"dependencies": {
"@types/node": ">=20.0.0",
"@types/whatwg-mimetype": "^3.0.2",
"@types/ws": "^8.18.1",
"entities": "^6.0.1",
"whatwg-mimetype": "^3.0.0",
"ws": "^8.18.3"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/happy-dom/node_modules/entities": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
"dev": true,
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/happy-dom/node_modules/whatwg-mimetype": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
"integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
"dev": true,
"engines": {
"node": ">=12"
}
},
"node_modules/has": { "node_modules/has": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
@@ -4713,6 +5021,12 @@
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
}, },
"node_modules/ini": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"dev": true
},
"node_modules/internal-slot": { "node_modules/internal-slot": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz",
@@ -5018,6 +5332,27 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true
},
"node_modules/jackspeak": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
"dev": true,
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
},
"optionalDependencies": {
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/jake": { "node_modules/jake": {
"version": "10.8.7", "version": "10.8.7",
"resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz", "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz",
@@ -5141,6 +5476,72 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/js-beautify": {
"version": "1.15.4",
"resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz",
"integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==",
"dev": true,
"dependencies": {
"config-chain": "^1.1.13",
"editorconfig": "^1.0.4",
"glob": "^10.4.2",
"js-cookie": "^3.0.5",
"nopt": "^7.2.1"
},
"bin": {
"css-beautify": "js/bin/css-beautify.js",
"html-beautify": "js/bin/html-beautify.js",
"js-beautify": "js/bin/js-beautify.js"
},
"engines": {
"node": ">=14"
}
},
"node_modules/js-beautify/node_modules/glob": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
"dev": true,
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^3.1.2",
"minimatch": "^9.0.4",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^1.11.1"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/js-beautify/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/js-cookie": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
"dev": true,
"engines": {
"node": ">=14"
}
},
"node_modules/js-tokens": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -5428,6 +5829,15 @@
"concat-map": "0.0.1" "concat-map": "0.0.1"
} }
}, },
"node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"dev": true,
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/mrmime": { "node_modules/mrmime": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
@@ -5473,6 +5883,21 @@
"integrity": "sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==", "integrity": "sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==",
"dev": true "dev": true
}, },
"node_modules/nopt": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz",
"integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==",
"dev": true,
"dependencies": {
"abbrev": "^2.0.0"
},
"bin": {
"nopt": "bin/nopt.js"
},
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
"node_modules/object-inspect": { "node_modules/object-inspect": {
"version": "1.13.4", "version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
@@ -5540,6 +5965,12 @@
"wrappy": "1" "wrappy": "1"
} }
}, },
"node_modules/package-json-from-dist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
"dev": true
},
"node_modules/parse5": { "node_modules/parse5": {
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz",
@@ -5581,12 +6012,43 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/path-parse": { "node_modules/path-parse": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true "dev": true
}, },
"node_modules/path-scurry": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
"dev": true,
"dependencies": {
"lru-cache": "^10.2.0",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
},
"engines": {
"node": ">=16 || 14 >=14.18"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/path-scurry/node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"dev": true
},
"node_modules/path-to-regexp": { "node_modules/path-to-regexp": {
"version": "8.3.0", "version": "8.3.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
@@ -5702,6 +6164,12 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/proto-list": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
"integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==",
"dev": true
},
"node_modules/proxy-addr": { "node_modules/proxy-addr": {
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -6116,6 +6584,27 @@
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
}, },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/shell-quote": { "node_modules/shell-quote": {
"version": "1.8.3", "version": "1.8.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
@@ -6202,6 +6691,18 @@
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
"dev": true "dev": true
}, },
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"dev": true,
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/sirv": { "node_modules/sirv": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",
@@ -6296,6 +6797,21 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/string-width-cjs": {
"name": "string-width",
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/string.prototype.matchall": { "node_modules/string.prototype.matchall": {
"version": "4.0.8", "version": "4.0.8",
"resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz",
@@ -6386,6 +6902,19 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/strip-ansi-cjs": {
"name": "strip-ansi",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-comments": { "node_modules/strip-comments": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz", "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz",
@@ -7582,6 +8111,12 @@
} }
} }
}, },
"node_modules/vue-component-type-helpers": {
"version": "2.2.12",
"resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.2.12.tgz",
"integrity": "sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==",
"dev": true
},
"node_modules/vue-router": { "node_modules/vue-router": {
"version": "4.6.4", "version": "4.6.4",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz",
@@ -7645,6 +8180,21 @@
"webidl-conversions": "^4.0.2" "webidl-conversions": "^4.0.2"
} }
}, },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/which-boxed-primitive": { "node_modules/which-boxed-primitive": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz",
@@ -7917,6 +8467,57 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1" "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
} }
}, },
"node_modules/wrap-ansi-cjs": {
"name": "wrap-ansi",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs/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==",
"dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs/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==",
"dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/wrap-ansi-cjs/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==",
"dev": true
},
"node_modules/wrap-ansi/node_modules/ansi-styles": { "node_modules/wrap-ansi/node_modules/ansi-styles": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",

View File

@@ -11,10 +11,13 @@
"serve": "vite build && node server.js", "serve": "vite build && node server.js",
"test": "vitest", "test": "vitest",
"test:unit": "vitest run tests/unit tests/integration", "test:unit": "vitest run tests/unit tests/integration",
"test:component": "vitest run tests/component",
"test:stress": "vitest run tests/stress",
"test:all": "vitest run",
"test:ui": "vitest --ui", "test:ui": "vitest --ui",
"test:e2e": "playwright test", "test:e2e": "playwright test --config=playwright.config.cjs",
"test:e2e:ui": "playwright test --ui", "test:e2e:ui": "playwright test --config=playwright.config.cjs --ui",
"test:e2e:codegen": "playwright codegen" "test:e2e:codegen": "playwright codegen --config=playwright.config.cjs"
}, },
"dependencies": { "dependencies": {
"express": "^5.2.1", "express": "^5.2.1",
@@ -24,14 +27,17 @@
"ws": "^8.19.0" "ws": "^8.19.0"
}, },
"devDependencies": { "devDependencies": {
"@axe-core/playwright": "^4.11.1",
"@playwright/test": "^1.58.2", "@playwright/test": "^1.58.2",
"@types/node": "^25.2.3", "@types/node": "^25.2.3",
"@vitejs/plugin-vue": "^4.1.0", "@vitejs/plugin-vue": "^4.1.0",
"@vitest/ui": "^4.0.18", "@vitest/ui": "^4.0.18",
"@vue/test-utils": "^2.4.6",
"concurrently": "^9.2.1", "concurrently": "^9.2.1",
"happy-dom": "^20.6.1",
"jsdom": "^28.0.0", "jsdom": "^28.0.0",
"vite": "^4.3.9", "vite": "^4.3.9",
"vite-plugin-pwa": "^0.16.0", "vite-plugin-pwa": "^0.16.0",
"vitest": "^4.0.18" "vitest": "^4.0.18"
} }
} }

View File

@@ -0,0 +1,255 @@
// @vitest-environment happy-dom
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { mount } from '@vue/test-utils'
import ControllerPage from '../../src/components/ControllerPage.vue'
// Mock globale WebSocket per jsdom
class MockWebSocket {
static OPEN = 1
static CONNECTING = 0
readyState = 0
onopen = null
onclose = null
onmessage = null
onerror = null
send = vi.fn()
close = vi.fn()
constructor() {
// Simula connessione immediata
setTimeout(() => {
this.readyState = 1
if (this.onopen) this.onopen()
}, 0)
}
}
vi.stubGlobal('WebSocket', MockWebSocket)
// Helper per creare il componente con stato personalizzato
function mountController(stateOverrides = {}) {
const wrapper = mount(ControllerPage, {
global: {
stubs: { 'w-app': true, 'w-button': true }
}
})
if (Object.keys(stateOverrides).length > 0) {
wrapper.vm.state = { ...wrapper.vm.state, ...stateOverrides }
}
return wrapper
}
describe('ControllerPage.vue', () => {
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
})
// =============================================
// RENDERING INIZIALE
// =============================================
describe('Rendering iniziale', () => {
it('dovrebbe mostrare i nomi dei team', () => {
const wrapper = mountController()
const text = wrapper.text()
expect(text).toContain('Antoniana')
expect(text).toContain('Guest')
})
it('dovrebbe mostrare punteggio 0-0', () => {
const wrapper = mountController()
const pts = wrapper.findAll('.team-pts')
expect(pts[0].text()).toBe('0')
expect(pts[1].text()).toBe('0')
})
it('dovrebbe mostrare SET 0 per entrambi i team', () => {
const wrapper = mountController()
const sets = wrapper.findAll('.team-set')
expect(sets[0].text()).toContain('SET 0')
expect(sets[1].text()).toContain('SET 0')
})
})
// =============================================
// CLICK PUNTEGGIO
// =============================================
describe('Click punteggio', () => {
it('dovrebbe chiamare sendAction con incPunt home al click sul team home', async () => {
const wrapper = mountController()
const spy = vi.spyOn(wrapper.vm, 'sendAction')
await wrapper.find('.team-score.home-bg').trigger('click')
expect(spy).toHaveBeenCalledWith({ type: 'incPunt', team: 'home' })
})
it('dovrebbe chiamare sendAction con incPunt guest al click sul team guest', async () => {
const wrapper = mountController()
const spy = vi.spyOn(wrapper.vm, 'sendAction')
await wrapper.find('.team-score.guest-bg').trigger('click')
expect(spy).toHaveBeenCalledWith({ type: 'incPunt', team: 'guest' })
})
})
// =============================================
// BOTTONE CAMBIO PALLA
// =============================================
describe('Cambio Palla', () => {
it('dovrebbe essere abilitato a 0-0', () => {
const wrapper = mountController()
const btn = wrapper.findAll('.btn-ctrl').find(b => b.text().includes('Cambio Palla'))
expect(btn.attributes('disabled')).toBeUndefined()
})
it('dovrebbe essere disabilitato se il punteggio non è 0-0', async () => {
const wrapper = mountController()
wrapper.vm.state.sp.punt.home = 5
await wrapper.vm.$nextTick()
const btn = wrapper.findAll('.btn-ctrl').find(b => b.text().includes('Cambio Palla'))
expect(btn.attributes('disabled')).toBeDefined()
})
})
// =============================================
// DIALOG RESET
// =============================================
describe('Dialog Reset', () => {
it('click Reset dovrebbe aprire la conferma', async () => {
const wrapper = mountController()
expect(wrapper.find('.overlay').exists()).toBe(false)
await wrapper.find('.btn-danger').trigger('click')
expect(wrapper.vm.confirmReset).toBe(true)
expect(wrapper.find('.overlay').exists()).toBe(true)
})
it('click NO dovrebbe chiudere la conferma', async () => {
const wrapper = mountController()
wrapper.vm.confirmReset = true
await wrapper.vm.$nextTick()
await wrapper.find('.btn-cancel').trigger('click')
expect(wrapper.vm.confirmReset).toBe(false)
})
it('click SI dovrebbe chiamare doReset', async () => {
const wrapper = mountController()
const spy = vi.spyOn(wrapper.vm, 'sendAction')
wrapper.vm.confirmReset = true
await wrapper.vm.$nextTick()
await wrapper.find('.btn-confirm').trigger('click')
expect(spy).toHaveBeenCalledWith({ type: 'resetta' })
expect(wrapper.vm.confirmReset).toBe(false)
})
})
// =============================================
// COMPUTED cambiValid
// =============================================
describe('cambiValid', () => {
it('dovrebbe essere false se tutti i campi sono vuoti', () => {
const wrapper = mountController()
wrapper.vm.cambiData = [{ in: '', out: '' }, { in: '', out: '' }]
expect(wrapper.vm.cambiValid).toBe(false)
})
it('dovrebbe essere true con un cambio completo', () => {
const wrapper = mountController()
wrapper.vm.cambiData = [{ in: '10', out: '1' }, { in: '', out: '' }]
expect(wrapper.vm.cambiValid).toBe(true)
})
it('dovrebbe essere false con un cambio parziale (solo IN)', () => {
const wrapper = mountController()
wrapper.vm.cambiData = [{ in: '10', out: '' }, { in: '', out: '' }]
expect(wrapper.vm.cambiValid).toBe(false)
})
it('dovrebbe essere false con un cambio parziale (solo OUT)', () => {
const wrapper = mountController()
wrapper.vm.cambiData = [{ in: '', out: '1' }, { in: '', out: '' }]
expect(wrapper.vm.cambiValid).toBe(false)
})
it('dovrebbe essere true con due cambi completi', () => {
const wrapper = mountController()
wrapper.vm.cambiData = [{ in: '10', out: '1' }, { in: '11', out: '2' }]
expect(wrapper.vm.cambiValid).toBe(true)
})
})
// =============================================
// SPEAK
// =============================================
describe('speak', () => {
it('dovrebbe generare "zero a zero" a 0-0', () => {
const wrapper = mountController()
wrapper.vm.wsConnected = true
wrapper.vm.ws = { readyState: 1, send: vi.fn() }
wrapper.vm.speak()
const sent = JSON.parse(wrapper.vm.ws.send.mock.calls[0][0])
expect(sent.type).toBe('speak')
expect(sent.text).toBe('zero a zero')
})
it('dovrebbe generare "N pari" a punteggio uguale', () => {
const wrapper = mountController()
wrapper.vm.state.sp.punt.home = 5
wrapper.vm.state.sp.punt.guest = 5
wrapper.vm.wsConnected = true
wrapper.vm.ws = { readyState: 1, send: vi.fn() }
wrapper.vm.speak()
const sent = JSON.parse(wrapper.vm.ws.send.mock.calls[0][0])
expect(sent.text).toBe('5 pari')
})
it('dovrebbe annunciare prima il punteggio di chi batte (home serve)', () => {
const wrapper = mountController()
wrapper.vm.state.sp.punt.home = 15
wrapper.vm.state.sp.punt.guest = 10
wrapper.vm.state.sp.servHome = true
wrapper.vm.wsConnected = true
wrapper.vm.ws = { readyState: 1, send: vi.fn() }
wrapper.vm.speak()
const sent = JSON.parse(wrapper.vm.ws.send.mock.calls[0][0])
expect(sent.text).toBe('15 a 10')
})
it('dovrebbe annunciare prima il punteggio di chi batte (guest serve)', () => {
const wrapper = mountController()
wrapper.vm.state.sp.punt.home = 10
wrapper.vm.state.sp.punt.guest = 15
wrapper.vm.state.sp.servHome = false
wrapper.vm.wsConnected = true
wrapper.vm.ws = { readyState: 1, send: vi.fn() }
wrapper.vm.speak()
const sent = JSON.parse(wrapper.vm.ws.send.mock.calls[0][0])
expect(sent.text).toBe('15 a 10')
})
})
// =============================================
// BARRA CONNESSIONE
// =============================================
describe('Barra connessione', () => {
it('dovrebbe avere classe "connected" quando connesso', async () => {
const wrapper = mountController()
wrapper.vm.wsConnected = true
await wrapper.vm.$nextTick()
expect(wrapper.find('.conn-bar').classes()).toContain('connected')
})
it('non dovrebbe avere classe "connected" quando disconnesso', () => {
const wrapper = mountController()
wrapper.vm.wsConnected = false
expect(wrapper.find('.conn-bar').classes()).not.toContain('connected')
})
it('dovrebbe mostrare "Connesso" quando connesso', async () => {
const wrapper = mountController()
wrapper.vm.wsConnected = true
await wrapper.vm.$nextTick()
expect(wrapper.find('.conn-bar').text()).toContain('Connesso')
})
})
})

View File

@@ -0,0 +1,195 @@
// @vitest-environment happy-dom
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { mount } from '@vue/test-utils'
import DisplayPage from '../../src/components/DisplayPage.vue'
// Mock globale WebSocket per jsdom
class MockWebSocket {
static OPEN = 1
static CONNECTING = 0
readyState = 0
onopen = null
onclose = null
onmessage = null
onerror = null
send = vi.fn()
close = vi.fn()
constructor() {
setTimeout(() => {
this.readyState = 1
if (this.onopen) this.onopen()
}, 0)
}
}
vi.stubGlobal('WebSocket', MockWebSocket)
// Mock requestFullscreen e speechSynthesis
vi.stubGlobal('speechSynthesis', {
speak: vi.fn(),
cancel: vi.fn(),
getVoices: () => []
})
function mountDisplay() {
return mount(DisplayPage, {
global: {
stubs: { 'w-app': true }
}
})
}
describe('DisplayPage.vue', () => {
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
})
// =============================================
// RENDERING PUNTEGGIO
// =============================================
describe('Rendering punteggio', () => {
it('dovrebbe mostrare i nomi dei team', () => {
const wrapper = mountDisplay()
const text = wrapper.text()
expect(text).toContain('Antoniana')
expect(text).toContain('Guest')
})
it('dovrebbe mostrare punteggio iniziale 0-0', () => {
const wrapper = mountDisplay()
const punti = wrapper.findAll('.punt')
expect(punti[0].text()).toBe('0')
expect(punti[1].text()).toBe('0')
})
it('dovrebbe mostrare i set corretti', () => {
const wrapper = mountDisplay()
const text = wrapper.text()
expect(text).toContain('set 0')
})
it('dovrebbe aggiornare il punteggio quando lo stato cambia', async () => {
const wrapper = mountDisplay()
wrapper.vm.state.sp.punt.home = 15
wrapper.vm.state.sp.punt.guest = 12
await wrapper.vm.$nextTick()
const punti = wrapper.findAll('.punt')
expect(punti[0].text()).toBe('15')
expect(punti[1].text()).toBe('12')
})
})
// =============================================
// ORDINE TEAM
// =============================================
describe('Ordine team', () => {
it('order=true → Home prima di Guest', () => {
const wrapper = mountDisplay()
const headers = wrapper.findAll('.hea')
expect(headers[0].classes()).toContain('home')
expect(headers[1].classes()).toContain('guest')
})
it('order=false → Guest prima di Home', async () => {
const wrapper = mountDisplay()
wrapper.vm.state.order = false
await wrapper.vm.$nextTick()
const headers = wrapper.findAll('.hea')
expect(headers[0].classes()).toContain('guest')
expect(headers[1].classes()).toContain('home')
})
})
// =============================================
// FORMAZIONE vs PUNTEGGIO
// =============================================
describe('visuForm toggle', () => {
it('visuForm=false → mostra punteggio grande', () => {
const wrapper = mountDisplay()
expect(wrapper.find('.punteggio-container').exists()).toBe(true)
expect(wrapper.find('.form').exists()).toBe(false)
})
it('visuForm=true → mostra formazione', async () => {
const wrapper = mountDisplay()
wrapper.vm.state.visuForm = true
await wrapper.vm.$nextTick()
expect(wrapper.findAll('.form').length).toBeGreaterThan(0)
expect(wrapper.find('.punteggio-container').exists()).toBe(false)
})
it('formazione mostra 6 giocatori per team', async () => {
const wrapper = mountDisplay()
wrapper.vm.state.visuForm = true
await wrapper.vm.$nextTick()
const formDivs = wrapper.findAll('.formdiv')
// 6 per home + 6 per guest = 12
expect(formDivs).toHaveLength(12)
})
})
// =============================================
// STRISCIA
// =============================================
describe('visuStriscia toggle', () => {
it('visuStriscia=true → mostra la striscia', () => {
const wrapper = mountDisplay()
expect(wrapper.find('.striscia').exists()).toBe(true)
})
it('visuStriscia=false → nasconde la striscia', async () => {
const wrapper = mountDisplay()
wrapper.vm.state.visuStriscia = false
await wrapper.vm.$nextTick()
expect(wrapper.find('.striscia').exists()).toBe(false)
})
})
// =============================================
// INDICATORE CONNESSIONE
// =============================================
describe('Indicatore connessione', () => {
it('dovrebbe avere classe "disconnected" quando non connesso', () => {
const wrapper = mountDisplay()
const status = wrapper.find('.connection-status')
expect(status.classes()).toContain('disconnected')
})
it('dovrebbe avere classe "connected" quando connesso', async () => {
const wrapper = mountDisplay()
wrapper.vm.wsConnected = true
await wrapper.vm.$nextTick()
const status = wrapper.find('.connection-status')
expect(status.classes()).toContain('connected')
})
it('dovrebbe mostrare "Disconnesso" quando non connesso', () => {
const wrapper = mountDisplay()
const status = wrapper.find('.connection-status')
expect(status.text()).toContain('Disconnesso')
})
})
// =============================================
// ICONA SERVIZIO
// =============================================
describe('Icona servizio', () => {
it('dovrebbe mostrare l\'icona servizio sul team home quando servHome=true', () => {
const wrapper = mountDisplay()
// v-show imposta display:none. In happy-dom controlliamo lo style.
const imgs = wrapper.findAll('.serv-slot img')
// Con state.order=true e servHome=true:
// - la prima img (home) è visibile (no display:none)
// - la seconda img (guest) ha display:none
const homeStyle = imgs[0].attributes('style') || ''
const guestStyle = imgs[1].attributes('style') || ''
expect(homeStyle).not.toContain('display: none')
expect(guestStyle).toContain('display: none')
})
})
})

View File

@@ -16,68 +16,388 @@ class MockWebSocketServer extends EventEmitter {
clients = new Set() clients = new Set()
} }
// Helper: connette e registra un client
function connectAndRegister(wss, role) {
const ws = new MockWebSocket()
wss.emit('connection', ws)
wss.clients.add(ws)
ws.emit('message', JSON.stringify({ type: 'register', role }))
ws.send.mockClear()
return ws
}
// Helper: ultimo messaggio inviato a un ws
function lastSent(ws) {
const calls = ws.send.mock.calls
return JSON.parse(calls[calls.length - 1][0])
}
describe('WebSocket Integration (websocket-handler.js)', () => { describe('WebSocket Integration (websocket-handler.js)', () => {
let wss let wss
let handler let handler
let ws
beforeEach(() => { beforeEach(() => {
wss = new MockWebSocketServer() wss = new MockWebSocketServer()
handler = setupWebSocketHandler(wss) handler = setupWebSocketHandler(wss)
ws = new MockWebSocket()
// Simuliamo la connessione
wss.emit('connection', ws)
// Aggiungiamo il client al set del server (come farebbe 'ws' realmente)
wss.clients.add(ws)
}) })
afterEach(() => { afterEach(() => {
vi.restoreAllMocks() vi.restoreAllMocks()
}) })
it('dovrebbe registrare un client come "display" e inviare lo stato', () => { // =============================================
ws.emit('message', JSON.stringify({ type: 'register', role: 'display' })) // REGISTRAZIONE
// =============================================
describe('Registrazione', () => {
it('dovrebbe registrare un client come "display" e inviare lo stato', () => {
const ws = new MockWebSocket()
wss.emit('connection', ws)
wss.clients.add(ws)
ws.emit('message', JSON.stringify({ type: 'register', role: 'display' }))
// Verifica che abbia inviato lo stato iniziale expect(ws.send).toHaveBeenCalled()
expect(ws.send).toHaveBeenCalled() const sentMsg = JSON.parse(ws.send.mock.calls[0][0])
const sentMsg = JSON.parse(ws.send.mock.calls[0][0]) expect(sentMsg.type).toBe('state')
expect(sentMsg.type).toBe('state') expect(sentMsg.state).toBeDefined()
expect(sentMsg.state).toBeDefined() })
it('dovrebbe registrare un client come "controller"', () => {
connectAndRegister(wss, 'controller')
expect(handler.getClients().size).toBe(1)
})
it('dovrebbe rifiutare ruolo non valido', () => {
const ws = new MockWebSocket()
wss.emit('connection', ws)
wss.clients.add(ws)
ws.emit('message', JSON.stringify({ type: 'register', role: 'hacker' }))
const sentMsg = JSON.parse(ws.send.mock.calls[0][0])
expect(sentMsg.type).toBe('error')
expect(sentMsg.message).toContain('Invalid role')
})
it('dovrebbe usare "display" come ruolo default se mancante', () => {
const ws = new MockWebSocket()
wss.emit('connection', ws)
wss.clients.add(ws)
ws.emit('message', JSON.stringify({ type: 'register' }))
const sentMsg = JSON.parse(ws.send.mock.calls[0][0])
expect(sentMsg.type).toBe('state')
})
}) })
it('dovrebbe permettere al controller di cambiare il punteggio', () => { // =============================================
// 1. Registra Controller // AZIONI
ws.emit('message', JSON.stringify({ type: 'register', role: 'controller' })) // =============================================
ws.send.mockClear() // pulisco chiamate precedenti describe('Azioni', () => {
it('dovrebbe permettere al controller di cambiare il punteggio', () => {
const controller = connectAndRegister(wss, 'controller')
// 2. Invia Azione controller.emit('message', JSON.stringify({
ws.emit('message', JSON.stringify({ type: 'action',
type: 'action', action: { type: 'incPunt', team: 'home' }
action: { type: 'incPunt', team: 'home' } }))
}))
// 3. Verifica Broadcast del nuovo stato expect(controller.send).toHaveBeenCalled()
expect(ws.send).toHaveBeenCalled() const sentMsg = lastSent(controller)
const sentMsg = JSON.parse(ws.send.mock.calls[0][0]) expect(sentMsg.type).toBe('state')
expect(sentMsg.type).toBe('state') expect(sentMsg.state.sp.punt.home).toBe(1)
expect(sentMsg.state.sp.punt.home).toBe(1) })
it('dovrebbe impedire al display di inviare azioni', () => {
const display = connectAndRegister(wss, 'display')
display.emit('message', JSON.stringify({
type: 'action',
action: { type: 'incPunt', team: 'home' }
}))
const sentMsg = lastSent(display)
expect(sentMsg.type).toBe('error')
expect(sentMsg.message).toContain('Only controllers')
})
it('dovrebbe impedire azioni da client non registrati', () => {
const ws = new MockWebSocket()
wss.emit('connection', ws)
wss.clients.add(ws)
ws.emit('message', JSON.stringify({
type: 'action',
action: { type: 'incPunt', team: 'home' }
}))
const sentMsg = JSON.parse(ws.send.mock.calls[0][0])
expect(sentMsg.type).toBe('error')
expect(sentMsg.message).toContain('Only controllers')
})
it('dovrebbe rifiutare azione con formato invalido (missing action)', () => {
const controller = connectAndRegister(wss, 'controller')
controller.emit('message', JSON.stringify({
type: 'action'
}))
const sentMsg = lastSent(controller)
expect(sentMsg.type).toBe('error')
expect(sentMsg.message).toContain('Invalid action format')
})
it('dovrebbe rifiutare azione con formato invalido (missing action.type)', () => {
const controller = connectAndRegister(wss, 'controller')
controller.emit('message', JSON.stringify({
type: 'action',
action: { team: 'home' }
}))
const sentMsg = lastSent(controller)
expect(sentMsg.type).toBe('error')
expect(sentMsg.message).toContain('Invalid action format')
})
}) })
it('dovrebbe impedire al display di inviare azioni', () => { // =============================================
// 1. Registra Display // BROADCAST MULTI-CLIENT
ws.emit('message', JSON.stringify({ type: 'register', role: 'display' })) // =============================================
ws.send.mockClear() describe('Broadcast', () => {
it('dovrebbe inviare lo stato a tutti i client dopo un\'azione', () => {
const controller = connectAndRegister(wss, 'controller')
const display1 = connectAndRegister(wss, 'display')
const display2 = connectAndRegister(wss, 'display')
// 2. Tenta Azione controller.emit('message', JSON.stringify({
ws.emit('message', JSON.stringify({ type: 'action',
type: 'action', action: { type: 'incPunt', team: 'home' }
action: { type: 'incPunt', team: 'home' } }))
}))
// 3. Verifica Errore // Tutti i client nel set dovrebbero aver ricevuto lo stato
expect(ws.send).toHaveBeenCalled() expect(controller.send).toHaveBeenCalled()
const sentMsg = JSON.parse(ws.send.mock.calls[0][0]) expect(display1.send).toHaveBeenCalled()
expect(sentMsg.type).toBe('error') expect(display2.send).toHaveBeenCalled()
expect(sentMsg.message).toContain('Only controllers')
const msg1 = lastSent(display1)
const msg2 = lastSent(display2)
expect(msg1.type).toBe('state')
expect(msg1.state.sp.punt.home).toBe(1)
expect(msg2.state.sp.punt.home).toBe(1)
})
it('non dovrebbe inviare a client con readyState != OPEN', () => {
const controller = connectAndRegister(wss, 'controller')
const closedClient = connectAndRegister(wss, 'display')
closedClient.readyState = 3 // CLOSED
controller.emit('message', JSON.stringify({
type: 'action',
action: { type: 'incPunt', team: 'home' }
}))
// closedClient non dovrebbe aver ricevuto il broadcast
expect(closedClient.send).not.toHaveBeenCalled()
})
})
// =============================================
// SPEAK
// =============================================
describe('Speak', () => {
it('dovrebbe inoltrare il messaggio speak solo ai display', () => {
const controller = connectAndRegister(wss, 'controller')
const display = connectAndRegister(wss, 'display')
controller.emit('message', JSON.stringify({
type: 'speak',
text: 'quindici a dieci'
}))
// Il display riceve il messaggio speak
expect(display.send).toHaveBeenCalled()
const msg = lastSent(display)
expect(msg.type).toBe('speak')
expect(msg.text).toBe('quindici a dieci')
})
it('non dovrebbe permettere al display di inviare speak', () => {
const display = connectAndRegister(wss, 'display')
display.emit('message', JSON.stringify({
type: 'speak',
text: 'test'
}))
const msg = lastSent(display)
expect(msg.type).toBe('error')
expect(msg.message).toContain('Only controllers')
})
it('dovrebbe rifiutare speak con testo vuoto', () => {
const controller = connectAndRegister(wss, 'controller')
controller.emit('message', JSON.stringify({
type: 'speak',
text: ' '
}))
const msg = lastSent(controller)
expect(msg.type).toBe('error')
expect(msg.message).toContain('Invalid speak payload')
})
it('dovrebbe rifiutare speak senza testo', () => {
const controller = connectAndRegister(wss, 'controller')
controller.emit('message', JSON.stringify({
type: 'speak'
}))
const msg = lastSent(controller)
expect(msg.type).toBe('error')
})
it('dovrebbe fare trim del testo speak', () => {
const controller = connectAndRegister(wss, 'controller')
const display = connectAndRegister(wss, 'display')
controller.emit('message', JSON.stringify({
type: 'speak',
text: ' dieci a otto '
}))
const msg = lastSent(display)
expect(msg.text).toBe('dieci a otto')
})
})
// =============================================
// MESSAGGI MALFORMATI
// =============================================
describe('Messaggi malformati', () => {
it('dovrebbe gestire JSON non valido senza crash', () => {
const ws = new MockWebSocket()
wss.emit('connection', ws)
wss.clients.add(ws)
expect(() => {
ws.emit('message', 'questo non è JSON {{{')
}).not.toThrow()
const msg = lastSent(ws)
expect(msg.type).toBe('error')
expect(msg.message).toContain('Invalid message format')
})
it('dovrebbe gestire Buffer come input', () => {
const controller = connectAndRegister(wss, 'controller')
const buf = Buffer.from(JSON.stringify({
type: 'action',
action: { type: 'incPunt', team: 'home' }
}))
controller.emit('message', buf)
const msg = lastSent(controller)
expect(msg.type).toBe('state')
expect(msg.state.sp.punt.home).toBe(1)
})
})
// =============================================
// DISCONNESSIONE
// =============================================
describe('Disconnessione', () => {
it('dovrebbe rimuovere il client dalla mappa alla disconnessione', () => {
const controller = connectAndRegister(wss, 'controller')
expect(handler.getClients().size).toBe(1)
controller.emit('close')
expect(handler.getClients().size).toBe(0)
})
it('i client rimanenti non dovrebbero essere affetti dalla disconnessione', () => {
const controller = connectAndRegister(wss, 'controller')
const display = connectAndRegister(wss, 'display')
expect(handler.getClients().size).toBe(2)
controller.emit('close')
expect(handler.getClients().size).toBe(1)
expect(handler.getClients().has(display)).toBe(true)
})
})
// =============================================
// ERRORI WEBSOCKET
// =============================================
describe('Errori WebSocket', () => {
it('dovrebbe terminare la connessione per errore UTF8 invalido', () => {
const ws = new MockWebSocket()
wss.emit('connection', ws)
const err = new Error('Invalid UTF8')
err.code = 'WS_ERR_INVALID_UTF8'
ws.emit('error', err)
expect(ws.terminate).toHaveBeenCalled()
})
it('dovrebbe terminare la connessione per close code invalido', () => {
const ws = new MockWebSocket()
wss.emit('connection', ws)
const err = new Error('Invalid close code')
err.code = 'WS_ERR_INVALID_CLOSE_CODE'
ws.emit('error', err)
expect(ws.terminate).toHaveBeenCalled()
})
it('non dovrebbe terminare per altri errori', () => {
const ws = new MockWebSocket()
wss.emit('connection', ws)
const err = new Error('Generic error')
ws.emit('error', err)
expect(ws.terminate).not.toHaveBeenCalled()
})
})
// =============================================
// API PUBBLICA
// =============================================
describe('API pubblica', () => {
it('getState dovrebbe restituire lo stato corrente', () => {
const state = handler.getState()
expect(state.sp.punt.home).toBe(0)
expect(state.sp.punt.guest).toBe(0)
})
it('setState dovrebbe sovrascrivere lo stato', () => {
const newState = handler.getState()
newState.sp.punt.home = 99
handler.setState(newState)
expect(handler.getState().sp.punt.home).toBe(99)
})
it('broadcastState dovrebbe inviare a tutti i client', () => {
const display = connectAndRegister(wss, 'display')
handler.broadcastState()
expect(display.send).toHaveBeenCalled()
const msg = lastSent(display)
expect(msg.type).toBe('state')
})
it('getClients dovrebbe restituire la mappa dei client', () => {
expect(handler.getClients()).toBeInstanceOf(Map)
expect(handler.getClients().size).toBe(0)
connectAndRegister(wss, 'display')
expect(handler.getClients().size).toBe(1)
})
}) })
}) })

View File

@@ -0,0 +1,118 @@
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
import { setupWebSocketHandler } from '../../src/websocket-handler.js'
import { EventEmitter } from 'events'
class MockWebSocket extends EventEmitter {
constructor() {
super()
this.readyState = 1
}
send = vi.fn()
terminate = vi.fn()
}
class MockWebSocketServer extends EventEmitter {
clients = new Set()
}
function connectAndRegister(wss, role) {
const ws = new MockWebSocket()
wss.emit('connection', ws)
wss.clients.add(ws)
ws.emit('message', JSON.stringify({ type: 'register', role }))
ws.send.mockClear()
return ws
}
describe('Stress Test WebSocket', () => {
let wss
let handler
beforeEach(() => {
wss = new MockWebSocketServer()
handler = setupWebSocketHandler(wss)
})
afterEach(() => {
vi.restoreAllMocks()
})
it('dovrebbe gestire 50 client display connessi simultaneamente', () => {
const displays = []
for (let i = 0; i < 50; i++) {
displays.push(connectAndRegister(wss, 'display'))
}
expect(handler.getClients().size).toBe(50)
// Un controller invia un'azione
const controller = connectAndRegister(wss, 'controller')
controller.emit('message', JSON.stringify({
type: 'action',
action: { type: 'incPunt', team: 'home' }
}))
// Tutti i display devono aver ricevuto il broadcast
for (const display of displays) {
expect(display.send).toHaveBeenCalled()
const msg = JSON.parse(display.send.mock.calls[display.send.mock.calls.length - 1][0])
expect(msg.type).toBe('state')
expect(msg.state.sp.punt.home).toBe(1)
}
})
it('dovrebbe gestire 100 azioni rapide in sequenza con stato finale corretto', () => {
const controller = connectAndRegister(wss, 'controller')
// 60 punti home, 40 punti guest
for (let i = 0; i < 60; i++) {
controller.emit('message', JSON.stringify({
type: 'action',
action: { type: 'incPunt', team: 'home' }
}))
}
for (let i = 0; i < 40; i++) {
controller.emit('message', JSON.stringify({
type: 'action',
action: { type: 'incPunt', team: 'guest' }
}))
}
// Lo stato finale dipende da checkVittoria che blocca a 25+2
// Home arriva a 25-0 → vittoria → blocca. Quindi punti home = 25
const state = handler.getState()
expect(state.sp.punt.home).toBe(25)
// Guest: non può segnare dopo vittoria? No, checkVittoria blocca solo il team che ha vinto?
// Controlliamo: checkVittoria controlla ENTRAMBI i team.
// A 25-0 → vittoria=true → incPunt per guest è anche bloccato
expect(state.sp.punt.guest).toBe(0)
})
it('dovrebbe garantire che tutti i display ricevano ogni update sotto carico', () => {
const displays = []
for (let i = 0; i < 10; i++) {
displays.push(connectAndRegister(wss, 'display'))
}
const controller = connectAndRegister(wss, 'controller')
// 5 azioni rapide
for (let i = 0; i < 5; i++) {
controller.emit('message', JSON.stringify({
type: 'action',
action: { type: 'incPunt', team: 'home' }
}))
}
// Ogni display deve aver ricevuto esattamente 5 broadcast
for (const display of displays) {
expect(display.send).toHaveBeenCalledTimes(5)
}
// Verifica stato finale su tutti i display
for (const display of displays) {
const lastMsg = JSON.parse(display.send.mock.calls[4][0])
expect(lastMsg.state.sp.punt.home).toBe(5)
}
})
})

View File

@@ -8,7 +8,10 @@ describe('Game Logic (gameState.js)', () => {
state = createInitialState() state = createInitialState()
}) })
describe('Initial State', () => { // =============================================
// STATO INIZIALE
// =============================================
describe('Stato iniziale', () => {
it('dovrebbe iniziare con 0-0', () => { it('dovrebbe iniziare con 0-0', () => {
expect(state.sp.punt.home).toBe(0) expect(state.sp.punt.home).toBe(0)
expect(state.sp.punt.guest).toBe(0) expect(state.sp.punt.guest).toBe(0)
@@ -18,40 +21,451 @@ describe('Game Logic (gameState.js)', () => {
expect(state.sp.set.home).toBe(0) expect(state.sp.set.home).toBe(0)
expect(state.sp.set.guest).toBe(0) expect(state.sp.set.guest).toBe(0)
}) })
it('dovrebbe avere servizio Home', () => {
expect(state.sp.servHome).toBe(true)
})
it('dovrebbe avere formazione di default [1-6]', () => {
expect(state.sp.form.home).toEqual(["1", "2", "3", "4", "5", "6"])
expect(state.sp.form.guest).toEqual(["1", "2", "3", "4", "5", "6"])
})
it('dovrebbe avere la striscia iniziale a [0]', () => {
expect(state.sp.striscia.home).toEqual([0])
expect(state.sp.striscia.guest).toEqual([0])
})
it('dovrebbe avere storico servizio vuoto', () => {
expect(state.sp.storicoServizio).toEqual([])
})
it('dovrebbe avere modalità 3/5 di default', () => {
expect(state.modalitaPartita).toBe("3/5")
})
it('dovrebbe avere visuForm false e visuStriscia true', () => {
expect(state.visuForm).toBe(false)
expect(state.visuStriscia).toBe(true)
})
}) })
describe('Punteggio', () => { // =============================================
it('dovrebbe incrementare i punti (Home)', () => { // IMMUTABILITÀ
// =============================================
describe('Immutabilità', () => {
it('applyAction non dovrebbe mutare lo stato originale', () => {
const original = JSON.stringify(state)
applyAction(state, { type: 'incPunt', team: 'home' })
expect(JSON.stringify(state)).toBe(original)
})
it('dovrebbe restituire un nuovo oggetto', () => {
const newState = applyAction(state, { type: 'incPunt', team: 'home' })
expect(newState).not.toBe(state)
})
})
// =============================================
// INCREMENTO PUNTI (incPunt)
// =============================================
describe('incPunt', () => {
it('dovrebbe incrementare i punti Home', () => {
const newState = applyAction(state, { type: 'incPunt', team: 'home' }) const newState = applyAction(state, { type: 'incPunt', team: 'home' })
expect(newState.sp.punt.home).toBe(1) expect(newState.sp.punt.home).toBe(1)
expect(newState.sp.punt.guest).toBe(0) expect(newState.sp.punt.guest).toBe(0)
}) })
it('dovrebbe gestire il cambio palla', () => { it('dovrebbe incrementare i punti Guest', () => {
// Home batte const newState = applyAction(state, { type: 'incPunt', team: 'guest' })
state.sp.servHome = true expect(newState.sp.punt.guest).toBe(1)
// Punto Guest -> Cambio palla expect(newState.sp.punt.home).toBe(0)
const s1 = applyAction(state, { type: 'incPunt', team: 'guest' })
expect(s1.sp.servHome).toBe(false) // Ora batte Guest
// Punto Home -> Cambio palla
const s2 = applyAction(s1, { type: 'incPunt', team: 'home' })
expect(s2.sp.servHome).toBe(true) // Torna a battere Home
}) })
it('dovrebbe gestire la rotazione formazione al cambio palla', () => { it('dovrebbe gestire il cambio palla (Guest segna, batteva Home)', () => {
state.sp.servHome = true // Batte Home state.sp.servHome = true
const s1 = applyAction(state, { type: 'incPunt', team: 'guest' })
expect(s1.sp.servHome).toBe(false)
})
it('dovrebbe gestire il cambio palla (Home segna, batteva Guest)', () => {
state.sp.servHome = false
const s1 = applyAction(state, { type: 'incPunt', team: 'home' })
expect(s1.sp.servHome).toBe(true)
})
it('non dovrebbe cambiare palla se segna chi batte', () => {
state.sp.servHome = true
const s1 = applyAction(state, { type: 'incPunt', team: 'home' })
expect(s1.sp.servHome).toBe(true)
})
it('dovrebbe ruotare la formazione al cambio palla', () => {
state.sp.servHome = true
state.sp.form.guest = ["1", "2", "3", "4", "5", "6"] state.sp.form.guest = ["1", "2", "3", "4", "5", "6"]
// Punto Guest -> Cambio palla e rotazione Guest
const newState = applyAction(state, { type: 'incPunt', team: 'guest' }) const newState = applyAction(state, { type: 'incPunt', team: 'guest' })
// Verifica che la formazione sia ruotata (il primo elemento diventa ultimo)
expect(newState.sp.form.guest).toEqual(["2", "3", "4", "5", "6", "1"]) expect(newState.sp.form.guest).toEqual(["2", "3", "4", "5", "6", "1"])
}) })
it('non dovrebbe ruotare la formazione se non c\'è cambio palla', () => {
state.sp.servHome = true
state.sp.form.home = ["1", "2", "3", "4", "5", "6"]
const newState = applyAction(state, { type: 'incPunt', team: 'home' })
expect(newState.sp.form.home).toEqual(["1", "2", "3", "4", "5", "6"])
})
it('dovrebbe aggiornare la striscia per punto Home', () => {
const s = applyAction(state, { type: 'incPunt', team: 'home' })
expect(s.sp.striscia.home).toEqual([0, 1])
expect(s.sp.striscia.guest).toEqual([0, " "])
})
it('dovrebbe aggiornare la striscia per punto Guest', () => {
const s = applyAction(state, { type: 'incPunt', team: 'guest' })
expect(s.sp.striscia.guest).toEqual([0, 1])
expect(s.sp.striscia.home).toEqual([0, " "])
})
it('dovrebbe registrare lo storico servizio', () => {
const s = applyAction(state, { type: 'incPunt', team: 'home' })
expect(s.sp.storicoServizio).toHaveLength(1)
expect(s.sp.storicoServizio[0]).toHaveProperty('servHome')
expect(s.sp.storicoServizio[0]).toHaveProperty('cambioPalla')
})
it('non dovrebbe incrementare i punti dopo vittoria', () => {
state.sp.punt.home = 25
state.sp.punt.guest = 23
const s = applyAction(state, { type: 'incPunt', team: 'home' })
expect(s.sp.punt.home).toBe(25)
})
}) })
describe('Vittoria Set', () => { // =============================================
// DECREMENTO PUNTI (decPunt)
// =============================================
describe('decPunt', () => {
it('dovrebbe annullare l\'ultimo punto Home', () => {
const s1 = applyAction(state, { type: 'incPunt', team: 'home' })
const s2 = applyAction(s1, { type: 'decPunt' })
expect(s2.sp.punt.home).toBe(0)
expect(s2.sp.punt.guest).toBe(0)
})
it('dovrebbe annullare l\'ultimo punto Guest', () => {
const s1 = applyAction(state, { type: 'incPunt', team: 'guest' })
const s2 = applyAction(s1, { type: 'decPunt' })
expect(s2.sp.punt.home).toBe(0)
expect(s2.sp.punt.guest).toBe(0)
})
it('non dovrebbe fare nulla sullo stato iniziale', () => {
const s = applyAction(state, { type: 'decPunt' })
expect(s.sp.punt.home).toBe(0)
expect(s.sp.punt.guest).toBe(0)
})
it('dovrebbe ripristinare il servizio dopo undo con cambio palla', () => {
state.sp.servHome = true
const s1 = applyAction(state, { type: 'incPunt', team: 'guest' })
expect(s1.sp.servHome).toBe(false)
const s2 = applyAction(s1, { type: 'decPunt' })
expect(s2.sp.servHome).toBe(true)
})
it('dovrebbe invertire la rotazione dopo undo con cambio palla', () => {
state.sp.servHome = true
state.sp.form.guest = ["1", "2", "3", "4", "5", "6"]
const s1 = applyAction(state, { type: 'incPunt', team: 'guest' })
expect(s1.sp.form.guest).toEqual(["2", "3", "4", "5", "6", "1"])
const s2 = applyAction(s1, { type: 'decPunt' })
expect(s2.sp.form.guest).toEqual(["1", "2", "3", "4", "5", "6"])
})
it('dovrebbe ripristinare la striscia', () => {
const s1 = applyAction(state, { type: 'incPunt', team: 'home' })
const s2 = applyAction(s1, { type: 'decPunt' })
expect(s2.sp.striscia.home).toEqual([0])
})
it('dovrebbe gestire undo multipli in sequenza', () => {
let s = state
s = applyAction(s, { type: 'incPunt', team: 'home' })
s = applyAction(s, { type: 'incPunt', team: 'guest' })
s = applyAction(s, { type: 'incPunt', team: 'home' })
expect(s.sp.punt.home).toBe(2)
expect(s.sp.punt.guest).toBe(1)
s = applyAction(s, { type: 'decPunt' })
expect(s.sp.punt.home).toBe(1)
s = applyAction(s, { type: 'decPunt' })
expect(s.sp.punt.guest).toBe(0)
s = applyAction(s, { type: 'decPunt' })
expect(s.sp.punt.home).toBe(0)
})
})
// =============================================
// INCREMENTO SET (incSet)
// =============================================
describe('incSet', () => {
it('dovrebbe incrementare il set Home', () => {
const s = applyAction(state, { type: 'incSet', team: 'home' })
expect(s.sp.set.home).toBe(1)
})
it('dovrebbe incrementare il set Guest', () => {
const s = applyAction(state, { type: 'incSet', team: 'guest' })
expect(s.sp.set.guest).toBe(1)
})
it('dovrebbe fare wrap da 2 a 0', () => {
state.sp.set.home = 2
const s = applyAction(state, { type: 'incSet', team: 'home' })
expect(s.sp.set.home).toBe(0)
})
it('dovrebbe incrementare da 1 a 2', () => {
state.sp.set.home = 1
const s = applyAction(state, { type: 'incSet', team: 'home' })
expect(s.sp.set.home).toBe(2)
})
})
// =============================================
// CAMBIO PALLA (cambiaPalla)
// =============================================
describe('cambiaPalla', () => {
it('dovrebbe invertire il servizio a 0-0', () => {
expect(state.sp.servHome).toBe(true)
const s = applyAction(state, { type: 'cambiaPalla' })
expect(s.sp.servHome).toBe(false)
})
it('dovrebbe tornare a Home con doppio toggle', () => {
let s = applyAction(state, { type: 'cambiaPalla' })
s = applyAction(s, { type: 'cambiaPalla' })
expect(s.sp.servHome).toBe(true)
})
it('non dovrebbe cambiare palla se il punteggio non è 0-0', () => {
state.sp.punt.home = 1
const s = applyAction(state, { type: 'cambiaPalla' })
expect(s.sp.servHome).toBe(true)
})
it('non dovrebbe cambiare palla se Guest ha punti', () => {
state.sp.punt.guest = 3
const s = applyAction(state, { type: 'cambiaPalla' })
expect(s.sp.servHome).toBe(true)
})
})
// =============================================
// TOGGLE (toggleFormazione, toggleStriscia, toggleOrder)
// =============================================
describe('Toggle', () => {
it('toggleFormazione: false → true', () => {
expect(state.visuForm).toBe(false)
const s = applyAction(state, { type: 'toggleFormazione' })
expect(s.visuForm).toBe(true)
})
it('toggleFormazione: true → false', () => {
state.visuForm = true
const s = applyAction(state, { type: 'toggleFormazione' })
expect(s.visuForm).toBe(false)
})
it('toggleStriscia: true → false', () => {
expect(state.visuStriscia).toBe(true)
const s = applyAction(state, { type: 'toggleStriscia' })
expect(s.visuStriscia).toBe(false)
})
it('toggleOrder: true → false', () => {
expect(state.order).toBe(true)
const s = applyAction(state, { type: 'toggleOrder' })
expect(s.order).toBe(false)
})
})
// =============================================
// NOMI (setNomi)
// =============================================
describe('setNomi', () => {
it('dovrebbe aggiornare entrambi i nomi', () => {
const s = applyAction(state, { type: 'setNomi', home: 'Volley A', guest: 'Volley B' })
expect(s.sp.nomi.home).toBe('Volley A')
expect(s.sp.nomi.guest).toBe('Volley B')
})
it('dovrebbe aggiornare solo il nome Home se guest è undefined', () => {
const s = applyAction(state, { type: 'setNomi', home: 'Volley A' })
expect(s.sp.nomi.home).toBe('Volley A')
expect(s.sp.nomi.guest).toBe('Guest')
})
it('dovrebbe aggiornare solo il nome Guest se home è undefined', () => {
const s = applyAction(state, { type: 'setNomi', guest: 'Volley B' })
expect(s.sp.nomi.home).toBe('Antoniana')
expect(s.sp.nomi.guest).toBe('Volley B')
})
})
// =============================================
// MODALITÀ (setModalita)
// =============================================
describe('setModalita', () => {
it('dovrebbe cambiare in 2/3', () => {
const s = applyAction(state, { type: 'setModalita', modalita: '2/3' })
expect(s.modalitaPartita).toBe('2/3')
})
it('dovrebbe cambiare in 3/5', () => {
state.modalitaPartita = '2/3'
const s = applyAction(state, { type: 'setModalita', modalita: '3/5' })
expect(s.modalitaPartita).toBe('3/5')
})
})
// =============================================
// FORMAZIONE (setFormazione)
// =============================================
describe('setFormazione', () => {
it('dovrebbe sostituire la formazione Home', () => {
const nuova = ["10", "11", "12", "13", "14", "15"]
const s = applyAction(state, { type: 'setFormazione', team: 'home', form: nuova })
expect(s.sp.form.home).toEqual(nuova)
})
it('dovrebbe sostituire la formazione Guest', () => {
const nuova = ["7", "8", "9", "10", "11", "12"]
const s = applyAction(state, { type: 'setFormazione', team: 'guest', form: nuova })
expect(s.sp.form.guest).toEqual(nuova)
})
it('non dovrebbe modificare se manca team', () => {
const s = applyAction(state, { type: 'setFormazione', form: ["7", "8", "9", "10", "11", "12"] })
expect(s.sp.form.home).toEqual(["1", "2", "3", "4", "5", "6"])
})
it('non dovrebbe modificare se manca form', () => {
const s = applyAction(state, { type: 'setFormazione', team: 'home' })
expect(s.sp.form.home).toEqual(["1", "2", "3", "4", "5", "6"])
})
})
// =============================================
// CAMBI GIOCATORI (confermaCambi)
// =============================================
describe('confermaCambi', () => {
it('dovrebbe effettuare una sostituzione valida', () => {
state.sp.form.home = ["1", "2", "3", "4", "5", "6"]
const s = applyAction(state, {
type: 'confermaCambi',
team: 'home',
cambi: [{ in: "10", out: "3" }]
})
expect(s.sp.form.home).toContain("10")
expect(s.sp.form.home).not.toContain("3")
})
it('dovrebbe gestire doppia sostituzione', () => {
state.sp.form.home = ["1", "2", "3", "4", "5", "6"]
const s = applyAction(state, {
type: 'confermaCambi',
team: 'home',
cambi: [
{ in: "10", out: "1" },
{ in: "11", out: "2" }
]
})
expect(s.sp.form.home).toContain("10")
expect(s.sp.form.home).toContain("11")
expect(s.sp.form.home).not.toContain("1")
expect(s.sp.form.home).not.toContain("2")
})
it('non dovrebbe accettare input non numerico', () => {
state.sp.form.home = ["1", "2", "3", "4", "5", "6"]
const s = applyAction(state, {
type: 'confermaCambi',
team: 'home',
cambi: [{ in: "abc", out: "1" }]
})
expect(s.sp.form.home).toEqual(["1", "2", "3", "4", "5", "6"])
})
it('non dovrebbe accettare in == out', () => {
const s = applyAction(state, {
type: 'confermaCambi',
team: 'home',
cambi: [{ in: "1", out: "1" }]
})
expect(s.sp.form.home).toEqual(["1", "2", "3", "4", "5", "6"])
})
it('non dovrebbe accettare giocatore IN già in formazione', () => {
const s = applyAction(state, {
type: 'confermaCambi',
team: 'home',
cambi: [{ in: "2", out: "1" }]
})
expect(s.sp.form.home).toEqual(["1", "2", "3", "4", "5", "6"])
})
it('non dovrebbe accettare giocatore OUT non in formazione', () => {
const s = applyAction(state, {
type: 'confermaCambi',
team: 'home',
cambi: [{ in: "10", out: "99" }]
})
expect(s.sp.form.home).toEqual(["1", "2", "3", "4", "5", "6"])
})
it('dovrebbe saltare cambi con campo vuoto', () => {
const s = applyAction(state, {
type: 'confermaCambi',
team: 'home',
cambi: [
{ in: "", out: "" },
{ in: "10", out: "1" }
]
})
expect(s.sp.form.home).toContain("10")
})
it('dovrebbe mantenere la posizione del giocatore sostituito', () => {
state.sp.form.home = ["1", "2", "3", "4", "5", "6"]
const s = applyAction(state, {
type: 'confermaCambi',
team: 'home',
cambi: [{ in: "10", out: "3" }]
})
expect(s.sp.form.home[2]).toBe("10")
})
it('dovrebbe gestire cambi sequenziali che dipendono l\'uno dall\'altro', () => {
// Sostituisci 1→10, poi 10→20 (il secondo dipende dal risultato del primo)
state.sp.form.home = ["1", "2", "3", "4", "5", "6"]
const s = applyAction(state, {
type: 'confermaCambi',
team: 'home',
cambi: [
{ in: "10", out: "1" },
{ in: "20", out: "10" }
]
})
expect(s.sp.form.home).toContain("20")
expect(s.sp.form.home).not.toContain("1")
expect(s.sp.form.home).not.toContain("10")
})
})
// =============================================
// VITTORIA SET (checkVittoria)
// =============================================
describe('checkVittoria', () => {
it('non dovrebbe dare vittoria a 24-24', () => { it('non dovrebbe dare vittoria a 24-24', () => {
state.sp.punt.home = 24 state.sp.punt.home = 24
state.sp.punt.guest = 24 state.sp.punt.guest = 24
@@ -64,28 +478,182 @@ describe('Game Logic (gameState.js)', () => {
expect(checkVittoria(state)).toBe(true) expect(checkVittoria(state)).toBe(true)
}) })
it('dovrebbe richiedere 2 punti di scarto (26-24)', () => { it('non dovrebbe dare vittoria a 25-24 (serve 2 punti di scarto)', () => {
state.sp.punt.home = 25 state.sp.punt.home = 25
state.sp.punt.guest = 24 state.sp.punt.guest = 24
expect(checkVittoria(state)).toBe(false) expect(checkVittoria(state)).toBe(false)
})
it('dovrebbe dare vittoria a 26-24', () => {
state.sp.punt.home = 26 state.sp.punt.home = 26
state.sp.punt.guest = 24
expect(checkVittoria(state)).toBe(true) expect(checkVittoria(state)).toBe(true)
}) })
it('dovrebbe dare vittoria Guest a 25-20', () => {
state.sp.punt.home = 20
state.sp.punt.guest = 25
expect(checkVittoria(state)).toBe(true)
})
it('dovrebbe dare vittoria ai vantaggi (30-28)', () => {
state.sp.punt.home = 30
state.sp.punt.guest = 28
expect(checkVittoria(state)).toBe(true)
})
it('non dovrebbe dare vittoria ai vantaggi senza scarto (28-27)', () => {
state.sp.punt.home = 28
state.sp.punt.guest = 27
expect(checkVittoria(state)).toBe(false)
})
}) })
describe('Reset', () => { // =============================================
it('dovrebbe resettare tutto a zero', () => { // SET DECISIVO (15 punti)
state.sp.punt.home = 10 // =============================================
describe('Set decisivo', () => {
it('modalità 3/5: set decisivo dopo 4 set totali → vittoria a 15', () => {
state.modalitaPartita = "3/5"
state.sp.set.home = 2
state.sp.set.guest = 2
state.sp.punt.home = 15
state.sp.punt.guest = 10
expect(checkVittoria(state)).toBe(true)
})
it('modalità 3/5: non vittoria a 14-10 nel set decisivo', () => {
state.modalitaPartita = "3/5"
state.sp.set.home = 2
state.sp.set.guest = 2
state.sp.punt.home = 14
state.sp.punt.guest = 10
expect(checkVittoria(state)).toBe(false)
})
it('modalità 3/5: vittoria a 15-13 nel set decisivo', () => {
state.modalitaPartita = "3/5"
state.sp.set.home = 2
state.sp.set.guest = 2
state.sp.punt.home = 15
state.sp.punt.guest = 13
expect(checkVittoria(state)).toBe(true)
})
it('modalità 3/5: non vittoria a 15-14 nel set decisivo (serve scarto)', () => {
state.modalitaPartita = "3/5"
state.sp.set.home = 2
state.sp.set.guest = 2
state.sp.punt.home = 15
state.sp.punt.guest = 14
expect(checkVittoria(state)).toBe(false)
})
it('modalità 3/5: vittoria a 16-14 nel set decisivo', () => {
state.modalitaPartita = "3/5"
state.sp.set.home = 2
state.sp.set.guest = 2
state.sp.punt.home = 16
state.sp.punt.guest = 14
expect(checkVittoria(state)).toBe(true)
})
it('modalità 2/3: set decisivo dopo 2 set totali → vittoria a 15', () => {
state.modalitaPartita = "2/3"
state.sp.set.home = 1 state.sp.set.home = 1
state.sp.set.guest = 1
state.sp.punt.home = 15
state.sp.punt.guest = 10
expect(checkVittoria(state)).toBe(true)
})
const newState = applyAction(state, { type: 'resetta' }) it('modalità 2/3: non vittoria a 24-20 nel set decisivo (soglia 15)', () => {
state.modalitaPartita = "2/3"
state.sp.set.home = 1
state.sp.set.guest = 1
state.sp.punt.home = 14
state.sp.punt.guest = 10
expect(checkVittoria(state)).toBe(false)
})
expect(newState.sp.punt.home).toBe(0) it('modalità 2/3: set non decisivo (1-0) → soglia 25', () => {
expect(newState.sp.set.home).toBe(0) // Nota: il reset attuale resetta solo i punti o tutto? state.modalitaPartita = "2/3"
// Controllo il codice: "s.sp.punt.home = 0... s.sp.storicoServizio = []" state.sp.set.home = 1
// Attenzione: nel codice originale `resetta` NON sembra resettare i set! state.sp.set.guest = 0
// Verifichiamo il comportamento attuale del codice. state.sp.punt.home = 15
state.sp.punt.guest = 10
expect(checkVittoria(state)).toBe(false)
})
it('modalità 3/5: set non decisivo (2-1) → soglia 25', () => {
state.modalitaPartita = "3/5"
state.sp.set.home = 2
state.sp.set.guest = 1
state.sp.punt.home = 15
state.sp.punt.guest = 10
expect(checkVittoria(state)).toBe(false)
})
})
// =============================================
// RESET
// =============================================
describe('Reset', () => {
it('dovrebbe resettare punti e set a zero', () => {
state.sp.punt.home = 10
state.sp.punt.guest = 8
state.sp.set.home = 1
state.sp.set.guest = 1
const s = applyAction(state, { type: 'resetta' })
expect(s.sp.punt.home).toBe(0)
expect(s.sp.punt.guest).toBe(0)
expect(s.sp.set.home).toBe(0)
expect(s.sp.set.guest).toBe(0)
})
it('dovrebbe resettare formazioni a default', () => {
state.sp.form.home = ["10", "11", "12", "13", "14", "15"]
const s = applyAction(state, { type: 'resetta' })
expect(s.sp.form.home).toEqual(["1", "2", "3", "4", "5", "6"])
expect(s.sp.form.guest).toEqual(["1", "2", "3", "4", "5", "6"])
})
it('dovrebbe resettare la striscia', () => {
state.sp.striscia = { home: [0, 1, 2, 3], guest: [0, " ", " ", 1] }
const s = applyAction(state, { type: 'resetta' })
expect(s.sp.striscia.home).toEqual([0])
expect(s.sp.striscia.guest).toEqual([0])
})
it('dovrebbe resettare lo storico servizio', () => {
state.sp.storicoServizio = [{ servHome: true, cambioPalla: false }]
const s = applyAction(state, { type: 'resetta' })
expect(s.sp.storicoServizio).toEqual([])
})
it('dovrebbe impostare visuForm a false', () => {
state.visuForm = true
const s = applyAction(state, { type: 'resetta' })
expect(s.visuForm).toBe(false)
})
it('dovrebbe mantenere nomi e modalità', () => {
state.sp.nomi.home = "Squadra A"
state.modalitaPartita = "2/3"
const s = applyAction(state, { type: 'resetta' })
expect(s.sp.nomi.home).toBe("Squadra A")
expect(s.modalitaPartita).toBe("2/3")
})
})
// =============================================
// AZIONE SCONOSCIUTA
// =============================================
describe('Azione sconosciuta', () => {
it('dovrebbe restituire lo stato invariato con azione non riconosciuta', () => {
const s = applyAction(state, { type: 'azioneInesistente' })
expect(s.sp.punt.home).toBe(0)
expect(s.sp.punt.guest).toBe(0)
}) })
}) })
}) })

View File

@@ -1,22 +1,148 @@
import { describe, it, expect } from 'vitest' import { describe, it, expect, vi, afterEach } from 'vitest'
import { printServerInfo } from '../../src/server-utils.js' import * as os from 'os'
// Mocking console.log per evitare output sporchi durante i test vi.mock('os', async (importOriginal) => {
import { vi } from 'vitest' return {
...await importOriginal(),
networkInterfaces: vi.fn(() => ({}))
}
})
import { getNetworkIPs, printServerInfo } from '../../src/server-utils.js'
describe('Server Utils', () => { describe('Server Utils', () => {
it('printServerInfo dovrebbe stampare le porte corrette', () => {
const consoleSpy = vi.spyOn(console, 'log')
printServerInfo(3000, 3001)
expect(consoleSpy).toHaveBeenCalled() afterEach(() => {
vi.restoreAllMocks()
})
// Unisce tutti i messaggi loggati in un'unica stringa per facilitare la ricerca // =============================================
const allLogs = consoleSpy.mock.calls.map(args => args[0]).join('\n') // getNetworkIPs
// =============================================
describe('getNetworkIPs', () => {
it('dovrebbe restituire indirizzi IPv4 non-loopback', () => {
os.networkInterfaces.mockReturnValue({
eth0: [
{ family: 'IPv4', internal: false, address: '192.168.1.100' }
]
})
expect(getNetworkIPs()).toEqual(['192.168.1.100'])
})
expect(allLogs).toContain('3000') it('dovrebbe escludere indirizzi loopback (internal)', () => {
expect(allLogs).toContain('3001') os.networkInterfaces.mockReturnValue({
lo: [
{ family: 'IPv4', internal: true, address: '127.0.0.1' }
],
eth0: [
{ family: 'IPv4', internal: false, address: '192.168.1.100' }
]
})
const ips = getNetworkIPs()
expect(ips).not.toContain('127.0.0.1')
expect(ips).toContain('192.168.1.100')
})
consoleSpy.mockRestore() it('dovrebbe escludere indirizzi IPv6', () => {
os.networkInterfaces.mockReturnValue({
eth0: [
{ family: 'IPv6', internal: false, address: 'fe80::1' },
{ family: 'IPv4', internal: false, address: '192.168.1.100' }
]
})
const ips = getNetworkIPs()
expect(ips).toEqual(['192.168.1.100'])
})
it('dovrebbe escludere bridge Docker 172.17.x.x', () => {
os.networkInterfaces.mockReturnValue({
docker0: [
{ family: 'IPv4', internal: false, address: '172.17.0.1' }
],
eth0: [
{ family: 'IPv4', internal: false, address: '10.0.0.5' }
]
})
const ips = getNetworkIPs()
expect(ips).not.toContain('172.17.0.1')
expect(ips).toContain('10.0.0.5')
})
it('dovrebbe escludere bridge Docker 172.18.x.x', () => {
os.networkInterfaces.mockReturnValue({
br0: [
{ family: 'IPv4', internal: false, address: '172.18.0.1' }
]
})
expect(getNetworkIPs()).toEqual([])
})
it('dovrebbe restituire array vuoto se nessuna interfaccia disponibile', () => {
os.networkInterfaces.mockReturnValue({})
expect(getNetworkIPs()).toEqual([])
})
it('dovrebbe restituire più indirizzi da interfacce diverse', () => {
os.networkInterfaces.mockReturnValue({
eth0: [
{ family: 'IPv4', internal: false, address: '192.168.1.100' }
],
wlan0: [
{ family: 'IPv4', internal: false, address: '192.168.1.101' }
]
})
const ips = getNetworkIPs()
expect(ips).toHaveLength(2)
expect(ips).toContain('192.168.1.100')
expect(ips).toContain('192.168.1.101')
})
})
// =============================================
// printServerInfo
// =============================================
describe('printServerInfo', () => {
it('dovrebbe stampare le porte corrette (default)', () => {
os.networkInterfaces.mockReturnValue({})
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
printServerInfo()
const allLogs = consoleSpy.mock.calls.map(c => c[0]).join('\n')
expect(allLogs).toContain('5173')
expect(allLogs).toContain('3001')
consoleSpy.mockRestore()
})
it('dovrebbe stampare le porte personalizzate', () => {
os.networkInterfaces.mockReturnValue({})
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
printServerInfo(3000, 4000)
const allLogs = consoleSpy.mock.calls.map(c => c[0]).join('\n')
expect(allLogs).toContain('3000')
expect(allLogs).toContain('4000')
consoleSpy.mockRestore()
})
it('dovrebbe mostrare gli URL remoti se ci sono IP di rete', () => {
os.networkInterfaces.mockReturnValue({
eth0: [
{ family: 'IPv4', internal: false, address: '192.168.1.50' }
]
})
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
printServerInfo(3000, 3001)
const allLogs = consoleSpy.mock.calls.map(c => c[0]).join('\n')
expect(allLogs).toContain('192.168.1.50')
expect(allLogs).toContain('remoti')
consoleSpy.mockRestore()
})
it('non dovrebbe mostrare sezione remoti se nessun IP di rete', () => {
os.networkInterfaces.mockReturnValue({})
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
printServerInfo(3000, 3001)
const allLogs = consoleSpy.mock.calls.map(c => c[0]).join('\n')
expect(allLogs).not.toContain('remoti')
consoleSpy.mockRestore()
})
}) })
}) })

View File

@@ -1,9 +1,19 @@
import { defineConfig } from 'vitest/config' import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
export default defineConfig({ export default defineConfig({
plugins: [vue()],
test: { test: {
include: ['tests/unit/**/*.{test,spec}.js', 'tests/integration/**/*.{test,spec}.js'], include: [
globals: true, // permette di usare describe/it/expect senza import 'tests/unit/**/*.{test,spec}.js',
environment: 'node', // per backend tests. Se testi componenti Vue, usa 'jsdom' 'tests/integration/**/*.{test,spec}.js',
'tests/component/**/*.{test,spec}.js',
'tests/stress/**/*.{test,spec}.js',
],
globals: true,
environment: 'node',
environmentMatchGlobs: [
['tests/component/**', 'happy-dom'],
],
}, },
}) })