openvibers/openvibe.live Commits
Found by the browser check on /vod/… and /clip/… (Host scripts/browser-check.js, WS-Q task 3): the VOD, clip and channel players' volume sliders are named ("Volume"; axe label, critical), and a guest's page no longer asks /api/coins/channel-balance, which answers 401 to a guest (a console error on every channel, VOD and clip page): the navbar points chip and the rewards panel ask only when signed in. test/player-a11y.test.js
Changes
@@@ -36,7 +36,7 @@<button class="vc-btn" id="btn-volume" title="Mute/Unmute"><i class="fa-solid fa-volume-high"></i></button>-- <input type="range" id="volume-slider" min="0" max="100" value="75" class="volume-slider">++ <input type="range" id="volume-slider" min="0" max="100" value="75" class="volume-slider" aria-label="Volume"></div><span class="dvr-time-display" id="dvr-time-display" style="display:none">0:00 / 0:00</span><div class="vc-spacer"></div>@@@ -16,7 +16,7 @@<button class="vc-btn" id="clp-btn-play" title="Play/Pause"><i class="fa-solid fa-play"></i></button><div class="vc-volume-group"><button class="vc-btn" id="clp-btn-vol" title="Mute"><i class="fa-solid fa-volume-high"></i></button>-- <input type="range" id="clp-vol-slider" min="0" max="100" value="100" class="volume-slider">++ <input type="range" id="clp-vol-slider" min="0" max="100" value="100" class="volume-slider" aria-label="Volume"></div><span class="vc-time-display" id="clp-time">0:00 / 0:00</span><div class="vc-spacer"></div>@@@ -25,7 +25,7 @@<button class="vc-btn" id="vp-btn-play" title="Play/Pause"><i class="fa-solid fa-play"></i></button><div class="vc-volume-group"><button class="vc-btn" id="vp-btn-vol" title="Mute"><i class="fa-solid fa-volume-high"></i></button>-- <input type="range" id="vp-vol-slider" min="0" max="100" value="100" class="volume-slider">++ <input type="range" id="vp-vol-slider" min="0" max="100" value="100" class="volume-slider" aria-label="Volume"></div><span class="vc-time-display" id="vp-time">0:00 / 0:00</span><div class="vc-spacer"></div>@@@ -791,7 +791,8 @@ async function loadBalance() {let _navPointsStreamerId = null;async function updateChannelPointsNav(streamerId) {_navPointsStreamerId = (streamerId && (!currentUser || String(streamerId) !== String(currentUser.id))) ? streamerId : null;-- if (!_navPointsStreamerId) return;++ // A guest has no points to show, and the API answers 401 (a console error on every channel, VOD and clip page).++ if (!_navPointsStreamerId || !currentUser) return;try {const d = await api(`/coins/channel-balance?streamerId=${_navPointsStreamerId}`);const bal = (d.balance || 0).toLocaleString();@@@ -154,8 +154,8 @@ async function loadRewardsPanel() {grids.forEach(g => { g.innerHTML = '<p class="muted" style="padding:8px">Failed to load rewards</p>'; });}-- // Update this channel's points balance in all reward panels-- try {++ // Update this channel's points balance in all reward panels (signed in only: a guest's request answers 401).++ if (typeof currentUser !== 'undefined' && currentUser) try {const coinData = await api(`/coins/channel-balance?streamerId=${ownerId}`);document.querySelectorAll('.rewards-coin-balance').forEach(el => {el.textContent = (coinData.balance || 0).toLocaleString();nnew file mode 100644@@@ -0,0 +1,23 @@++'use strict';++// Found by the browser check (OpenVibe.Host scripts/browser-check.js, WS-Q task 3) on /vod/… and /clip/…:++// - the players' volume sliders had no accessible name (axe "label", critical); the channel player's too;++// - a guest's page asked /api/coins/channel-balance, which answers 401 to a guest: a console error on every++// channel, VOD and clip page. Only a signed-in viewer asks now.++const assert = require('assert');++const fs = require('fs');++const path = require('path');++++const pub = (f) => fs.readFileSync(path.join(__dirname, '..', 'public', f), 'utf8');++for (const f of ['fragments/vod-player.html', 'fragments/clip-player.html', 'fragments/channel.html']) {++ const html = pub(f);++ const ranges = html.match(/<input[^>]*type="range"[^>]*>/g) || [];++ assert.ok(ranges.length, `${f} has a volume slider`);++ for (const r of ranges) {++ const id = (r.match(/id="([^"]+)"/) || [])[1];++ const labelled = /aria-label="[^"]+"/.test(r) || /aria-labelledby="[^"]+"/.test(r) || (id && new RegExp(`<label[^>]*for="${id}"`).test(html));++ assert.ok(labelled, `${f}: ${r} has an accessible name`);++ }++}++assert.match(pub('js/app.js'), /if \(!_navPointsStreamerId \|\| !currentUser\) return;/, 'the navbar points chip asks only when signed in');++assert.match(pub('js/opencoins.js'), /if \(typeof currentUser !== 'undefined' && currentUser\) try \{\n\s*const coinData = await api\(`\/coins\/channel-balance/, 'the rewards panel asks only when signed in');++console.log('player a11y: sliders named; guests never ask for a channel balance');
@@ -36,7 +36,7 @@
@@ -36,7 +36,7 @@
<button class="vc-btn" id="btn-volume" title="Mute/Unmute">
<button class="vc-btn" id="btn-volume" title="Mute/Unmute">
<i class="fa-solid fa-volume-high"></i>
<i class="fa-solid fa-volume-high"></i>
</button>
</button>
- <input type="range" id="volume-slider" min="0" max="100" value="75" class="volume-slider">
+ <input type="range" id="volume-slider" min="0" max="100" value="75" class="volume-slider" aria-label="Volume">
</div>
</div>
<span class="dvr-time-display" id="dvr-time-display" style="display:none">0:00 / 0:00</span>
<span class="dvr-time-display" id="dvr-time-display" style="display:none">0:00 / 0:00</span>
<div class="vc-spacer"></div>
<div class="vc-spacer"></div>
@@ -16,7 +16,7 @@
@@ -16,7 +16,7 @@
<button class="vc-btn" id="clp-btn-play" title="Play/Pause"><i class="fa-solid fa-play"></i></button>
<button class="vc-btn" id="clp-btn-play" title="Play/Pause"><i class="fa-solid fa-play"></i></button>
<div class="vc-volume-group">
<div class="vc-volume-group">
<button class="vc-btn" id="clp-btn-vol" title="Mute"><i class="fa-solid fa-volume-high"></i></button>
<button class="vc-btn" id="clp-btn-vol" title="Mute"><i class="fa-solid fa-volume-high"></i></button>
- <input type="range" id="clp-vol-slider" min="0" max="100" value="100" class="volume-slider">
+ <input type="range" id="clp-vol-slider" min="0" max="100" value="100" class="volume-slider" aria-label="Volume">
</div>
</div>
<span class="vc-time-display" id="clp-time">0:00 / 0:00</span>
<span class="vc-time-display" id="clp-time">0:00 / 0:00</span>
<div class="vc-spacer"></div>
<div class="vc-spacer"></div>
@@ -25,7 +25,7 @@
@@ -25,7 +25,7 @@
<button class="vc-btn" id="vp-btn-play" title="Play/Pause"><i class="fa-solid fa-play"></i></button>
<button class="vc-btn" id="vp-btn-play" title="Play/Pause"><i class="fa-solid fa-play"></i></button>
<div class="vc-volume-group">
<div class="vc-volume-group">
<button class="vc-btn" id="vp-btn-vol" title="Mute"><i class="fa-solid fa-volume-high"></i></button>
<button class="vc-btn" id="vp-btn-vol" title="Mute"><i class="fa-solid fa-volume-high"></i></button>
- <input type="range" id="vp-vol-slider" min="0" max="100" value="100" class="volume-slider">
+ <input type="range" id="vp-vol-slider" min="0" max="100" value="100" class="volume-slider" aria-label="Volume">
</div>
</div>
<span class="vc-time-display" id="vp-time">0:00 / 0:00</span>
<span class="vc-time-display" id="vp-time">0:00 / 0:00</span>
<div class="vc-spacer"></div>
<div class="vc-spacer"></div>
@@ -791,7 +791,8 @@ async function loadBalance() {
@@ -791,7 +791,8 @@ async function loadBalance() {
let _navPointsStreamerId = null;
let _navPointsStreamerId = null;
async function updateChannelPointsNav(streamerId) {
async function updateChannelPointsNav(streamerId) {
_navPointsStreamerId = (streamerId && (!currentUser || String(streamerId) !== String(currentUser.id))) ? streamerId : null;
_navPointsStreamerId = (streamerId && (!currentUser || String(streamerId) !== String(currentUser.id))) ? streamerId : null;
- if (!_navPointsStreamerId) return;
+ // A guest has no points to show, and the API answers 401 (a console error on every channel, VOD and clip page).
+ if (!_navPointsStreamerId || !currentUser) return;
try {
try {
const d = await api(`/coins/channel-balance?streamerId=${_navPointsStreamerId}`);
const d = await api(`/coins/channel-balance?streamerId=${_navPointsStreamerId}`);
const bal = (d.balance || 0).toLocaleString();
const bal = (d.balance || 0).toLocaleString();
@@ -154,8 +154,8 @@ async function loadRewardsPanel() {
@@ -154,8 +154,8 @@ async function loadRewardsPanel() {
grids.forEach(g => { g.innerHTML = '<p class="muted" style="padding:8px">Failed to load rewards</p>'; });
grids.forEach(g => { g.innerHTML = '<p class="muted" style="padding:8px">Failed to load rewards</p>'; });
}
}
- // Update this channel's points balance in all reward panels
- try {
+ // Update this channel's points balance in all reward panels (signed in only: a guest's request answers 401).
+ if (typeof currentUser !== 'undefined' && currentUser) try {
const coinData = await api(`/coins/channel-balance?streamerId=${ownerId}`);
const coinData = await api(`/coins/channel-balance?streamerId=${ownerId}`);
document.querySelectorAll('.rewards-coin-balance').forEach(el => {
document.querySelectorAll('.rewards-coin-balance').forEach(el => {
el.textContent = (coinData.balance || 0).toLocaleString();
el.textContent = (coinData.balance || 0).toLocaleString();
new file mode 100644
new file mode 100644
@@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
+'use strict';
+// Found by the browser check (OpenVibe.Host scripts/browser-check.js, WS-Q task 3) on /vod/… and /clip/…:
+// - the players' volume sliders had no accessible name (axe "label", critical); the channel player's too;
+// - a guest's page asked /api/coins/channel-balance, which answers 401 to a guest: a console error on every
+// channel, VOD and clip page. Only a signed-in viewer asks now.
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+
+const pub = (f) => fs.readFileSync(path.join(__dirname, '..', 'public', f), 'utf8');
+for (const f of ['fragments/vod-player.html', 'fragments/clip-player.html', 'fragments/channel.html']) {
+ const html = pub(f);
+ const ranges = html.match(/<input[^>]*type="range"[^>]*>/g) || [];
+ assert.ok(ranges.length, `${f} has a volume slider`);
+ for (const r of ranges) {
+ const id = (r.match(/id="([^"]+)"/) || [])[1];
+ const labelled = /aria-label="[^"]+"/.test(r) || /aria-labelledby="[^"]+"/.test(r) || (id && new RegExp(`<label[^>]*for="${id}"`).test(html));
+ assert.ok(labelled, `${f}: ${r} has an accessible name`);
+ }
+}
+assert.match(pub('js/app.js'), /if \(!_navPointsStreamerId \|\| !currentUser\) return;/, 'the navbar points chip asks only when signed in');
+assert.match(pub('js/opencoins.js'), /if \(typeof currentUser !== 'undefined' && currentUser\) try \{\n\s*const coinData = await api\(`\/coins\/channel-balance/, 'the rewards panel asks only when signed in');
+console.log('player a11y: sliders named; guests never ask for a channel balance');