diff --git a/tests/blockhint.test.js b/tests/blockhint.test.js new file mode 100644 index 00000000..885a1f42 --- /dev/null +++ b/tests/blockhint.test.js @@ -0,0 +1,238 @@ +import { expect } from 'chai'; +import { + areBlockHintsEnabled, + setBlockHintsEnabled, + showBlockHint, + showBlockHintMessage, + clearBlockHint, + hideBlockHint, +} from '../ui/blockHint.js'; + +// blockHint.js reads `enabled` once, at module evaluation, so the mobile +// default can only be observed by re-importing with a fresh module identity. +// The module imports nothing, so a duplicate instance is fully self-contained. +// @vite-ignore keeps Vite's dynamic-import-vars plugin from rejecting the +// varying query string; the browser resolves the URL itself. +async function importWithLayout(matches) { + const original = window.matchMedia; + window.matchMedia = (query) => ({ matches, media: query }); + try { + return await import(/* @vite-ignore */ `../ui/blockHint.js?blockhint-fresh=${Math.random()}`); + } finally { + window.matchMedia = original; + } +} + +export function runBlockHintTests(_flock) { + describe('ui/blockHint @blockhint', function () { + let container; + let text; + let originalEnabled; + + beforeEach(function () { + // tests.html doesn't include the app's #blockHint markup, and every + // exported function early-returns without it — so omitting this would + // make every assertion below pass against a no-op. + container = document.createElement('div'); + container.id = 'blockHint'; + container.hidden = true; + text = document.createElement('p'); + text.id = 'blockHintText'; + container.appendChild(text); + document.body.appendChild(container); + + originalEnabled = areBlockHintsEnabled(); + setBlockHintsEnabled(true); + showBlockHint(''); // lastHintText has no accessor; this is the only reset + }); + + afterEach(function () { + showBlockHint(''); + setBlockHintsEnabled(originalEnabled); + container.remove(); + }); + + describe('showBlockHint', function () { + it('reveals the container and renders the text', function () { + showBlockHint('Move the object'); + expect(container.hidden).to.be.false; + expect(text.textContent).to.equal('Move the object'); + }); + + it("strips the trailing 'Keyword: x' search term", function () { + showBlockHint('Move the object.\nKeyword: move'); + expect(text.textContent).to.equal('Move the object.'); + }); + + it('trims surrounding whitespace', function () { + showBlockHint(' Move the object '); + expect(text.textContent).to.equal('Move the object'); + }); + + it('hides the container and clears content for empty text', function () { + showBlockHint('Move the object'); + showBlockHint(''); + expect(container.hidden).to.be.true; + expect(text.textContent).to.equal(''); + }); + + it('treats undefined as empty rather than rendering "undefined"', function () { + showBlockHint(undefined); + expect(container.hidden).to.be.true; + expect(text.textContent).to.equal(''); + }); + + it('replaces the previous hint rather than appending to it', function () { + showBlockHint('First'); + showBlockHint('Second'); + expect(text.textContent).to.equal('Second'); + }); + + it('renders as a text node, never as markup', function () { + showBlockHint('bold'); + expect(text.querySelector('b')).to.be.null; + expect(text.textContent).to.equal('bold'); + }); + }); + + describe('enabled toggle', function () { + it('areBlockHintsEnabled reflects setBlockHintsEnabled', function () { + setBlockHintsEnabled(false); + expect(areBlockHintsEnabled()).to.be.false; + setBlockHintsEnabled(true); + expect(areBlockHintsEnabled()).to.be.true; + }); + + it('does not display a hint while disabled', function () { + setBlockHintsEnabled(false); + showBlockHint('Move the object'); + expect(container.hidden).to.be.true; + }); + + it('hides an already-visible hint when disabled', function () { + showBlockHint('Move the object'); + expect(container.hidden).to.be.false; + setBlockHintsEnabled(false); + expect(container.hidden).to.be.true; + }); + + it('restores the last hint when re-enabled', function () { + showBlockHint('Move the object'); + setBlockHintsEnabled(false); + setBlockHintsEnabled(true); + expect(container.hidden).to.be.false; + expect(text.textContent).to.equal('Move the object'); + }); + + it('remembers hints shown while disabled and renders them on re-enable', function () { + setBlockHintsEnabled(false); + showBlockHint('Move the object'); + setBlockHintsEnabled(true); + expect(text.textContent).to.equal('Move the object'); + }); + }); + + describe('clearBlockHint / hideBlockHint', function () { + it('clearBlockHint hides the container', function () { + showBlockHint('Move the object'); + clearBlockHint(); + expect(container.hidden).to.be.true; + }); + + it('clearBlockHint forgets the text, so re-enabling shows nothing', function () { + showBlockHint('Move the object'); + clearBlockHint(); + setBlockHintsEnabled(false); + setBlockHintsEnabled(true); + expect(container.hidden).to.be.true; + expect(text.textContent).to.equal(''); + }); + + it('hideBlockHint hides without forgetting the text', function () { + showBlockHint('Move the object'); + hideBlockHint(); + expect(container.hidden).to.be.true; + setBlockHintsEnabled(false); + setBlockHintsEnabled(true); + expect(text.textContent).to.equal('Move the object'); + }); + + it('hideBlockHint is not sticky — the next hint shows again', function () { + showBlockHint('First'); + hideBlockHint(); + showBlockHint('Second'); + expect(container.hidden).to.be.false; + expect(text.textContent).to.equal('Second'); + }); + }); + + describe('showBlockHintMessage', function () { + it('renders plain text when no boldPart is given', function () { + showBlockHintMessage('Tips live in the Tools menu'); + expect(container.hidden).to.be.false; + expect(text.textContent).to.equal('Tips live in the Tools menu'); + expect(text.querySelector('strong')).to.be.null; + }); + + it('wraps boldPart in and keeps the surrounding text', function () { + showBlockHintMessage('ℹ️ Show hints: in the Tools menu', { boldPart: 'Show hints' }); + const strong = text.querySelector('strong'); + expect(strong).to.exist; + expect(strong.textContent).to.equal('Show hints'); + expect(text.textContent).to.equal('ℹ️ Show hints: in the Tools menu'); + }); + + it('falls back to plain text when boldPart is absent from the message', function () { + showBlockHintMessage('Tips live in the Tools menu', { boldPart: 'Hide hints' }); + expect(text.querySelector('strong')).to.be.null; + expect(text.textContent).to.equal('Tips live in the Tools menu'); + }); + + it('hides the container for empty text', function () { + showBlockHintMessage('Something'); + showBlockHintMessage(''); + expect(container.hidden).to.be.true; + expect(text.textContent).to.equal(''); + }); + + it('shows even while hints are disabled (that is when the tip matters)', function () { + setBlockHintsEnabled(false); + showBlockHintMessage('Show hints: in the Tools menu', { boldPart: 'Show hints' }); + expect(container.hidden).to.be.false; + expect(text.textContent).to.equal('Show hints: in the Tools menu'); + }); + + it("does not strip 'Keyword:' — unlike showBlockHint, messages are not tooltips", function () { + showBlockHintMessage('A message.\nKeyword: move'); + expect(text.textContent).to.equal('A message.\nKeyword: move'); + }); + + it('renders boldPart as text, never as markup', function () { + showBlockHintMessage('before x after', { boldPart: 'x' }); + expect(text.querySelector('b')).to.be.null; + expect(text.querySelector('strong').textContent).to.equal('x'); + }); + + it('does not become the remembered hint — toggling hints on replaces it', function () { + showBlockHintMessage('Show hints: in the Tools menu', { boldPart: 'Show hints' }); + expect(container.hidden).to.be.false; + setBlockHintsEnabled(false); + setBlockHintsEnabled(true); + expect(container.hidden).to.be.true; + expect(text.textContent).to.equal(''); + }); + }); + + describe('mobile layout default', function () { + it('defaults to disabled at the mobile breakpoint', async function () { + const mod = await importWithLayout(true); + expect(mod.areBlockHintsEnabled()).to.be.false; + }); + + it('defaults to enabled above the mobile breakpoint', async function () { + const mod = await importWithLayout(false); + expect(mod.areBlockHintsEnabled()).to.be.true; + }); + }); + }); +} diff --git a/tests/tests.html b/tests/tests.html index 82288646..e1dc38a4 100644 --- a/tests/tests.html +++ b/tests/tests.html @@ -244,6 +244,13 @@

Flock Test Example

importFn: 'runGizmoMobileHudTests', pattern: 'ui/gizmo-mobile-hud', }, + { + id: 'blockhint', + name: 'Block Hint Tests', + importPath: './blockhint.test.js', + importFn: 'runBlockHintTests', + pattern: 'ui/blockHint', + }, { id: 'contextmenu', name: 'Context Menu Tests',