mirror of
https://github.com/acamarata/moon-cycle.git
synced 2026-07-01 11:14:31 +00:00
Port to TypeScript with dual CJS/ESM build via tsup. Fix off-by-one bug in both cycleMonth and cycleYear (images are 1-indexed; v1 returned 000.webp/0000.webp which don't exist). Add imageFolder and cdnUrl helpers for jsDelivr CDN integration. Export constants and types. Separate the ~438 MB image dataset from the npm package via the files field, making moon-cycle publishable to npm for the first time. Add CI workflow with Node 20/22/24 matrix, typecheck, and pack-check jobs. Add wiki-sync workflow and full .wiki/ documentation (Home, API Reference, Architecture, Migration Guide).
240 lines
9 KiB
JavaScript
240 lines
9 KiB
JavaScript
/**
|
|
* ESM test suite for moon-cycle v2.
|
|
* Uses Node.js built-in assert — no test framework required.
|
|
*/
|
|
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
cycleMonth,
|
|
cycleYear,
|
|
imageFolder,
|
|
cdnUrl,
|
|
SYNODIC_MONTH,
|
|
MONTH_IMAGES,
|
|
YEAR_IMAGES,
|
|
MONTH_ANCHOR,
|
|
YEAR_ANCHOR,
|
|
} from './dist/index.mjs';
|
|
|
|
let passed = 0;
|
|
let total = 0;
|
|
|
|
function test(name, fn) {
|
|
total++;
|
|
try {
|
|
fn();
|
|
console.log(`[${name}]... PASS`);
|
|
passed++;
|
|
} catch (err) {
|
|
console.error(`[${name}]... FAIL: ${err.message}`);
|
|
process.exitCode = 1;
|
|
}
|
|
}
|
|
|
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
|
|
test('SYNODIC_MONTH is correct IAU value', () => {
|
|
assert.strictEqual(SYNODIC_MONTH, 29.53058821398858);
|
|
});
|
|
|
|
test('MONTH_IMAGES is 708', () => {
|
|
assert.strictEqual(MONTH_IMAGES, 708);
|
|
});
|
|
|
|
test('YEAR_IMAGES is 8760', () => {
|
|
assert.strictEqual(YEAR_IMAGES, 8760);
|
|
});
|
|
|
|
test('MONTH_ANCHOR is 2023-11-13T09:27:00Z', () => {
|
|
assert.strictEqual(MONTH_ANCHOR.toISOString(), '2023-11-13T09:27:00.000Z');
|
|
});
|
|
|
|
test('YEAR_ANCHOR is 2023-01-01T00:00:00Z', () => {
|
|
assert.strictEqual(YEAR_ANCHOR.toISOString(), '2023-01-01T00:00:00.000Z');
|
|
});
|
|
|
|
// ─── cycleMonth: return format ────────────────────────────────────────────────
|
|
|
|
test('cycleMonth returns a string', () => {
|
|
assert.strictEqual(typeof cycleMonth(), 'string');
|
|
});
|
|
|
|
test('cycleMonth result matches /^\\d{3}\\.webp$/', () => {
|
|
const result = cycleMonth();
|
|
assert.match(result, /^\d{3}\.webp$/);
|
|
});
|
|
|
|
test('cycleMonth result is never 000.webp (images are 1-indexed)', () => {
|
|
// Test many dates across different lunar phases
|
|
const anchor = MONTH_ANCHOR.getTime();
|
|
const synodic_ms = SYNODIC_MONTH * 24 * 60 * 60 * 1000;
|
|
for (let i = 0; i < 100; i++) {
|
|
const date = new Date(anchor + i * (synodic_ms / 100));
|
|
const result = cycleMonth(date);
|
|
assert.notStrictEqual(result, '000.webp', `Got 000.webp for offset ${i}`);
|
|
}
|
|
});
|
|
|
|
// ─── cycleMonth: anchor date ──────────────────────────────────────────────────
|
|
|
|
test('cycleMonth at anchor date returns 001.webp (start of synodic cycle)', () => {
|
|
assert.strictEqual(cycleMonth(MONTH_ANCHOR), '001.webp');
|
|
});
|
|
|
|
test('cycleMonth one synodic month + 1 min after anchor returns near start of next cycle', () => {
|
|
// Adding exactly one synodic month in floating-point can land on either side of the
|
|
// cycle boundary (001 or 708) due to IEEE 754 rounding. Adding 1 minute steps past it.
|
|
const oneMonthPlus = new Date(
|
|
MONTH_ANCHOR.getTime() + SYNODIC_MONTH * 24 * 60 * 60 * 1000 + 60_000
|
|
);
|
|
const result = cycleMonth(oneMonthPlus);
|
|
const index = parseInt(result.replace('.webp', ''), 10);
|
|
assert(index <= 3, `Expected near start of next cycle (index <= 3), got ${index}`);
|
|
});
|
|
|
|
test('cycleMonth at halfway through synodic month returns ~354.webp', () => {
|
|
const half = new Date(
|
|
MONTH_ANCHOR.getTime() + (SYNODIC_MONTH / 2) * 24 * 60 * 60 * 1000
|
|
);
|
|
const result = cycleMonth(half);
|
|
const index = parseInt(result.replace('.webp', ''), 10);
|
|
// Allow ±1 for rounding
|
|
assert(index >= 353 && index <= 355, `Expected ~354, got ${index}`);
|
|
});
|
|
|
|
test('cycleMonth result is always in range [001, 708]', () => {
|
|
// Test dates spanning 5 years
|
|
const start = new Date('2020-01-01T00:00:00Z');
|
|
const step = 24 * 60 * 60 * 1000 * 7; // weekly
|
|
for (let i = 0; i < 260; i++) {
|
|
const date = new Date(start.getTime() + i * step);
|
|
const result = cycleMonth(date);
|
|
const index = parseInt(result.replace('.webp', ''), 10);
|
|
assert(index >= 1, `Index ${index} below minimum (date: ${date.toISOString()})`);
|
|
assert(index <= 708, `Index ${index} above maximum (date: ${date.toISOString()})`);
|
|
}
|
|
});
|
|
|
|
// ─── cycleMonth: past dates ───────────────────────────────────────────────────
|
|
|
|
test('cycleMonth handles dates before the anchor (pre-2023)', () => {
|
|
const past = new Date('2020-06-15T00:00:00Z');
|
|
const result = cycleMonth(past);
|
|
assert.match(result, /^\d{3}\.webp$/);
|
|
const index = parseInt(result.replace('.webp', ''), 10);
|
|
assert(index >= 1 && index <= 708);
|
|
});
|
|
|
|
// ─── cycleYear: return format ─────────────────────────────────────────────────
|
|
|
|
test('cycleYear returns a string', () => {
|
|
assert.strictEqual(typeof cycleYear(), 'string');
|
|
});
|
|
|
|
test('cycleYear result matches /^\\d{4}\\.webp$/', () => {
|
|
const result = cycleYear();
|
|
assert.match(result, /^\d{4}\.webp$/);
|
|
});
|
|
|
|
test('cycleYear result is never 0000.webp (images are 1-indexed)', () => {
|
|
const anchor = YEAR_ANCHOR.getTime();
|
|
const year_ms = YEAR_IMAGES * 60 * 60 * 1000;
|
|
for (let i = 0; i < 100; i++) {
|
|
const date = new Date(anchor + i * (year_ms / 100));
|
|
const result = cycleYear(date);
|
|
assert.notStrictEqual(result, '0000.webp', `Got 0000.webp for offset ${i}`);
|
|
}
|
|
});
|
|
|
|
// ─── cycleYear: anchor date ───────────────────────────────────────────────────
|
|
|
|
test('cycleYear at anchor date returns 0001.webp (start of year)', () => {
|
|
assert.strictEqual(cycleYear(YEAR_ANCHOR), '0001.webp');
|
|
});
|
|
|
|
test('cycleYear at exactly one year after anchor returns 0001.webp', () => {
|
|
const oneYear = new Date(
|
|
YEAR_ANCHOR.getTime() + YEAR_IMAGES * 60 * 60 * 1000
|
|
);
|
|
assert.strictEqual(cycleYear(oneYear), '0001.webp');
|
|
});
|
|
|
|
test('cycleYear at halfway through year returns ~4380.webp', () => {
|
|
const half = new Date(
|
|
YEAR_ANCHOR.getTime() + (YEAR_IMAGES / 2) * 60 * 60 * 1000
|
|
);
|
|
const result = cycleYear(half);
|
|
const index = parseInt(result.replace('.webp', ''), 10);
|
|
assert(index >= 4379 && index <= 4381, `Expected ~4380, got ${index}`);
|
|
});
|
|
|
|
test('cycleYear result is always in range [0001, 8760]', () => {
|
|
const start = new Date('2020-01-01T00:00:00Z');
|
|
const step = 24 * 60 * 60 * 1000 * 7;
|
|
for (let i = 0; i < 260; i++) {
|
|
const date = new Date(start.getTime() + i * step);
|
|
const result = cycleYear(date);
|
|
const index = parseInt(result.replace('.webp', ''), 10);
|
|
assert(index >= 1, `Index ${index} below minimum`);
|
|
assert(index <= 8760, `Index ${index} above maximum`);
|
|
}
|
|
});
|
|
|
|
test('cycleYear handles dates before 2023', () => {
|
|
const past = new Date('2021-06-15T00:00:00Z');
|
|
const result = cycleYear(past);
|
|
assert.match(result, /^\d{4}\.webp$/);
|
|
const index = parseInt(result.replace('.webp', ''), 10);
|
|
assert(index >= 1 && index <= 8760);
|
|
});
|
|
|
|
// ─── cycleMonth/cycleYear default parameter ───────────────────────────────────
|
|
|
|
test('cycleMonth() with no args returns a valid result', () => {
|
|
const result = cycleMonth();
|
|
assert.match(result, /^\d{3}\.webp$/);
|
|
});
|
|
|
|
test('cycleYear() with no args returns a valid result', () => {
|
|
const result = cycleYear();
|
|
assert.match(result, /^\d{4}\.webp$/);
|
|
});
|
|
|
|
// ─── imageFolder ──────────────────────────────────────────────────────────────
|
|
|
|
test('imageFolder returns correct folder name', () => {
|
|
assert.strictEqual(imageFolder('mm', 256, 75), 'mm-256-75');
|
|
assert.strictEqual(imageFolder('mm', 512, 85), 'mm-512-85');
|
|
assert.strictEqual(imageFolder('my', 256, 75), 'my-256-75');
|
|
assert.strictEqual(imageFolder('my', 512, 85), 'my-512-85');
|
|
});
|
|
|
|
// ─── cdnUrl ───────────────────────────────────────────────────────────────────
|
|
|
|
test('cdnUrl returns a valid jsDelivr URL', () => {
|
|
const url = cdnUrl('354.webp', 'mm', 256, 75);
|
|
assert.strictEqual(
|
|
url,
|
|
'https://cdn.jsdelivr.net/gh/acamarata/moon-cycle@main/mm-256-75/354.webp'
|
|
);
|
|
});
|
|
|
|
test('cdnUrl respects custom ref parameter', () => {
|
|
const url = cdnUrl('001.webp', 'my', 512, 85, 'v2.0.0');
|
|
assert.strictEqual(
|
|
url,
|
|
'https://cdn.jsdelivr.net/gh/acamarata/moon-cycle@v2.0.0/my-512-85/001.webp'
|
|
);
|
|
});
|
|
|
|
test('cdnUrl integrates with cycleMonth output', () => {
|
|
const filename = cycleMonth(MONTH_ANCHOR);
|
|
const url = cdnUrl(filename, 'mm', 256, 75);
|
|
assert(url.startsWith('https://cdn.jsdelivr.net/gh/acamarata/moon-cycle@main/mm-256-75/'));
|
|
assert(url.endsWith('.webp'));
|
|
});
|
|
|
|
// ─── Summary ─────────────────────────────────────────────────────────────────
|
|
|
|
console.log(`\n${passed}/${total} tests passed`);
|
|
if (passed < total) process.exit(1);
|