mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
style(ui): format shortcut and parameter components
This commit is contained in:
1 parent
7a5f615d55
commit
c9be877f9c
5 files changed
+262
-72
No files matched your search
@@ -129,5 +129,10 @@
|
||||
"src/ui/panelMeasurement.js",
|
||||
"src/ui/panelRailGeometry.js",
|
||||
"src/ui/panelRails.test.mjs",
|
||||
"src/panelStackLayout.js"
|
||||
"src/panelStackLayout.js",
|
||||
"src/ui/applicationShortcuts.js",
|
||||
"src/ui/styleParameters.js",
|
||||
"src/ui/visualInput.js",
|
||||
"src/ui/visualInput.test.mjs",
|
||||
"scripts/qa-visual-input.mjs"
|
||||
]
|
||||
+122
-39
@@ -5,7 +5,12 @@ import puppeteer from 'puppeteer';
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', ...(process.platform === 'darwin' ? ['--use-angle=metal', '--enable-gpu'] : ['--use-gl=angle', '--use-angle=swiftshader'])],
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
...(process.platform === 'darwin'
|
||||
? ['--use-angle=metal', '--enable-gpu']
|
||||
: ['--use-gl=angle', '--use-angle=swiftshader']),
|
||||
],
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
const errors = [];
|
||||
@@ -20,64 +25,129 @@ try {
|
||||
const url = new URL(process.env.QA_BASE_URL || 'http://127.0.0.1:4173');
|
||||
url.searchParams.set('welcome', '0');
|
||||
await page.goto(url.href, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForFunction(() => window.__godsEyeView?.styleManager?._applicationShortcuts, { timeout: 60_000 });
|
||||
await page.waitForFunction(() => {
|
||||
const cover = document.getElementById('loading-screen');
|
||||
return cover?.classList.contains('hidden') && Number(getComputedStyle(cover).opacity) === 0;
|
||||
}, { timeout: 60_000 });
|
||||
await page.waitForFunction(
|
||||
() => window.__godsEyeView?.styleManager?._applicationShortcuts,
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
const cover = document.getElementById('loading-screen');
|
||||
return (
|
||||
cover?.classList.contains('hidden') &&
|
||||
Number(getComputedStyle(cover).opacity) === 0
|
||||
);
|
||||
},
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
await page.evaluate(() => {
|
||||
const manager = window.__godsEyeView.styleManager;
|
||||
manager.setStyle('normal');
|
||||
document.activeElement?.blur();
|
||||
});
|
||||
await page.keyboard.press('2');
|
||||
check('native number shortcut selects CRT', await page.evaluate(() => window.__godsEyeView.styleManager.activeStyle === 'retro'));
|
||||
check(
|
||||
'native number shortcut selects CRT',
|
||||
await page.evaluate(
|
||||
() => window.__godsEyeView.styleManager.activeStyle === 'retro',
|
||||
),
|
||||
);
|
||||
const initial = await page.evaluate(() => {
|
||||
const manager = window.__godsEyeView.styleManager;
|
||||
manager.setPanelCollapsed('pp-toggles', false, { persist: false, syncShare: false });
|
||||
manager.setPanelCollapsed('pp-toggles', false, {
|
||||
persist: false,
|
||||
syncShare: false,
|
||||
});
|
||||
manager._sliderPanel.classList.remove('collapsed');
|
||||
const slider = manager._sliderContainer.querySelector('input');
|
||||
window.__qaOldSlider = slider;
|
||||
const name = 'pixelation';
|
||||
window.__qaParameter = { name, before: manager.stages.retro.uniforms[name] };
|
||||
window.__qaParameter = {
|
||||
name,
|
||||
before: manager.stages.retro.uniforms[name],
|
||||
};
|
||||
slider.focus();
|
||||
return { before: Number(slider.value), step: Number(slider.step), label: slider.getAttribute('aria-label') };
|
||||
return {
|
||||
before: Number(slider.value),
|
||||
step: Number(slider.step),
|
||||
label: slider.getAttribute('aria-label'),
|
||||
};
|
||||
});
|
||||
check('generated range has an accessible label and a numeric step', initial.label === 'Pixelation' && initial.step > 0);
|
||||
check(
|
||||
'generated range has an accessible label and a numeric step',
|
||||
initial.label === 'Pixelation' && initial.step > 0,
|
||||
);
|
||||
await page.keyboard.press('ArrowRight');
|
||||
const changed = await page.evaluate(() => {
|
||||
const slider = window.__qaOldSlider;
|
||||
const manager = window.__godsEyeView.styleManager;
|
||||
return { value: Number(slider.value), readout: slider.nextElementSibling.textContent, uniform: manager.stages.retro.uniforms[window.__qaParameter.name] };
|
||||
return {
|
||||
value: Number(slider.value),
|
||||
readout: slider.nextElementSibling.textContent,
|
||||
uniform: manager.stages.retro.uniforms[window.__qaParameter.name],
|
||||
};
|
||||
});
|
||||
check('native range input changes the shader and formatted readout', changed.value > initial.before && changed.uniform === changed.value && Number(changed.readout) === changed.value);
|
||||
check(
|
||||
'native range input changes the shader and formatted readout',
|
||||
changed.value > initial.before &&
|
||||
changed.uniform === changed.value &&
|
||||
Number(changed.readout) === changed.value,
|
||||
);
|
||||
await page.keyboard.press('3');
|
||||
check('focused range retains native input instead of changing style', await page.evaluate(() => window.__godsEyeView.styleManager.activeStyle === 'retro'));
|
||||
check('replacing a style revokes its detached slider', await page.evaluate(() => {
|
||||
const manager = window.__godsEyeView.styleManager;
|
||||
const before = manager.stages.retro.uniforms[window.__qaParameter.name];
|
||||
manager.setStyle('thermal');
|
||||
window.__qaOldSlider.value = '0';
|
||||
window.__qaOldSlider.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
return manager.stages.retro.uniforms[window.__qaParameter.name] === before;
|
||||
}));
|
||||
check('normal style clears rows; reopening restores current shader values', await page.evaluate(() => {
|
||||
const manager = window.__godsEyeView.styleManager;
|
||||
manager.setStyle('normal');
|
||||
const cleared = manager._sliderContainer.children.length === 0;
|
||||
manager.setStyle('retro');
|
||||
return cleared && Number(manager._sliderContainer.querySelector('input').value) === manager.stages.retro.uniforms[window.__qaParameter.name];
|
||||
}));
|
||||
check(
|
||||
'focused range retains native input instead of changing style',
|
||||
await page.evaluate(
|
||||
() => window.__godsEyeView.styleManager.activeStyle === 'retro',
|
||||
),
|
||||
);
|
||||
check(
|
||||
'replacing a style revokes its detached slider',
|
||||
await page.evaluate(() => {
|
||||
const manager = window.__godsEyeView.styleManager;
|
||||
const before = manager.stages.retro.uniforms[window.__qaParameter.name];
|
||||
manager.setStyle('thermal');
|
||||
window.__qaOldSlider.value = '0';
|
||||
window.__qaOldSlider.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
return (
|
||||
manager.stages.retro.uniforms[window.__qaParameter.name] === before
|
||||
);
|
||||
}),
|
||||
);
|
||||
check(
|
||||
'normal style clears rows; reopening restores current shader values',
|
||||
await page.evaluate(() => {
|
||||
const manager = window.__godsEyeView.styleManager;
|
||||
manager.setStyle('normal');
|
||||
const cleared = manager._sliderContainer.children.length === 0;
|
||||
manager.setStyle('retro');
|
||||
return (
|
||||
cleared &&
|
||||
Number(manager._sliderContainer.querySelector('input').value) ===
|
||||
manager.stages.retro.uniforms[window.__qaParameter.name]
|
||||
);
|
||||
}),
|
||||
);
|
||||
const hudBefore = await page.evaluate(() => {
|
||||
document.getElementById('hud-layout-select').focus();
|
||||
return window.__godsEyeView.styleManager.hud.visible;
|
||||
});
|
||||
await page.keyboard.press('h');
|
||||
check('HUD select keeps its native typing and selection', await page.evaluate((visible) => document.activeElement?.id === 'hud-layout-select' && window.__godsEyeView.styleManager.hud.visible === visible, hudBefore));
|
||||
check(
|
||||
'HUD select keeps its native typing and selection',
|
||||
await page.evaluate(
|
||||
(visible) =>
|
||||
document.activeElement?.id === 'hud-layout-select' &&
|
||||
window.__godsEyeView.styleManager.hud.visible === visible,
|
||||
hudBefore,
|
||||
),
|
||||
);
|
||||
fs.mkdirSync('qa-shots/visual-input', { recursive: true });
|
||||
await page.screenshot({ path: 'qa-shots/visual-input/desktop.png' });
|
||||
await page.setViewport({ width: 620, height: 900 });
|
||||
await page.waitForFunction(() => document.getElementById('left-panel-stack')?.dataset.layoutMode === 'mobile');
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
document.getElementById('left-panel-stack')?.dataset.layoutMode ===
|
||||
'mobile',
|
||||
);
|
||||
await page.screenshot({ path: 'qa-shots/visual-input/narrow.png' });
|
||||
await page.evaluate(() => {
|
||||
const manager = window.__godsEyeView.styleManager;
|
||||
@@ -87,14 +157,27 @@ try {
|
||||
document.activeElement?.blur();
|
||||
});
|
||||
await page.keyboard.press('3');
|
||||
check('destroyed application shortcuts cannot change the selected style', await page.evaluate(() => window.__godsEyeView.styleManager.activeStyle === 'retro'));
|
||||
check('destroyed parameter controls cannot write through a retained element', await page.evaluate(() => {
|
||||
const manager = window.__godsEyeView.styleManager;
|
||||
const before = manager.stages.retro.uniforms[window.__qaParameter.name];
|
||||
window.__qaCurrentSlider.value = '0';
|
||||
window.__qaCurrentSlider.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
return manager._sliderContainer.children.length === 0 && manager.stages.retro.uniforms[window.__qaParameter.name] === before;
|
||||
}));
|
||||
check(
|
||||
'destroyed application shortcuts cannot change the selected style',
|
||||
await page.evaluate(
|
||||
() => window.__godsEyeView.styleManager.activeStyle === 'retro',
|
||||
),
|
||||
);
|
||||
check(
|
||||
'destroyed parameter controls cannot write through a retained element',
|
||||
await page.evaluate(() => {
|
||||
const manager = window.__godsEyeView.styleManager;
|
||||
const before = manager.stages.retro.uniforms[window.__qaParameter.name];
|
||||
window.__qaCurrentSlider.value = '0';
|
||||
window.__qaCurrentSlider.dispatchEvent(
|
||||
new Event('input', { bubbles: true }),
|
||||
);
|
||||
return (
|
||||
manager._sliderContainer.children.length === 0 &&
|
||||
manager.stages.retro.uniforms[window.__qaParameter.name] === before
|
||||
);
|
||||
}),
|
||||
);
|
||||
check('runtime has no uncaught browser errors', errors.length === 0);
|
||||
} finally {
|
||||
await browser.close();
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
const STYLE_KEYS = Object.freeze({
|
||||
'1': 'normal', '2': 'retro', '3': 'surveillance',
|
||||
'4': 'thermal', '5': 'anime', '6': 'noir', '7': 'snow',
|
||||
1: 'normal',
|
||||
2: 'retro',
|
||||
3: 'surveillance',
|
||||
4: 'thermal',
|
||||
5: 'anime',
|
||||
6: 'noir',
|
||||
7: 'snow',
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -13,10 +18,15 @@ const STYLE_KEYS = Object.freeze({
|
||||
* @param {object} options.actions Existing application operations.
|
||||
* @returns {{destroy: Function}} Synchronous, idempotent listener cleanup.
|
||||
*/
|
||||
export function bindApplicationShortcuts({ documentRef, searchInput, actions }) {
|
||||
export function bindApplicationShortcuts({
|
||||
documentRef,
|
||||
searchInput,
|
||||
actions,
|
||||
}) {
|
||||
const onKeyDown = (event) => {
|
||||
const isFormControl = event.target?.matches?.('select, input, textarea')
|
||||
|| event.target === searchInput;
|
||||
const isFormControl =
|
||||
event.target?.matches?.('select, input, textarea') ||
|
||||
event.target === searchInput;
|
||||
if (isFormControl && event.key !== 'Escape') return;
|
||||
|
||||
if (STYLE_KEYS[event.key]) actions.setStyle(STYLE_KEYS[event.key]);
|
||||
@@ -30,5 +40,9 @@ export function bindApplicationShortcuts({ documentRef, searchInput, actions })
|
||||
if (key === 'c') actions.toggleCctv();
|
||||
};
|
||||
documentRef.addEventListener('keydown', onKeyDown);
|
||||
return { destroy() { documentRef.removeEventListener('keydown', onKeyDown); } };
|
||||
return {
|
||||
destroy() {
|
||||
documentRef.removeEventListener('keydown', onKeyDown);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -7,12 +7,16 @@
|
||||
* @param {Document} [options.documentRef] Document used to construct rows.
|
||||
* @returns {{render: Function, clear: Function, destroy: Function}}
|
||||
*/
|
||||
export function createStyleParameters({ container, documentRef = container.ownerDocument }) {
|
||||
export function createStyleParameters({
|
||||
container,
|
||||
documentRef = container.ownerDocument,
|
||||
}) {
|
||||
let listeners = [];
|
||||
let destroyed = false;
|
||||
const clear = () => {
|
||||
if (destroyed) return;
|
||||
for (const [slider, listener] of listeners) slider.removeEventListener('input', listener);
|
||||
for (const [slider, listener] of listeners)
|
||||
slider.removeEventListener('input', listener);
|
||||
listeners = [];
|
||||
container.replaceChildren();
|
||||
};
|
||||
|
||||
+108
-24
@@ -1,6 +1,9 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { bindApplicationShortcuts, createStyleParameters } from './visualInput.js';
|
||||
import {
|
||||
bindApplicationShortcuts,
|
||||
createStyleParameters,
|
||||
} from './visualInput.js';
|
||||
|
||||
class Element {
|
||||
constructor(tagName = 'DIV', ownerDocument) {
|
||||
@@ -11,46 +14,98 @@ class Element {
|
||||
this.attributes = new Map();
|
||||
}
|
||||
addEventListener(type, listener, capture) {
|
||||
assert.notEqual(capture, true, 'these controls must not preempt capture-phase surfaces');
|
||||
assert.notEqual(
|
||||
capture,
|
||||
true,
|
||||
'these controls must not preempt capture-phase surfaces',
|
||||
);
|
||||
if (!this.listeners.has(type)) this.listeners.set(type, new Set());
|
||||
this.listeners.get(type).add(listener);
|
||||
}
|
||||
removeEventListener(type, listener) { this.listeners.get(type)?.delete(listener); }
|
||||
emit(type, event = {}) { for (const listener of [...(this.listeners.get(type) || [])]) listener(event); }
|
||||
setAttribute(name, value) { this.attributes.set(name, value); }
|
||||
append(...children) { this.children.push(...children); }
|
||||
appendChild(child) { this.children.push(child); }
|
||||
replaceChildren(...children) { this.children = children; }
|
||||
matches() { return ['INPUT', 'SELECT', 'TEXTAREA'].includes(this.tagName); }
|
||||
removeEventListener(type, listener) {
|
||||
this.listeners.get(type)?.delete(listener);
|
||||
}
|
||||
emit(type, event = {}) {
|
||||
for (const listener of [...(this.listeners.get(type) || [])])
|
||||
listener(event);
|
||||
}
|
||||
setAttribute(name, value) {
|
||||
this.attributes.set(name, value);
|
||||
}
|
||||
append(...children) {
|
||||
this.children.push(...children);
|
||||
}
|
||||
appendChild(child) {
|
||||
this.children.push(child);
|
||||
}
|
||||
replaceChildren(...children) {
|
||||
this.children = children;
|
||||
}
|
||||
matches() {
|
||||
return ['INPUT', 'SELECT', 'TEXTAREA'].includes(this.tagName);
|
||||
}
|
||||
}
|
||||
|
||||
function shortcuts() {
|
||||
const documentRef = new Element();
|
||||
const searchInput = new Element();
|
||||
const calls = [];
|
||||
const names = ['setStyle', 'dismissSearch', 'toggleHud', 'toggleOrbit', 'toggleCleanView', 'toggleLayers', 'cycleDetection', 'toggleCctv'];
|
||||
const actions = Object.fromEntries(names.map((name) => [name, (...args) => calls.push([name, ...args])]));
|
||||
const controller = bindApplicationShortcuts({ documentRef, searchInput, actions });
|
||||
const press = (key, target = new Element(), rest = {}) => documentRef.emit('keydown', { key, target, ...rest });
|
||||
const names = [
|
||||
'setStyle',
|
||||
'dismissSearch',
|
||||
'toggleHud',
|
||||
'toggleOrbit',
|
||||
'toggleCleanView',
|
||||
'toggleLayers',
|
||||
'cycleDetection',
|
||||
'toggleCctv',
|
||||
];
|
||||
const actions = Object.fromEntries(
|
||||
names.map((name) => [name, (...args) => calls.push([name, ...args])]),
|
||||
);
|
||||
const controller = bindApplicationShortcuts({
|
||||
documentRef,
|
||||
searchInput,
|
||||
actions,
|
||||
});
|
||||
const press = (key, target = new Element(), rest = {}) =>
|
||||
documentRef.emit('keydown', { key, target, ...rest });
|
||||
return { documentRef, searchInput, calls, controller, press };
|
||||
}
|
||||
|
||||
test('number keys retain the seven style mappings', () => {
|
||||
const f = shortcuts();
|
||||
for (const key of ['1', '2', '3', '4', '5', '6', '7', '8', 'Space']) f.press(key);
|
||||
assert.deepEqual(f.calls, ['normal', 'retro', 'surveillance', 'thermal', 'anime', 'noir', 'snow'].map((style) => ['setStyle', style]));
|
||||
for (const key of ['1', '2', '3', '4', '5', '6', '7', '8', 'Space'])
|
||||
f.press(key);
|
||||
assert.deepEqual(
|
||||
f.calls,
|
||||
['normal', 'retro', 'surveillance', 'thermal', 'anime', 'noir', 'snow'].map(
|
||||
(style) => ['setStyle', style],
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('letter shortcuts retain uppercase handling and existing actions', () => {
|
||||
const f = shortcuts();
|
||||
for (const key of ['H', 'o', 'V', 'f', 'D', 'c']) f.press(key);
|
||||
assert.deepEqual(f.calls, ['toggleHud', 'toggleOrbit', 'toggleCleanView', 'toggleLayers', 'cycleDetection', 'toggleCctv'].map((name) => [name]));
|
||||
assert.deepEqual(
|
||||
f.calls,
|
||||
[
|
||||
'toggleHud',
|
||||
'toggleOrbit',
|
||||
'toggleCleanView',
|
||||
'toggleLayers',
|
||||
'cycleDetection',
|
||||
'toggleCctv',
|
||||
].map((name) => [name]),
|
||||
);
|
||||
});
|
||||
|
||||
for (const tag of ['INPUT', 'SELECT', 'TEXTAREA']) {
|
||||
test(`${tag} retains native editing, while Escape reaches search dismissal`, () => {
|
||||
const f = shortcuts();
|
||||
for (const key of ['1', 'h', 'o', 'v', 'f', 'd', 'c']) f.press(key, new Element(tag));
|
||||
for (const key of ['1', 'h', 'o', 'v', 'f', 'd', 'c'])
|
||||
f.press(key, new Element(tag));
|
||||
assert.deepEqual(f.calls, []);
|
||||
f.press('Escape', new Element(tag));
|
||||
assert.deepEqual(f.calls, [['dismissSearch']]);
|
||||
@@ -77,20 +132,39 @@ test('destroy synchronously removes shortcuts and a replacement binds once', ()
|
||||
f.press('h');
|
||||
assert.deepEqual(f.calls, []);
|
||||
assert.equal(f.documentRef.listeners.get('keydown').size, 0);
|
||||
const replacement = bindApplicationShortcuts({ documentRef: f.documentRef, searchInput: f.searchInput, actions: { toggleHud: () => f.calls.push('new') } });
|
||||
const replacement = bindApplicationShortcuts({
|
||||
documentRef: f.documentRef,
|
||||
searchInput: f.searchInput,
|
||||
actions: { toggleHud: () => f.calls.push('new') },
|
||||
});
|
||||
f.press('h');
|
||||
assert.deepEqual(f.calls, ['new']);
|
||||
replacement.destroy();
|
||||
});
|
||||
|
||||
function parameters() {
|
||||
const documentRef = { createElement: (tag) => new Element(tag.toUpperCase(), documentRef) };
|
||||
const documentRef = {
|
||||
createElement: (tag) => new Element(tag.toUpperCase(), documentRef),
|
||||
};
|
||||
const container = new Element('DIV', documentRef);
|
||||
const controller = createStyleParameters({ container });
|
||||
const values = { amount: 0.25, scale: 2 };
|
||||
const uniforms = { amount: { label: '<Amount>', min: 0, max: 1 }, scale: { label: 'Scale', min: 0, max: 10 } };
|
||||
const uniforms = {
|
||||
amount: { label: '<Amount>', min: 0, max: 1 },
|
||||
scale: { label: 'Scale', min: 0, max: 10 },
|
||||
};
|
||||
const order = [];
|
||||
const options = { uniforms, readValue: (name) => values[name], writeValue: (name, value) => { order.push(['write', name, value]); values[name] = value; }, onChange: () => { order.push(['change', container.children[0]?.children[2].textContent]); } };
|
||||
const options = {
|
||||
uniforms,
|
||||
readValue: (name) => values[name],
|
||||
writeValue: (name, value) => {
|
||||
order.push(['write', name, value]);
|
||||
values[name] = value;
|
||||
},
|
||||
onChange: () => {
|
||||
order.push(['change', container.children[0]?.children[2].textContent]);
|
||||
},
|
||||
};
|
||||
controller.render(options);
|
||||
return { container, controller, values, options, order };
|
||||
}
|
||||
@@ -109,7 +183,11 @@ test('parameter rows preserve labels, bounds, precision and initial values', ()
|
||||
assert.equal(value.textContent, '0.25');
|
||||
assert.equal(f.container.children[1].children[1].step, '0.1');
|
||||
assert.equal(f.container.children[1].children[2].textContent, '2.0');
|
||||
assert.deepEqual(f.order, [], 'rendering must not write shader values or publish changes');
|
||||
assert.deepEqual(
|
||||
f.order,
|
||||
[],
|
||||
'rendering must not write shader values or publish changes',
|
||||
);
|
||||
});
|
||||
|
||||
test('input writes numeric values, updates the readout, then requests a change', () => {
|
||||
@@ -118,13 +196,19 @@ test('input writes numeric values, updates the readout, then requests a change',
|
||||
slider.value = '0.75';
|
||||
slider.emit('input');
|
||||
assert.equal(f.values.amount, 0.75);
|
||||
assert.deepEqual(f.order, [['write', 'amount', 0.75], ['change', '0.75']]);
|
||||
assert.deepEqual(f.order, [
|
||||
['write', 'amount', 0.75],
|
||||
['change', '0.75'],
|
||||
]);
|
||||
});
|
||||
|
||||
test('rebuilding the parameter panel revokes detached slider listeners', () => {
|
||||
const f = parameters();
|
||||
const oldSlider = f.container.children[0].children[1];
|
||||
f.controller.render({ ...f.options, uniforms: { scale: f.options.uniforms.scale } });
|
||||
f.controller.render({
|
||||
...f.options,
|
||||
uniforms: { scale: f.options.uniforms.scale },
|
||||
});
|
||||
oldSlider.value = '0.8';
|
||||
oldSlider.emit('input');
|
||||
assert.equal(f.values.amount, 0.25);
|
||||
|
||||
Reference in new issue
Block a user