From e7766fb08844042982492eb76f14657cefa42f56 Mon Sep 17 00:00:00 2001 From: Daniele Loreto Date: Sun, 20 Aug 2023 22:55:57 +0100 Subject: [PATCH 1/2] fix: prevent animations's keyframes removal before they finish check the animation play state with the DOM API before removing the keyframes --- .../svelte/src/runtime/internal/animations.js | 13 ++++-- .../svelte/src/runtime/internal/private.d.ts | 6 +++ .../src/runtime/internal/style_manager.js | 35 +++++++++++----- .../src/runtime/internal/transitions.js | 40 +++++++++++++++---- 4 files changed, 74 insertions(+), 20 deletions(-) diff --git a/packages/svelte/src/runtime/internal/animations.js b/packages/svelte/src/runtime/internal/animations.js index d2471f12602f..0085415ead0b 100644 --- a/packages/svelte/src/runtime/internal/animations.js +++ b/packages/svelte/src/runtime/internal/animations.js @@ -31,11 +31,14 @@ export function create_animation(node, from, fn, params) { } = fn(node, { from, to }, params); let running = true; let started = false; - let name; + let animation_name; + /** @type {import("./private.js").AnimationInformation | undefined} */ + let css_animation_info = undefined; /** @returns {void} */ function start() { if (css) { - name = create_rule(node, 0, 1, duration, delay, easing, css); + css_animation_info = create_rule(node, 0, 1, duration, delay, easing, css); + animation_name = css_animation_info.name; } if (!delay) { started = true; @@ -43,14 +46,16 @@ export function create_animation(node, from, fn, params) { } /** @returns {void} */ function stop() { - if (css) delete_rule(node, name); + if (css) delete_rule(node, animation_name); running = false; } loop((now) => { if (!started && now >= start_time) { started = true; } - if (started && now >= end) { + const css_animation_state = css_animation_info?.animation?.playState; + const is_css_animation_finished = !css_animation_state || css_animation_state === 'finished'; + if (started && now >= end && is_css_animation_finished) { tick(1, 0); stop(); } diff --git a/packages/svelte/src/runtime/internal/private.d.ts b/packages/svelte/src/runtime/internal/private.d.ts index ef2245256ef7..daacb06faa79 100644 --- a/packages/svelte/src/runtime/internal/private.d.ts +++ b/packages/svelte/src/runtime/internal/private.d.ts @@ -6,6 +6,12 @@ export type AnimationFn = ( params: any ) => AnimationConfig; +export interface AnimationInformation { + name: string; + /** is not valorized in Node environment */ + animation?: CSSAnimation; +} + export type Listener = (entry: ResizeObserverEntry) => any; //todo: documentation says it is DOMRect, but in IE it would be ClientRect diff --git a/packages/svelte/src/runtime/internal/style_manager.js b/packages/svelte/src/runtime/internal/style_manager.js index a98984788f19..5e488d4b2438 100644 --- a/packages/svelte/src/runtime/internal/style_manager.js +++ b/packages/svelte/src/runtime/internal/style_manager.js @@ -40,7 +40,7 @@ function create_style_information(doc, node) { * @param {(t: number) => number} ease * @param {(t: number, u: number) => string} fn * @param {number} uid - * @returns {string} + * @returns {import("./private.d.ts").AnimationInformation} */ export function create_rule(node, a, b, duration, delay, ease, fn, uid = 0) { const step = 16.666 / duration; @@ -50,19 +50,36 @@ export function create_rule(node, a, b, duration, delay, ease, fn, uid = 0) { keyframes += p * 100 + `%{${fn(t, 1 - t)}}\n`; } const rule = keyframes + `100% {${fn(b, 1 - b)}}\n}`; - const name = `__svelte_${hash(rule)}_${uid}`; + const keyframe_name = `__svelte_${hash(rule)}_${uid}`; const doc = get_root_for_style(node); const { stylesheet, rules } = managed_styles.get(doc) || create_style_information(doc, node); - if (!rules[name]) { - rules[name] = true; - stylesheet.insertRule(`@keyframes ${name} ${rule}`, stylesheet.cssRules.length); + if (!rules[keyframe_name]) { + rules[keyframe_name] = true; + stylesheet.insertRule(`@keyframes ${keyframe_name} ${rule}`, stylesheet.cssRules.length); } - const animation = node.style.animation || ''; + const current_animation_style = node.style.animation || ''; node.style.animation = `${ - animation ? `${animation}, ` : '' - }${name} ${duration}ms linear ${delay}ms 1 both`; + current_animation_style ? `${current_animation_style}, ` : '' + }${keyframe_name} ${duration}ms linear ${delay}ms 1 both`; + + /** @type {CSSAnimation[] | undefined} */ + const all_animations = /** @type {any} */ (node.getAnimations?.()); // `getAnimations` is not supported in Node environment + /** @type {import("./private.d.ts").AnimationInformation} */ + let animation_info = { + name: keyframe_name + }; + if (all_animations) { + for (const a of all_animations) { + // TODO: perhaps a map may avoid unnecessary iterations + if (a.animationName === keyframe_name) { + animation_info.animation = a; + break; + } + } + } + active += 1; - return name; + return animation_info; } /** diff --git a/packages/svelte/src/runtime/internal/transitions.js b/packages/svelte/src/runtime/internal/transitions.js index 4575e184acb5..4e1cbf1f37e9 100644 --- a/packages/svelte/src/runtime/internal/transitions.js +++ b/packages/svelte/src/runtime/internal/transitions.js @@ -132,7 +132,14 @@ export function create_in_transition(node, fn, params) { tick = noop, css } = config || null_transition; - if (css) animation_name = create_rule(node, 0, 1, duration, delay, easing, css, uid++); + + /** @type {import("./private.js").AnimationInformation | undefined} */ + let css_animation_info = undefined; + if (css) { + css_animation_info = create_rule(node, 0, 1, duration, delay, easing, css, uid++); + animation_name = css_animation_info.name; + } + tick(0, 1); const start_time = now() + delay; const end_time = start_time + duration; @@ -141,7 +148,10 @@ export function create_in_transition(node, fn, params) { add_render_callback(() => dispatch(node, true, 'start')); task = loop((now) => { if (running) { - if (now >= end_time) { + const css_animation_state = css_animation_info?.animation?.playState; + const is_css_animation_finished = + !css_animation_state || css_animation_state === 'finished'; + if (now >= end_time && is_css_animation_finished) { tick(1, 0); dispatch(node, true, 'end'); cleanup(); @@ -208,7 +218,12 @@ export function create_out_transition(node, fn, params) { css } = config || null_transition; - if (css) animation_name = create_rule(node, 1, 0, duration, delay, easing, css); + /** @type {import("./private.js").AnimationInformation | undefined} */ + let css_animation_info = undefined; + if (css) { + css_animation_info = create_rule(node, 1, 0, duration, delay, easing, css); + animation_name = css_animation_info.name; + } const start_time = now() + delay; const end_time = start_time + duration; @@ -221,7 +236,10 @@ export function create_out_transition(node, fn, params) { loop((now) => { if (running) { - if (now >= end_time) { + const css_animation_state = css_animation_info?.animation?.playState; + const is_css_animation_finished = + !css_animation_state || css_animation_state === 'finished'; + if (now >= end_time && is_css_animation_finished) { tick(0, 1); dispatch(node, false, 'end'); if (!--group.r) { @@ -358,15 +376,19 @@ export function create_bidirectional_transition(node, fn, params, intro) { if (running_program || pending_program) { pending_program = program; } else { + /** @type {import("./private.js").AnimationInformation | undefined} */ + let css_animation_info = undefined; // if this is an intro, and there's a delay, we need to do // an initial tick and/or apply CSS animation immediately if (css) { clear_animation(); - animation_name = create_rule(node, t, b, duration, delay, easing, css); + css_animation_info = create_rule(node, t, b, duration, delay, easing, css); + animation_name = css_animation_info.name; } if (b) tick(0, 1); running_program = init(program, duration); add_render_callback(() => dispatch(node, b, 'start')); + loop((now) => { if (pending_program && now > pending_program.start) { running_program = init(pending_program, duration); @@ -374,7 +396,7 @@ export function create_bidirectional_transition(node, fn, params, intro) { dispatch(node, running_program.b, 'start'); if (css) { clear_animation(); - animation_name = create_rule( + css_animation_info = create_rule( node, t, running_program.b, @@ -383,10 +405,14 @@ export function create_bidirectional_transition(node, fn, params, intro) { easing, config.css ); + animation_name = css_animation_info.name; } } if (running_program) { - if (now >= running_program.end) { + const css_animation_state = css_animation_info?.animation?.playState; + const is_css_animation_finished = + !css_animation_state || css_animation_state === 'finished'; + if (now >= running_program.end && is_css_animation_finished) { tick((t = running_program.b), 1 - t); dispatch(node, running_program.b, 'end'); if (!pending_program) { From fa4b1a3bc91aca957f845fea1726f371f5bf5084 Mon Sep 17 00:00:00 2001 From: Daniele Loreto Date: Sun, 20 Aug 2023 22:48:29 +0100 Subject: [PATCH 2/2] fix: implement tests to check that animations are really finished when keyframes are removed --- .../samples/animation-css/_config.js | 99 ++++++++++++ .../samples/animation-css/main.svelte | 17 ++ .../transition-css-in-intro/_config.js | 150 ++++++++++++++++++ .../transition-css-in-intro/main.svelte | 42 +++++ .../transition-css-in-out-intro/_config.js | 150 ++++++++++++++++++ .../transition-css-in-out-intro/main.svelte | 42 +++++ .../samples/transition-css-in-out/_config.js | 128 +++++++++++++++ .../samples/transition-css-in-out/main.svelte | 42 +++++ .../samples/transition-css-in/_config.js | 128 +++++++++++++++ .../samples/transition-css-in/main.svelte | 42 +++++ .../samples/transition-css-out-in/_config.js | 14 -- .../samples/transition-css-out-in/main.svelte | 20 --- 12 files changed, 840 insertions(+), 34 deletions(-) create mode 100644 packages/svelte/test/runtime-browser/samples/animation-css/_config.js create mode 100644 packages/svelte/test/runtime-browser/samples/animation-css/main.svelte create mode 100644 packages/svelte/test/runtime-browser/samples/transition-css-in-intro/_config.js create mode 100644 packages/svelte/test/runtime-browser/samples/transition-css-in-intro/main.svelte create mode 100644 packages/svelte/test/runtime-browser/samples/transition-css-in-out-intro/_config.js create mode 100644 packages/svelte/test/runtime-browser/samples/transition-css-in-out-intro/main.svelte create mode 100644 packages/svelte/test/runtime-browser/samples/transition-css-in-out/_config.js create mode 100644 packages/svelte/test/runtime-browser/samples/transition-css-in-out/main.svelte create mode 100644 packages/svelte/test/runtime-browser/samples/transition-css-in/_config.js create mode 100644 packages/svelte/test/runtime-browser/samples/transition-css-in/main.svelte delete mode 100644 packages/svelte/test/runtime-browser/samples/transition-css-out-in/_config.js delete mode 100644 packages/svelte/test/runtime-browser/samples/transition-css-out-in/main.svelte diff --git a/packages/svelte/test/runtime-browser/samples/animation-css/_config.js b/packages/svelte/test/runtime-browser/samples/animation-css/_config.js new file mode 100644 index 000000000000..875310a22048 --- /dev/null +++ b/packages/svelte/test/runtime-browser/samples/animation-css/_config.js @@ -0,0 +1,99 @@ +export default { + props: { + things: [ + { id: 1, name: 'a' }, + { id: 2, name: 'b' }, + { id: 3, name: 'c' }, + { id: 4, name: 'd' }, + { id: 5, name: 'e' } + ] + }, + + html: ` +
a
+
b
+
c
+
d
+
e
+ `, + + async test({ assert, component, target, waitUntil }) { + let divs = target.querySelectorAll('div'); + divs.forEach((div) => { + div.getBoundingClientRect = function () { + const index = [...this.parentNode.children].indexOf(this); + const top = index * 30; + + return { + left: 0, + right: 100, + top, + bottom: top + 20 + }; + }; + }); + + component.things = [ + { id: 5, name: 'e' }, + { id: 2, name: 'b' }, + { id: 3, name: 'c' }, + { id: 4, name: 'd' }, + { id: 1, name: 'a' } + ]; + + divs = target.querySelectorAll('div'); + assert.ok(~divs[0].style.animation.indexOf('__svelte')); + assert.equal(divs[1].style.animation, ''); + assert.equal(divs[2].style.animation, ''); + assert.equal(divs[3].style.animation, ''); + assert.ok(~divs[4].style.animation.indexOf('__svelte')); + assert.equal(divs[0].getAnimations().length, 1); + assert.equal(divs[1].getAnimations().length, 0); + assert.equal(divs[2].getAnimations().length, 0); + assert.equal(divs[3].getAnimations().length, 0); + assert.equal(divs[4].getAnimations().length, 1); + + const animations = [divs[0].getAnimations().at(0), divs[4].getAnimations().at(0)]; + + animations.forEach((animation) => { + assert.equal(animation.playState, 'running'); + }); + + await Promise.all(animations.map((animation) => animation.finished)).catch((e) => { + if (e.name === 'AbortError') { + throw new Error( + 'The animation was aborted, keyframes have been removed before the animation execution end.' + ); + } + throw e; + }); + assert.ok(~divs[0].style.animation.indexOf('__svelte')); + assert.equal(divs[1].style.animation, ''); + assert.equal(divs[2].style.animation, ''); + assert.equal(divs[3].style.animation, ''); + assert.ok(~divs[4].style.animation.indexOf('__svelte')); + assert.equal(divs[0].getAnimations().length, 1); + assert.equal(divs[1].getAnimations().length, 0); + assert.equal(divs[2].getAnimations().length, 0); + assert.equal(divs[3].getAnimations().length, 0); + assert.equal(divs[4].getAnimations().length, 1); + animations.forEach((animation) => { + assert.equal(animation.playState, 'finished'); + }); + + await waitUntil(() => !divs[4].style.animation); + assert.ok(~divs[0].style.animation, ''); + assert.equal(divs[1].style.animation, ''); + assert.equal(divs[2].style.animation, ''); + assert.equal(divs[3].style.animation, ''); + assert.ok(~divs[4].style.animation, ''); + assert.equal(divs[0].getAnimations().length, 0); + assert.equal(divs[1].getAnimations().length, 0); + assert.equal(divs[2].getAnimations().length, 0); + assert.equal(divs[3].getAnimations().length, 0); + assert.equal(divs[4].getAnimations().length, 0); + animations.forEach((animation) => { + assert.equal(animation.playState, 'idle'); + }); + } +}; diff --git a/packages/svelte/test/runtime-browser/samples/animation-css/main.svelte b/packages/svelte/test/runtime-browser/samples/animation-css/main.svelte new file mode 100644 index 000000000000..673878f3bc69 --- /dev/null +++ b/packages/svelte/test/runtime-browser/samples/animation-css/main.svelte @@ -0,0 +1,17 @@ + + +{#each things as thing (thing.id)} +
{thing.name}
+{/each} diff --git a/packages/svelte/test/runtime-browser/samples/transition-css-in-intro/_config.js b/packages/svelte/test/runtime-browser/samples/transition-css-in-intro/_config.js new file mode 100644 index 000000000000..43b18dd1f87e --- /dev/null +++ b/packages/svelte/test/runtime-browser/samples/transition-css-in-intro/_config.js @@ -0,0 +1,150 @@ +export default { + props: { + visible: true + }, + intro: true, + async test({ assert, component, window, waitUntil, target }) { + /** @type {HTMLElement} */ + const elAnimatedOnMount = target.querySelector('#animated-on-mount'); + /** @type {HTMLElement} */ + const elAnimatedOnInit = target.querySelector('#animated-on-init'); + + assert.equal(elAnimatedOnInit.getAnimations().length, 1); + assert.equal(elAnimatedOnMount.getAnimations().length, 1); + const elAnimatedOnInitAnimation = elAnimatedOnInit.getAnimations().at(0); + const elAnimatedOnMountAnimation = elAnimatedOnMount.getAnimations().at(0); + + assert.equal(elAnimatedOnInitAnimation.playState, 'running'); + assert.equal(elAnimatedOnMountAnimation.playState, 'running'); + assert.equal(!!elAnimatedOnInit.style.animation, true); + assert.equal(!!elAnimatedOnMount.style.animation, true); + assert.equal(window.document.head.querySelector('style')?.sheet.rules.length, 2); + + await elAnimatedOnInitAnimation.finished.catch((e) => { + if (e.name === 'AbortError') { + throw new Error( + 'The animation was aborted, keyframes have been removed before the animation execution end.' + ); + } + throw e; + }); + assert.equal(elAnimatedOnInitAnimation.playState, 'finished'); + assert.equal(elAnimatedOnMountAnimation.playState, 'running'); + assert.equal(!!elAnimatedOnInit.style.animation, true); + assert.equal(!!elAnimatedOnMount.style.animation, true); + assert.equal(window.document.head.querySelector('style')?.sheet.rules.length, 2); + assert.equal(elAnimatedOnInit.getAnimations().length, 1); + assert.equal(elAnimatedOnMount.getAnimations().length, 1); + + await waitUntil(() => elAnimatedOnInit.getAnimations().length === 0); + assert.equal(elAnimatedOnInitAnimation.playState, 'idle'); + assert.equal(elAnimatedOnMountAnimation.playState, 'running'); + assert.equal(!!elAnimatedOnInit.style.animation, false); + assert.equal(!!elAnimatedOnMount.style.animation, true); + assert.equal(window.document.head.querySelector('style')?.sheet.rules.length, 2); + assert.equal(elAnimatedOnInit.getAnimations().length, 0); + assert.equal(elAnimatedOnMount.getAnimations().length, 1); + + await elAnimatedOnMountAnimation.finished.catch((e) => { + if (e.name === 'AbortError') { + throw new Error( + 'The animation was aborted, keyframes have been removed before the animation execution end.' + ); + } + throw e; + }); + assert.equal(elAnimatedOnInitAnimation.playState, 'idle'); + assert.equal(elAnimatedOnMountAnimation.playState, 'finished'); + assert.equal(!!elAnimatedOnInit.style.animation, false); + assert.equal(!!elAnimatedOnMount.style.animation, true); + assert.equal(window.document.head.querySelector('style')?.sheet.rules.length, 2); + assert.equal(elAnimatedOnInit.getAnimations().length, 0); + assert.equal(elAnimatedOnMount.getAnimations().length, 1); + + await waitUntil(() => window.document.head.querySelector('style') === null); + assert.equal(elAnimatedOnInitAnimation.playState, 'idle'); + assert.equal(elAnimatedOnMountAnimation.playState, 'idle'); + assert.equal(!!elAnimatedOnInit.style.animation, false); + assert.equal(!!elAnimatedOnMount.style.animation, false); + assert.equal(window.document.head.querySelector('style'), null); + assert.equal(elAnimatedOnInit.getAnimations().length, 0); + assert.equal(elAnimatedOnMount.getAnimations().length, 0); + + /** @type {NodeListOf} */ + let divs = target.querySelectorAll('.fading-div'); + /** @type {Animation[]} */ + let animations; + assert.equal(divs.length, 1); + divs.forEach((div) => assert.equal(div.getAnimations().length, 0)); + + component.visible = false; + divs = target.querySelectorAll('.fading-div'); + assert.equal(divs.length, 1); + divs.forEach((div) => assert.equal(div.getAnimations().length, 1)); + animations = Array.from(divs).map((div) => div.getAnimations().at(0)); + animations.forEach((animation) => assert.equal(animation.playState, 'running')); + assert.equal(window.document.head.querySelector('style')?.sheet.rules.length, 1); + assert.equal(elAnimatedOnInit.getAnimations().length, 0); + assert.equal(elAnimatedOnMount.getAnimations().length, 0); + + await animations[0].finished.catch((e) => { + if (e.name === 'AbortError') { + throw new Error( + 'The animation was aborted, keyframes have been removed before the animation execution end.' + ); + } + throw e; + }); + divs = target.querySelectorAll('.fading-div'); + assert.equal(divs.length, 1); + divs.forEach((div) => assert.equal(div.getAnimations().length, 1)); + animations.forEach((animation) => assert.equal(animation.playState, 'finished')); + assert.equal(window.document.head.querySelector('style')?.sheet.rules.length, 1); + assert.equal(elAnimatedOnInit.getAnimations().length, 0); + assert.equal(elAnimatedOnMount.getAnimations().length, 0); + + await waitUntil(() => window.document.head.querySelector('style') === null); + divs = target.querySelectorAll('.fading-div'); + assert.equal(divs.length, 1); + divs.forEach((div) => assert.equal(div.getAnimations().length, 0)); + animations.forEach((animation) => assert.equal(animation.playState, 'idle')); + assert.equal(window.document.head.querySelector('style'), null); + assert.equal(elAnimatedOnInit.getAnimations().length, 0); + assert.equal(elAnimatedOnMount.getAnimations().length, 0); + + component.visible = true; + divs = target.querySelectorAll('.fading-div'); + assert.equal(divs.length, 1); + divs.forEach((div) => assert.equal(div.getAnimations().length, 1)); + animations = Array.from(divs).map((div) => div.getAnimations().at(0)); + animations.forEach((animation) => assert.equal(animation.playState, 'running')); + assert.equal(window.document.head.querySelector('style')?.sheet.rules.length, 1); + assert.equal(elAnimatedOnInit.getAnimations().length, 0); + assert.equal(elAnimatedOnMount.getAnimations().length, 0); + + await animations[0].finished.catch((e) => { + if (e.name === 'AbortError') { + throw new Error( + 'The animation was aborted, keyframes have been removed before the animation execution end.' + ); + } + throw e; + }); + divs = target.querySelectorAll('.fading-div'); + assert.equal(divs.length, 1); + divs.forEach((div) => assert.equal(div.getAnimations().length, 1)); + animations.forEach((animation) => assert.equal(animation.playState, 'finished')); + assert.equal(window.document.head.querySelector('style')?.sheet.rules.length, 1); + assert.equal(elAnimatedOnInit.getAnimations().length, 0); + assert.equal(elAnimatedOnMount.getAnimations().length, 0); + + await waitUntil(() => window.document.head.querySelector('style') === null); + divs = target.querySelectorAll('.fading-div'); + assert.equal(divs.length, 1); + divs.forEach((div) => assert.equal(div.getAnimations().length, 0)); + animations.forEach((animation) => assert.equal(animation.playState, 'idle')); + assert.equal(window.document.head.querySelector('style'), null); + assert.equal(elAnimatedOnInit.getAnimations().length, 0); + assert.equal(elAnimatedOnMount.getAnimations().length, 0); + } +}; diff --git a/packages/svelte/test/runtime-browser/samples/transition-css-in-intro/main.svelte b/packages/svelte/test/runtime-browser/samples/transition-css-in-intro/main.svelte new file mode 100644 index 000000000000..66bfff856acc --- /dev/null +++ b/packages/svelte/test/runtime-browser/samples/transition-css-in-intro/main.svelte @@ -0,0 +1,42 @@ + + +{#if visible} +
+{/if} + +{#if !visible} +
+{/if} + +
+ +{#if mounted} +
+{/if} diff --git a/packages/svelte/test/runtime-browser/samples/transition-css-in-out-intro/_config.js b/packages/svelte/test/runtime-browser/samples/transition-css-in-out-intro/_config.js new file mode 100644 index 000000000000..a874aa20c270 --- /dev/null +++ b/packages/svelte/test/runtime-browser/samples/transition-css-in-out-intro/_config.js @@ -0,0 +1,150 @@ +export default { + props: { + visible: true + }, + intro: true, + test: async ({ assert, component, window, waitUntil, target }) => { + /** @type {HTMLElement} */ + const elAnimatedOnMount = target.querySelector('#animated-on-mount'); + /** @type {HTMLElement} */ + const elAnimatedOnInit = target.querySelector('#animated-on-init'); + + assert.equal(elAnimatedOnInit.getAnimations().length, 1); + assert.equal(elAnimatedOnMount.getAnimations().length, 1); + const elAnimatedOnInitAnimation = elAnimatedOnInit.getAnimations().at(0); + const elAnimatedOnMountAnimation = elAnimatedOnMount.getAnimations().at(0); + + assert.equal(elAnimatedOnInitAnimation.playState, 'running'); + assert.equal(elAnimatedOnMountAnimation.playState, 'running'); + assert.equal(!!elAnimatedOnInit.style.animation, true); + assert.equal(!!elAnimatedOnMount.style.animation, true); + assert.equal(window.document.head.querySelector('style')?.sheet.rules.length, 2); + + await elAnimatedOnInitAnimation.finished.catch((e) => { + if (e.name === 'AbortError') { + throw new Error( + 'The animation was aborted, keyframes have been removed before the animation execution end.' + ); + } + throw e; + }); + assert.equal(elAnimatedOnInitAnimation.playState, 'finished'); + assert.equal(elAnimatedOnMountAnimation.playState, 'running'); + assert.equal(!!elAnimatedOnInit.style.animation, true); + assert.equal(!!elAnimatedOnMount.style.animation, true); + assert.equal(window.document.head.querySelector('style')?.sheet.rules.length, 2); + assert.equal(elAnimatedOnInit.getAnimations().length, 1); + assert.equal(elAnimatedOnMount.getAnimations().length, 1); + + await waitUntil(() => elAnimatedOnInit.getAnimations().length === 0); + assert.equal(elAnimatedOnInitAnimation.playState, 'idle'); + assert.equal(elAnimatedOnMountAnimation.playState, 'running'); + assert.equal(!!elAnimatedOnInit.style.animation, false); + assert.equal(!!elAnimatedOnMount.style.animation, true); + assert.equal(window.document.head.querySelector('style')?.sheet.rules.length, 2); + assert.equal(elAnimatedOnInit.getAnimations().length, 0); + assert.equal(elAnimatedOnMount.getAnimations().length, 1); + + await elAnimatedOnMountAnimation.finished.catch((e) => { + if (e.name === 'AbortError') { + throw new Error( + 'The animation was aborted, keyframes have been removed before the animation execution end.' + ); + } + throw e; + }); + assert.equal(elAnimatedOnInitAnimation.playState, 'idle'); + assert.equal(elAnimatedOnMountAnimation.playState, 'finished'); + assert.equal(!!elAnimatedOnInit.style.animation, false); + assert.equal(!!elAnimatedOnMount.style.animation, true); + assert.equal(window.document.head.querySelector('style')?.sheet.rules.length, 2); + assert.equal(elAnimatedOnInit.getAnimations().length, 0); + assert.equal(elAnimatedOnMount.getAnimations().length, 1); + + await waitUntil(() => window.document.head.querySelector('style') === null); + assert.equal(elAnimatedOnInitAnimation.playState, 'idle'); + assert.equal(elAnimatedOnMountAnimation.playState, 'idle'); + assert.equal(!!elAnimatedOnInit.style.animation, false); + assert.equal(!!elAnimatedOnMount.style.animation, false); + assert.equal(window.document.head.querySelector('style'), null); + assert.equal(elAnimatedOnInit.getAnimations().length, 0); + assert.equal(elAnimatedOnMount.getAnimations().length, 0); + + /** @type {NodeListOf} */ + let divs = target.querySelectorAll('.fading-div'); + /** @type {Animation[]} */ + let animations; + assert.equal(divs.length, 1); + divs.forEach((div) => assert.equal(div.getAnimations().length, 0)); + + component.visible = false; + divs = target.querySelectorAll('.fading-div'); + assert.equal(divs.length, 2); + divs.forEach((div) => assert.equal(div.getAnimations().length, 1)); + animations = Array.from(divs).map((div) => div.getAnimations().at(0)); + animations.forEach((animation) => assert.equal(animation.playState, 'running')); + assert.equal(window.document.head.querySelector('style')?.sheet.rules.length, 2); + assert.equal(elAnimatedOnInit.getAnimations().length, 0); + assert.equal(elAnimatedOnMount.getAnimations().length, 0); + + await animations[0].finished.catch((e) => { + if (e.name === 'AbortError') { + throw new Error( + 'The animation was aborted, keyframes have been removed before the animation execution end.' + ); + } + throw e; + }); + divs = target.querySelectorAll('.fading-div'); + assert.equal(divs.length, 2); + divs.forEach((div) => assert.equal(div.getAnimations().length, 1)); + animations.forEach((animation) => assert.equal(animation.playState, 'finished')); + assert.equal(window.document.head.querySelector('style')?.sheet.rules.length, 2); + assert.equal(elAnimatedOnInit.getAnimations().length, 0); + assert.equal(elAnimatedOnMount.getAnimations().length, 0); + + await waitUntil(() => window.document.head.querySelector('style') === null); + divs = target.querySelectorAll('.fading-div'); + assert.equal(divs.length, 1); + divs.forEach((div) => assert.equal(div.getAnimations().length, 0)); + animations.forEach((animation) => assert.equal(animation.playState, 'idle')); + assert.equal(window.document.head.querySelector('style'), null); + assert.equal(elAnimatedOnInit.getAnimations().length, 0); + assert.equal(elAnimatedOnMount.getAnimations().length, 0); + + component.visible = true; + divs = target.querySelectorAll('.fading-div'); + assert.equal(divs.length, 2); + divs.forEach((div) => assert.equal(div.getAnimations().length, 1)); + animations = Array.from(divs).map((div) => div.getAnimations().at(0)); + animations.forEach((animation) => assert.equal(animation.playState, 'running')); + assert.equal(window.document.head.querySelector('style')?.sheet.rules.length, 2); + assert.equal(elAnimatedOnInit.getAnimations().length, 0); + assert.equal(elAnimatedOnMount.getAnimations().length, 0); + + await animations[0].finished.catch((e) => { + if (e.name === 'AbortError') { + throw new Error( + 'The animation was aborted, keyframes have been removed before the animation execution end.' + ); + } + throw e; + }); + divs = target.querySelectorAll('.fading-div'); + assert.equal(divs.length, 2); + divs.forEach((div) => assert.equal(div.getAnimations().length, 1)); + animations.forEach((animation) => assert.equal(animation.playState, 'finished')); + assert.equal(window.document.head.querySelector('style')?.sheet.rules.length, 2); + assert.equal(elAnimatedOnInit.getAnimations().length, 0); + assert.equal(elAnimatedOnMount.getAnimations().length, 0); + + await waitUntil(() => window.document.head.querySelector('style') === null); + divs = target.querySelectorAll('.fading-div'); + assert.equal(divs.length, 1); + divs.forEach((div) => assert.equal(div.getAnimations().length, 0)); + animations.forEach((animation) => assert.equal(animation.playState, 'idle')); + assert.equal(window.document.head.querySelector('style'), null); + assert.equal(elAnimatedOnInit.getAnimations().length, 0); + assert.equal(elAnimatedOnMount.getAnimations().length, 0); + } +}; diff --git a/packages/svelte/test/runtime-browser/samples/transition-css-in-out-intro/main.svelte b/packages/svelte/test/runtime-browser/samples/transition-css-in-out-intro/main.svelte new file mode 100644 index 000000000000..23f0621ddb65 --- /dev/null +++ b/packages/svelte/test/runtime-browser/samples/transition-css-in-out-intro/main.svelte @@ -0,0 +1,42 @@ + + +{#if visible} +
+{/if} + +{#if !visible} +
+{/if} + +
+ +{#if mounted} +
+{/if} diff --git a/packages/svelte/test/runtime-browser/samples/transition-css-in-out/_config.js b/packages/svelte/test/runtime-browser/samples/transition-css-in-out/_config.js new file mode 100644 index 000000000000..de2994b2cc6c --- /dev/null +++ b/packages/svelte/test/runtime-browser/samples/transition-css-in-out/_config.js @@ -0,0 +1,128 @@ +export default { + props: { + visible: true + }, + async test({ assert, component, window, waitUntil, target }) { + /** @type {HTMLElement} */ + const elAnimatedOnMount = target.querySelector('#animated-on-mount'); + /** @type {HTMLElement} */ + const elAnimatedOnInit = target.querySelector('#animated-on-init'); + + assert.equal(elAnimatedOnInit.getAnimations().length, 0); + assert.equal(elAnimatedOnMount.getAnimations().length, 1); + const elAnimatedOnMountAnimation = elAnimatedOnMount.getAnimations().at(0); + + assert.equal(elAnimatedOnMountAnimation.playState, 'running'); + assert.equal(!!elAnimatedOnInit.style.animation, false); + assert.equal(!!elAnimatedOnMount.style.animation, true); + assert.equal(window.document.head.querySelector('style')?.sheet.rules.length, 1); + + await elAnimatedOnMountAnimation.finished.catch((e) => { + if (e.name === 'AbortError') { + throw new Error( + 'The animation was aborted, keyframes have been removed before the animation execution end.' + ); + } + throw e; + }); + assert.equal(elAnimatedOnMountAnimation.playState, 'finished'); + assert.equal(!!elAnimatedOnInit.style.animation, false); + assert.equal(!!elAnimatedOnMount.style.animation, true); + assert.equal(window.document.head.querySelector('style')?.sheet.rules.length, 1); + assert.equal(elAnimatedOnInit.getAnimations().length, 0); + assert.equal(elAnimatedOnMount.getAnimations().length, 1); + + await waitUntil(() => elAnimatedOnMount.getAnimations().length === 0); + assert.equal(elAnimatedOnMountAnimation.playState, 'idle'); + assert.equal(!!elAnimatedOnInit.style.animation, false); + assert.equal(!!elAnimatedOnMount.style.animation, false); + assert.equal(window.document.head.querySelector('style')?.sheet.rules.length, 1); + assert.equal(elAnimatedOnInit.getAnimations().length, 0); + assert.equal(elAnimatedOnMount.getAnimations().length, 0); + + await waitUntil(() => window.document.head.querySelector('style') === null); + assert.equal(elAnimatedOnMountAnimation.playState, 'idle'); + assert.equal(!!elAnimatedOnInit.style.animation, false); + assert.equal(!!elAnimatedOnMount.style.animation, false); + assert.equal(window.document.head.querySelector('style'), null); + assert.equal(elAnimatedOnInit.getAnimations().length, 0); + assert.equal(elAnimatedOnMount.getAnimations().length, 0); + + /** @type {NodeListOf} */ + let divs = target.querySelectorAll('.fading-div'); + /** @type {Animation[]} */ + let animations; + assert.equal(divs.length, 1); + divs.forEach((div) => assert.equal(div.getAnimations().length, 0)); + + component.visible = false; + divs = target.querySelectorAll('.fading-div'); + assert.equal(divs.length, 2); + divs.forEach((div) => assert.equal(div.getAnimations().length, 1)); + animations = Array.from(divs).map((div) => div.getAnimations().at(0)); + animations.forEach((animation) => assert.equal(animation.playState, 'running')); + assert.equal(window.document.head.querySelector('style')?.sheet.rules.length, 2); + assert.equal(elAnimatedOnInit.getAnimations().length, 0); + assert.equal(elAnimatedOnMount.getAnimations().length, 0); + + await animations[0].finished.catch((e) => { + if (e.name === 'AbortError') { + throw new Error( + 'The animation was aborted, keyframes have been removed before the animation execution end.' + ); + } + throw e; + }); + divs = target.querySelectorAll('.fading-div'); + assert.equal(divs.length, 2); + divs.forEach((div) => assert.equal(div.getAnimations().length, 1)); + animations.forEach((animation) => assert.equal(animation.playState, 'finished')); + assert.equal(window.document.head.querySelector('style')?.sheet.rules.length, 2); + assert.equal(elAnimatedOnInit.getAnimations().length, 0); + assert.equal(elAnimatedOnMount.getAnimations().length, 0); + + await waitUntil(() => window.document.head.querySelector('style') === null); + divs = target.querySelectorAll('.fading-div'); + assert.equal(divs.length, 1); + divs.forEach((div) => assert.equal(div.getAnimations().length, 0)); + animations.forEach((animation) => assert.equal(animation.playState, 'idle')); + assert.equal(window.document.head.querySelector('style'), null); + assert.equal(elAnimatedOnInit.getAnimations().length, 0); + assert.equal(elAnimatedOnMount.getAnimations().length, 0); + + component.visible = true; + divs = target.querySelectorAll('.fading-div'); + assert.equal(divs.length, 2); + divs.forEach((div) => assert.equal(div.getAnimations().length, 1)); + animations = Array.from(divs).map((div) => div.getAnimations().at(0)); + animations.forEach((animation) => assert.equal(animation.playState, 'running')); + assert.equal(window.document.head.querySelector('style')?.sheet.rules.length, 2); + assert.equal(elAnimatedOnInit.getAnimations().length, 0); + assert.equal(elAnimatedOnMount.getAnimations().length, 0); + + await animations[0].finished.catch((e) => { + if (e.name === 'AbortError') { + throw new Error( + 'The animation was aborted, keyframes have been removed before the animation execution end.' + ); + } + throw e; + }); + divs = target.querySelectorAll('.fading-div'); + assert.equal(divs.length, 2); + divs.forEach((div) => assert.equal(div.getAnimations().length, 1)); + animations.forEach((animation) => assert.equal(animation.playState, 'finished')); + assert.equal(window.document.head.querySelector('style')?.sheet.rules.length, 2); + assert.equal(elAnimatedOnInit.getAnimations().length, 0); + assert.equal(elAnimatedOnMount.getAnimations().length, 0); + + await waitUntil(() => window.document.head.querySelector('style') === null); + divs = target.querySelectorAll('.fading-div'); + assert.equal(divs.length, 1); + divs.forEach((div) => assert.equal(div.getAnimations().length, 0)); + animations.forEach((animation) => assert.equal(animation.playState, 'idle')); + assert.equal(window.document.head.querySelector('style'), null); + assert.equal(elAnimatedOnInit.getAnimations().length, 0); + assert.equal(elAnimatedOnMount.getAnimations().length, 0); + } +}; diff --git a/packages/svelte/test/runtime-browser/samples/transition-css-in-out/main.svelte b/packages/svelte/test/runtime-browser/samples/transition-css-in-out/main.svelte new file mode 100644 index 000000000000..23f0621ddb65 --- /dev/null +++ b/packages/svelte/test/runtime-browser/samples/transition-css-in-out/main.svelte @@ -0,0 +1,42 @@ + + +{#if visible} +
+{/if} + +{#if !visible} +
+{/if} + +
+ +{#if mounted} +
+{/if} diff --git a/packages/svelte/test/runtime-browser/samples/transition-css-in/_config.js b/packages/svelte/test/runtime-browser/samples/transition-css-in/_config.js new file mode 100644 index 000000000000..fc3fc993da43 --- /dev/null +++ b/packages/svelte/test/runtime-browser/samples/transition-css-in/_config.js @@ -0,0 +1,128 @@ +export default { + props: { + visible: true + }, + async test({ assert, component, window, waitUntil, target }) { + /** @type {HTMLElement} */ + const elAnimatedOnMount = target.querySelector('#animated-on-mount'); + /** @type {HTMLElement} */ + const elAnimatedOnInit = target.querySelector('#animated-on-init'); + + assert.equal(elAnimatedOnInit.getAnimations().length, 0); + assert.equal(elAnimatedOnMount.getAnimations().length, 1); + const elAnimatedOnMountAnimation = elAnimatedOnMount.getAnimations().at(0); + + assert.equal(elAnimatedOnMountAnimation.playState, 'running'); + assert.equal(!!elAnimatedOnInit.style.animation, false); + assert.equal(!!elAnimatedOnMount.style.animation, true); + assert.equal(window.document.head.querySelector('style')?.sheet.rules.length, 1); + + await elAnimatedOnMountAnimation.finished.catch((e) => { + if (e.name === 'AbortError') { + throw new Error( + 'The animation was aborted, keyframes have been removed before the animation execution end.' + ); + } + throw e; + }); + assert.equal(elAnimatedOnMountAnimation.playState, 'finished'); + assert.equal(!!elAnimatedOnInit.style.animation, false); + assert.equal(!!elAnimatedOnMount.style.animation, true); + assert.equal(window.document.head.querySelector('style')?.sheet.rules.length, 1); + assert.equal(elAnimatedOnInit.getAnimations().length, 0); + assert.equal(elAnimatedOnMount.getAnimations().length, 1); + + await waitUntil(() => elAnimatedOnMount.getAnimations().length === 0); + assert.equal(elAnimatedOnMountAnimation.playState, 'idle'); + assert.equal(!!elAnimatedOnInit.style.animation, false); + assert.equal(!!elAnimatedOnMount.style.animation, false); + assert.equal(window.document.head.querySelector('style')?.sheet.rules.length, 1); + assert.equal(elAnimatedOnInit.getAnimations().length, 0); + assert.equal(elAnimatedOnMount.getAnimations().length, 0); + + await waitUntil(() => window.document.head.querySelector('style') === null); + assert.equal(elAnimatedOnMountAnimation.playState, 'idle'); + assert.equal(!!elAnimatedOnInit.style.animation, false); + assert.equal(!!elAnimatedOnMount.style.animation, false); + assert.equal(window.document.head.querySelector('style'), null); + assert.equal(elAnimatedOnInit.getAnimations().length, 0); + assert.equal(elAnimatedOnMount.getAnimations().length, 0); + + /** @type {NodeListOf} */ + let divs = target.querySelectorAll('.fading-div'); + /** @type {Animation[]} */ + let animations; + assert.equal(divs.length, 1); + divs.forEach((div) => assert.equal(div.getAnimations().length, 0)); + + component.visible = false; + divs = target.querySelectorAll('.fading-div'); + assert.equal(divs.length, 1); + divs.forEach((div) => assert.equal(div.getAnimations().length, 1)); + animations = Array.from(divs).map((div) => div.getAnimations().at(0)); + animations.forEach((animation) => assert.equal(animation.playState, 'running')); + assert.equal(window.document.head.querySelector('style')?.sheet.rules.length, 1); + assert.equal(elAnimatedOnInit.getAnimations().length, 0); + assert.equal(elAnimatedOnMount.getAnimations().length, 0); + + await animations[0].finished.catch((e) => { + if (e.name === 'AbortError') { + throw new Error( + 'The animation was aborted, keyframes have been removed before the animation execution end.' + ); + } + throw e; + }); + divs = target.querySelectorAll('.fading-div'); + assert.equal(divs.length, 1); + divs.forEach((div) => assert.equal(div.getAnimations().length, 1)); + animations.forEach((animation) => assert.equal(animation.playState, 'finished')); + assert.equal(window.document.head.querySelector('style')?.sheet.rules.length, 1); + assert.equal(elAnimatedOnInit.getAnimations().length, 0); + assert.equal(elAnimatedOnMount.getAnimations().length, 0); + + await waitUntil(() => window.document.head.querySelector('style') === null); + divs = target.querySelectorAll('.fading-div'); + assert.equal(divs.length, 1); + divs.forEach((div) => assert.equal(div.getAnimations().length, 0)); + animations.forEach((animation) => assert.equal(animation.playState, 'idle')); + assert.equal(window.document.head.querySelector('style'), null); + assert.equal(elAnimatedOnInit.getAnimations().length, 0); + assert.equal(elAnimatedOnMount.getAnimations().length, 0); + + component.visible = true; + divs = target.querySelectorAll('.fading-div'); + assert.equal(divs.length, 1); + divs.forEach((div) => assert.equal(div.getAnimations().length, 1)); + animations = Array.from(divs).map((div) => div.getAnimations().at(0)); + animations.forEach((animation) => assert.equal(animation.playState, 'running')); + assert.equal(window.document.head.querySelector('style')?.sheet.rules.length, 1); + assert.equal(elAnimatedOnInit.getAnimations().length, 0); + assert.equal(elAnimatedOnMount.getAnimations().length, 0); + + await animations[0].finished.catch((e) => { + if (e.name === 'AbortError') { + throw new Error( + 'The animation was aborted, keyframes have been removed before the animation execution end.' + ); + } + throw e; + }); + divs = target.querySelectorAll('.fading-div'); + assert.equal(divs.length, 1); + divs.forEach((div) => assert.equal(div.getAnimations().length, 1)); + animations.forEach((animation) => assert.equal(animation.playState, 'finished')); + assert.equal(window.document.head.querySelector('style')?.sheet.rules.length, 1); + assert.equal(elAnimatedOnInit.getAnimations().length, 0); + assert.equal(elAnimatedOnMount.getAnimations().length, 0); + + await waitUntil(() => window.document.head.querySelector('style') === null); + divs = target.querySelectorAll('.fading-div'); + assert.equal(divs.length, 1); + divs.forEach((div) => assert.equal(div.getAnimations().length, 0)); + animations.forEach((animation) => assert.equal(animation.playState, 'idle')); + assert.equal(window.document.head.querySelector('style'), null); + assert.equal(elAnimatedOnInit.getAnimations().length, 0); + assert.equal(elAnimatedOnMount.getAnimations().length, 0); + } +}; diff --git a/packages/svelte/test/runtime-browser/samples/transition-css-in/main.svelte b/packages/svelte/test/runtime-browser/samples/transition-css-in/main.svelte new file mode 100644 index 000000000000..66bfff856acc --- /dev/null +++ b/packages/svelte/test/runtime-browser/samples/transition-css-in/main.svelte @@ -0,0 +1,42 @@ + + +{#if visible} +
+{/if} + +{#if !visible} +
+{/if} + +
+ +{#if mounted} +
+{/if} diff --git a/packages/svelte/test/runtime-browser/samples/transition-css-out-in/_config.js b/packages/svelte/test/runtime-browser/samples/transition-css-out-in/_config.js deleted file mode 100644 index a50d28b257f4..000000000000 --- a/packages/svelte/test/runtime-browser/samples/transition-css-out-in/_config.js +++ /dev/null @@ -1,14 +0,0 @@ -export default { - test: async ({ assert, component, window, waitUntil }) => { - component.visible = true; - await waitUntil(() => window.document.head.querySelector('style').sheet.rules.length === 2); - assert.equal(window.document.head.querySelector('style').sheet.rules.length, 2); - await waitUntil(() => window.document.head.querySelector('style') === null); - assert.equal(window.document.head.querySelector('style'), null); - component.visible = false; - await waitUntil(() => window.document.head.querySelector('style').sheet.rules.length === 2); - assert.equal(window.document.head.querySelector('style').sheet.rules.length, 2); - await waitUntil(() => window.document.head.querySelector('style') === null); - assert.equal(window.document.head.querySelector('style'), null); - } -}; diff --git a/packages/svelte/test/runtime-browser/samples/transition-css-out-in/main.svelte b/packages/svelte/test/runtime-browser/samples/transition-css-out-in/main.svelte deleted file mode 100644 index 3ee93f0b5564..000000000000 --- a/packages/svelte/test/runtime-browser/samples/transition-css-out-in/main.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - -{#if visible} -
-{/if} - -{#if !visible} -
-{/if}