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) {
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}