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
@@ -2,6 +2,12 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## 2.21.2 - 2026-10-26
|
||||
|
||||
### Fixed
|
||||
|
||||
- Animation delays now work in slide and template previews and player playback. Outro timing is configured as time before transition, preview-only region staggering is removed, and slide animation controls enforce the same advanced limits as templates.
|
||||
|
||||
## 2.21.1 - 2026-10-26
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pulse-signage-player",
|
||||
"version": "2.21.1",
|
||||
"version": "2.21.2",
|
||||
"private": false,
|
||||
"description": "Pulse Signage player application bundle",
|
||||
"engines": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pulse-signage-web",
|
||||
"version": "2.21.1",
|
||||
"version": "2.21.2",
|
||||
"private": false,
|
||||
"description": "Pulse Signage web and bridge application bundle",
|
||||
"engines": {
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "pulse-signage",
|
||||
"version": "2.21.1",
|
||||
"version": "2.21.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pulse-signage",
|
||||
"version": "2.21.1",
|
||||
"version": "2.21.2",
|
||||
"dependencies": {
|
||||
"@better-auth/passkey": "^1.7.13",
|
||||
"animate.css": "^4.1.1",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pulse-signage",
|
||||
"version": "2.21.1",
|
||||
"version": "2.21.2",
|
||||
"private": false,
|
||||
"description": "Pulse Signage application with MySQL and media storage",
|
||||
"engines": {
|
||||
|
||||
@@ -144,6 +144,7 @@ function clearRegionAnimationClasses(root) {
|
||||
element.style.removeProperty('--animate-duration');
|
||||
element.style.removeProperty('--animate-delay');
|
||||
element.style.removeProperty('--animate-repeat');
|
||||
element.style.removeProperty('animation-delay');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -161,13 +162,16 @@ function applyAnimationStep(element, step, phase) {
|
||||
return;
|
||||
}
|
||||
|
||||
element.classList.add('animate__animated', 'animate__' + preset);
|
||||
element.style.setProperty('--animate-duration', String(Math.max(1, Number(step.duration_ms || 0) || 1000)) + 'ms');
|
||||
if (Number(step.delay_ms || 0) > 0) {
|
||||
element.style.setProperty('--animate-delay', String(Math.max(0, Number(step.delay_ms || 0))) + 'ms');
|
||||
var delayMs = phase === 'outro' ? 0 : Math.max(0, Number(step.delay_ms || 0));
|
||||
if (delayMs > 0) {
|
||||
element.style.setProperty('--animate-delay', String(delayMs) + 'ms');
|
||||
element.style.setProperty('animation-delay', String(delayMs) + 'ms');
|
||||
} else {
|
||||
element.style.removeProperty('--animate-delay');
|
||||
element.style.removeProperty('animation-delay');
|
||||
}
|
||||
element.classList.add('animate__animated', 'animate__' + preset);
|
||||
|
||||
if (phase === 'loop') {
|
||||
var repeatCount = Number(step.iterations);
|
||||
|
||||
@@ -76,8 +76,22 @@
|
||||
});
|
||||
return normalize(config);
|
||||
}
|
||||
function clampAdvancedField(input) {
|
||||
if (!input || !input.dataset) return;
|
||||
var field = input.dataset.animationModalField;
|
||||
var value = String(input.value || '').trim();
|
||||
if (!value) return;
|
||||
var number = Number(value);
|
||||
if (!Number.isFinite(number)) return;
|
||||
var normalized;
|
||||
if (field === 'duration_ms') normalized = Math.min(200, Math.max(1, Math.round(number)));
|
||||
else if (field === 'iterations' && input.dataset.animationModalStep === 'loop') normalized = Math.min(999, Math.max(1, Math.round(number)));
|
||||
else return;
|
||||
if (String(normalized) !== String(input.value)) input.value = String(normalized);
|
||||
}
|
||||
window.pulseRegionAnimationControls = {
|
||||
read: read, write: write, setPreset: setPreset, bind: bind,
|
||||
populateAdvanced: populateAdvanced, readAdvanced: readAdvanced
|
||||
populateAdvanced: populateAdvanced, readAdvanced: readAdvanced,
|
||||
clampAdvancedField: clampAdvancedField
|
||||
};
|
||||
})();
|
||||
@@ -141,7 +141,7 @@ export function createSlideAnimationEditor(options) {
|
||||
element.className.split(/\s+/).forEach(function (className) {
|
||||
if (className.indexOf('animate__') === 0) element.classList.remove(className);
|
||||
});
|
||||
['--animate-duration', '--animate-delay', '--animate-repeat'].forEach(function (property) { element.style.removeProperty(property); });
|
||||
['--animate-duration', '--animate-delay', '--animate-repeat', 'animation-delay'].forEach(function (property) { element.style.removeProperty(property); });
|
||||
});
|
||||
updatePreviewButtons();
|
||||
}
|
||||
@@ -168,16 +168,18 @@ export function createSlideAnimationEditor(options) {
|
||||
previewMode = mode;
|
||||
var token = previewToken;
|
||||
var longest = 0;
|
||||
getPreviewElements().forEach(function (element, index) {
|
||||
getPreviewElements().forEach(function (element) {
|
||||
var config = window.templateDesignerUtils.normalizeAnimationConfig(getConfig(element.dataset.regionKey));
|
||||
var step = config[mode] || {};
|
||||
var preset = window.templateAnimationPresets.normalize(step.preset);
|
||||
if (preset === 'none') return;
|
||||
var delay = Math.max(0, Number(step.delay_ms) || 0) + index * 90;
|
||||
var configuredDelay = mode === 'outro' ? 0 : Math.max(0, Number(step.delay_ms) || 0);
|
||||
var delay = configuredDelay;
|
||||
var duration = Math.max(1, Number(step.duration_ms) || 1000);
|
||||
var iterations = mode === 'loop' ? Math.max(1, Number(step.iterations) || 1) : 1;
|
||||
element.style.setProperty('--animate-duration', duration + 'ms');
|
||||
element.style.setProperty('--animate-delay', delay + 'ms');
|
||||
element.style.setProperty('animation-delay', delay + 'ms');
|
||||
element.style.setProperty('--animate-repeat', String(iterations));
|
||||
element.classList.remove('animate__animated', 'animate__' + preset, 'animate__repeat-1');
|
||||
void element.offsetWidth;
|
||||
@@ -266,6 +268,9 @@ export function createSlideAnimationEditor(options) {
|
||||
advanced.addEventListener('shown.bs.modal', function () {
|
||||
advanced.querySelector('[data-animation-modal-field]').focus();
|
||||
});
|
||||
advanced.addEventListener('input', function (event) {
|
||||
controls.clampAdvancedField(event.target);
|
||||
});
|
||||
advanced.addEventListener('hidden.bs.modal', function () {
|
||||
window.pulseModal.getOrCreate(advanced)._scrollBar = advancedScrollBar;
|
||||
advancedScrollBar = null;
|
||||
|
||||
@@ -363,6 +363,7 @@
|
||||
rect.style.removeProperty('--animate-duration');
|
||||
rect.style.removeProperty('--animate-delay');
|
||||
rect.style.removeProperty('--animate-repeat');
|
||||
rect.style.removeProperty('animation-delay');
|
||||
rect.style.opacity = '';
|
||||
rect.style.transform = '';
|
||||
rect.style.filter = '';
|
||||
@@ -442,15 +443,20 @@
|
||||
var durationMs = Number(step && step.duration_ms);
|
||||
var delayMs = Number(step && step.delay_ms);
|
||||
var repeatCount = Number(step && step.iterations);
|
||||
if (mode === 'out') delayMs = 0;
|
||||
var isAttentionSeeker = preset === 'bounce' || preset === 'flash' || preset === 'pulse' || preset === 'rubberBand' || preset === 'shakeX' || preset === 'shakeY' || preset === 'headShake' || preset === 'swing' || preset === 'tada' || preset === 'wobble' || preset === 'jello' || preset === 'heartBeat';
|
||||
|
||||
rect.classList.remove('animate__animated', 'animate__infinite');
|
||||
rect.style.removeProperty('--animate-duration');
|
||||
rect.style.removeProperty('--animate-delay');
|
||||
rect.style.removeProperty('--animate-repeat');
|
||||
rect.style.removeProperty('animation-delay');
|
||||
rect.style.setProperty('--animate-duration', Math.max(0, Number.isFinite(durationMs) && durationMs > 0 ? durationMs : 1000) + 'ms');
|
||||
if (Number.isFinite(delayMs) && delayMs > 0) {
|
||||
rect.style.setProperty('--animate-delay', Math.max(0, delayMs) + 'ms');
|
||||
rect.style.setProperty('animation-delay', Math.max(0, delayMs) + 'ms');
|
||||
} else {
|
||||
rect.style.removeProperty('animation-delay');
|
||||
}
|
||||
rect.classList.add('animate__animated', animateClass);
|
||||
if (isAttentionSeeker) {
|
||||
@@ -475,6 +481,9 @@
|
||||
if (!Number.isFinite(delayMs) || delayMs < 0) {
|
||||
delayMs = 0;
|
||||
}
|
||||
if (normalizedMode === 'out') {
|
||||
delayMs = 0;
|
||||
}
|
||||
if (!Number.isFinite(iterations) || iterations < 1) {
|
||||
iterations = 1;
|
||||
}
|
||||
@@ -595,39 +604,6 @@
|
||||
window.pulseRegionAnimationControls.populateAdvanced(animationAdvancedModal, config);
|
||||
}
|
||||
|
||||
function readNumberField(value) {
|
||||
var raw = String(value || '').trim();
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
var next = Number(raw);
|
||||
return Number.isFinite(next) ? Math.round(next) : null;
|
||||
}
|
||||
|
||||
function clampAnimationModalField(field) {
|
||||
if (!field || !field.matches) {
|
||||
return;
|
||||
}
|
||||
|
||||
var isLoopIterationsField = field.matches('[data-animation-modal-step="loop"][data-animation-modal-field="iterations"]');
|
||||
var isDurationField = field.matches('[data-animation-modal-field="duration_ms"]');
|
||||
if (!isLoopIterationsField && !isDurationField) {
|
||||
return;
|
||||
}
|
||||
|
||||
var fieldValue = readNumberField(field.value);
|
||||
if (fieldValue === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var normalizedValue = isLoopIterationsField
|
||||
? Math.min(MAX_ANIMATION_REPEAT_COUNT, Math.max(1, fieldValue))
|
||||
: Math.min(200, Math.max(1, fieldValue));
|
||||
if (String(normalizedValue) !== String(field.value)) {
|
||||
field.value = String(normalizedValue);
|
||||
}
|
||||
}
|
||||
|
||||
function readAnimationModalConfig() {
|
||||
if (!animationAdvancedModal) {
|
||||
return null;
|
||||
@@ -732,7 +708,6 @@
|
||||
function animatePreviewRects(mode, regions) {
|
||||
var rects = getPreviewRects();
|
||||
var token = previewState.token;
|
||||
var stagger = 90;
|
||||
var duration = 900;
|
||||
var overlayRect = getOverlayRect();
|
||||
var canvasMovement = Math.max(12, Math.round(overlayRect.height * 0.02));
|
||||
@@ -740,11 +715,11 @@
|
||||
|
||||
previewState.animations = [];
|
||||
|
||||
rects.forEach(function (rect, index) {
|
||||
rects.forEach(function (rect) {
|
||||
var region = regions && regions[index] ? regions[index] : null;
|
||||
var step = getPreviewRegionAnimationStep(region, mode);
|
||||
var preset = normalizePreviewAnimation(step && step.preset);
|
||||
var delay = index * stagger;
|
||||
var delay = mode === 'out' ? 0 : Math.max(0, Number(step && step.delay_ms) || 0);
|
||||
var animation = null;
|
||||
var animateCssMode = isAnimateCssPreset(preset);
|
||||
var timingMs = delay + getPreviewAnimationStepTimingMs(step, mode, animateCssMode);
|
||||
@@ -1785,7 +1760,7 @@
|
||||
activeAnimationCard = null;
|
||||
});
|
||||
animationAdvancedModal.addEventListener('input', function (event) {
|
||||
clampAnimationModalField(event.target);
|
||||
window.pulseRegionAnimationControls.clampAdvancedField(event.target);
|
||||
markTemplateFormDirty();
|
||||
});
|
||||
animationAdvancedModal.addEventListener('change', function () {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<div class="card-body d-grid gap-3 align-content-start">
|
||||
<label class="d-grid gap-1">Preset<select class="form-select" data-animation-modal-step="outro" data-animation-modal-field="preset">{{#each animationPresets.advanced.outro}}<option value="{{value}}">{{label}}</option>{{/each}}</select></label>
|
||||
<label class="d-grid gap-1">Duration %<input class="form-control" type="number" min="1" max="200" step="1" data-animation-modal-step="outro" data-animation-modal-field="duration_ms" placeholder="100" /></label>
|
||||
<label class="d-grid gap-1">Delay ms<input class="form-control" type="number" min="0" step="1" data-animation-modal-step="outro" data-animation-modal-field="delay_ms" placeholder="0" /></label>
|
||||
<label class="d-grid gap-1">Finish before transition (ms)<input class="form-control" type="number" min="0" step="1" data-animation-modal-step="outro" data-animation-modal-field="delay_ms" placeholder="0" /></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1182,3 +1182,49 @@ test('playlist removal of the current slide queues without rerendering it immedi
|
||||
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');"));
|
||||
@@ -585,3 +586,27 @@ test('template designer restores region default content and read-only state from
|
||||
});
|
||||
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