Release 2.21.2: fix animation timing
This commit is contained in:
1 parent
c923dcc1c5
commit
554678b3c4
13 files changed
+151
-54
No files matched your search
@@ -1181,4 +1181,50 @@ test('playlist removal of the current slide queues without rerendering it immedi
|
||||
assert.equal(JSON.stringify(sandbox.slides), JSON.stringify([{ id: 1, duration_seconds: 12 }, { id: 2, duration_seconds: 12 }]));
|
||||
assert.equal(calls.showCurrent, 0);
|
||||
assert.equal(calls.logDebug.some((entry) => entry.includes('applying on next slide transition')), true);
|
||||
});
|
||||
|
||||
test('player applies animation delays and schedules outro triggers to finish at slide advance', () => {
|
||||
const animationPath = path.join(__dirname, '..', 'src', 'player', 'public', 'js', 'player-page-animation.js');
|
||||
const transitionPath = path.join(__dirname, '..', 'src', 'player', 'public', 'js', 'player-page-transition.js');
|
||||
const values = {};
|
||||
const classes = new Set();
|
||||
const config = JSON.stringify({
|
||||
intro: { preset: 'fadeIn', duration_ms: 600, delay_ms: 250 },
|
||||
outro: { preset: 'fadeOut', duration_ms: 700, delay_ms: 400 }
|
||||
});
|
||||
const element = {
|
||||
dataset: {},
|
||||
isConnected: true,
|
||||
style: {
|
||||
setProperty(name, value) { values[name] = value; },
|
||||
removeProperty(name) { delete values[name]; }
|
||||
},
|
||||
classList: {
|
||||
add(...names) { names.forEach(name => classes.add(name)); },
|
||||
remove(...names) { names.forEach(name => classes.delete(name)); },
|
||||
[Symbol.iterator]() { return classes[Symbol.iterator](); }
|
||||
},
|
||||
getAttribute(name) { return name === 'data-animation-json' ? config : null; },
|
||||
addEventListener() {},
|
||||
removeEventListener() {},
|
||||
querySelectorAll() { return []; }
|
||||
};
|
||||
const root = { querySelectorAll() { return [element]; } };
|
||||
const timers = [];
|
||||
const sandbox = {
|
||||
window: null,
|
||||
app: { querySelectorAll() { return [root]; }, firstElementChild: root },
|
||||
setTimeout(callback, delay) { timers.push({ callback, delay }); return timers.length; },
|
||||
clearTimeout() {}
|
||||
};
|
||||
sandbox.window = sandbox;
|
||||
vm.runInNewContext(fs.readFileSync(animationPath, 'utf8') + '\n' + fs.readFileSync(transitionPath, 'utf8'), sandbox);
|
||||
|
||||
sandbox.playRegionAnimation(element, 'intro');
|
||||
assert.equal(values['animation-delay'], '250ms');
|
||||
|
||||
sandbox.scheduleSlideOutro(5000);
|
||||
assert.equal(timers[0].delay, 3900);
|
||||
timers[0].callback();
|
||||
assert.equal(values['animation-delay'], undefined);
|
||||
});
|
||||
@@ -322,6 +322,12 @@ test('shared animation controls round-trip advanced settings with template norma
|
||||
assert.equal(clamped.intro.duration_ms, 2000);
|
||||
assert.equal(clamped.intro.delay_ms, 0);
|
||||
assert.equal(clamped.loop.iterations, 999);
|
||||
fields[1].value = '999';
|
||||
fields[4].value = '2000';
|
||||
controls.clampAdvancedField(fields[1]);
|
||||
controls.clampAdvancedField(fields[4]);
|
||||
assert.equal(fields[1].value, '200');
|
||||
assert.equal(fields[4].value, '999');
|
||||
});
|
||||
|
||||
test('slide advanced popup applies changes, cancels drafts and preserves the parent animation draft', function () {
|
||||
@@ -330,7 +336,8 @@ test('slide advanced popup applies changes, cancels drafts and preserves the par
|
||||
addEventListener(type, handler) { this.handlers[type] = handler; },
|
||||
querySelectorAll() { return []; }, replaceChildren() {}, appendChild() {}, focus() {},
|
||||
setAttribute() {}, removeAttribute() {},
|
||||
style: { setProperty() {}, removeProperty() {} }, classList: { add() {}, remove() {}, toggle() {} } };
|
||||
style: { values: {}, setProperty(name, value) { this.values[name] = value; }, removeProperty(name) { delete this.values[name]; } },
|
||||
classList: { add() {}, remove() {}, toggle() {} } };
|
||||
}
|
||||
const ids = Object.fromEntries([
|
||||
'slide-animation-modal', 'adjust-slide-animations', 'slide-animation-region',
|
||||
@@ -352,9 +359,12 @@ test('slide advanced popup applies changes, cancels drafts and preserves the par
|
||||
add(...classNames) { readonlyPreviewRegion.className += ' ' + classNames.join(' '); },
|
||||
remove(...classNames) { readonlyPreviewRegion.className = readonlyPreviewRegion.className.split(/\s+/).filter(name => !classNames.includes(name)).join(' '); }
|
||||
};
|
||||
const secondPreviewRegion = element();
|
||||
secondPreviewRegion.className = 'slide-preview-region';
|
||||
secondPreviewRegion.dataset.regionKey = 'fixed_brand_second';
|
||||
advanced.querySelectorAll = () => fields;
|
||||
ids['slide-animation-modal'].querySelectorAll = selector => selector === '[data-slide-animation-preview]' ? animationPreviewButtons : [];
|
||||
ids['slide-animation-preview'].querySelectorAll = () => [readonlyPreviewRegion];
|
||||
ids['slide-animation-preview'].querySelectorAll = () => [readonlyPreviewRegion, secondPreviewRegion];
|
||||
advanced.querySelector = selector => selector === '[data-animation-modal-apply]' ? advancedApply
|
||||
: selector === '[data-animation-modal-reset]' ? advancedReset : element();
|
||||
const hidden = { value: '{}' };
|
||||
@@ -399,17 +409,29 @@ test('slide advanced popup applies changes, cancels drafts and preserves the par
|
||||
context.createSlideAnimationEditor({ stage: { parentNode: { insertBefore() {} }, nextSibling: null },
|
||||
getTemplate() { return { regions: [
|
||||
{ region_key: 'hero', animation_json: { intro: { preset: 'bounceIn' }, outro: { preset: 'fadeOut' } } },
|
||||
{ region_key: 'fixed_brand', read_only: true, animation_json: { loop: { preset: 'pulse', duration_ms: 200, iterations: 1 } } }
|
||||
{ region_key: 'fixed_brand', read_only: true, animation_json: {
|
||||
outro: { preset: 'fadeOut', duration_ms: 200, delay_ms: 500 },
|
||||
loop: { preset: 'pulse', duration_ms: 200, delay_ms: 240, iterations: 1 }
|
||||
} },
|
||||
{ region_key: 'fixed_brand_second', read_only: true, animation_json: { loop: { preset: 'pulse', duration_ms: 200, iterations: 1 } } }
|
||||
] }; },
|
||||
getAnimationOverrides() { return {}; }, requestPreviewRender() {}, markEdited() {},
|
||||
setAnimationOverrides(value) { applied = value; }
|
||||
});
|
||||
ids['slide-animation-region'].value = 'hero';
|
||||
ids['adjust-slide-animations'].handlers.click();
|
||||
fields[1].value = '999';
|
||||
ids['animation-advanced-modal'].handlers.input({ target: fields[1] });
|
||||
assert.equal(fields[1].value, '200');
|
||||
assert.equal(animationPreviewButtons[2].disabled, false);
|
||||
animationPreviewButtons[2].handlers.click();
|
||||
assert.match(readonlyPreviewRegion.className, /animate__pulse/);
|
||||
assert.equal(readonlyPreviewRegion.style.values['animation-delay'], '240ms');
|
||||
assert.equal(secondPreviewRegion.style.values['animation-delay'], '0ms');
|
||||
animationPreviewButtons[2].handlers.click();
|
||||
animationPreviewButtons[1].handlers.click();
|
||||
assert.equal(readonlyPreviewRegion.style.values['animation-delay'], '0ms');
|
||||
animationPreviewButtons[1].handlers.click();
|
||||
const resetRegion = ids['slide-animation-reset-region'];
|
||||
const resetAll = ids['slide-animation-reset-all'];
|
||||
assert.equal(resetRegion.disabled, true);
|
||||
|
||||
+26
-1
@@ -338,7 +338,7 @@ test('template animation controls mark the form dirty', () => {
|
||||
|
||||
assert.match(script, /animationPresetInputs[\s\S]*markTemplateFormDirty\(\);/);
|
||||
assert.ok(script.includes("animationAdvancedModal.addEventListener('input'"));
|
||||
assert.ok(script.includes('clampAnimationModalField(event.target);'));
|
||||
assert.ok(script.includes('window.pulseRegionAnimationControls.clampAdvancedField(event.target);'));
|
||||
assert.ok(script.includes("animationAdvancedModal.addEventListener('change'"));
|
||||
assert.ok(script.includes("animationModalApplyButton.addEventListener('click', function () {"));
|
||||
assert.ok(script.includes("saveAnimationModal();"));
|
||||
@@ -355,6 +355,7 @@ test('template animation durations use percent in the advanced modal', () => {
|
||||
|
||||
assert.match(modal, /signage\/templates\/animation-advanced-fields/);
|
||||
assert.match(modal, /data-animation-modal-title>Advanced animations:/);
|
||||
assert.match(advancedFields, /Finish before transition \(ms\).*data-animation-modal-step="outro"/);
|
||||
assert.ok(modal.indexOf('data-animation-modal-apply') < modal.lastIndexOf('>Cancel</button>'));
|
||||
assert.match(modal, /class="btn btn-warning" data-bs-dismiss="modal">Cancel<\/button>/);
|
||||
assert.ok(script.includes("title.textContent = 'Advanced animations: ' + (label ? label.textContent : 'Region');"));
|
||||
@@ -584,4 +585,28 @@ test('template designer restores region default content and read-only state from
|
||||
type: 'html', value: '<div>Template-owned</div>'
|
||||
});
|
||||
assert.equal(fields['[data-region-read-only]'].checked, true);
|
||||
});
|
||||
|
||||
test('template animation preview applies configured delays to the CSS animation property', () => {
|
||||
const script = fs.readFileSync(require.resolve('../src/web/public/js/templates/template-designer.js'), 'utf8');
|
||||
assert.doesNotMatch(script, /var stagger = 90/);
|
||||
assert.match(script, /var delay = mode === 'out' \? 0 : Math\.max\(0, Number\(step && step\.delay_ms\) \|\| 0\);/);
|
||||
const match = script.match(/ function applyAnimateCssPreview\([^]*?\n }/);
|
||||
assert.ok(match);
|
||||
const context = vm.createContext({});
|
||||
vm.runInContext(match[0] + '\nthis.applyAnimateCssPreview = applyAnimateCssPreview;', context);
|
||||
const values = {};
|
||||
const rect = {
|
||||
classList: { add() {}, remove() {} },
|
||||
style: {
|
||||
setProperty(name, value) { values[name] = value; },
|
||||
removeProperty(name) { delete values[name]; }
|
||||
}
|
||||
};
|
||||
|
||||
context.applyAnimateCssPreview(rect, 'fadeIn', { duration_ms: 1000, delay_ms: 425 }, 'intro');
|
||||
|
||||
assert.equal(values['animation-delay'], '425ms');
|
||||
context.applyAnimateCssPreview(rect, 'fadeOut', { duration_ms: 1000, delay_ms: 425 }, 'out');
|
||||
assert.equal(values['animation-delay'], undefined);
|
||||
});
|
||||
Reference in new issue
Block a user