generated from nhcarrigan/template
feat: testy
This commit is contained in:
+7
-1
@@ -14,4 +14,10 @@ Thumbs.db
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
.idea/
|
||||
|
||||
# Coverage
|
||||
coverage/
|
||||
.coverage
|
||||
htmlcov/
|
||||
*.lcov
|
||||
@@ -1,4 +1,4 @@
|
||||
.PHONY: help install install-ts install-py build lint lint-ts lint-py format format-py format-check format-check-py test clean run
|
||||
.PHONY: help install install-ts install-py build lint lint-ts lint-py format format-py format-check format-check-py test test-ts test-py coverage coverage-ts coverage-py clean run
|
||||
|
||||
# Default target - show help
|
||||
help:
|
||||
@@ -12,7 +12,12 @@ help:
|
||||
@echo " make lint-py - Run Python linter only"
|
||||
@echo " make format - Format Python code"
|
||||
@echo " make format-check - Check Python formatting without modifying"
|
||||
@echo " make test - Run tests"
|
||||
@echo " make test - Run all tests (TypeScript and Python)"
|
||||
@echo " make test-ts - Run TypeScript tests only"
|
||||
@echo " make test-py - Run Python tests only"
|
||||
@echo " make coverage - Run all tests with coverage"
|
||||
@echo " make coverage-ts - Run TypeScript tests with coverage"
|
||||
@echo " make coverage-py - Run Python tests with coverage"
|
||||
@echo " make clean - Clean build artifacts and caches"
|
||||
@echo ""
|
||||
@echo "Running scripts:"
|
||||
@@ -61,10 +66,27 @@ format-check: format-check-py
|
||||
format-check-py:
|
||||
cd python && uv run ruff format --check .
|
||||
|
||||
# Run tests
|
||||
test:
|
||||
@echo "No tests configured yet"
|
||||
@exit 0
|
||||
# Run all tests
|
||||
test: test-ts test-py
|
||||
|
||||
# Run TypeScript tests
|
||||
test-ts:
|
||||
cd typescript && pnpm test
|
||||
|
||||
# Run Python tests
|
||||
test-py:
|
||||
cd python && uv run pytest -v
|
||||
|
||||
# Run all tests with coverage
|
||||
coverage: coverage-ts coverage-py
|
||||
|
||||
# Run TypeScript tests with coverage
|
||||
coverage-ts:
|
||||
cd typescript && pnpm test:coverage
|
||||
|
||||
# Run Python tests with coverage
|
||||
coverage-py:
|
||||
cd python && uv run pytest --cov=. --cov-report=term-missing -v
|
||||
|
||||
# Clean build artifacts and caches
|
||||
clean:
|
||||
|
||||
@@ -91,4 +91,9 @@ known-first-party = ["py"]
|
||||
quote-style = "double"
|
||||
indent-style = "space"
|
||||
skip-magic-trailing-comma = false
|
||||
line-ending = "auto"
|
||||
line-ending = "auto"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
python_files = ["test_*.py"]
|
||||
python_functions = ["test_*"]
|
||||
@@ -1,5 +1,9 @@
|
||||
# Development dependencies
|
||||
ruff==0.14.14
|
||||
pytest==8.3.5
|
||||
pytest-mock==3.14.0
|
||||
responses==0.25.3
|
||||
pytest-asyncio==0.24.0
|
||||
|
||||
# Runtime dependencies
|
||||
requests==2.32.3
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# Test package for ephemere Python scripts
|
||||
@@ -0,0 +1,207 @@
|
||||
"""Tests for analyse_availability functions.
|
||||
|
||||
@copyright NHCarrigan
|
||||
@license Naomi's Public License
|
||||
@author Naomi Carrigan
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
# Add the cohort directory to the path for imports
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "cohort"))
|
||||
|
||||
from analyse_availability import (
|
||||
analyze_applicant_availability,
|
||||
get_utc_blocks_for_hour,
|
||||
local_hour_to_utc,
|
||||
parse_time_slots,
|
||||
parse_utc_offset,
|
||||
)
|
||||
|
||||
|
||||
class TestParseUtcOffset:
|
||||
"""Tests for parse_utc_offset function."""
|
||||
|
||||
def test_positive_offset(self):
|
||||
"""Should parse positive UTC offset."""
|
||||
assert parse_utc_offset("Europe/London (UTC+0)") == 0
|
||||
assert parse_utc_offset("Europe/Paris (UTC+1)") == 1
|
||||
assert parse_utc_offset("Asia/Tokyo (UTC+9)") == 9
|
||||
|
||||
def test_negative_offset(self):
|
||||
"""Should parse negative UTC offset."""
|
||||
assert parse_utc_offset("America/New_York (UTC-5)") == -5
|
||||
assert parse_utc_offset("America/Los_Angeles (UTC-8)") == -8
|
||||
|
||||
def test_offset_with_minutes(self):
|
||||
"""Should parse offset with minutes component."""
|
||||
assert parse_utc_offset("Asia/Kolkata (UTC+5:30)") == 5.5
|
||||
assert parse_utc_offset("Asia/Kathmandu (UTC+5:45)") == 5.75
|
||||
|
||||
def test_negative_offset_with_minutes(self):
|
||||
"""Should parse negative offset with minutes."""
|
||||
assert parse_utc_offset("Canada/Newfoundland (UTC-3:30)") == -3.5
|
||||
|
||||
def test_no_match_returns_zero(self):
|
||||
"""Should return 0 when no UTC offset found."""
|
||||
assert parse_utc_offset("Unknown/Timezone") == 0
|
||||
assert parse_utc_offset("") == 0
|
||||
|
||||
|
||||
class TestParseTimeSlots:
|
||||
"""Tests for parse_time_slots function."""
|
||||
|
||||
def test_single_slot(self):
|
||||
"""Should parse a single time slot."""
|
||||
result = parse_time_slots("17:00-18:00")
|
||||
assert result == [(17, 18)]
|
||||
|
||||
def test_multiple_slots(self):
|
||||
"""Should parse multiple time slots separated by semicolon."""
|
||||
result = parse_time_slots("07:00-08:00; 19:00-20:00")
|
||||
assert result == [(7, 8), (19, 20)]
|
||||
|
||||
def test_na_values(self):
|
||||
"""Should return empty list for N/A values."""
|
||||
assert parse_time_slots("N/A") == []
|
||||
assert parse_time_slots("na") == []
|
||||
assert parse_time_slots("") == []
|
||||
|
||||
def test_wider_time_range(self):
|
||||
"""Should parse wider time ranges."""
|
||||
result = parse_time_slots("09:00-17:00")
|
||||
assert result == [(9, 17)]
|
||||
|
||||
def test_midnight_crossing_slots(self):
|
||||
"""Should parse time slots that approach midnight."""
|
||||
result = parse_time_slots("22:00-23:00")
|
||||
assert result == [(22, 23)]
|
||||
|
||||
|
||||
class TestLocalHourToUtc:
|
||||
"""Tests for local_hour_to_utc function."""
|
||||
|
||||
def test_positive_offset(self):
|
||||
"""Should convert local hour with positive UTC offset."""
|
||||
assert local_hour_to_utc(12, 1) == 11 # Noon in UTC+1 -> 11:00 UTC
|
||||
assert local_hour_to_utc(0, 9) == 15 # Midnight in UTC+9 -> 15:00 UTC
|
||||
|
||||
def test_negative_offset(self):
|
||||
"""Should convert local hour with negative UTC offset."""
|
||||
assert local_hour_to_utc(12, -5) == 17 # Noon in UTC-5 -> 17:00 UTC
|
||||
assert local_hour_to_utc(20, -8) == 4 # 8pm in UTC-8 -> 4:00 UTC (next day)
|
||||
|
||||
def test_wrapping_around_midnight(self):
|
||||
"""Should wrap around correctly at midnight."""
|
||||
assert local_hour_to_utc(1, 5) == 20 # 1am in UTC+5 -> 20:00 UTC (prev day)
|
||||
assert local_hour_to_utc(23, -3) == 2 # 11pm in UTC-3 -> 2:00 UTC (next day)
|
||||
|
||||
def test_zero_offset(self):
|
||||
"""Should return same hour for zero offset."""
|
||||
assert local_hour_to_utc(15, 0) == 15
|
||||
|
||||
|
||||
class TestGetUtcBlocksForHour:
|
||||
"""Tests for get_utc_blocks_for_hour function."""
|
||||
|
||||
def test_mornings_block(self):
|
||||
"""Should return mornings for 6-11 UTC."""
|
||||
assert "mornings" in get_utc_blocks_for_hour(6)
|
||||
assert "mornings" in get_utc_blocks_for_hour(9)
|
||||
assert "mornings" in get_utc_blocks_for_hour(11)
|
||||
assert "mornings" not in get_utc_blocks_for_hour(12)
|
||||
|
||||
def test_afternoons_block(self):
|
||||
"""Should return afternoons for 12-17 UTC."""
|
||||
assert "afternoons" in get_utc_blocks_for_hour(12)
|
||||
assert "afternoons" in get_utc_blocks_for_hour(15)
|
||||
assert "afternoons" in get_utc_blocks_for_hour(17)
|
||||
assert "afternoons" not in get_utc_blocks_for_hour(18)
|
||||
|
||||
def test_evenings_block(self):
|
||||
"""Should return evenings for 18-23 UTC."""
|
||||
assert "evenings" in get_utc_blocks_for_hour(18)
|
||||
assert "evenings" in get_utc_blocks_for_hour(21)
|
||||
assert "evenings" in get_utc_blocks_for_hour(23)
|
||||
assert "evenings" not in get_utc_blocks_for_hour(0)
|
||||
|
||||
def test_nights_block(self):
|
||||
"""Should return nights for 0-5 UTC."""
|
||||
assert "nights" in get_utc_blocks_for_hour(0)
|
||||
assert "nights" in get_utc_blocks_for_hour(3)
|
||||
assert "nights" in get_utc_blocks_for_hour(5)
|
||||
assert "nights" not in get_utc_blocks_for_hour(6)
|
||||
|
||||
|
||||
class TestAnalyzeApplicantAvailability:
|
||||
"""Tests for analyze_applicant_availability function."""
|
||||
|
||||
def test_basic_availability(self):
|
||||
"""Should analyze basic availability correctly."""
|
||||
day_slots = {
|
||||
"Monday": [(9, 12)],
|
||||
"Tuesday": [(9, 12)],
|
||||
"Wednesday": [(9, 12)],
|
||||
"Thursday": [],
|
||||
"Friday": [],
|
||||
"Saturday": [],
|
||||
"Sunday": [],
|
||||
}
|
||||
result = analyze_applicant_availability("UTC (UTC+0)", day_slots)
|
||||
|
||||
assert result["utc_offset"] == 0
|
||||
assert "mornings" in result["available_blocks"]
|
||||
|
||||
def test_no_availability(self):
|
||||
"""Should return empty blocks when no availability."""
|
||||
day_slots = {
|
||||
"Monday": [],
|
||||
"Tuesday": [],
|
||||
"Wednesday": [],
|
||||
"Thursday": [],
|
||||
"Friday": [],
|
||||
"Saturday": [],
|
||||
"Sunday": [],
|
||||
}
|
||||
result = analyze_applicant_availability("UTC (UTC+0)", day_slots)
|
||||
|
||||
assert result["available_blocks"] == []
|
||||
assert result["total_unique_utc_hours"] == 0
|
||||
|
||||
def test_timezone_conversion(self):
|
||||
"""Should correctly convert timezones."""
|
||||
day_slots = {
|
||||
"Monday": [(17, 20)], # 5pm-8pm local
|
||||
"Tuesday": [(17, 20)],
|
||||
"Wednesday": [(17, 20)],
|
||||
"Thursday": [],
|
||||
"Friday": [],
|
||||
"Saturday": [],
|
||||
"Sunday": [],
|
||||
}
|
||||
result = analyze_applicant_availability(
|
||||
"America/New_York (UTC-5)", day_slots
|
||||
)
|
||||
|
||||
assert result["utc_offset"] == -5
|
||||
# 5pm-8pm in UTC-5 = 22:00-01:00 UTC -> evenings/nights
|
||||
assert "evenings" in result["available_blocks"]
|
||||
|
||||
def test_block_count_threshold(self):
|
||||
"""Should only include blocks with 3+ occurrences."""
|
||||
day_slots = {
|
||||
"Monday": [(9, 10)], # Only 1 hour
|
||||
"Tuesday": [(9, 10)], # Only 1 hour
|
||||
"Wednesday": [],
|
||||
"Thursday": [],
|
||||
"Friday": [],
|
||||
"Saturday": [],
|
||||
"Sunday": [],
|
||||
}
|
||||
result = analyze_applicant_availability("UTC (UTC+0)", day_slots)
|
||||
|
||||
# Only 2 hours, threshold is 3
|
||||
assert "mornings" not in result["available_blocks"]
|
||||
@@ -5,7 +5,10 @@
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "op run --env-file=prod.env --no-masking -- tsx"
|
||||
"start": "op run --env-file=prod.env --no-masking -- tsx",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest run --coverage"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
@@ -24,5 +27,9 @@
|
||||
"open": "11.0.0",
|
||||
"tsx": "4.20.5",
|
||||
"typescript": "5.9.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"vitest": "3.2.4"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+332
@@ -44,9 +44,20 @@ importers:
|
||||
typescript:
|
||||
specifier: 5.9.2
|
||||
version: 5.9.2
|
||||
devDependencies:
|
||||
'@vitest/coverage-v8':
|
||||
specifier: ^3.2.4
|
||||
version: 3.2.4(vitest@3.2.4(@types/node@24.3.0)(tsx@4.20.5))
|
||||
vitest:
|
||||
specifier: 3.2.4
|
||||
version: 3.2.4(@types/node@24.3.0)(tsx@4.20.5)
|
||||
|
||||
packages:
|
||||
|
||||
'@ampproject/remapping@2.3.0':
|
||||
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
'@aws-crypto/crc32@5.2.0':
|
||||
resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
@@ -210,10 +221,31 @@ packages:
|
||||
resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-string-parser@7.27.1':
|
||||
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-validator-identifier@7.27.1':
|
||||
resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-validator-identifier@7.28.5':
|
||||
resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/parser@7.29.0':
|
||||
resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
|
||||
'@babel/types@7.29.0':
|
||||
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@bcoe/v8-coverage@1.0.2':
|
||||
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@es-joy/jsdoccomment@0.49.0':
|
||||
resolution: {integrity: sha512-xjZTSFgECpb9Ohuk5yMX5RhUEbfeQcuOp8IF60e+wyzWEF0M5xeSgqsfLtvPEX8BIyOX9saZqzuGPmZ8oWc+5Q==}
|
||||
engines: {node: '>=16'}
|
||||
@@ -589,9 +621,27 @@ packages:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
'@istanbuljs/schema@0.1.3':
|
||||
resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
||||
|
||||
'@jridgewell/resolve-uri@3.1.2':
|
||||
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.5':
|
||||
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
|
||||
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||
|
||||
'@nhcarrigan/eslint-config@5.2.0':
|
||||
resolution: {integrity: sha512-YpTTqhviKMlRwKF+RC/GYiA5i2jTCmg8uftuiufldneNV5HMbGpTfBbV7tpa8++5mpYJc4+eZaf40QbDiz84dQ==}
|
||||
engines: {node: '>=22', pnpm: '>=9'}
|
||||
@@ -672,6 +722,10 @@ packages:
|
||||
'@octokit/types@14.1.0':
|
||||
resolution: {integrity: sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==}
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
'@pkgr/core@0.1.2':
|
||||
resolution: {integrity: sha512-fdDH1LSGfZdTH2sxdpVMw31BanV28K/Gry0cVFxaNP77neJSkd82mM8ErPNYs9e+0O7SdHBLTDzDgwUuy18RnQ==}
|
||||
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
||||
@@ -1136,6 +1190,15 @@ packages:
|
||||
resolution: {integrity: sha512-+GeGMebMCy0elMNg67LRNoVnUFPIm37iu5CmHESVx56/9Jsfdpsvbv605DQ81Pi/x11IdKUsS5nzgTYbCQU9fg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@vitest/coverage-v8@3.2.4':
|
||||
resolution: {integrity: sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==}
|
||||
peerDependencies:
|
||||
'@vitest/browser': 3.2.4
|
||||
vitest: 3.2.4
|
||||
peerDependenciesMeta:
|
||||
'@vitest/browser':
|
||||
optional: true
|
||||
|
||||
'@vitest/eslint-plugin@1.1.24':
|
||||
resolution: {integrity: sha512-7IaENe4NNy33g0iuuy5bHY69JYYRjpv4lMx6H5Wp30W7ez2baLHwxsXF5TM4wa8JDYZt8ut99Ytoj7GiDO01hw==}
|
||||
peerDependencies:
|
||||
@@ -1200,10 +1263,18 @@ packages:
|
||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
ansi-regex@6.2.2:
|
||||
resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
ansi-styles@4.3.0:
|
||||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
ansi-styles@6.2.3:
|
||||
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
are-docs-informative@0.0.2:
|
||||
resolution: {integrity: sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==}
|
||||
engines: {node: '>=14'}
|
||||
@@ -1251,6 +1322,9 @@ packages:
|
||||
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
ast-v8-to-istanbul@0.3.11:
|
||||
resolution: {integrity: sha512-Qya9fkoofMjCBNVdWINMjB5KZvkYfaO9/anwkWnjxibpWUxo5iHl2sOdP7/uAqaRuUYuoo8rDwnbaaKVFxoUvw==}
|
||||
|
||||
async-function@1.0.0:
|
||||
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -1434,12 +1508,18 @@ packages:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
eastasianwidth@0.2.0:
|
||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||
|
||||
electron-to-chromium@1.5.210:
|
||||
resolution: {integrity: sha512-20kSVv1tyNBN2VFsjCIJZfyvxqo7ylHPrJLME040f/030lzNMA7uQNpxtqJjWSNpccD8/2sqe53EAjrFPvQmjw==}
|
||||
|
||||
emoji-regex@8.0.0:
|
||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||
|
||||
emoji-regex@9.2.2:
|
||||
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
|
||||
|
||||
error-ex@1.3.2:
|
||||
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
|
||||
|
||||
@@ -1679,6 +1759,10 @@ packages:
|
||||
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
foreground-child@3.3.1:
|
||||
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
fsevents@2.3.2:
|
||||
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
@@ -1722,6 +1806,10 @@ packages:
|
||||
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
glob@10.5.0:
|
||||
resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==}
|
||||
hasBin: true
|
||||
|
||||
globals@13.24.0:
|
||||
resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1779,6 +1867,9 @@ packages:
|
||||
hosted-git-info@2.8.9:
|
||||
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
|
||||
|
||||
html-escaper@2.0.2:
|
||||
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
|
||||
|
||||
iconv-lite@0.7.0:
|
||||
resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -1938,10 +2029,32 @@ packages:
|
||||
isexe@2.0.0:
|
||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||
|
||||
istanbul-lib-coverage@3.2.2:
|
||||
resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
istanbul-lib-report@3.0.1:
|
||||
resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
istanbul-lib-source-maps@5.0.6:
|
||||
resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
istanbul-reports@3.2.0:
|
||||
resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
iterator.prototype@1.1.5:
|
||||
resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
jackspeak@3.4.3:
|
||||
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
||||
|
||||
js-tokens@10.0.0:
|
||||
resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==}
|
||||
|
||||
js-tokens@4.0.0:
|
||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||
|
||||
@@ -2013,9 +2126,19 @@ packages:
|
||||
loupe@3.2.1:
|
||||
resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
|
||||
|
||||
lru-cache@10.4.3:
|
||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||
|
||||
magic-string@0.30.18:
|
||||
resolution: {integrity: sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==}
|
||||
|
||||
magicast@0.3.5:
|
||||
resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==}
|
||||
|
||||
make-dir@4.0.0:
|
||||
resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
math-intrinsics@1.1.0:
|
||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -2042,6 +2165,10 @@ packages:
|
||||
minimist@1.2.8:
|
||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||
|
||||
minipass@7.1.2:
|
||||
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
@@ -2127,6 +2254,9 @@ packages:
|
||||
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
package-json-from-dist@1.0.1:
|
||||
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
|
||||
|
||||
parent-module@1.0.1:
|
||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -2150,6 +2280,10 @@ packages:
|
||||
path-parse@1.0.7:
|
||||
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
|
||||
|
||||
path-scurry@1.11.1:
|
||||
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
|
||||
engines: {node: '>=16 || 14 >=14.18'}
|
||||
|
||||
path-type@4.0.0:
|
||||
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -2379,6 +2513,9 @@ packages:
|
||||
stackback@0.0.2:
|
||||
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
|
||||
|
||||
std-env@3.10.0:
|
||||
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
|
||||
|
||||
std-env@3.9.0:
|
||||
resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==}
|
||||
|
||||
@@ -2390,6 +2527,10 @@ packages:
|
||||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
string-width@5.1.2:
|
||||
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
string.prototype.matchall@4.0.12:
|
||||
resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -2413,6 +2554,10 @@ packages:
|
||||
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
strip-ansi@7.1.2:
|
||||
resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
strip-bom@3.0.0:
|
||||
resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -2443,6 +2588,10 @@ packages:
|
||||
resolution: {integrity: sha512-JJoOEKTfL1urb1mDoEblhD9NhEbWmq9jHEMEnxoC4ujUaZ4itA8vKgwkFAyNClgxplLi9tsUKX+EduK0p/l7sg==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
|
||||
test-exclude@7.0.1:
|
||||
resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
tinybench@2.9.0:
|
||||
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
|
||||
|
||||
@@ -2658,6 +2807,14 @@ packages:
|
||||
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
wrap-ansi@8.1.0:
|
||||
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
wsl-utils@0.3.0:
|
||||
resolution: {integrity: sha512-3sFIGLiaDP7rTO4xh3g+b3AzhYDIUGGywE/WsmqzJWDxus5aJXVnPTNC/6L+r2WzrwXqVOdD262OaO+cEyPMSQ==}
|
||||
engines: {node: '>=20'}
|
||||
@@ -2672,6 +2829,11 @@ packages:
|
||||
|
||||
snapshots:
|
||||
|
||||
'@ampproject/remapping@2.3.0':
|
||||
dependencies:
|
||||
'@jridgewell/gen-mapping': 0.3.13
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
|
||||
'@aws-crypto/crc32@5.2.0':
|
||||
dependencies:
|
||||
'@aws-crypto/util': 5.2.0
|
||||
@@ -3157,8 +3319,23 @@ snapshots:
|
||||
js-tokens: 4.0.0
|
||||
picocolors: 1.1.1
|
||||
|
||||
'@babel/helper-string-parser@7.27.1': {}
|
||||
|
||||
'@babel/helper-validator-identifier@7.27.1': {}
|
||||
|
||||
'@babel/helper-validator-identifier@7.28.5': {}
|
||||
|
||||
'@babel/parser@7.29.0':
|
||||
dependencies:
|
||||
'@babel/types': 7.29.0
|
||||
|
||||
'@babel/types@7.29.0':
|
||||
dependencies:
|
||||
'@babel/helper-string-parser': 7.27.1
|
||||
'@babel/helper-validator-identifier': 7.28.5
|
||||
|
||||
'@bcoe/v8-coverage@1.0.2': {}
|
||||
|
||||
'@es-joy/jsdoccomment@0.49.0':
|
||||
dependencies:
|
||||
comment-parser: 1.4.1
|
||||
@@ -3451,8 +3628,31 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/node': 24.3.0
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
dependencies:
|
||||
string-width: 5.1.2
|
||||
string-width-cjs: string-width@4.2.3
|
||||
strip-ansi: 7.1.2
|
||||
strip-ansi-cjs: strip-ansi@6.0.1
|
||||
wrap-ansi: 8.1.0
|
||||
wrap-ansi-cjs: wrap-ansi@7.0.0
|
||||
|
||||
'@istanbuljs/schema@0.1.3': {}
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
|
||||
'@jridgewell/resolve-uri@3.1.2': {}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.5': {}
|
||||
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
dependencies:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
'@nhcarrigan/eslint-config@5.2.0(@typescript-eslint/utils@8.41.0(eslint@9.34.0)(typescript@5.9.2))(eslint@9.34.0)(playwright@1.55.0)(react@19.1.1)(typescript@5.9.2)(vitest@3.2.4(@types/node@24.3.0)(tsx@4.20.5))':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-plugin-eslint-comments': 4.4.1(eslint@9.34.0)
|
||||
@@ -3560,6 +3760,9 @@ snapshots:
|
||||
dependencies:
|
||||
'@octokit/openapi-types': 25.1.0
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
optional: true
|
||||
|
||||
'@pkgr/core@0.1.2': {}
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.49.0':
|
||||
@@ -4163,6 +4366,25 @@ snapshots:
|
||||
'@typescript-eslint/types': 8.41.0
|
||||
eslint-visitor-keys: 4.2.1
|
||||
|
||||
'@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@24.3.0)(tsx@4.20.5))':
|
||||
dependencies:
|
||||
'@ampproject/remapping': 2.3.0
|
||||
'@bcoe/v8-coverage': 1.0.2
|
||||
ast-v8-to-istanbul: 0.3.11
|
||||
debug: 4.4.1
|
||||
istanbul-lib-coverage: 3.2.2
|
||||
istanbul-lib-report: 3.0.1
|
||||
istanbul-lib-source-maps: 5.0.6
|
||||
istanbul-reports: 3.2.0
|
||||
magic-string: 0.30.18
|
||||
magicast: 0.3.5
|
||||
std-env: 3.10.0
|
||||
test-exclude: 7.0.1
|
||||
tinyrainbow: 2.0.0
|
||||
vitest: 3.2.4(@types/node@24.3.0)(tsx@4.20.5)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@vitest/eslint-plugin@1.1.24(@typescript-eslint/utils@8.41.0(eslint@9.34.0)(typescript@5.9.2))(eslint@9.34.0)(typescript@5.9.2)(vitest@3.2.4(@types/node@24.3.0)(tsx@4.20.5))':
|
||||
dependencies:
|
||||
'@typescript-eslint/utils': 8.41.0(eslint@9.34.0)(typescript@5.9.2)
|
||||
@@ -4234,10 +4456,14 @@ snapshots:
|
||||
|
||||
ansi-regex@5.0.1: {}
|
||||
|
||||
ansi-regex@6.2.2: {}
|
||||
|
||||
ansi-styles@4.3.0:
|
||||
dependencies:
|
||||
color-convert: 2.0.1
|
||||
|
||||
ansi-styles@6.2.3: {}
|
||||
|
||||
are-docs-informative@0.0.2: {}
|
||||
|
||||
argparse@2.0.1: {}
|
||||
@@ -4313,6 +4539,12 @@ snapshots:
|
||||
|
||||
assertion-error@2.0.1: {}
|
||||
|
||||
ast-v8-to-istanbul@0.3.11:
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
estree-walker: 3.0.3
|
||||
js-tokens: 10.0.0
|
||||
|
||||
async-function@1.0.0: {}
|
||||
|
||||
available-typed-arrays@1.0.7:
|
||||
@@ -4488,10 +4720,14 @@ snapshots:
|
||||
es-errors: 1.3.0
|
||||
gopd: 1.2.0
|
||||
|
||||
eastasianwidth@0.2.0: {}
|
||||
|
||||
electron-to-chromium@1.5.210: {}
|
||||
|
||||
emoji-regex@8.0.0: {}
|
||||
|
||||
emoji-regex@9.2.2: {}
|
||||
|
||||
error-ex@1.3.2:
|
||||
dependencies:
|
||||
is-arrayish: 0.2.1
|
||||
@@ -4900,6 +5136,11 @@ snapshots:
|
||||
dependencies:
|
||||
is-callable: 1.2.7
|
||||
|
||||
foreground-child@3.3.1:
|
||||
dependencies:
|
||||
cross-spawn: 7.0.6
|
||||
signal-exit: 4.1.0
|
||||
|
||||
fsevents@2.3.2:
|
||||
optional: true
|
||||
|
||||
@@ -4955,6 +5196,15 @@ snapshots:
|
||||
dependencies:
|
||||
is-glob: 4.0.3
|
||||
|
||||
glob@10.5.0:
|
||||
dependencies:
|
||||
foreground-child: 3.3.1
|
||||
jackspeak: 3.4.3
|
||||
minimatch: 9.0.5
|
||||
minipass: 7.1.2
|
||||
package-json-from-dist: 1.0.1
|
||||
path-scurry: 1.11.1
|
||||
|
||||
globals@13.24.0:
|
||||
dependencies:
|
||||
type-fest: 0.20.2
|
||||
@@ -5005,6 +5255,8 @@ snapshots:
|
||||
|
||||
hosted-git-info@2.8.9: {}
|
||||
|
||||
html-escaper@2.0.2: {}
|
||||
|
||||
iconv-lite@0.7.0:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
@@ -5157,6 +5409,27 @@ snapshots:
|
||||
|
||||
isexe@2.0.0: {}
|
||||
|
||||
istanbul-lib-coverage@3.2.2: {}
|
||||
|
||||
istanbul-lib-report@3.0.1:
|
||||
dependencies:
|
||||
istanbul-lib-coverage: 3.2.2
|
||||
make-dir: 4.0.0
|
||||
supports-color: 7.2.0
|
||||
|
||||
istanbul-lib-source-maps@5.0.6:
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
debug: 4.4.1
|
||||
istanbul-lib-coverage: 3.2.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
istanbul-reports@3.2.0:
|
||||
dependencies:
|
||||
html-escaper: 2.0.2
|
||||
istanbul-lib-report: 3.0.1
|
||||
|
||||
iterator.prototype@1.1.5:
|
||||
dependencies:
|
||||
define-data-property: 1.1.4
|
||||
@@ -5166,6 +5439,14 @@ snapshots:
|
||||
has-symbols: 1.1.0
|
||||
set-function-name: 2.0.2
|
||||
|
||||
jackspeak@3.4.3:
|
||||
dependencies:
|
||||
'@isaacs/cliui': 8.0.2
|
||||
optionalDependencies:
|
||||
'@pkgjs/parseargs': 0.11.0
|
||||
|
||||
js-tokens@10.0.0: {}
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
|
||||
js-tokens@9.0.1: {}
|
||||
@@ -5226,10 +5507,22 @@ snapshots:
|
||||
|
||||
loupe@3.2.1: {}
|
||||
|
||||
lru-cache@10.4.3: {}
|
||||
|
||||
magic-string@0.30.18:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
magicast@0.3.5:
|
||||
dependencies:
|
||||
'@babel/parser': 7.29.0
|
||||
'@babel/types': 7.29.0
|
||||
source-map-js: 1.2.1
|
||||
|
||||
make-dir@4.0.0:
|
||||
dependencies:
|
||||
semver: 7.7.2
|
||||
|
||||
math-intrinsics@1.1.0: {}
|
||||
|
||||
merge2@1.4.1: {}
|
||||
@@ -5251,6 +5544,8 @@ snapshots:
|
||||
|
||||
minimist@1.2.8: {}
|
||||
|
||||
minipass@7.1.2: {}
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
mute-stream@2.0.0: {}
|
||||
@@ -5352,6 +5647,8 @@ snapshots:
|
||||
|
||||
p-try@2.2.0: {}
|
||||
|
||||
package-json-from-dist@1.0.1: {}
|
||||
|
||||
parent-module@1.0.1:
|
||||
dependencies:
|
||||
callsites: 3.1.0
|
||||
@@ -5374,6 +5671,11 @@ snapshots:
|
||||
|
||||
path-parse@1.0.7: {}
|
||||
|
||||
path-scurry@1.11.1:
|
||||
dependencies:
|
||||
lru-cache: 10.4.3
|
||||
minipass: 7.1.2
|
||||
|
||||
path-type@4.0.0: {}
|
||||
|
||||
pathe@2.0.3: {}
|
||||
@@ -5627,6 +5929,8 @@ snapshots:
|
||||
|
||||
stackback@0.0.2: {}
|
||||
|
||||
std-env@3.10.0: {}
|
||||
|
||||
std-env@3.9.0: {}
|
||||
|
||||
stop-iteration-iterator@1.1.0:
|
||||
@@ -5640,6 +5944,12 @@ snapshots:
|
||||
is-fullwidth-code-point: 3.0.0
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
string-width@5.1.2:
|
||||
dependencies:
|
||||
eastasianwidth: 0.2.0
|
||||
emoji-regex: 9.2.2
|
||||
strip-ansi: 7.1.2
|
||||
|
||||
string.prototype.matchall@4.0.12:
|
||||
dependencies:
|
||||
call-bind: 1.0.8
|
||||
@@ -5688,6 +5998,10 @@ snapshots:
|
||||
dependencies:
|
||||
ansi-regex: 5.0.1
|
||||
|
||||
strip-ansi@7.1.2:
|
||||
dependencies:
|
||||
ansi-regex: 6.2.2
|
||||
|
||||
strip-bom@3.0.0: {}
|
||||
|
||||
strip-indent@3.0.0:
|
||||
@@ -5713,6 +6027,12 @@ snapshots:
|
||||
'@pkgr/core': 0.1.2
|
||||
tslib: 2.8.1
|
||||
|
||||
test-exclude@7.0.1:
|
||||
dependencies:
|
||||
'@istanbuljs/schema': 0.1.3
|
||||
glob: 10.5.0
|
||||
minimatch: 9.0.5
|
||||
|
||||
tinybench@2.9.0: {}
|
||||
|
||||
tinyexec@0.3.2: {}
|
||||
@@ -5960,6 +6280,18 @@ snapshots:
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
wrap-ansi@8.1.0:
|
||||
dependencies:
|
||||
ansi-styles: 6.2.3
|
||||
string-width: 5.1.2
|
||||
strip-ansi: 7.1.2
|
||||
|
||||
wsl-utils@0.3.0:
|
||||
dependencies:
|
||||
is-wsl: 3.1.0
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* @copyright NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { getFiles } from "../getFiles.js";
|
||||
|
||||
describe("getFiles", () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(global, "fetch");
|
||||
vi.spyOn(console, "log").mockImplementation(() => {
|
||||
return undefined;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should fetch files from a project", async() => {
|
||||
const mockFiles = [
|
||||
{ data: { id: 1, name: "file1.json", path: "/file1.json" } },
|
||||
{ data: { id: 2, name: "file2.json", path: "/file2.json" } },
|
||||
];
|
||||
|
||||
vi.mocked(global.fetch).mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve({ data: mockFiles });
|
||||
},
|
||||
} as Response);
|
||||
|
||||
const result = await getFiles(
|
||||
"123",
|
||||
"https://api.crowdin.com/api/v2",
|
||||
"test-token",
|
||||
);
|
||||
|
||||
expect(result).toEqual(mockFiles);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
"https://api.crowdin.com/api/v2/projects/123/files?limit=500",
|
||||
{
|
||||
headers: {
|
||||
authorization: "Bearer test-token",
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle pagination when there are 500+ files", async() => {
|
||||
const files500 = Array.from({ length: 500 }, (_, index) => {
|
||||
return { data: { id: index, name: `file${String(index)}.json` } };
|
||||
});
|
||||
const filesRemaining = [
|
||||
{ data: { id: 500, name: "file500.json" } },
|
||||
{ data: { id: 501, name: "file501.json" } },
|
||||
];
|
||||
|
||||
vi.mocked(global.fetch).
|
||||
mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve({ data: files500 });
|
||||
},
|
||||
} as Response).
|
||||
mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve({ data: filesRemaining });
|
||||
},
|
||||
} as Response);
|
||||
|
||||
const result = await getFiles(
|
||||
"123",
|
||||
"https://api.crowdin.com/api/v2",
|
||||
"token",
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(502);
|
||||
expect(global.fetch).toHaveBeenCalledTimes(2);
|
||||
expect(global.fetch).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
"https://api.crowdin.com/api/v2/projects/123/files?limit=500",
|
||||
expect.any(Object),
|
||||
);
|
||||
expect(global.fetch).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
"https://api.crowdin.com/api/v2/projects/123/files?limit=500&offset=500",
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("should return empty array when no files exist", async() => {
|
||||
vi.mocked(global.fetch).mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve({ data: [] });
|
||||
},
|
||||
} as Response);
|
||||
|
||||
const result = await getFiles(
|
||||
"456",
|
||||
"https://api.crowdin.com/api/v2",
|
||||
"token",
|
||||
);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(global.fetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* @copyright NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { getLanguages } from "../getLanguages.js";
|
||||
|
||||
describe("getLanguages", () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(global, "fetch");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should fetch project and return target language IDs", async() => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
id: 1,
|
||||
name: "Test Project",
|
||||
targetLanguageIds: [ "de", "fr", "es", "ja" ],
|
||||
},
|
||||
};
|
||||
|
||||
vi.mocked(global.fetch).mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve(mockResponse);
|
||||
},
|
||||
} as Response);
|
||||
|
||||
const result = await getLanguages(
|
||||
"123",
|
||||
"https://api.crowdin.com/api/v2",
|
||||
"test-token",
|
||||
);
|
||||
|
||||
expect(result).toEqual([ "de", "fr", "es", "ja" ]);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
"https://api.crowdin.com/api/v2/projects/123",
|
||||
{
|
||||
headers: {
|
||||
authorization: "Bearer test-token",
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("should return empty array when no target languages", async() => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
id: 2,
|
||||
name: "Empty Project",
|
||||
targetLanguageIds: [],
|
||||
},
|
||||
};
|
||||
|
||||
vi.mocked(global.fetch).mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve(mockResponse);
|
||||
},
|
||||
} as Response);
|
||||
|
||||
const result = await getLanguages(
|
||||
"456",
|
||||
"https://api.crowdin.com/api/v2",
|
||||
"token",
|
||||
);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("should use the correct API URL", async() => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
targetLanguageIds: [ "en" ],
|
||||
},
|
||||
};
|
||||
|
||||
vi.mocked(global.fetch).mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve(mockResponse);
|
||||
},
|
||||
} as Response);
|
||||
|
||||
await getLanguages(
|
||||
"789",
|
||||
"https://custom.crowdin.com/v2",
|
||||
"my-token",
|
||||
);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
"https://custom.crowdin.com/v2/projects/789",
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* @copyright NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { getStrings } from "../getStrings.js";
|
||||
|
||||
describe("getStrings", () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(global, "fetch");
|
||||
vi.spyOn(console, "log").mockImplementation(() => {
|
||||
return undefined;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should fetch strings from a project", async() => {
|
||||
const mockStrings = [
|
||||
{ data: { id: 1, text: "Hello", identifier: "greeting" } },
|
||||
{ data: { id: 2, text: "Goodbye", identifier: "farewell" } },
|
||||
];
|
||||
|
||||
vi.mocked(global.fetch).mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve({ data: mockStrings });
|
||||
},
|
||||
} as Response);
|
||||
|
||||
const result = await getStrings(
|
||||
"123",
|
||||
"https://api.crowdin.com/api/v2",
|
||||
"test-token",
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ id: 1, text: "Hello", identifier: "greeting" },
|
||||
{ id: 2, text: "Goodbye", identifier: "farewell" },
|
||||
]);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
"https://api.crowdin.com/api/v2/projects/123/strings?limit=500",
|
||||
{
|
||||
headers: {
|
||||
authorization: "Bearer test-token",
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle pagination when there are 500+ strings", async() => {
|
||||
const strings500 = Array.from({ length: 500 }, (_, index) => {
|
||||
return { data: { id: index, text: `String ${String(index)}` } };
|
||||
});
|
||||
const stringsRemaining = [
|
||||
{ data: { id: 500, text: "String 500" } },
|
||||
{ data: { id: 501, text: "String 501" } },
|
||||
];
|
||||
|
||||
vi.mocked(global.fetch).
|
||||
mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve({ data: strings500 });
|
||||
},
|
||||
} as Response).
|
||||
mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve({ data: stringsRemaining });
|
||||
},
|
||||
} as Response);
|
||||
|
||||
const result = await getStrings(
|
||||
"123",
|
||||
"https://api.crowdin.com/api/v2",
|
||||
"token",
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(502);
|
||||
expect(global.fetch).toHaveBeenCalledTimes(2);
|
||||
expect(global.fetch).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
"https://api.crowdin.com/api/v2/projects/123/strings?limit=500&offset=500",
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("should return empty array when no strings exist", async() => {
|
||||
vi.mocked(global.fetch).mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve({ data: [] });
|
||||
},
|
||||
} as Response);
|
||||
|
||||
const result = await getStrings(
|
||||
"456",
|
||||
"https://api.crowdin.com/api/v2",
|
||||
"token",
|
||||
);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(global.fetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should unwrap the data from each string object", async() => {
|
||||
const mockStrings = [
|
||||
{
|
||||
data: {
|
||||
context: "greeting context",
|
||||
id: 1,
|
||||
identifier: "hello",
|
||||
isHidden: false,
|
||||
text: "Hello World",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(global.fetch).mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve({ data: mockStrings });
|
||||
},
|
||||
} as Response);
|
||||
|
||||
const result = await getStrings(
|
||||
"789",
|
||||
"https://api.crowdin.com/api/v2",
|
||||
"token",
|
||||
);
|
||||
|
||||
expect(result[0]).toEqual({
|
||||
context: "greeting context",
|
||||
id: 1,
|
||||
identifier: "hello",
|
||||
isHidden: false,
|
||||
text: "Hello World",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* @copyright NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { backoffAndRetry } from "../backoffAndRetry.js";
|
||||
|
||||
describe("backoffAndRetry", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.spyOn(global, "fetch");
|
||||
vi.spyOn(console, "error").mockImplementation(() => {
|
||||
return undefined;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should return JSON data on successful response", async() => {
|
||||
const mockData = { success: true, data: "test" };
|
||||
vi.mocked(global.fetch).mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve(mockData);
|
||||
},
|
||||
ok: true,
|
||||
} as Response);
|
||||
|
||||
const result = await backoffAndRetry<typeof mockData>(
|
||||
"https://example.com/api",
|
||||
);
|
||||
|
||||
expect(result).toEqual(mockData);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
"https://example.com/api",
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
it("should pass options to fetch", async() => {
|
||||
const mockData = { result: "success" };
|
||||
const options: RequestInit = {
|
||||
headers: { authorization: "Bearer token" },
|
||||
method: "POST",
|
||||
};
|
||||
|
||||
vi.mocked(global.fetch).mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve(mockData);
|
||||
},
|
||||
ok: true,
|
||||
} as Response);
|
||||
|
||||
await backoffAndRetry("https://example.com/api", options);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
"https://example.com/api",
|
||||
options,
|
||||
);
|
||||
});
|
||||
|
||||
it("should retry after 5 seconds on 429 response", async() => {
|
||||
const mockData = { result: "success" };
|
||||
|
||||
vi.mocked(global.fetch).
|
||||
mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 429,
|
||||
} as Response).
|
||||
mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve(mockData);
|
||||
},
|
||||
ok: true,
|
||||
} as Response);
|
||||
|
||||
const resultPromise = backoffAndRetry<typeof mockData>(
|
||||
"https://example.com/api",
|
||||
);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
|
||||
const result = await resultPromise;
|
||||
|
||||
expect(result).toEqual(mockData);
|
||||
expect(global.fetch).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("should handle multiple 429 responses with backoff", async() => {
|
||||
const mockData = { result: "finally" };
|
||||
|
||||
vi.mocked(global.fetch).
|
||||
mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 429,
|
||||
} as Response).
|
||||
mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 429,
|
||||
} as Response).
|
||||
mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve(mockData);
|
||||
},
|
||||
ok: true,
|
||||
} as Response);
|
||||
|
||||
const resultPromise = backoffAndRetry<typeof mockData>(
|
||||
"https://example.com/api",
|
||||
);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
|
||||
const result = await resultPromise;
|
||||
|
||||
expect(result).toEqual(mockData);
|
||||
expect(global.fetch).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it("should return null on non-429 error response", async() => {
|
||||
vi.mocked(global.fetch).mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 500,
|
||||
} as Response);
|
||||
|
||||
const result = await backoffAndRetry("https://example.com/api");
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(console.error).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should return null on fetch error", async() => {
|
||||
vi.mocked(global.fetch).mockRejectedValueOnce(new Error("Network error"));
|
||||
|
||||
const result = await backoffAndRetry("https://example.com/api");
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(console.error).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should return null on JSON parse error", async() => {
|
||||
vi.mocked(global.fetch).mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.reject(new Error("Invalid JSON"));
|
||||
},
|
||||
ok: true,
|
||||
} as Response);
|
||||
|
||||
const result = await backoffAndRetry("https://example.com/api");
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(console.error).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,242 @@
|
||||
/**
|
||||
* @copyright NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { getMimeType } from "../mimeType.js";
|
||||
|
||||
describe("getMimeType", () => {
|
||||
describe("image types", () => {
|
||||
it("should return image/png for .png files", () => {
|
||||
expect(getMimeType("image.png")).toBe("image/png");
|
||||
expect(getMimeType("/path/to/image.png")).toBe("image/png");
|
||||
});
|
||||
|
||||
it("should return image/jpeg for .jpg and .jpeg files", () => {
|
||||
expect(getMimeType("photo.jpg")).toBe("image/jpeg");
|
||||
expect(getMimeType("photo.jpeg")).toBe("image/jpeg");
|
||||
});
|
||||
|
||||
it("should return image/gif for .gif files", () => {
|
||||
expect(getMimeType("animation.gif")).toBe("image/gif");
|
||||
});
|
||||
|
||||
it("should return image/webp for .webp files", () => {
|
||||
expect(getMimeType("image.webp")).toBe("image/webp");
|
||||
});
|
||||
|
||||
it("should return image/svg+xml for .svg files", () => {
|
||||
expect(getMimeType("icon.svg")).toBe("image/svg+xml");
|
||||
});
|
||||
|
||||
it("should return image/bmp for .bmp files", () => {
|
||||
expect(getMimeType("bitmap.bmp")).toBe("image/bmp");
|
||||
});
|
||||
|
||||
it("should return image/tiff for .tif and .tiff files", () => {
|
||||
expect(getMimeType("scan.tif")).toBe("image/tiff");
|
||||
expect(getMimeType("scan.tiff")).toBe("image/tiff");
|
||||
});
|
||||
|
||||
it("should return image/x-icon for .ico files", () => {
|
||||
expect(getMimeType("favicon.ico")).toBe("image/x-icon");
|
||||
});
|
||||
});
|
||||
|
||||
describe("video types", () => {
|
||||
it("should return video/mp4 for .mp4 files", () => {
|
||||
expect(getMimeType("video.mp4")).toBe("video/mp4");
|
||||
});
|
||||
|
||||
it("should return video/webm for .webm files", () => {
|
||||
expect(getMimeType("video.webm")).toBe("video/webm");
|
||||
});
|
||||
|
||||
it("should return video/x-msvideo for .avi files", () => {
|
||||
expect(getMimeType("video.avi")).toBe("video/x-msvideo");
|
||||
});
|
||||
|
||||
it("should return video/quicktime for .mov files", () => {
|
||||
expect(getMimeType("video.mov")).toBe("video/quicktime");
|
||||
});
|
||||
|
||||
it("should return video/x-matroska for .mkv files", () => {
|
||||
expect(getMimeType("video.mkv")).toBe("video/x-matroska");
|
||||
});
|
||||
});
|
||||
|
||||
describe("audio types", () => {
|
||||
it("should return audio/mpeg for .mp3 files", () => {
|
||||
expect(getMimeType("song.mp3")).toBe("audio/mpeg");
|
||||
});
|
||||
|
||||
it("should return audio/wav for .wav files", () => {
|
||||
expect(getMimeType("sound.wav")).toBe("audio/wav");
|
||||
});
|
||||
|
||||
it("should return audio/ogg for .ogg files", () => {
|
||||
expect(getMimeType("audio.ogg")).toBe("audio/ogg");
|
||||
});
|
||||
|
||||
it("should return audio/aac for .aac files", () => {
|
||||
expect(getMimeType("audio.aac")).toBe("audio/aac");
|
||||
});
|
||||
|
||||
it("should return audio/flac for .flac files", () => {
|
||||
expect(getMimeType("music.flac")).toBe("audio/flac");
|
||||
});
|
||||
});
|
||||
|
||||
describe("document types", () => {
|
||||
it("should return application/pdf for .pdf files", () => {
|
||||
expect(getMimeType("document.pdf")).toBe("application/pdf");
|
||||
});
|
||||
|
||||
it("should return application/json for .json files", () => {
|
||||
expect(getMimeType("data.json")).toBe("application/json");
|
||||
});
|
||||
|
||||
it("should return text/plain for .txt files", () => {
|
||||
expect(getMimeType("readme.txt")).toBe("text/plain");
|
||||
});
|
||||
|
||||
it("should return text/markdown for .md files", () => {
|
||||
expect(getMimeType("README.md")).toBe("text/markdown");
|
||||
});
|
||||
|
||||
it("should return text/html for .html and .htm files", () => {
|
||||
expect(getMimeType("page.html")).toBe("text/html");
|
||||
expect(getMimeType("page.htm")).toBe("text/html");
|
||||
});
|
||||
|
||||
it("should return text/css for .css files", () => {
|
||||
expect(getMimeType("styles.css")).toBe("text/css");
|
||||
});
|
||||
|
||||
it("should return text/javascript for .js files", () => {
|
||||
expect(getMimeType("script.js")).toBe("text/javascript");
|
||||
});
|
||||
|
||||
it("should return text/csv for .csv files", () => {
|
||||
expect(getMimeType("data.csv")).toBe("text/csv");
|
||||
});
|
||||
|
||||
it("should return application/xml for .xml files", () => {
|
||||
expect(getMimeType("data.xml")).toBe("application/xml");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Microsoft Office types", () => {
|
||||
it("should return correct MIME type for .doc files", () => {
|
||||
expect(getMimeType("document.doc")).toBe("application/msword");
|
||||
});
|
||||
|
||||
it("should return correct MIME type for .docx files", () => {
|
||||
expect(getMimeType("document.docx")).toBe(
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
);
|
||||
});
|
||||
|
||||
it("should return correct MIME type for .xls files", () => {
|
||||
expect(getMimeType("spreadsheet.xls")).toBe("application/vnd.ms-excel");
|
||||
});
|
||||
|
||||
it("should return correct MIME type for .xlsx files", () => {
|
||||
expect(getMimeType("spreadsheet.xlsx")).toBe(
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
);
|
||||
});
|
||||
|
||||
it("should return correct MIME type for .ppt files", () => {
|
||||
expect(getMimeType("presentation.ppt")).toBe(
|
||||
"application/vnd.ms-powerpoint",
|
||||
);
|
||||
});
|
||||
|
||||
it("should return correct MIME type for .pptx files", () => {
|
||||
expect(getMimeType("presentation.pptx")).toBe(
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("font types", () => {
|
||||
it("should return font/ttf for .ttf files", () => {
|
||||
expect(getMimeType("font.ttf")).toBe("font/ttf");
|
||||
});
|
||||
|
||||
it("should return font/otf for .otf files", () => {
|
||||
expect(getMimeType("font.otf")).toBe("font/otf");
|
||||
});
|
||||
|
||||
it("should return font/woff for .woff files", () => {
|
||||
expect(getMimeType("font.woff")).toBe("font/woff");
|
||||
});
|
||||
|
||||
it("should return font/woff2 for .woff2 files", () => {
|
||||
expect(getMimeType("font.woff2")).toBe("font/woff2");
|
||||
});
|
||||
|
||||
it("should return application/vnd.ms-fontobject for .eot files", () => {
|
||||
expect(getMimeType("font.eot")).toBe("application/vnd.ms-fontobject");
|
||||
});
|
||||
});
|
||||
|
||||
describe("archive types", () => {
|
||||
it("should return application/zip for .zip files", () => {
|
||||
expect(getMimeType("archive.zip")).toBe("application/zip");
|
||||
});
|
||||
|
||||
it("should return application/gzip for .gz files", () => {
|
||||
expect(getMimeType("archive.gz")).toBe("application/gzip");
|
||||
});
|
||||
|
||||
it("should return application/x-tar for .tar files", () => {
|
||||
expect(getMimeType("archive.tar")).toBe("application/x-tar");
|
||||
});
|
||||
|
||||
it("should return application/x-rar-compressed for .rar files", () => {
|
||||
expect(getMimeType("archive.rar")).toBe("application/x-rar-compressed");
|
||||
});
|
||||
|
||||
it("should return application/x-7z-compressed for .7z files", () => {
|
||||
expect(getMimeType("archive.7z")).toBe("application/x-7z-compressed");
|
||||
});
|
||||
});
|
||||
|
||||
describe("case insensitivity", () => {
|
||||
it("should handle uppercase extensions", () => {
|
||||
expect(getMimeType("IMAGE.PNG")).toBe("image/png");
|
||||
expect(getMimeType("VIDEO.MP4")).toBe("video/mp4");
|
||||
});
|
||||
|
||||
it("should handle mixed case extensions", () => {
|
||||
expect(getMimeType("file.JpG")).toBe("image/jpeg");
|
||||
expect(getMimeType("file.Mp3")).toBe("audio/mpeg");
|
||||
});
|
||||
});
|
||||
|
||||
describe("unknown types", () => {
|
||||
it("should return undefined for unknown extensions", () => {
|
||||
expect(getMimeType("file.xyz")).toBeUndefined();
|
||||
expect(getMimeType("file.unknown")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return undefined for files without extensions", () => {
|
||||
expect(getMimeType("filename")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("path handling", () => {
|
||||
it("should handle full paths", () => {
|
||||
expect(getMimeType("/home/user/documents/image.png")).toBe("image/png");
|
||||
expect(getMimeType("./relative/path/video.mp4")).toBe("video/mp4");
|
||||
});
|
||||
|
||||
it("should handle filenames with multiple dots", () => {
|
||||
expect(getMimeType("file.name.with.dots.png")).toBe("image/png");
|
||||
expect(getMimeType("archive.tar.gz")).toBe("application/gzip");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,258 @@
|
||||
/**
|
||||
* @copyright NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { paginatedFetch } from "../paginatedFetch.js";
|
||||
|
||||
type TestItem = Record<string, unknown>;
|
||||
|
||||
describe("paginatedFetch", () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(global, "fetch");
|
||||
vi.spyOn(console, "log").mockImplementation(() => {
|
||||
return undefined;
|
||||
});
|
||||
vi.spyOn(console, "error").mockImplementation(() => {
|
||||
return undefined;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should fetch a single page when results fit in one request", async() => {
|
||||
const mockData = [
|
||||
{ id: 1, name: "item1" },
|
||||
{ id: 2, name: "item2" },
|
||||
];
|
||||
|
||||
vi.mocked(global.fetch).
|
||||
mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve(mockData);
|
||||
},
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
} as Response).
|
||||
mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve([]);
|
||||
},
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
} as Response);
|
||||
|
||||
const result = await paginatedFetch<Array<TestItem>>(
|
||||
"https://example.com/api",
|
||||
10,
|
||||
);
|
||||
|
||||
expect(result).toEqual(mockData);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
"https://example.com/api?limit=10&page=1&offset=0",
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle multiple pages", async() => {
|
||||
const page1 = [
|
||||
{ id: 1 },
|
||||
{ id: 2 },
|
||||
];
|
||||
const page2 = [
|
||||
{ id: 3 },
|
||||
{ id: 4 },
|
||||
];
|
||||
const page3: Array<TestItem> = [];
|
||||
|
||||
vi.mocked(global.fetch).
|
||||
mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve(page1);
|
||||
},
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
} as Response).
|
||||
mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve(page2);
|
||||
},
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
} as Response).
|
||||
mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve(page3);
|
||||
},
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
} as Response);
|
||||
|
||||
const result = await paginatedFetch<Array<TestItem>>(
|
||||
"https://example.com/api",
|
||||
2,
|
||||
);
|
||||
|
||||
expect(result).toEqual([ ...page1, ...page2 ]);
|
||||
expect(global.fetch).toHaveBeenCalledTimes(3);
|
||||
expect(global.fetch).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
"https://example.com/api?limit=2&page=1&offset=0",
|
||||
{},
|
||||
);
|
||||
expect(global.fetch).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
"https://example.com/api?limit=2&page=2&offset=2",
|
||||
{},
|
||||
);
|
||||
expect(global.fetch).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
"https://example.com/api?limit=2&page=3&offset=4",
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
it("should pass options to fetch", async() => {
|
||||
const options: RequestInit = {
|
||||
headers: { authorization: "Bearer token" },
|
||||
};
|
||||
|
||||
vi.mocked(global.fetch).
|
||||
mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve([{ id: 1 }]);
|
||||
},
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
} as Response).
|
||||
mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve([]);
|
||||
},
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
} as Response);
|
||||
|
||||
await paginatedFetch<Array<TestItem>>(
|
||||
"https://example.com/api",
|
||||
10,
|
||||
options,
|
||||
);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
options,
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw on first page error", async() => {
|
||||
vi.mocked(global.fetch).mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 500,
|
||||
statusText: "Internal Server Error",
|
||||
} as Response);
|
||||
|
||||
await expect(
|
||||
paginatedFetch<Array<TestItem>>("https://example.com/api", 10),
|
||||
).rejects.toThrow(
|
||||
"Failed to fetch https://example.com/api?limit=10&page=1&offset=0: 500 Internal Server Error",
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw if first page is not an array", async() => {
|
||||
vi.mocked(global.fetch).mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve({ data: "not an array" });
|
||||
},
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
} as Response);
|
||||
|
||||
await expect(
|
||||
paginatedFetch<Array<TestItem>>("https://example.com/api", 10),
|
||||
).rejects.toThrow("Expected array response but got object");
|
||||
});
|
||||
|
||||
it("should stop pagination on subsequent page error", async() => {
|
||||
vi.mocked(global.fetch).
|
||||
mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve([{ id: 1 }]);
|
||||
},
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
} as Response).
|
||||
mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 500,
|
||||
statusText: "Internal Server Error",
|
||||
} as Response);
|
||||
|
||||
const result = await paginatedFetch<Array<TestItem>>(
|
||||
"https://example.com/api",
|
||||
10,
|
||||
);
|
||||
|
||||
expect(result).toEqual([{ id: 1 }]);
|
||||
expect(console.error).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should stop pagination if subsequent page is not an array", async() => {
|
||||
vi.mocked(global.fetch).
|
||||
mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve([{ id: 1 }]);
|
||||
},
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
} as Response).
|
||||
mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve({ notAnArray: true });
|
||||
},
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
} as Response);
|
||||
|
||||
const result = await paginatedFetch<Array<TestItem>>(
|
||||
"https://example.com/api",
|
||||
10,
|
||||
);
|
||||
|
||||
expect(result).toEqual([{ id: 1 }]);
|
||||
expect(console.error).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle empty first page", async() => {
|
||||
vi.mocked(global.fetch).mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve([]);
|
||||
},
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
} as Response);
|
||||
|
||||
const result = await paginatedFetch<Array<TestItem>>(
|
||||
"https://example.com/api",
|
||||
10,
|
||||
);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(global.fetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* @copyright NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { serialiseJsonOrError } from "../serialiseJsonOrError.js";
|
||||
|
||||
describe("serialiseJsonOrError", () => {
|
||||
describe("valid JSON parsing", () => {
|
||||
it("should parse a simple object", () => {
|
||||
const result = serialiseJsonOrError('{"key": "value"}');
|
||||
expect(result).toEqual({ key: "value" });
|
||||
});
|
||||
|
||||
it("should parse an object with multiple properties", () => {
|
||||
const result = serialiseJsonOrError(
|
||||
'{"name": "test", "count": 42, "active": true}',
|
||||
);
|
||||
expect(result).toEqual({ name: "test", count: 42, active: true });
|
||||
});
|
||||
|
||||
it("should parse nested objects", () => {
|
||||
const result = serialiseJsonOrError(
|
||||
'{"outer": {"inner": {"deep": "value"}}}',
|
||||
);
|
||||
expect(result).toEqual({ outer: { inner: { deep: "value" } } });
|
||||
});
|
||||
|
||||
it("should parse objects with arrays", () => {
|
||||
const result = serialiseJsonOrError('{"items": [1, 2, 3]}');
|
||||
expect(result).toEqual({ items: [ 1, 2, 3 ] });
|
||||
});
|
||||
|
||||
it("should parse objects with null values", () => {
|
||||
const result = serialiseJsonOrError('{"value": null}');
|
||||
expect(result).toEqual({ value: null });
|
||||
});
|
||||
|
||||
it("should parse an empty object", () => {
|
||||
const result = serialiseJsonOrError("{}");
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it("should parse objects with special characters in strings", () => {
|
||||
const result = serialiseJsonOrError('{"text": "hello\\nworld"}');
|
||||
expect(result).toEqual({ text: "hello\nworld" });
|
||||
});
|
||||
|
||||
it("should parse objects with unicode characters", () => {
|
||||
const result = serialiseJsonOrError('{"emoji": "🎉", "text": "日本語"}');
|
||||
expect(result).toEqual({ emoji: "🎉", text: "日本語" });
|
||||
});
|
||||
|
||||
it("should parse objects with numeric keys (as strings)", () => {
|
||||
const result = serialiseJsonOrError('{"123": "numeric key"}');
|
||||
expect(result).toEqual({ "123": "numeric key" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("invalid JSON handling", () => {
|
||||
it("should return null for invalid JSON syntax", () => {
|
||||
expect(serialiseJsonOrError("{invalid}")).toBeNull();
|
||||
expect(serialiseJsonOrError("not json")).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null for unclosed braces", () => {
|
||||
expect(serialiseJsonOrError('{"key": "value"')).toBeNull();
|
||||
expect(serialiseJsonOrError('{"key": {')).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null for trailing commas", () => {
|
||||
expect(serialiseJsonOrError('{"key": "value",}')).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null for single quotes", () => {
|
||||
expect(serialiseJsonOrError("{'key': 'value'}")).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null for unquoted keys", () => {
|
||||
expect(serialiseJsonOrError('{key: "value"}')).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null for empty string", () => {
|
||||
expect(serialiseJsonOrError("")).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null for whitespace only", () => {
|
||||
expect(serialiseJsonOrError(" ")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("edge cases", () => {
|
||||
it("should parse a plain array (returns as object)", () => {
|
||||
const result = serialiseJsonOrError("[1, 2, 3]");
|
||||
expect(result).toEqual([ 1, 2, 3 ]);
|
||||
});
|
||||
|
||||
it("should parse primitive values", () => {
|
||||
expect(serialiseJsonOrError("42")).toBe(42);
|
||||
expect(serialiseJsonOrError('"string"')).toBe("string");
|
||||
expect(serialiseJsonOrError("true")).toBe(true);
|
||||
expect(serialiseJsonOrError("false")).toBe(false);
|
||||
expect(serialiseJsonOrError("null")).toBeNull();
|
||||
});
|
||||
|
||||
it("should handle very large numbers", () => {
|
||||
const result = serialiseJsonOrError('{"big": 9007199254740991}');
|
||||
expect(result).toEqual({ big: 9007199254740991 });
|
||||
});
|
||||
|
||||
it("should handle scientific notation", () => {
|
||||
const result = serialiseJsonOrError('{"sci": 1.23e10}');
|
||||
expect(result).toEqual({ sci: 1.23e10 });
|
||||
});
|
||||
|
||||
it("should handle deeply nested structures", () => {
|
||||
const deepJson = '{"a":{"b":{"c":{"d":{"e":"deep"}}}}}';
|
||||
const result = serialiseJsonOrError(deepJson);
|
||||
expect(result).toEqual({ a: { b: { c: { d: { e: "deep" } } } } });
|
||||
});
|
||||
|
||||
it("should handle JSON with whitespace", () => {
|
||||
const result = serialiseJsonOrError(' { "key" : "value" } ');
|
||||
expect(result).toEqual({ key: "value" });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* @copyright NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { sleep } from "../sleep.js";
|
||||
|
||||
describe("sleep", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("should resolve after the specified time", async() => {
|
||||
const sleepPromise = sleep(1000);
|
||||
|
||||
vi.advanceTimersByTime(1000);
|
||||
|
||||
await expect(sleepPromise).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("should not resolve before the specified time", async() => {
|
||||
let resolved = false;
|
||||
const sleepPromise = sleep(1000).then(() => {
|
||||
resolved = true;
|
||||
});
|
||||
|
||||
vi.advanceTimersByTime(999);
|
||||
await Promise.resolve();
|
||||
|
||||
expect(resolved).toBe(false);
|
||||
|
||||
vi.advanceTimersByTime(1);
|
||||
await sleepPromise;
|
||||
|
||||
expect(resolved).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle zero milliseconds", async() => {
|
||||
const sleepPromise = sleep(0);
|
||||
|
||||
vi.advanceTimersByTime(0);
|
||||
|
||||
await expect(sleepPromise).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("should handle small delays", async() => {
|
||||
const sleepPromise = sleep(10);
|
||||
|
||||
vi.advanceTimersByTime(10);
|
||||
|
||||
await expect(sleepPromise).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("should handle large delays", async() => {
|
||||
const sleepPromise = sleep(60000);
|
||||
|
||||
vi.advanceTimersByTime(60000);
|
||||
|
||||
await expect(sleepPromise).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
describe("real timer tests", () => {
|
||||
beforeEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("should actually wait with real timers (short delay)", async() => {
|
||||
const start = Date.now();
|
||||
await sleep(50);
|
||||
const elapsed = Date.now() - start;
|
||||
|
||||
expect(elapsed).toBeGreaterThanOrEqual(45);
|
||||
expect(elapsed).toBeLessThan(150);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* @copyright NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: false,
|
||||
environment: "node",
|
||||
include: ["src/**/__tests__/**/*.test.ts"],
|
||||
coverage: {
|
||||
provider: "v8",
|
||||
reporter: ["text", "html"],
|
||||
exclude: ["node_modules/", "**/__tests__/**"],
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user