Back to Repositories

Testing SvelteDate Reactive Operations in sveltejs/svelte

This test suite validates the SvelteDate class implementation, focusing on date manipulation methods and reactivity in Svelte applications. The tests cover comprehensive date operations, timezone handling, and reactive updates through Svelte’s effect system.

Test Coverage Overview

The test suite provides extensive coverage of date manipulation methods including:
  • Standard date operations (setDate, setMonth, setYear, etc.)
  • UTC operations and timezone handling
  • Millisecond precision timing
  • Edge cases for date rollovers and invalid inputs
Integration points include Svelte’s reactivity system through render_effect and effect_root.

Implementation Analysis

The testing approach employs Vitest for assertions and uses Svelte’s internal reactivity primitives. Tests follow a pattern of setting up reactive effects, performing date manipulations, and verifying both the immediate changes and cascading updates through the reactive system.

Key patterns include effect cleanup, synchronous flushing of updates, and comprehensive state logging.

Technical Details

Testing tools and configuration:
  • Vitest for test execution and assertions
  • Svelte’s render_effect and effect_root for reactivity testing
  • flushSync for synchronous update handling
  • Custom SvelteDate class extending native Date
  • Typescript for type safety

Best Practices Demonstrated

The test suite exemplifies high-quality testing practices through:
  • Isolated test cases with proper cleanup
  • Comprehensive edge case coverage
  • Reactive state verification
  • Granular method testing
  • Clear test descriptions and assertions

sveltejs/svelte

packages/svelte/src/reactivity/date.test.ts

            
import { render_effect, effect_root } from '../internal/client/reactivity/effects.js';
import { flushSync } from '../index-client.js';
import { SvelteDate } from './date.js';
import { assert, test } from 'vitest';
import { derived, get } from 'svelte/internal/client';

const initial_date = new Date(2023, 0, 2, 0, 0, 0, 0);
const a = new Date(2024, 1, 3, 1, 1, 1, 1);
const b = new Date(2025, 2, 4, 2, 2, 2, 2);
const c = new Date(2026, 3, 5, 3, 3, 3, 3);

test('date.setDate and date.setUTCDate', () => {
	const date = new SvelteDate(initial_date);
	const log: any = [];

	const cleanup = effect_root(() => {
		render_effect(() => {
			log.push(date.getDate());
		});
		render_effect(() => {
			log.push(date.getUTCDate());
		});
	});

	flushSync(() => {
		date.setDate(a.getDate());
	});

	flushSync(() => {
		date.setDate(date.getDate() + 1);
	});

	flushSync(() => {
		date.setDate(date.getDate()); // no change expected
	});

	flushSync(() => {
		date.setUTCDate(date.getUTCDate() + 1);
	});

	// Date/UTCDate may vary on some timezones
	const date_plus_zero = new Date(initial_date);
	date_plus_zero.setDate(a.getDate());
	const date_plus_one = new Date(initial_date);
	date_plus_one.setDate(a.getDate() + 1);
	const date_plus_two = new Date(initial_date);
	date_plus_two.setDate(a.getDate() + 2);

	assert.deepEqual(log, [
		initial_date.getDate(),
		initial_date.getUTCDate(),
		date_plus_zero.getDate(),
		date_plus_zero.getUTCDate(),
		date_plus_one.getDate(),
		date_plus_one.getUTCDate(),
		date_plus_two.getDate(),
		date_plus_two.getUTCDate()
	]);

	cleanup();
});

test('date.setFullYear and date.setUTCFullYear', () => {
	const date = new SvelteDate(initial_date);
	const log: any = [];

	const cleanup = effect_root(() => {
		render_effect(() => {
			log.push(date.getFullYear());
		});
		render_effect(() => {
			log.push(date.getUTCFullYear());
		});
	});

	flushSync(() => {
		date.setFullYear(a.getFullYear());
	});

	flushSync(() => {
		date.setFullYear(b.getFullYear());
	});

	flushSync(() => {
		date.setFullYear(b.getFullYear()); // no change expected
	});

	flushSync(() => {
		date.setUTCFullYear(c.getUTCFullYear());
	});

	assert.deepEqual(log, [
		initial_date.getFullYear(),
		initial_date.getUTCFullYear(),
		a.getFullYear(),
		a.getUTCFullYear(),
		b.getFullYear(),
		b.getUTCFullYear(),
		c.getFullYear(),
		c.getUTCFullYear()
	]);

	cleanup();
});

test('date.setHours and date.setUTCHours', () => {
	const date = new SvelteDate(initial_date);
	const log: any = [];

	const cleanup = effect_root(() => {
		render_effect(() => {
			log.push(date.getHours() % 24);
		});
		render_effect(() => {
			log.push(date.getUTCHours() % 24);
		});
	});

	flushSync(() => {
		date.setHours(a.getHours());
	});

	flushSync(() => {
		date.setHours(date.getHours() + 1);
	});

	flushSync(() => {
		date.setHours(date.getHours()); // no change expected
	});

	flushSync(() => {
		date.setUTCHours(date.getUTCHours() + 1);
	});

	assert.deepEqual(log, [
		initial_date.getHours(),
		initial_date.getUTCHours(),
		a.getHours() % 24,
		a.getUTCHours() % 24,
		(a.getHours() + 1) % 24,
		(a.getUTCHours() + 1) % 24,
		(a.getHours() + 2) % 24,
		(a.getUTCHours() + 2) % 24
	]);

	cleanup();
});

test('date.setMilliseconds and date.setUTCMilliseconds', () => {
	const date = new SvelteDate(initial_date);
	const log: any = [];

	const cleanup = effect_root(() => {
		render_effect(() => {
			log.push(date.getMilliseconds());
		});
		render_effect(() => {
			log.push(date.getUTCMilliseconds());
		});
	});

	flushSync(() => {
		date.setMilliseconds(a.getMilliseconds());
	});

	flushSync(() => {
		date.setMilliseconds(b.getMilliseconds());
	});

	flushSync(() => {
		date.setMilliseconds(b.getMilliseconds()); // no change expected
	});

	flushSync(() => {
		date.setUTCMilliseconds(c.getUTCMilliseconds());
	});

	assert.deepEqual(log, [
		initial_date.getMilliseconds(),
		initial_date.getUTCMilliseconds(),
		a.getMilliseconds(),
		a.getUTCMilliseconds(),
		b.getMilliseconds(),
		b.getUTCMilliseconds(),
		c.getMilliseconds(),
		c.getUTCMilliseconds()
	]);

	cleanup();
});

test('date.setMinutes and date.setUTCMinutes', () => {
	const date = new SvelteDate(initial_date);
	const log: any = [];

	const cleanup = effect_root(() => {
		render_effect(() => {
			log.push(date.getMinutes());
		});
		render_effect(() => {
			log.push(date.getUTCMinutes());
		});
	});

	flushSync(() => {
		date.setMinutes(a.getMinutes());
	});

	flushSync(() => {
		date.setMinutes(b.getMinutes());
	});

	flushSync(() => {
		date.setMinutes(b.getMinutes()); // no change expected
	});

	flushSync(() => {
		date.setUTCMinutes(c.getUTCMinutes());
	});

	assert.deepEqual(log, [
		initial_date.getMinutes(),
		initial_date.getUTCMinutes(),
		a.getMinutes(),
		a.getUTCMinutes(),
		b.getMinutes(),
		b.getUTCMinutes(),
		c.getMinutes(),
		c.getUTCMinutes()
	]);

	cleanup();
});

test('date.setMonth and date.setUTCMonth', () => {
	const date = new SvelteDate(initial_date);
	const log: any = [];

	const cleanup = effect_root(() => {
		render_effect(() => {
			log.push(date.getMonth());
		});
		render_effect(() => {
			log.push(date.getUTCMonth());
		});
	});

	flushSync(() => {
		date.setMonth(a.getMonth());
	});

	flushSync(() => {
		date.setMonth(b.getMonth());
	});

	flushSync(() => {
		date.setMonth(b.getMonth()); // no change expected
	});

	flushSync(() => {
		date.setUTCMonth(c.getUTCMonth());
	});

	assert.deepEqual(log, [
		initial_date.getMonth(),
		initial_date.getUTCMonth(),
		a.getMonth(),
		a.getUTCMonth(),
		b.getMonth(),
		b.getUTCMonth(),
		c.getMonth(),
		c.getUTCMonth()
	]);

	cleanup();
});

test('date.setSeconds and date.setUTCSeconds', () => {
	const date = new SvelteDate(initial_date);
	const log: any = [];

	const cleanup = effect_root(() => {
		render_effect(() => {
			log.push(date.getSeconds());
		});
		render_effect(() => {
			log.push(date.getUTCSeconds());
		});
	});

	flushSync(() => {
		date.setSeconds(a.getSeconds());
	});

	flushSync(() => {
		date.setSeconds(b.getSeconds());
	});

	flushSync(() => {
		date.setSeconds(b.getSeconds()); // no change expected
	});

	flushSync(() => {
		date.setUTCSeconds(c.getUTCSeconds());
	});

	assert.deepEqual(log, [
		initial_date.getSeconds(),
		initial_date.getUTCSeconds(),
		a.getSeconds(),
		a.getUTCSeconds(),
		b.getSeconds(),
		b.getUTCSeconds(),
		c.getSeconds(),
		c.getUTCSeconds()
	]);

	cleanup();
});

test('date.setTime', () => {
	const date = new SvelteDate(initial_date);
	const log: any = [];

	const cleanup = effect_root(() => {
		render_effect(() => {
			log.push(date.getTime());
		});
	});

	flushSync(() => {
		date.setTime(a.getTime());
	});

	flushSync(() => {
		date.setTime(b.getTime());
	});

	flushSync(() => {
		// nothing should happen here
		date.setTime(b.getTime());
	});

	assert.deepEqual(log, [initial_date.getTime(), a.getTime(), b.getTime()]);

	cleanup();
});

test('date.setYear', () => {
	const date = new SvelteDate(initial_date);
	const log: any = [];

	// @ts-expect-error
	if (!date.setYear) {
		return;
	}
	const cleanup = effect_root(() => {
		render_effect(() => {
			// @ts-expect-error
			log.push(date.getYear());
		});
	});

	flushSync(() => {
		// @ts-expect-error
		date.setYear(22);
	});

	flushSync(() => {
		// @ts-expect-error
		date.setYear(23);
	});

	flushSync(() => {
		// nothing should happen here
		// @ts-expect-error
		date.setYear(23);
	});

	// @ts-expect-error
	assert.deepEqual(log, [initial_date.getYear(), 22, 23]);

	cleanup();
});

test('date.setSeconds - edge cases', () => {
	const date = new SvelteDate(initial_date);
	const log: any = [];

	const cleanup = effect_root(() => {
		render_effect(() => {
			log.push(date.getSeconds());
		});
		render_effect(() => {
			log.push(date.getMinutes());
		});
	});

	flushSync(() => {
		date.setSeconds(60);
	});

	flushSync(() => {
		date.setSeconds(61);
	});

	assert.deepEqual(log, [
		initial_date.getSeconds(),
		initial_date.getMinutes(),
		initial_date.getMinutes() + 1,
		initial_date.getSeconds() + 1,
		initial_date.getMinutes() + 2
	]);

	cleanup();
});

test('Date propagated changes', () => {
	const date = new SvelteDate(initial_date);
	const log: any = [];

	const cleanup = effect_root(() => {
		render_effect(() => {
			log.push(date.getSeconds());
		});
		render_effect(() => {
			log.push(date.getMonth());
		});
		render_effect(() => {
			log.push(date.getFullYear());
		});
	});

	flushSync(() => {
		date.setMonth(13);
	});

	assert.deepEqual(log, [
		initial_date.getSeconds(),
		initial_date.getMonth(),
		initial_date.getFullYear(),
		1,
		2024
	]);

	cleanup();
});

test('Date fine grained tests', () => {
	const date = new SvelteDate(initial_date);

	let changes: Record<string, boolean> = {
		getFullYear: true,
		getUTCFullYear: true,
		getMonth: true,
		getUTCMonth: true,
		getDate: true,
		getUTCDate: true,
		getDay: true,
		getUTCDay: true,
		getHours: true,
		getUTCHours: true,
		getMinutes: true,
		getUTCMinutes: true,
		getSeconds: true,
		getUTCSeconds: true,
		getMilliseconds: true,
		getUTCMilliseconds: true,
		getTime: true,
		toISOString: true,
		toJSON: true,
		toUTCString: true,
		toString: true,
		toLocaleString: true
	};
	let test_description: string = '';

	const expect_all_changes_to_be_false = () => {
		for (const key of Object.keys(changes) as Array<keyof typeof Date>) {
			assert.equal(changes[key], false, `${test_description}: effect for ${key} was not fired`);
		}
	};

	const cleanup = effect_root(() => {
		for (const key of Object.keys(changes)) {
			render_effect(() => {
				// @ts-ignore
				date[key]();
				assert.equal(changes[key], true, `${test_description}: for ${key}`);
				changes[key] = false;
			});
		}
	});

	flushSync(() => {
		expect_all_changes_to_be_false();
		changes = {
			...changes,
			getFullYear: true,
			getUTCFullYear: true,
			getMonth: true,
			getUTCMonth: true,
			getDay: true,
			getUTCDay: true,
			getTime: true,
			toISOString: true,
			toJSON: true,
			toUTCString: true,
			toString: true,
			toLocaleString: true
		};
		test_description = 'changing setFullYear that will cause month/day change as well';
		date.setFullYear(initial_date.getFullYear() + 1, initial_date.getMonth() + 1);
	});

	flushSync(() => {
		expect_all_changes_to_be_false();
		changes = {
			...changes,
			getDate: true,
			getUTCDate: true,
			getDay: true,
			getUTCDay: true,
			getHours: true,
			getUTCHours: true,
			getMinutes: true,
			getUTCMinutes: true,
			getSeconds: true,
			getUTCSeconds: true,
			getMilliseconds: true,
			getUTCMilliseconds: true,
			getTime: true,
			toISOString: true,
			toJSON: true,
			toUTCString: true,
			toString: true,
			toLocaleString: true
		};
		test_description = 'changing seconds that will change day/hour/minutes/seconds/milliseconds';
		date.setSeconds(61 * 60 * 25 + 1, 10);
	});

	flushSync(() => {
		expect_all_changes_to_be_false();
		changes = {
			...changes,
			getMonth: true,
			getUTCMonth: true,
			getDay: true,
			getUTCDay: true,
			getMilliseconds: true,
			getUTCMilliseconds: true,
			getTime: true,
			toISOString: true,
			toJSON: true,
			toUTCString: true,
			toString: true,
			toLocaleString: true
		};
		test_description = 'changing month';
		date.setMonth(date.getMonth() + 1);
	});

	cleanup();
});

test('Date.toLocaleString', () => {
	const date = new SvelteDate(initial_date);

	const log: any = [];

	const cleanup = effect_root(() => {
		render_effect(() => {
			log.push(date.toLocaleString(undefined, { month: 'long', year: 'numeric' }));
		});
		render_effect(() => {
			log.push(date.toLocaleString(undefined, { month: 'long' }));
		});
	});

	flushSync();

	assert.deepEqual(log, [
		initial_date.toLocaleString(undefined, { month: 'long', year: 'numeric' }),
		initial_date.toLocaleString(undefined, { month: 'long' })
	]);

	cleanup();
});

test('Date.valueOf', () => {
	const date = new SvelteDate(initial_date);

	const log: any = [];

	const cleanup = effect_root(() => {
		render_effect(() => {
			log.push(date.valueOf());
		});
	});

	flushSync();

	assert.deepEqual(log, [initial_date.valueOf()]);

	flushSync(() => {
		date.setTime(date.getTime() + 10);
	});

	assert.deepEqual(log, [initial_date.valueOf(), new Date(initial_date.getTime() + 10).valueOf()]);

	cleanup();
});

test('Date.instanceOf', () => {
	assert.equal(new SvelteDate() instanceof Date, true);
});

test('Date methods invoked for the first time in a derived', () => {
	const date = new SvelteDate(initial_date);
	const log: any = [];

	const cleanup = effect_root(() => {
		const months = derived(() => {
			return date.getMonth();
		});

		render_effect(() => {
			log.push(get(months));
		});

		flushSync(() => {
			date.setMonth(date.getMonth() + 1);
		});

		flushSync(() => {
			date.setMonth(date.getMonth() + 1);
		});
	});

	assert.deepEqual(log, [0, 1, 2]);

	cleanup();
});

test('Date methods shared between deriveds', () => {
	const date = new SvelteDate(initial_date);
	const log: any = [];

	const cleanup = effect_root(() => {
		const year = derived(() => {
			return date.getFullYear();
		});
		const year2 = derived(() => {
			return date.getTime(), date.getFullYear();
		});

		render_effect(() => {
			log.push(get(year) + '/' + get(year2).toString());
		});

		flushSync(() => {
			date.setFullYear(date.getFullYear() + 1);
		});

		flushSync(() => {
			date.setFullYear(date.getFullYear() + 1);
		});
	});

	assert.deepEqual(log, ['2023/2023', '2024/2024', '2025/2025']);

	cleanup();
});