feat(freeze-ui): freeze/live param toggle interaction
When frozen, param rows dim to indicate frozen state. Tapping a row toggles it to "live" (highlighted, receives ML deltas). Unfreezing clears all visual indicators.
This commit is contained in:
parent
8bb965740e
commit
70da83d6dc
2 changed files with 525 additions and 0 deletions
159
playground/js/shapeseq/freeze-ui.js
Normal file
159
playground/js/shapeseq/freeze-ui.js
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
/**
|
||||||
|
* Freeze UI — adds freeze/live toggle interaction to chain param rows.
|
||||||
|
*
|
||||||
|
* When frozen:
|
||||||
|
* - Param rows are dimmed by default (frozen state)
|
||||||
|
* - Tapping a param row toggles it to "live" (highlighted, receives ML deltas)
|
||||||
|
* - Live params show a secondary indicator (delta range bar) on the slider
|
||||||
|
*
|
||||||
|
* When not frozen:
|
||||||
|
* - All params show normally (no freeze indicators)
|
||||||
|
*
|
||||||
|
* @module shapeseq/freeze-ui
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ── Inject styles once ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
let stylesInjected = false;
|
||||||
|
|
||||||
|
function injectStyles() {
|
||||||
|
if (stylesInjected) return;
|
||||||
|
stylesInjected = true;
|
||||||
|
|
||||||
|
const css = `
|
||||||
|
/* ── Freeze UI states ───────────────────────────────────────── */
|
||||||
|
|
||||||
|
.param-frozen {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-frozen .chain-param-slider {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-live {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-live .chain-param-slider {
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 200, 100, 0.5);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = css;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── FreezeUI ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export class FreezeUI {
|
||||||
|
/**
|
||||||
|
* @param {{ container: HTMLElement, freezeManager: import('./freeze.js').FreezeManager, onToggle: (flatIndex: number) => void }} opts
|
||||||
|
*/
|
||||||
|
constructor({ container, freezeManager, onToggle }) {
|
||||||
|
injectStyles();
|
||||||
|
|
||||||
|
this._container = container;
|
||||||
|
this._fm = freezeManager;
|
||||||
|
this._onToggle = onToggle;
|
||||||
|
this._clickHandler = this._handleClick.bind(this);
|
||||||
|
|
||||||
|
// Delegate click handling on param rows
|
||||||
|
this._container.addEventListener('click', this._clickHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update visual state of all param rows based on freeze state.
|
||||||
|
* Call this after freeze/unfreeze/toggle.
|
||||||
|
*/
|
||||||
|
update() {
|
||||||
|
if (!this._fm.isFrozen) {
|
||||||
|
// Remove all freeze styling
|
||||||
|
this._container.querySelectorAll('.chain-param-row').forEach(row => {
|
||||||
|
row.classList.remove('param-frozen', 'param-live');
|
||||||
|
row.style.opacity = '';
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const liveFlags = this._fm.getLiveFlags();
|
||||||
|
if (!liveFlags) return;
|
||||||
|
|
||||||
|
this._container.querySelectorAll('.chain-param-row').forEach(row => {
|
||||||
|
const idx = parseInt(row.dataset.flatIndex, 10);
|
||||||
|
if (isNaN(idx) || idx >= liveFlags.length) return;
|
||||||
|
|
||||||
|
if (liveFlags[idx]) {
|
||||||
|
row.classList.add('param-live');
|
||||||
|
row.classList.remove('param-frozen');
|
||||||
|
row.style.opacity = '';
|
||||||
|
} else {
|
||||||
|
row.classList.add('param-frozen');
|
||||||
|
row.classList.remove('param-live');
|
||||||
|
row.style.opacity = '0.4';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show delta indicators on live param sliders.
|
||||||
|
* @param {Float32Array} frozenParams
|
||||||
|
* @param {Float32Array} effectiveParams
|
||||||
|
*/
|
||||||
|
updateDeltaIndicators(frozenParams, effectiveParams) {
|
||||||
|
if (!this._fm.isFrozen || !frozenParams || !effectiveParams) return;
|
||||||
|
|
||||||
|
this._container.querySelectorAll('.chain-param-row.param-live').forEach(row => {
|
||||||
|
const idx = parseInt(row.dataset.flatIndex, 10);
|
||||||
|
if (isNaN(idx)) return;
|
||||||
|
|
||||||
|
// Show delta as a secondary mark on the slider
|
||||||
|
let indicator = row.querySelector('.delta-indicator');
|
||||||
|
if (!indicator) {
|
||||||
|
indicator = document.createElement('div');
|
||||||
|
indicator.className = 'delta-indicator';
|
||||||
|
indicator.style.cssText = 'position:absolute;height:4px;background:rgba(0,140,255,0.6);border-radius:2px;bottom:0;pointer-events:none;transition:left 0.1s,width 0.1s;';
|
||||||
|
const slider = row.querySelector('.chain-param-slider');
|
||||||
|
if (slider) {
|
||||||
|
slider.parentElement.style.position = 'relative';
|
||||||
|
slider.parentElement.appendChild(indicator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position indicator between frozen value and effective value
|
||||||
|
const frozen = frozenParams[idx];
|
||||||
|
const effective = effectiveParams[idx];
|
||||||
|
const left = Math.min(frozen, effective) * 100;
|
||||||
|
const width = Math.abs(effective - frozen) * 100;
|
||||||
|
indicator.style.left = left + '%';
|
||||||
|
indicator.style.width = width + '%';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_handleClick(e) {
|
||||||
|
if (!this._fm.isFrozen) return;
|
||||||
|
|
||||||
|
const row = e.target.closest('.chain-param-row');
|
||||||
|
if (!row) return;
|
||||||
|
|
||||||
|
// Don't toggle when interacting with the slider itself
|
||||||
|
if (e.target.closest('.chain-param-slider')) return;
|
||||||
|
|
||||||
|
const idx = parseInt(row.dataset.flatIndex, 10);
|
||||||
|
if (isNaN(idx)) return;
|
||||||
|
|
||||||
|
this._onToggle(idx);
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleanup all DOM listeners.
|
||||||
|
*/
|
||||||
|
destroy() {
|
||||||
|
this._container.removeEventListener('click', this._clickHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
366
playground/js/shapeseq/tests/freeze-ui.test.js
Normal file
366
playground/js/shapeseq/tests/freeze-ui.test.js
Normal file
|
|
@ -0,0 +1,366 @@
|
||||||
|
import { describe, it, beforeEach } from 'node:test';
|
||||||
|
import { strict as assert } from 'node:assert';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Minimal DOM stubs — just enough for FreezeUI's DOM operations
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class StubClassList {
|
||||||
|
constructor() { this._set = new Set(); }
|
||||||
|
add(...names) { names.forEach(n => this._set.add(n)); }
|
||||||
|
remove(...names) { names.forEach(n => this._set.delete(n)); }
|
||||||
|
contains(name) { return this._set.has(name); }
|
||||||
|
}
|
||||||
|
|
||||||
|
class StubStyle {
|
||||||
|
constructor() {
|
||||||
|
this.opacity = '';
|
||||||
|
this.position = '';
|
||||||
|
this.cssText = '';
|
||||||
|
this.left = '';
|
||||||
|
this.width = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StubElement {
|
||||||
|
constructor(tag) {
|
||||||
|
this.tagName = tag.toUpperCase();
|
||||||
|
this._className = '';
|
||||||
|
this.textContent = '';
|
||||||
|
this.type = '';
|
||||||
|
this.dataset = {};
|
||||||
|
this.classList = new StubClassList();
|
||||||
|
this.style = new StubStyle();
|
||||||
|
this.children = [];
|
||||||
|
this.parentElement = null;
|
||||||
|
this._listeners = {};
|
||||||
|
}
|
||||||
|
get className() { return this._className; }
|
||||||
|
set className(val) {
|
||||||
|
this._className = val;
|
||||||
|
// Sync classList with className string
|
||||||
|
this.classList._set.clear();
|
||||||
|
val.split(/\s+/).filter(Boolean).forEach(c => this.classList._set.add(c));
|
||||||
|
}
|
||||||
|
appendChild(child) {
|
||||||
|
child.parentElement = this;
|
||||||
|
this.children.push(child);
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
querySelector(sel) {
|
||||||
|
return this._queryAll(sel)[0] || null;
|
||||||
|
}
|
||||||
|
querySelectorAll(sel) {
|
||||||
|
return this._queryAll(sel);
|
||||||
|
}
|
||||||
|
_queryAll(sel) {
|
||||||
|
const results = [];
|
||||||
|
this._walk(el => {
|
||||||
|
if (matchesSelector(el, sel)) results.push(el);
|
||||||
|
});
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
_walk(fn) {
|
||||||
|
for (const child of this.children) {
|
||||||
|
fn(child);
|
||||||
|
child._walk(fn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
closest(sel) {
|
||||||
|
let cur = this;
|
||||||
|
while (cur) {
|
||||||
|
if (matchesSelector(cur, sel)) return cur;
|
||||||
|
cur = cur.parentElement;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
addEventListener(type, fn) {
|
||||||
|
if (!this._listeners[type]) this._listeners[type] = [];
|
||||||
|
this._listeners[type].push(fn);
|
||||||
|
}
|
||||||
|
removeEventListener(type, fn) {
|
||||||
|
if (!this._listeners[type]) return;
|
||||||
|
this._listeners[type] = this._listeners[type].filter(f => f !== fn);
|
||||||
|
}
|
||||||
|
dispatchEvent(evt) {
|
||||||
|
const handlers = this._listeners[evt.type] || [];
|
||||||
|
for (const h of handlers) h(evt);
|
||||||
|
// Bubble
|
||||||
|
if (evt.bubbles && this.parentElement) {
|
||||||
|
this.parentElement.dispatchEvent(evt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Very basic selector matching: handles .class, .class1.class2, tag selectors
|
||||||
|
*/
|
||||||
|
function matchesSelector(el, sel) {
|
||||||
|
// Handle compound selectors like ".chain-param-row.param-live"
|
||||||
|
const parts = sel.split('.');
|
||||||
|
const tagPart = parts[0]; // empty string if selector starts with '.'
|
||||||
|
const classParts = parts.slice(1);
|
||||||
|
|
||||||
|
if (tagPart && el.tagName !== tagPart.toUpperCase()) return false;
|
||||||
|
for (const cls of classParts) {
|
||||||
|
if (!el.classList.contains(cls)) return false;
|
||||||
|
}
|
||||||
|
// If selector is just a tag with no classes
|
||||||
|
if (classParts.length === 0 && tagPart) {
|
||||||
|
return el.tagName === tagPart.toUpperCase();
|
||||||
|
}
|
||||||
|
return classParts.length > 0 || tagPart !== '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function createElement(tag) {
|
||||||
|
return new StubElement(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stub out globalThis.document so injectStyles() doesn't crash
|
||||||
|
globalThis.document = {
|
||||||
|
createElement,
|
||||||
|
head: { appendChild() {} },
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Minimal FreezeManager stub
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class StubFreezeManager {
|
||||||
|
constructor() {
|
||||||
|
this._frozen = false;
|
||||||
|
this._liveFlags = null;
|
||||||
|
}
|
||||||
|
get isFrozen() { return this._frozen; }
|
||||||
|
freeze(paramCount) {
|
||||||
|
this._frozen = true;
|
||||||
|
this._liveFlags = new Uint8Array(paramCount);
|
||||||
|
}
|
||||||
|
unfreeze() {
|
||||||
|
this._frozen = false;
|
||||||
|
this._liveFlags = null;
|
||||||
|
}
|
||||||
|
getLiveFlags() { return this._liveFlags; }
|
||||||
|
toggleParam(idx) {
|
||||||
|
if (!this._frozen) throw new Error('not frozen');
|
||||||
|
this._liveFlags[idx] = this._liveFlags[idx] ? 0 : 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function buildContainer(paramCount) {
|
||||||
|
const container = createElement('div');
|
||||||
|
for (let i = 0; i < paramCount; i++) {
|
||||||
|
const row = createElement('div');
|
||||||
|
row.classList.add('chain-param-row');
|
||||||
|
row.dataset.flatIndex = String(i);
|
||||||
|
|
||||||
|
const label = createElement('span');
|
||||||
|
label.classList.add('chain-param-label');
|
||||||
|
label.textContent = 'p' + i;
|
||||||
|
row.appendChild(label);
|
||||||
|
|
||||||
|
const slider = createElement('input');
|
||||||
|
slider.type = 'range';
|
||||||
|
slider.classList.add('chain-param-slider');
|
||||||
|
slider.dataset.flatIndex = String(i);
|
||||||
|
row.appendChild(slider);
|
||||||
|
|
||||||
|
container.appendChild(row);
|
||||||
|
}
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeClickEvent(bubbles = true) {
|
||||||
|
return { type: 'click', bubbles, target: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('FreezeUI', () => {
|
||||||
|
let FreezeUI;
|
||||||
|
let freezeUI;
|
||||||
|
let container;
|
||||||
|
let fm;
|
||||||
|
let toggleLog;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
if (!FreezeUI) {
|
||||||
|
const mod = await import('../freeze-ui.js');
|
||||||
|
FreezeUI = mod.FreezeUI;
|
||||||
|
}
|
||||||
|
|
||||||
|
container = buildContainer(4);
|
||||||
|
fm = new StubFreezeManager();
|
||||||
|
toggleLog = [];
|
||||||
|
|
||||||
|
freezeUI = new FreezeUI({
|
||||||
|
container,
|
||||||
|
freezeManager: fm,
|
||||||
|
onToggle: (idx) => toggleLog.push(idx),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Construction ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
it('can be constructed', () => {
|
||||||
|
assert.ok(freezeUI, 'FreezeUI instance should exist');
|
||||||
|
assert.ok(freezeUI._container === container, 'container reference stored');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── update() when frozen ──────────────────────────────────────────
|
||||||
|
|
||||||
|
it('update() adds param-frozen class to all rows when frozen', () => {
|
||||||
|
fm.freeze(4);
|
||||||
|
freezeUI.update();
|
||||||
|
|
||||||
|
const rows = container.querySelectorAll('.chain-param-row');
|
||||||
|
for (const row of rows) {
|
||||||
|
assert.ok(row.classList.contains('param-frozen'),
|
||||||
|
'row ' + row.dataset.flatIndex + ' should have param-frozen');
|
||||||
|
assert.ok(!row.classList.contains('param-live'),
|
||||||
|
'row ' + row.dataset.flatIndex + ' should NOT have param-live');
|
||||||
|
assert.strictEqual(row.style.opacity, '0.4',
|
||||||
|
'frozen row should be dimmed');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('update() adds param-live class for live params when frozen', () => {
|
||||||
|
fm.freeze(4);
|
||||||
|
fm.toggleParam(1);
|
||||||
|
fm.toggleParam(3);
|
||||||
|
freezeUI.update();
|
||||||
|
|
||||||
|
const rows = container.querySelectorAll('.chain-param-row');
|
||||||
|
|
||||||
|
// Row 0: frozen
|
||||||
|
assert.ok(rows[0].classList.contains('param-frozen'));
|
||||||
|
assert.ok(!rows[0].classList.contains('param-live'));
|
||||||
|
assert.strictEqual(rows[0].style.opacity, '0.4');
|
||||||
|
|
||||||
|
// Row 1: live
|
||||||
|
assert.ok(rows[1].classList.contains('param-live'));
|
||||||
|
assert.ok(!rows[1].classList.contains('param-frozen'));
|
||||||
|
assert.notStrictEqual(rows[1].style.opacity, '0.4');
|
||||||
|
|
||||||
|
// Row 2: frozen
|
||||||
|
assert.ok(rows[2].classList.contains('param-frozen'));
|
||||||
|
|
||||||
|
// Row 3: live
|
||||||
|
assert.ok(rows[3].classList.contains('param-live'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── update() when NOT frozen ──────────────────────────────────────
|
||||||
|
|
||||||
|
it('update() removes freeze styling when not frozen', () => {
|
||||||
|
fm.freeze(4);
|
||||||
|
fm.toggleParam(1);
|
||||||
|
freezeUI.update();
|
||||||
|
|
||||||
|
// Now unfreeze
|
||||||
|
fm.unfreeze();
|
||||||
|
freezeUI.update();
|
||||||
|
|
||||||
|
const rows = container.querySelectorAll('.chain-param-row');
|
||||||
|
for (const row of rows) {
|
||||||
|
assert.ok(!row.classList.contains('param-frozen'),
|
||||||
|
'row should NOT have param-frozen after unfreeze');
|
||||||
|
assert.ok(!row.classList.contains('param-live'),
|
||||||
|
'row should NOT have param-live after unfreeze');
|
||||||
|
assert.strictEqual(row.style.opacity, '',
|
||||||
|
'opacity should be cleared');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Click toggle ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
it('clicking a param row label calls onToggle with the flat index', () => {
|
||||||
|
fm.freeze(4);
|
||||||
|
freezeUI.update();
|
||||||
|
|
||||||
|
// Click on the label of row 2
|
||||||
|
const row2 = container.querySelectorAll('.chain-param-row')[2];
|
||||||
|
const label = row2.querySelector('.chain-param-label');
|
||||||
|
|
||||||
|
const evt = makeClickEvent();
|
||||||
|
evt.target = label;
|
||||||
|
label.dispatchEvent(evt);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(toggleLog, [2], 'onToggle should be called with index 2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clicking does nothing when not frozen', () => {
|
||||||
|
const row0 = container.querySelectorAll('.chain-param-row')[0];
|
||||||
|
const label = row0.querySelector('.chain-param-label');
|
||||||
|
|
||||||
|
const evt = makeClickEvent();
|
||||||
|
evt.target = label;
|
||||||
|
label.dispatchEvent(evt);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(toggleLog, [], 'no toggle when not frozen');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clicking the slider itself does not toggle', () => {
|
||||||
|
fm.freeze(4);
|
||||||
|
freezeUI.update();
|
||||||
|
|
||||||
|
const row1 = container.querySelectorAll('.chain-param-row')[1];
|
||||||
|
const slider = row1.querySelector('.chain-param-slider');
|
||||||
|
|
||||||
|
const evt = makeClickEvent();
|
||||||
|
evt.target = slider;
|
||||||
|
slider.dispatchEvent(evt);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(toggleLog, [], 'slider click should not trigger toggle');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── destroy() ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
it('destroy() removes the click listener', () => {
|
||||||
|
fm.freeze(4);
|
||||||
|
freezeUI.update();
|
||||||
|
freezeUI.destroy();
|
||||||
|
|
||||||
|
const row0 = container.querySelectorAll('.chain-param-row')[0];
|
||||||
|
const label = row0.querySelector('.chain-param-label');
|
||||||
|
|
||||||
|
const evt = makeClickEvent();
|
||||||
|
evt.target = label;
|
||||||
|
label.dispatchEvent(evt);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(toggleLog, [], 'no toggle after destroy');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── updateDeltaIndicators() ───────────────────────────────────────
|
||||||
|
|
||||||
|
it('updateDeltaIndicators() creates delta-indicator elements on live rows', () => {
|
||||||
|
fm.freeze(4);
|
||||||
|
fm.toggleParam(1);
|
||||||
|
freezeUI.update();
|
||||||
|
|
||||||
|
const frozenParams = new Float32Array([0.3, 0.5, 0.7, 0.2]);
|
||||||
|
const effectiveParams = new Float32Array([0.3, 0.8, 0.7, 0.2]);
|
||||||
|
|
||||||
|
freezeUI.updateDeltaIndicators(frozenParams, effectiveParams);
|
||||||
|
|
||||||
|
// Row 1 is live — should have a delta indicator
|
||||||
|
const row1 = container.querySelectorAll('.chain-param-row')[1];
|
||||||
|
const indicator = row1.querySelector('.delta-indicator');
|
||||||
|
assert.ok(indicator, 'live row should have a delta-indicator');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updateDeltaIndicators() does nothing when not frozen', () => {
|
||||||
|
const frozenParams = new Float32Array([0.3, 0.5, 0.7, 0.2]);
|
||||||
|
const effectiveParams = new Float32Array([0.3, 0.8, 0.7, 0.2]);
|
||||||
|
|
||||||
|
freezeUI.updateDeltaIndicators(frozenParams, effectiveParams);
|
||||||
|
|
||||||
|
const indicators = container.querySelectorAll('.delta-indicator');
|
||||||
|
assert.strictEqual(indicators.length, 0, 'no indicators when not frozen');
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue