Spilmenu på /host/: kort pr. spil og intro med speaker
/host/ er nu en menu bygget af registret (menu.js): et kort pr. spil med
navn, tagline, ikon, antal spillere og ca. varighed. Vælges et spil, kommer
introen med trinene, spillets egne indstillinger (Fup og Fakta's pakker inkl.
arbejde og fest, Tegn og Gæts regler til/fra), Start og Tilbage.
- Fjernbetjening og tastatur: pilene flytter fokus til det nærmeste valgbare
i den retning, Enter/OK vælger, Escape/Backspace/TV'ets Tilbage og
browserens tilbage går til menuen. Alt er knapper, også indstillingerne,
så Enter altid gælder det fokuserede (fejlen fra 64aac36 kan ikke komme
igen ad den vej). Mus og touch virker som før.
- Speakeren læser introen op: /host/intro/<spil> giver lyden til den faste
tekst (navn, tagline og trin) med standardstemmen, {} uden stemme.
- Uden JavaScript: én startknap pr. spil fra registret.
- Den gamle pakke-formular og dens CSS er væk.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
/* Spilmenuen på storskærmen (/host/): et kort pr. spil fra registret (party/games.py), og en intro med trin,
|
||||
spillets egne indstillinger, Start og Tilbage. Fjernbetjening/tastatur: pilene flytter fokus mellem det, man
|
||||
kan vælge, Enter/OK vælger, Tilbage/Escape går til menuen. Mus og touch virker som altid.
|
||||
Eksporterer window.WPPMenu = {render, mount}. */
|
||||
(function () {
|
||||
'use strict';
|
||||
var WPP = window.WPP, esc = WPP.esc;
|
||||
|
||||
function facts(g) {
|
||||
var players = g.min_players === g.max_players ? g.min_players : g.min_players + '-' + g.max_players;
|
||||
return '<span class="mn-fact">👥 ' + players + ' spillere</span><span class="mn-fact">⏱ ca. ' + esc(g.minutes) + ' min</span>';
|
||||
}
|
||||
|
||||
function menuScreen(games) {
|
||||
return '<header class="mn-head" data-k="mh"><h1>Weirsøe Party Protocol</h1><p>Vælg et spil</p></header>' +
|
||||
'<section class="mn-cards" data-k="mc">' + games.map(function (g, i) {
|
||||
return '<button type="button" class="mn-card" data-nav data-pick="' + esc(g.key) + '" style="--i:' + i + '">' +
|
||||
'<img src="' + esc(g.icon) + '" alt="" width="120" height="120">' +
|
||||
'<span class="mn-name">' + WPP.brandHtml(g.name) + '</span>' +
|
||||
'<span class="mn-tag">' + esc(g.tagline) + '</span>' +
|
||||
'<span class="mn-facts">' + facts(g) + '</span></button>';
|
||||
}).join('') + '</section>';
|
||||
}
|
||||
|
||||
function introScreen(g, values) {
|
||||
var sets = (g.settings || []).map(function (s) {
|
||||
var cur = values[s.name] != null ? values[s.name] : s.default;
|
||||
var hint = s.options.filter(function (o) { return o.value === cur; })[0];
|
||||
return '<div class="mn-set" data-k="set:' + esc(s.name + ':' + cur) + '"><span class="mn-set-l">' + esc(s.label) + '</span><div class="mn-opts">' +
|
||||
s.options.map(function (o) {
|
||||
return '<button type="button" class="mn-opt" data-nav data-set="' + esc(s.name) + '" data-val="' + esc(o.value) + '" aria-pressed="' +
|
||||
(o.value === cur ? 'true' : 'false') + '">' + esc(o.label) + '</button>';
|
||||
}).join('') + '</div>' + (hint && hint.hint ? '<p class="mn-hint">' + esc(hint.hint) + '</p>' : '') + '</div>';
|
||||
}).join('');
|
||||
/* nøgler pr. del: et skift af indstilling tegner kun den række igen (ingen ny indgangsanimation) */
|
||||
return '<section class="mn-intro" data-k="mi:' + esc(g.key) + '" data-patch>' +
|
||||
'<div class="mn-intro-top" data-k="top"><img src="' + esc(g.icon) + '" alt="" width="140" height="140">' +
|
||||
'<div><h1 class="mn-iname">' + WPP.brandHtml(g.name) + '</h1><p class="mn-tag">' + esc(g.tagline) + '</p>' +
|
||||
'<p class="mn-facts">' + facts(g) + '</p></div></div>' +
|
||||
'<ol class="mn-steps" data-k="steps">' + g.steps.map(function (s, i) { return '<li style="--i:' + i + '">' + esc(s) + '</li>'; }).join('') + '</ol>' +
|
||||
(sets ? '<div class="mn-sets" data-k="sets" data-patch>' + sets + '</div>' : '') +
|
||||
'<div class="mn-actions" data-k="act"><button type="button" class="btn mn-back" data-nav data-back>Tilbage</button>' +
|
||||
'<button type="button" class="btn btn-go btn-xl mn-start" data-nav data-start>Start ' + esc(g.name) + '</button></div></section>';
|
||||
}
|
||||
|
||||
/* view = {screen: 'menu'} eller {screen: 'intro', key, values} */
|
||||
function render(root, view) {
|
||||
var games = WPP.games, g = view.screen === 'intro' && WPP.game(view.key);
|
||||
var sh = WPP.shell(root);
|
||||
sh.screen.className = 'screen mn mn-' + (g ? 'intro' : 'menu');
|
||||
WPP.patch(sh.screen, '<main class="mn-main" data-k="mm:' + (g ? g.key : '') + '" data-patch>' +
|
||||
(g ? introScreen(g, view.values || {}) : menuScreen(games)) + '</main>');
|
||||
}
|
||||
|
||||
/* pilene: nærmeste valgbare element i den retning (målt fra midten), så både kort i et gitter og knapper
|
||||
i rækker virker uden at vide, hvordan skærmen er bygget */
|
||||
function move(root, key) {
|
||||
var items = Array.prototype.slice.call(root.querySelectorAll('[data-nav]'));
|
||||
if (!items.length) return;
|
||||
var cur = document.activeElement;
|
||||
if (items.indexOf(cur) < 0) { items[0].focus(); return; }
|
||||
var r = cur.getBoundingClientRect(), cx = r.left + r.width / 2, cy = r.top + r.height / 2;
|
||||
var dir = { ArrowLeft: [-1, 0], ArrowRight: [1, 0], ArrowUp: [0, -1], ArrowDown: [0, 1] }[key];
|
||||
var best = null, bestScore = Infinity;
|
||||
items.forEach(function (el) {
|
||||
if (el === cur) return;
|
||||
var b = el.getBoundingClientRect(), dx = b.left + b.width / 2 - cx, dy = b.top + b.height / 2 - cy;
|
||||
var along = dx * dir[0] + dy * dir[1], across = Math.abs(dx * dir[1] - dy * dir[0]);
|
||||
if (along <= 1) return;
|
||||
var score = along + across * 2;
|
||||
if (score < bestScore) { bestScore = score; best = el; }
|
||||
});
|
||||
if (best) best.focus();
|
||||
}
|
||||
|
||||
var BACK_KEYS = { Escape: 1, Backspace: 1, GoBack: 1, BrowserBack: 1 };
|
||||
var BACK_CODES = { 461: 1, 10009: 1 }; /* fjernbetjeningens Tilbage på nogle TV'er */
|
||||
|
||||
function mount(root) {
|
||||
var form = document.getElementById('mn-form');
|
||||
var view = { screen: 'menu' };
|
||||
|
||||
function focusFirst(sel) {
|
||||
var el = root.querySelector(sel) || root.querySelector('[data-nav]');
|
||||
if (el) el.focus();
|
||||
}
|
||||
function speak(key) {
|
||||
WPP.voice.stop();
|
||||
if (!window.fetch) return;
|
||||
fetch('/host/intro/' + encodeURIComponent(key), { credentials: 'same-origin' })
|
||||
.then(function (r) { return r.ok ? r.json() : {}; })
|
||||
.then(function (a) { if (a && a.url && view.screen === 'intro' && view.key === key) WPP.voice.now([a.url]); })
|
||||
.catch(function () { /* uden stemme */ });
|
||||
}
|
||||
function show(next, push) {
|
||||
var from = view;
|
||||
view = next;
|
||||
render(root, view);
|
||||
if (view.screen === 'intro') {
|
||||
if (push) try { history.pushState({ intro: view.key }, '', '#' + view.key); } catch (e) { /* */ }
|
||||
if (from.screen !== 'intro' || from.key !== view.key) speak(view.key);
|
||||
focusFirst(view.focusSet ? '[data-set="' + view.focusSet + '"][aria-pressed=true]' : '[data-start]');
|
||||
} else {
|
||||
WPP.voice.stop();
|
||||
focusFirst(from.key ? '[data-pick="' + from.key + '"]' : null);
|
||||
}
|
||||
}
|
||||
function back() {
|
||||
if (view.screen !== 'intro') return;
|
||||
if (history.state && history.state.intro) history.back(); else show({ screen: 'menu' });
|
||||
}
|
||||
function start() {
|
||||
var g = WPP.game(view.key);
|
||||
if (!g || !form) return;
|
||||
WPP.voice.stop();
|
||||
form.querySelector('[name=game]').value = g.key;
|
||||
Array.prototype.forEach.call(form.querySelectorAll('[data-setting]'), function (el) { el.parentNode.removeChild(el); });
|
||||
(g.settings || []).forEach(function (s) {
|
||||
var inp = document.createElement('input');
|
||||
inp.type = 'hidden'; inp.name = s.name; inp.setAttribute('data-setting', '');
|
||||
inp.value = view.values[s.name] != null ? view.values[s.name] : s.default;
|
||||
form.appendChild(inp);
|
||||
});
|
||||
root.classList.add('mn-busy');
|
||||
form.submit();
|
||||
}
|
||||
|
||||
root.addEventListener('click', function (e) {
|
||||
var b = e.target.closest('button');
|
||||
if (!b || !root.contains(b)) return;
|
||||
if (b.hasAttribute('data-pick')) show({ screen: 'intro', key: b.getAttribute('data-pick'), values: {} }, true);
|
||||
else if (b.hasAttribute('data-back')) back();
|
||||
else if (b.hasAttribute('data-start')) start();
|
||||
else if (b.hasAttribute('data-set')) {
|
||||
var values = Object.assign({}, view.values);
|
||||
values[b.getAttribute('data-set')] = b.getAttribute('data-val');
|
||||
show({ screen: 'intro', key: view.key, values: values, focusSet: b.getAttribute('data-set') });
|
||||
}
|
||||
});
|
||||
window.addEventListener('keydown', function (e) {
|
||||
if (e.key && e.key.indexOf('Arrow') === 0) { e.preventDefault(); move(root, e.key); return; }
|
||||
if (view.screen === 'intro' && (BACK_KEYS[e.key] || BACK_CODES[e.keyCode])) { e.preventDefault(); back(); }
|
||||
});
|
||||
window.addEventListener('popstate', function (e) {
|
||||
var key = (e.state && e.state.intro) || (location.hash || '').slice(1);
|
||||
if (WPP.game(key)) show({ screen: 'intro', key: key, values: {} });
|
||||
else show({ screen: 'menu' });
|
||||
});
|
||||
/* bfcache: kommer man tilbage fra lobbyen, må siden ikke stå som "starter…" */
|
||||
window.addEventListener('pageshow', function () { root.classList.remove('mn-busy'); });
|
||||
|
||||
var key = (location.hash || '').slice(1);
|
||||
if (WPP.game(key)) {
|
||||
try { history.replaceState({ intro: key }, '', '#' + key); } catch (e) { /* */ }
|
||||
show({ screen: 'intro', key: key, values: {} });
|
||||
} else show({ screen: 'menu' });
|
||||
}
|
||||
|
||||
window.WPPMenu = { render: render, mount: mount };
|
||||
if (!window.WPP_HARNESS) {
|
||||
var go = function () { var r = document.getElementById('menu'); if (r) mount(r); };
|
||||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', go); else go();
|
||||
}
|
||||
})();
|
||||
@@ -900,21 +900,8 @@ button, input { font-family: inherit; }
|
||||
.rl-row b { display: block; }
|
||||
.rl-row span { font-size: .95rem; color: var(--muted); }
|
||||
|
||||
/* ---------- nyt spil: vælg spørgsmålspakke ---------- */
|
||||
.new-game { display: flex; flex-direction: column; align-items: center; gap: 20px; }
|
||||
.new-game .games { display: flex; gap: 16px; flex-wrap: wrap; justify-content: center; }
|
||||
.packs { display: flex; gap: 8px; margin: 0; padding: 0; border: 0; flex-wrap: wrap; justify-content: center; }
|
||||
.packs legend { width: 100%; margin-bottom: 8px; color: var(--muted); font-weight: 600; }
|
||||
.packs label { cursor: pointer; }
|
||||
.packs input { position: absolute; opacity: 0; pointer-events: none; }
|
||||
.packs span { display: inline-block; padding: 10px 20px; border-radius: 999px; font-weight: 700; background: var(--card); box-shadow: inset 0 0 0 2px var(--card-b); }
|
||||
.packs input:checked + span { background: var(--yellow); color: var(--navy); box-shadow: 0 4px 0 var(--yellow-d); }
|
||||
.packs input:focus-visible + span { outline: 3px solid #fff; outline-offset: 2px; }
|
||||
.more-packs { width: 100%; margin-top: 6px; }
|
||||
.more-packs summary { cursor: pointer; color: var(--muted); font-size: .95rem; list-style: none; }
|
||||
.more-packs summary::-webkit-details-marker { display: none; }
|
||||
.more-packs[open] summary { margin-bottom: 8px; }
|
||||
.pack-hint { margin: 8px 0 0; font-size: .9rem; color: var(--muted); }
|
||||
/* ---------- nyt spil uden JavaScript: én startknap pr. spil ---------- */
|
||||
.new-game { display: flex; gap: 16px; flex-wrap: wrap; justify-content: center; }
|
||||
|
||||
/* fest: tår */
|
||||
.rv-res .sips { display: flex; flex-wrap: wrap; justify-content: center; gap: .6em; font-size: 1.35em; margin-top: .4em; animation: rise .4s .7s both; }
|
||||
@@ -1009,3 +996,51 @@ button, input { font-family: inherit; }
|
||||
.tg-end.survived .stamp { color: var(--green); border-color: var(--green); }
|
||||
.tg-end.survived .tg-vsbox { border-color: var(--green); }
|
||||
@media (prefers-reduced-motion: reduce) { .tg-bigicon, .tg-entry.now, .tg-intro, .tg-theme { animation: none; } }
|
||||
|
||||
/* ---------- spilmenuen (/host/): kort pr. spil og intro; fjernbetjening, tastatur og mus ---------- */
|
||||
.mn { min-height: 100vh; min-height: 100dvh; display: flex; align-items: center; justify-content: center; padding: 32px 24px; }
|
||||
.mn-main { width: 100%; max-width: 1400px; }
|
||||
.mn-head { text-align: center; margin-bottom: clamp(20px, 4vh, 48px); }
|
||||
.mn-head h1 { margin: 0; font-size: clamp(2rem, 5vw, 4.4rem); line-height: 1; text-shadow: 0 6px 0 rgba(0, 0, 0, .35); }
|
||||
.mn-head p { margin: 10px 0 0; font-size: clamp(1.2rem, 2.4vw, 2rem); color: var(--muted); font-weight: 600; }
|
||||
.mn-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 380px), 1fr)); gap: clamp(16px, 3vw, 40px); }
|
||||
.mn-card { display: flex; flex-direction: column; align-items: center; gap: 12px; padding: clamp(20px, 3vw, 40px) 24px; border: 0; border-radius: 28px;
|
||||
background: var(--card); box-shadow: inset 0 0 0 3px var(--card-b), 0 10px 30px rgba(0, 0, 0, .35); color: var(--ink); font: inherit; cursor: pointer;
|
||||
transition: transform .15s ease, box-shadow .15s ease, background .15s ease; animation: mn-in .4s ease both; animation-delay: calc(var(--i, 0) * 80ms); }
|
||||
.mn-card img { width: clamp(90px, 12vw, 160px); height: auto; filter: drop-shadow(0 6px 0 rgba(0, 0, 0, .3)); }
|
||||
.mn-name { font-size: clamp(1.8rem, 3.6vw, 3.2rem); font-weight: 700; line-height: 1; }
|
||||
.mn-name .b-og, .mn-iname .b-og { font-size: .6em; }
|
||||
.mn-tag { font-size: clamp(1rem, 1.6vw, 1.4rem); color: var(--muted); text-align: center; margin: 0; }
|
||||
.mn-facts { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; margin: 0; }
|
||||
.mn-fact { padding: 6px 14px; border-radius: 999px; background: var(--faint); font-weight: 600; font-size: clamp(.95rem, 1.3vw, 1.15rem); }
|
||||
.mn-card:hover, .mn-card:focus { background: rgba(255, 255, 255, .16); transform: translateY(-4px) scale(1.02); outline: none;
|
||||
box-shadow: inset 0 0 0 4px var(--yellow), 0 16px 40px rgba(0, 0, 0, .45); }
|
||||
.mn-intro { display: flex; flex-direction: column; gap: clamp(16px, 3vh, 32px); max-width: 1100px; margin: 0 auto; animation: mn-in .35s ease both; }
|
||||
.mn-intro-top { display: flex; gap: 28px; align-items: center; }
|
||||
.mn-intro-top img { width: clamp(90px, 12vw, 160px); height: auto; flex: none; }
|
||||
.mn-iname { margin: 0 0 6px; font-size: clamp(2.2rem, 5vw, 4.4rem); line-height: 1; }
|
||||
.mn-intro .mn-tag { text-align: left; margin-bottom: 10px; }
|
||||
.mn-intro .mn-facts { justify-content: flex-start; }
|
||||
.mn-steps { margin: 0; padding: 0; list-style: none; counter-reset: st; display: grid; gap: 12px; }
|
||||
.mn-steps li { counter-increment: st; display: flex; gap: 16px; align-items: center; padding: 14px 20px; border-radius: 18px; background: var(--card);
|
||||
font-size: clamp(1.1rem, 2vw, 1.7rem); font-weight: 600; animation: mn-in .4s ease both; animation-delay: calc(200ms + var(--i, 0) * 120ms); }
|
||||
.mn-steps li::before { content: counter(st); flex: none; width: 1.8em; height: 1.8em; border-radius: 50%; display: grid; place-items: center;
|
||||
background: var(--yellow); color: var(--navy); font-weight: 700; }
|
||||
.mn-sets { display: grid; gap: 12px; }
|
||||
.mn-set { display: flex; flex-wrap: wrap; gap: 10px 16px; align-items: center; }
|
||||
.mn-set-l { color: var(--muted); font-weight: 600; min-width: 7em; }
|
||||
.mn-opts { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.mn-opt { padding: 10px 20px; border: 0; border-radius: 999px; font: inherit; font-weight: 700; color: var(--ink); background: var(--card);
|
||||
box-shadow: inset 0 0 0 2px var(--card-b); cursor: pointer; }
|
||||
.mn-opt[aria-pressed=true] { background: var(--yellow); color: var(--navy); box-shadow: 0 4px 0 var(--yellow-d); }
|
||||
.mn-opt:focus { outline: 3px solid #fff; outline-offset: 3px; }
|
||||
.mn-hint { width: 100%; margin: 0; color: var(--muted); font-size: .95rem; }
|
||||
.mn-actions { display: flex; gap: 16px; justify-content: flex-end; align-items: center; flex-wrap: wrap; }
|
||||
.mn-actions .btn-xl { width: auto; }
|
||||
.mn-back { background: var(--card); color: var(--ink); box-shadow: inset 0 0 0 2px var(--card-b); }
|
||||
.mn-actions .btn:focus { outline: 4px solid #fff; outline-offset: 4px; }
|
||||
.mn-busy .mn-main { opacity: .6; pointer-events: none; }
|
||||
.mn-who { position: fixed; bottom: 12px; left: 0; right: 0; text-align: center; color: var(--muted); margin: 0; }
|
||||
@keyframes mn-in { from { opacity: 0; transform: translateY(14px); } }
|
||||
@media (max-width: 640px) { .mn-who { position: static; padding-bottom: 12px; } .mn-intro-top { flex-direction: column; text-align: center; } .mn-intro .mn-tag { text-align: center; } .mn-intro .mn-facts { justify-content: center; } }
|
||||
@media (prefers-reduced-motion: reduce) { .mn-card, .mn-intro, .mn-steps li { animation: none; } }
|
||||
|
||||
@@ -1,29 +1,25 @@
|
||||
{% extends "party/base.html" %}
|
||||
{% block title %}Weirsøe Party Protocol{% endblock %}
|
||||
{% load static %}
|
||||
{% block body_class %}page-menu{% endblock %}
|
||||
{% block content %}
|
||||
<main class="landing">
|
||||
<h1>Weirsøe Party Protocol</h1>
|
||||
<div class="sub"><span class="b-fup">Fup</span> <span class="b-og">og</span> <span class="b-fakta">Fakta</span></div>
|
||||
<form method="post" action="/host/new" class="new-game">{% csrf_token %}
|
||||
<fieldset class="packs">
|
||||
<legend>Spørgsmål til Fup og Fakta</legend>
|
||||
<label><input type="radio" name="pack" value="alle" checked><span>Alle</span></label>
|
||||
<label><input type="radio" name="pack" value="familie"><span>Familie</span></label>
|
||||
<label><input type="radio" name="pack" value="nørd"><span>Nørd</span></label>
|
||||
<details class="more-packs">
|
||||
<summary>Flere valg…</summary>
|
||||
<label><input type="radio" name="pack" value="arbejde"><span>{{ work_pack_label }}</span></label>
|
||||
<p class="pack-hint">Arbejdsspørgsmål: e-faktura, IT-sikkerhed og kategorier med pakken "arbejde".</p>
|
||||
<label><input type="radio" name="pack" value="fest"><span>Fest-Z 🥃 18+</span></label>
|
||||
<p class="pack-hint">Slang, memes og dansk pop med zoomer-speaker. Narret = tag en tår, finalen er en shot.</p>
|
||||
</details>
|
||||
</fieldset>
|
||||
<div id="menu"></div>
|
||||
{# menu.js sender denne formular med spillet og dets indstillinger #}
|
||||
<form id="mn-form" method="post" action="/host/new" hidden>{% csrf_token %}<input type="hidden" name="game"></form>
|
||||
<noscript>
|
||||
<main class="landing">
|
||||
<h1>Weirsøe Party Protocol</h1>
|
||||
{# hvert spil er sin egen knap: Enter/OK på en fokuseret radio sender formularen med den afkrydsede, ikke den fokuserede #}
|
||||
<div class="games">
|
||||
<button type="submit" name="game" value="fof" class="btn btn-go">Start Fup og Fakta</button>
|
||||
<button type="submit" name="game" value="tegn" class="btn btn-go">Start Tegn og Gæt ✏️</button>
|
||||
</div>
|
||||
</form>
|
||||
{% if username %}<p class="who">Logget ind som {{ username }}</p>{% endif %}
|
||||
</main>
|
||||
<form method="post" action="/host/new" class="new-game games">{% csrf_token %}
|
||||
{% for g in games %}<button type="submit" name="game" value="{{ g.key }}" class="btn btn-go">Start {{ g.name }}</button>
|
||||
{% endfor %}
|
||||
</form>
|
||||
</main>
|
||||
</noscript>
|
||||
{% if username %}<p class="who mn-who">Logget ind som {{ username }}</p>{% endif %}
|
||||
{% endblock %}
|
||||
{% block scripts %}
|
||||
{{ assets|json_script:"wpp-assets" }}
|
||||
{{ games|json_script:"wpp-games" }}
|
||||
<script src="{% static 'party/common.js' %}" defer></script>
|
||||
<script src="{% static 'party/menu.js' %}" defer></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -112,3 +112,34 @@ class StateNamesTheGameTests(TransactionTestCase):
|
||||
await phone.disconnect()
|
||||
await host.disconnect()
|
||||
room.close()
|
||||
|
||||
|
||||
@override_settings(TRUSTED_PROXIES=["127.0.0.1"], DEV_HOST_USER=None)
|
||||
class MenuViewTests(TestCase):
|
||||
def test_menu_page_has_the_register_and_menu_js(self):
|
||||
page = self.client.get("/host/", **HOST)
|
||||
self.assertEqual(page.status_code, 200)
|
||||
self.assertEqual(page.context["games"], games.menu_data())
|
||||
html = page.content.decode()
|
||||
self.assertIn('id="wpp-games"', html)
|
||||
self.assertIn("party/menu", html)
|
||||
self.assertIn('id="mn-form"', html)
|
||||
|
||||
def test_intro_is_the_fixed_line(self):
|
||||
said = []
|
||||
|
||||
async def fake_synth(text, voice=None):
|
||||
said.append((text, voice))
|
||||
return {"url": "/tts/x.mp3", "ms": 1234}
|
||||
|
||||
with mock.patch("party.tts.synth", side_effect=fake_synth):
|
||||
r = self.client.get("/host/intro/tegn", **HOST)
|
||||
self.assertEqual(r.json(), {"url": "/tts/x.mp3", "ms": 1234})
|
||||
self.assertEqual(said, [(games.GAMES["tegn"].intro(), None)]) # standardstemmen, som tts_warm
|
||||
|
||||
def test_intro_without_voice_unknown_game_and_no_host(self):
|
||||
with mock.patch("party.tts.enabled", return_value=False), \
|
||||
mock.patch("party.tts._fetch", side_effect=AssertionError("netværk")):
|
||||
self.assertEqual(self.client.get("/host/intro/fof", **HOST).json(), {})
|
||||
self.assertEqual(self.client.get("/host/intro/skak", **HOST).status_code, 404)
|
||||
self.assertEqual(self.client.get("/host/intro/fof").status_code, 403)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import asyncio
|
||||
import random
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
from channels.testing.websocket import WebsocketCommunicator
|
||||
@@ -209,6 +210,8 @@ class TegnViewTests(TestCase):
|
||||
afkrydse, og Enter/OK på den fokuserede "Tegn og Gæt" sendte formularen med game=fof."""
|
||||
page = self.client.get("/host/", headers={"x-authentik-username": "agw"}, REMOTE_ADDR="127.0.0.1")
|
||||
html = page.content.decode()
|
||||
self.assertNotRegex(html, r'<input[^>]*name="game"')
|
||||
for key in ("fof", "tegn"):
|
||||
menu = (Path(__file__).resolve().parents[1] / "static" / "party" / "menu.js").read_text(encoding="utf-8")
|
||||
self.assertNotRegex(html + menu, r'radio') # menuen vælger med knapper, også spillets indstillinger
|
||||
self.assertNotRegex(html, r'<input(?![^>]*type="hidden")[^>]*name="game"')
|
||||
for key in ("fof", "tegn"): # uden JavaScript: én startknap pr. spil
|
||||
self.assertRegex(html, rf'<button type="submit" name="game" value="{key}"')
|
||||
|
||||
@@ -8,6 +8,7 @@ urlpatterns = [
|
||||
path("", views.play, name="play"),
|
||||
path("host/", views.host_new_page, name="host_new_page"),
|
||||
path("host/new", views.host_new, name="host_new"),
|
||||
path("host/intro/<str:key>", views.host_intro, name="host_intro"),
|
||||
path("host/<str:code>/", views.host, name="host"),
|
||||
path("host/<str:code>/mic", views.host_mic, name="host_mic"),
|
||||
path("qr/<str:code>.svg", views.qr, name="qr"),
|
||||
|
||||
+12
-2
@@ -37,8 +37,18 @@ def play(request):
|
||||
def host_new_page(request):
|
||||
if not request.host_identity:
|
||||
return _forbidden()
|
||||
return render(request, "party/host_new.html", {"username": request.host_identity.username,
|
||||
"work_pack_label": settings.WORK_PACK_LABEL})
|
||||
return render(request, "party/host_new.html", {"username": request.host_identity.username, **_game_page(None)})
|
||||
|
||||
|
||||
@require_GET
|
||||
async def host_intro(request, key):
|
||||
"""Speakerens intro til et spil i menuen: {"url", "ms"}, eller {} uden stemme."""
|
||||
if not request.host_identity:
|
||||
return _forbidden()
|
||||
game = games.GAMES.get(key)
|
||||
if not game:
|
||||
raise Http404
|
||||
return JsonResponse(await tts.synth(game.intro()) or {})
|
||||
|
||||
|
||||
@require_POST
|
||||
|
||||
Reference in New Issue
Block a user