diff --git a/static/css/daw.css b/static/css/daw.css index d015e4a0..ec2127f1 100644 --- a/static/css/daw.css +++ b/static/css/daw.css @@ -5677,12 +5677,36 @@ input, textarea { font-family: inherit; } .rail-guide[aria-pressed="true"] { color: var(--accent); background: var(--panel); } /* Only the control being explained is marked, so the screen stays calm; the pill at the bottom says help mode is on. */ -.help-mode .help-target { cursor: help; } +/* With no song open, the studio turns several controls off for the pointer + (Export Mix, the song facts, the presence cards). Help mode still has to hear + the pointer over them to explain them, and it takes every click itself, so + nothing can be pressed by accident. */ +.help-mode .help-target { cursor: help; pointer-events: auto !important; } .help-ring { position: fixed; z-index: 9998; pointer-events: none; border: 2px solid var(--accent); border-radius: var(--radius-sm); box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 16%, transparent); } +/* The glow when Guide turns on: every explainable control at once, fading in + and out over --intro-ms (set from INTRO_MS in helpMode.js). With reduced + motion it simply shows, still, for the same time. */ +.help-intro { position: fixed; inset: 0; z-index: 9997; pointer-events: none; } +.help-intro-ring { + position: absolute; + border: 2px solid var(--accent); border-radius: var(--radius-sm); + box-shadow: 0 0 10px 2px color-mix(in srgb, var(--accent) 35%, transparent); + opacity: 0; + animation: help-intro-glow var(--intro-ms, 1600ms) ease-in-out forwards; +} +@keyframes help-intro-glow { + 0% { opacity: 0; } + 25% { opacity: 1; } + 65% { opacity: 1; } + 100% { opacity: 0; } +} +@media (prefers-reduced-motion: reduce) { + .help-intro-ring { animation: none; opacity: 0.8; } +} .help-pill { position: fixed; z-index: 9999; left: 50%; bottom: 18px; transform: translateX(-50%); max-width: calc(100vw - 32px); diff --git a/static/js/helpMode.js b/static/js/helpMode.js index 767b07e8..7749d4fe 100644 --- a/static/js/helpMode.js +++ b/static/js/helpMode.js @@ -12,6 +12,9 @@ import { t } from "./i18n.js"; /** Lane controls name their instrument in the text ("Silences the Drums"). */ const laneName = (el) => el.closest(".mx-row, .lane-header")?.querySelector(".mx-name")?.textContent.trim() || ""; const chipName = (el) => el.textContent.trim(); +/** The instrument a presence card measures, by its own name ("Drums"), not + * the card's heading ("Drum intensity"), so the sentence reads naturally. */ +const presenceName = (el) => t(`stem.${el.dataset.stem}`); // Order matters only where selectors overlap: the first match wins. const HELP = [ @@ -51,7 +54,13 @@ const HELP = [ [".mx-btn.mute", "help.mute", laneName], [".mx-btn.solo", "help.solo", laneName], [".lane-dl", "help.download", laneName], - ["#transport .daw-info-row", "help.songFacts", null, "help.songFactsTitle"], + ['.daw-meta-card[data-meta="key"]', "help.meta.key"], + ['.daw-meta-card[data-meta="bpm"]', "help.meta.bpm"], + ['.daw-meta-card[data-meta="lufs"]', "help.meta.lufs"], + ['.daw-meta-card[data-meta="duration"]', "help.meta.duration"], + ['.daw-meta-card[data-meta="dr"]', "help.meta.dr"], + ['.daw-meta-card[data-meta="stability"]', "help.meta.stability"], + [".stem-presence-panel .stem-card[data-stem]", "help.presence", presenceName], ["#ruler-time", "help.waves", null, "help.wavesTitle"], [".waves-column", "help.waves", null, "help.wavesTitle"], ["#t-stop", "help.stop"], @@ -108,6 +117,11 @@ let on = false; let tipEl = null; let pillEl = null; let ringEl = null; +let introEl = null; +let introTimer = null; +// How long the glow on every explainable control lasts when Guide turns on. +// daw.css reads it through --intro-ms, so the fade and the cleanup agree. +const INTRO_MS = 1600; let current = null; export function isHelpModeOn() { @@ -249,6 +263,40 @@ function markTargets() { } } +/** + * When Guide turns on, every control it can explain glows once and fades, so + * the user sees where to point without the screen staying busy. One ring per + * control, drawn the same way as the pointer's ring, so no panel clips them. + * Large areas (the waveforms, the song facts) are left out: a glowing box + * across half the screen says nothing about where to point. + */ +function playIntro() { + stopIntro(); + const vw = window.innerWidth, vh = window.innerHeight; + const big = vw * vh * 0.04; + for (const el of document.querySelectorAll(".help-target")) { + const r = visibleRect(el); + if (r.width * r.height > big || r.bottom < 0 || r.top > vh || r.right < 0 || r.left > vw) continue; + const ring = document.createElement("div"); + ring.className = "help-intro-ring"; + ring.style.left = `${Math.round(r.left)}px`; + ring.style.top = `${Math.round(r.top)}px`; + ring.style.width = `${Math.round(r.width)}px`; + ring.style.height = `${Math.round(r.height)}px`; + introEl.appendChild(ring); + } + introEl.hidden = false; + introTimer = setTimeout(stopIntro, INTRO_MS); +} + +function stopIntro() { + clearTimeout(introTimer); + introTimer = null; + if (!introEl) return; + introEl.hidden = true; + introEl.replaceChildren(); +} + function onPointerOver(e) { const entry = findEntry(e.target); if (entry) show(entry); @@ -302,6 +350,7 @@ export function setHelpMode(next) { window.addEventListener("resize", hide); document.addEventListener("scroll", hide, true); markTargets(); + playIntro(); } else { document.removeEventListener("pointerover", onPointerOver, true); for (const type of PRESS_EVENTS) document.removeEventListener(type, swallow, true); @@ -310,6 +359,7 @@ export function setHelpMode(next) { window.removeEventListener("resize", hide); document.removeEventListener("scroll", hide, true); hide(); + stopIntro(); markTargets(); } } @@ -331,6 +381,11 @@ export function initHelpMode() { ringEl.className = "help-ring"; ringEl.setAttribute("aria-hidden", "true"); ringEl.hidden = true; - document.body.append(ringEl, tipEl, pillEl); + introEl = document.createElement("div"); + introEl.className = "help-intro"; + introEl.setAttribute("aria-hidden", "true"); + introEl.style.setProperty("--intro-ms", `${INTRO_MS}ms`); + introEl.hidden = true; + document.body.append(introEl, ringEl, tipEl, pillEl); btn.addEventListener("click", () => setHelpMode(!on)); } diff --git a/static/js/i18n.js b/static/js/i18n.js index d6e9f29b..d66b0a56 100644 --- a/static/js/i18n.js +++ b/static/js/i18n.js @@ -481,8 +481,13 @@ const en = { "help.mute": "Silences {name}, so you can play or sing that part yourself.", "help.solo": "Plays only {name}, to hear it clearly.", "help.download": "Saves {name} as its own audio file.", - "help.songFactsTitle": "Song facts", - "help.songFacts": "Found for you: the key, the tempo in BPM, loudness, length and how much of each instrument the song has.", + "help.meta.key": "The song's key, found by listening to it. The small percentage says how sure that guess is.", + "help.meta.bpm": "The tempo, in beats per minute. The click track follows it.", + "help.meta.lufs": "How loud the song is on average, in LUFS. Closer to zero is louder; most streaming services play songs at about -14.", + "help.meta.duration": "How long the song is.", + "help.meta.dr": "The gap between the loudest moments and the average level, in dB. A higher number means more contrast between quiet and loud parts.", + "help.meta.stability": "How steady the tempo is. 100% means it never drifts; a lower number means it speeds up or slows down.", + "help.presence": "How loud the {name} part is next to the loudest instrument in the song. 100% is the loudest; a dash means it is not in the song.", "help.wavesTitle": "Waveforms", "help.waves": "Each instrument's sound, drawn over time. Click to jump to that moment. Drag across to loop that part.", "help.stop": "Stops the song.", @@ -1371,8 +1376,13 @@ const pl = { "help.mute": "Wycisza {name}, żebyś mógł sam zagrać lub zaśpiewać tę partię.", "help.solo": "Odtwarza tylko {name}, żeby było wyraźnie słychać.", "help.download": "Zapisuje {name} jako osobny plik audio.", - "help.songFactsTitle": "Dane utworu", - "help.songFacts": "Wyznaczone dla ciebie: tonacja, tempo w BPM, głośność, długość i udział każdego instrumentu.", + "help.meta.key": "Tonacja utworu, rozpoznana ze słuchu. Mały procent mówi, jak pewne jest to rozpoznanie.", + "help.meta.bpm": "Tempo w uderzeniach na minutę. Klik podąża za nim.", + "help.meta.lufs": "Średnia głośność utworu w LUFS. Bliżej zera znaczy głośniej; większość serwisów streamingowych odtwarza utwory na poziomie około -14.", + "help.meta.duration": "Długość utworu.", + "help.meta.dr": "Różnica między najgłośniejszymi momentami a średnim poziomem, w dB. Wyższa liczba oznacza większy kontrast między cichymi i głośnymi fragmentami.", + "help.meta.stability": "Jak równe jest tempo. 100% oznacza, że się nie zmienia; niższa liczba oznacza, że przyspiesza lub zwalnia.", + "help.presence": "Jak głośne jest {name} w porównaniu z najgłośniejszym instrumentem w utworze. 100% to najgłośniejszy; kreska oznacza, że go nie ma.", "help.wavesTitle": "Przebiegi", "help.waves": "Dźwięk każdego instrumentu w czasie. Kliknij, aby przejść do tego momentu. Przeciągnij, aby zapętlić ten fragment.", "help.stop": "Zatrzymuje utwór.", @@ -2251,8 +2261,13 @@ const ja = { "help.mute": "{name}をミュートします。そのパートを自分で演奏したり歌ったりできます。", "help.solo": "{name}だけを再生して、はっきり聞けるようにします。", "help.download": "{name}を単独の音声ファイルとして保存します。", - "help.songFactsTitle": "曲の情報", - "help.songFacts": "キー、BPM で表したテンポ、ラウドネス、長さ、各楽器の割合を自動で調べています。", + "help.meta.key": "曲を聴いて判定したキーです。小さなパーセントは、その判定の確かさです。", + "help.meta.bpm": "1 分あたりの拍数で表したテンポです。クリックはこれに合わせます。", + "help.meta.lufs": "曲の平均的な音量(LUFS)です。0 に近いほど大きく、多くの配信サービスは約 -14 で再生します。", + "help.meta.duration": "曲の長さです。", + "help.meta.dr": "最も大きい瞬間と平均レベルの差(dB)です。数値が大きいほど、静かな部分と大きな部分の差がはっきりしています。", + "help.meta.stability": "テンポの安定度です。100% はまったく揺れないこと、数値が低いほど速くなったり遅くなったりします。", + "help.presence": "曲の中で最も大きい楽器と比べた{name}の音量です。100% が最大、ダッシュはその楽器が入っていないことを示します。", "help.wavesTitle": "波形", "help.waves": "各楽器の音を時間に沿って描いたものです。クリックでその位置へ移動し、ドラッグでその部分をループします。", "help.stop": "曲を止めます。", @@ -3106,8 +3121,13 @@ const zhHans = { "help.mute": "将{name}静音,让你自己演奏或演唱这一部分。", "help.solo": "只播放{name},听得更清楚。", "help.download": "把{name}保存为单独的音频文件。", - "help.songFactsTitle": "歌曲信息", - "help.songFacts": "已为你分析:调性、以 BPM 表示的速度、响度、时长,以及每种乐器的占比。", + "help.meta.key": "通过聆听判断出的歌曲调性。小百分比表示这个判断有多可靠。", + "help.meta.bpm": "速度,以每分钟拍数表示。节拍声跟随它。", + "help.meta.lufs": "歌曲的平均响度,单位 LUFS。越接近零越响;大多数流媒体服务以约 -14 播放歌曲。", + "help.meta.duration": "歌曲的时长。", + "help.meta.dr": "最响时刻与平均电平之间的差值,单位 dB。数字越大,安静和响亮部分的对比越明显。", + "help.meta.stability": "速度有多稳定。100% 表示完全不漂移;数字越低,表示时快时慢。", + "help.presence": "{name}与歌曲中最响的乐器相比有多响。100% 为最响;横线表示歌曲中没有它。", "help.wavesTitle": "波形", "help.waves": "每种乐器的声音随时间的变化。点击跳到那一刻,拖动可循环那一段。", "help.stop": "停止播放。", @@ -3961,8 +3981,13 @@ const de = { "help.mute": "Schaltet {name} stumm, damit du diesen Part selbst spielen oder singen kannst.", "help.solo": "Spielt nur {name}, damit du es deutlich hörst.", "help.download": "Speichert {name} als eigene Audiodatei.", - "help.songFactsTitle": "Songdaten", - "help.songFacts": "Für dich ermittelt: Tonart, Tempo in BPM, Lautheit, Länge und wie viel von jedem Instrument der Song hat.", + "help.meta.key": "Die Tonart des Songs, durch Hören ermittelt. Die kleine Prozentzahl sagt, wie sicher diese Einschätzung ist.", + "help.meta.bpm": "Das Tempo in Schlägen pro Minute. Der Klick folgt ihm.", + "help.meta.lufs": "Wie laut der Song im Durchschnitt ist, in LUFS. Näher an null ist lauter; die meisten Streamingdienste spielen Songs bei etwa -14.", + "help.meta.duration": "Wie lang der Song ist.", + "help.meta.dr": "Der Abstand zwischen den lautesten Momenten und dem Durchschnittspegel, in dB. Eine höhere Zahl heißt mehr Kontrast zwischen leisen und lauten Teilen.", + "help.meta.stability": "Wie gleichmäßig das Tempo ist. 100 % heißt, es schwankt nie; eine niedrigere Zahl heißt, es wird schneller oder langsamer.", + "help.presence": "Wie laut {name} im Vergleich zum lautesten Instrument im Song ist. 100 % ist das lauteste; ein Strich heißt, es kommt nicht vor.", "help.wavesTitle": "Wellenformen", "help.waves": "Der Klang jedes Instruments über die Zeit. Klick, um an diese Stelle zu springen. Zieh darüber, um diesen Teil zu loopen.", "help.stop": "Stoppt den Song.", @@ -4827,8 +4852,13 @@ const pt = { "help.mute": "Silencia {name}, para você tocar ou cantar essa parte.", "help.solo": "Toca só {name}, para ouvir com clareza.", "help.download": "Salva {name} como um arquivo de áudio separado.", - "help.songFactsTitle": "Dados da música", - "help.songFacts": "Encontrados para você: o tom, o andamento em BPM, o volume, a duração e quanto há de cada instrumento.", + "help.meta.key": "O tom da música, encontrado ao ouvi-la. A pequena porcentagem diz o quão certo é esse palpite.", + "help.meta.bpm": "O andamento, em batidas por minuto. O clique segue ele.", + "help.meta.lufs": "O volume médio da música, em LUFS. Mais perto de zero é mais alto; a maioria dos serviços de streaming toca músicas em cerca de -14.", + "help.meta.duration": "A duração da música.", + "help.meta.dr": "A diferença entre os momentos mais altos e o nível médio, em dB. Um número maior significa mais contraste entre partes baixas e altas.", + "help.meta.stability": "O quão estável é o andamento. 100% significa que nunca varia; um número menor significa que acelera ou desacelera.", + "help.presence": "O quão alto {name} está em relação ao instrumento mais alto da música. 100% é o mais alto; um traço significa que não está na música.", "help.wavesTitle": "Formas de onda", "help.waves": "O som de cada instrumento ao longo do tempo. Clique para pular para esse momento. Arraste para repetir esse trecho em loop.", "help.stop": "Para a música.", @@ -5695,8 +5725,13 @@ const id = { "help.mute": "Membisukan {name}, agar Anda bisa memainkan atau menyanyikan bagian itu sendiri.", "help.solo": "Hanya memutar {name}, agar terdengar jelas.", "help.download": "Menyimpan {name} sebagai file audio tersendiri.", - "help.songFactsTitle": "Data lagu", - "help.songFacts": "Ditemukan untuk Anda: kunci nada, tempo dalam BPM, kekerasan suara, durasi, dan seberapa banyak tiap instrumen.", + "help.meta.key": "Kunci nada lagu, ditemukan dengan mendengarkannya. Persentase kecil menunjukkan seberapa yakin tebakan itu.", + "help.meta.bpm": "Tempo, dalam ketukan per menit. Ketukan klik mengikutinya.", + "help.meta.lufs": "Seberapa keras lagu secara rata-rata, dalam LUFS. Lebih dekat ke nol berarti lebih keras; kebanyakan layanan streaming memutar lagu di sekitar -14.", + "help.meta.duration": "Panjang lagu.", + "help.meta.dr": "Selisih antara momen paling keras dan tingkat rata-rata, dalam dB. Angka lebih tinggi berarti kontras lebih besar antara bagian pelan dan keras.", + "help.meta.stability": "Seberapa stabil temponya. 100% berarti tidak pernah bergeser; angka lebih rendah berarti tempo cepat atau melambat.", + "help.presence": "Seberapa keras {name} dibanding instrumen paling keras di lagu. 100% adalah yang paling keras; tanda strip berarti tidak ada di lagu.", "help.wavesTitle": "Bentuk gelombang", "help.waves": "Suara tiap instrumen dari waktu ke waktu. Klik untuk melompat ke saat itu. Seret untuk mengulang bagian itu.", "help.stop": "Menghentikan lagu.", @@ -6550,8 +6585,13 @@ const fr = { "help.mute": "Coupe {name}, pour que vous puissiez jouer ou chanter cette partie vous-même.", "help.solo": "Joue seulement {name}, pour l'entendre clairement.", "help.download": "Enregistre {name} comme fichier audio séparé.", - "help.songFactsTitle": "Infos du morceau", - "help.songFacts": "Trouvés pour vous : la tonalité, le tempo en BPM, le volume, la durée et la présence de chaque instrument.", + "help.meta.key": "La tonalité du morceau, trouvée à l'écoute. Le petit pourcentage indique la certitude de cette estimation.", + "help.meta.bpm": "Le tempo, en battements par minute. Le clic le suit.", + "help.meta.lufs": "Le volume moyen du morceau, en LUFS. Plus proche de zéro, c'est plus fort ; la plupart des plateformes de streaming diffusent vers -14.", + "help.meta.duration": "La durée du morceau.", + "help.meta.dr": "L'écart entre les moments les plus forts et le niveau moyen, en dB. Un nombre plus élevé signifie plus de contraste entre passages doux et forts.", + "help.meta.stability": "La régularité du tempo. 100 % signifie qu'il ne varie jamais ; un nombre plus bas signifie qu'il accélère ou ralentit.", + "help.presence": "Le volume de {name} par rapport à l'instrument le plus fort du morceau. 100 % est le plus fort ; un tiret signifie qu'il n'est pas dans le morceau.", "help.wavesTitle": "Formes d'onde", "help.waves": "Le son de chaque instrument dans le temps. Cliquez pour aller à ce moment. Faites glisser pour boucler ce passage.", "help.stop": "Arrête le morceau.", @@ -7590,8 +7630,13 @@ const es = { "help.mute": "Silencia {name}, para que puedas tocar o cantar esa parte tú.", "help.solo": "Reproduce solo {name}, para oírlo con claridad.", "help.download": "Guarda {name} como un archivo de audio aparte.", - "help.songFactsTitle": "Datos de la canción", - "help.songFacts": "Calculados para ti: la tonalidad, el tempo en BPM, el volumen, la duración y cuánto hay de cada instrumento.", + "help.meta.key": "La tonalidad de la canción, encontrada al escucharla. El pequeño porcentaje indica cuán segura es esa estimación.", + "help.meta.bpm": "El tempo, en pulsos por minuto. El clic lo sigue.", + "help.meta.lufs": "El volumen medio de la canción, en LUFS. Más cerca de cero es más fuerte; la mayoría de servicios de streaming reproducen canciones a unos -14.", + "help.meta.duration": "La duración de la canción.", + "help.meta.dr": "La diferencia entre los momentos más fuertes y el nivel medio, en dB. Un número mayor significa más contraste entre partes suaves y fuertes.", + "help.meta.stability": "Qué tan estable es el tempo. 100% significa que nunca varía; un número menor significa que se acelera o se frena.", + "help.presence": "Qué tan fuerte suena {name} frente al instrumento más fuerte de la canción. 100% es el más fuerte; un guion significa que no está en la canción.", "help.wavesTitle": "Formas de onda", "help.waves": "El sonido de cada instrumento a lo largo del tiempo. Haz clic para saltar a ese momento. Arrastra para repetir esa parte en bucle.", "help.stop": "Detiene la canción.", @@ -8479,8 +8524,13 @@ const ko = { "help.mute": "{name} 소리를 꺼서 그 부분을 직접 연주하거나 부를 수 있게 해요.", "help.solo": "{name}만 재생해서 또렷하게 들려줘요.", "help.download": "{name}을(를) 별도의 오디오 파일로 저장해요.", - "help.songFactsTitle": "곡 정보", - "help.songFacts": "자동으로 찾아 줘요: 키, BPM 단위의 템포, 음량, 길이, 그리고 악기별 비중이에요.", + "help.meta.key": "곡을 들어서 찾은 키예요. 작은 퍼센트는 그 판단이 얼마나 확실한지 보여 줘요.", + "help.meta.bpm": "분당 박자 수로 나타낸 템포예요. 클릭이 이를 따라가요.", + "help.meta.lufs": "곡의 평균 음량(LUFS)이에요. 0에 가까울수록 크고, 대부분의 스트리밍 서비스는 약 -14로 재생해요.", + "help.meta.duration": "곡의 길이예요.", + "help.meta.dr": "가장 큰 순간과 평균 레벨의 차이(dB)예요. 숫자가 클수록 조용한 부분과 큰 부분의 대비가 커요.", + "help.meta.stability": "템포가 얼마나 일정한지예요. 100%는 전혀 흔들리지 않는다는 뜻이고, 낮을수록 빨라지거나 느려져요.", + "help.presence": "곡에서 가장 큰 악기와 비교한 {name}의 음량이에요. 100%가 가장 크고, 대시는 곡에 없다는 뜻이에요.", "help.wavesTitle": "파형", "help.waves": "악기마다 소리를 시간에 따라 그린 거예요. 클릭하면 그 순간으로 가고, 끌면 그 부분을 반복해요.", "help.stop": "곡을 멈춰요.", diff --git a/tests/e2e/help-mode.spec.mjs b/tests/e2e/help-mode.spec.mjs index cdf7ac3b..a11ebc4e 100644 --- a/tests/e2e/help-mode.spec.mjs +++ b/tests/e2e/help-mode.spec.mjs @@ -5,7 +5,7 @@ import { test, expect } from "@playwright/test"; -import { openStudio } from "./helpers.mjs"; +import { openStudio, seedLibrary, stubUpdateCheck } from "./helpers.mjs"; const tip = (page) => page.locator("#helpTip"); @@ -29,6 +29,23 @@ test.describe("help mode", () => { await expect(page.locator(".help-pill")).toBeHidden(); }); + test("turning Guide on makes every explainable control glow once, then fade", async ({ page }) => { + await openStudio(page); + await page.locator("#helpModeBtn").click(); + const rings = page.locator(".help-intro-ring"); + expect(await rings.count()).toBeGreaterThan(20); + await expect(page.locator(".help-intro")).toBeVisible(); + // Gone by itself, so the screen is calm again. + await expect(rings).toHaveCount(0, { timeout: 4000 }); + await expect(page.locator(".help-intro")).toBeHidden(); + + // Leaving Guide mid-glow clears it at once. + await page.keyboard.press("Escape"); + await page.locator("#helpModeBtn").click(); + await page.keyboard.press("Escape"); + await expect(rings).toHaveCount(0); + }); + test("pointing at a control explains it, under its own label", async ({ page }) => { await openStudio(page); await helpOn(page); @@ -53,6 +70,31 @@ test.describe("help mode", () => { await expect(tip(page).locator("b")).toHaveText("Key of Drums"); }); + test("each song fact and presence card explains itself", async ({ page }) => { + await openStudio(page); + await helpOn(page); + await page.locator('.daw-meta-card[data-meta="lufs"]').hover(); + await expect(tip(page)).toContainText("streaming services"); + await page.locator('.daw-meta-card[data-meta="stability"]').hover(); + await expect(tip(page)).toContainText("How steady the tempo is"); + await page.locator('.stem-presence-panel .stem-card[data-stem="drums"]').hover(); + await expect(tip(page)).toContainText("How loud the Drums part is"); + }); + + test("with no song open, controls the studio switches off still explain themselves", async ({ page }) => { + await seedLibrary(page); + await stubUpdateCheck(page); + await page.goto("/", { waitUntil: "domcontentloaded" }); + await page.locator(".cat-item").first().waitFor(); + await helpOn(page); + for (const sel of ["#t-export-btn", "#np-details-btn", '.daw-meta-card[data-meta="key"]', '.stem-presence-panel .stem-card[data-stem="vocals"]']) { + const box = await page.locator(sel).first().boundingBox(); + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await expect(tip(page), sel).toBeVisible(); + await expect(page.locator(sel).first(), sel).toHaveClass(/help-current/); + } + }); + test("a click explains instead of acting", async ({ page }) => { await openStudio(page); await helpOn(page);