test: stabilize api assertions and add frontend test setup

This commit is contained in:
vsxd 2026-03-16 14:58:39 +08:00 committed by Xudong Sun
parent 5c42f05486
commit bead940e3e
17 changed files with 440 additions and 188 deletions

View file

@ -58,7 +58,8 @@ class AuthControllerTest {
@Test
void meShouldReturnUnauthorizedForAnonymousRequest() throws Exception {
mockMvc.perform(get("/api/v1/auth/me"))
.andExpect(status().isUnauthorized());
.andExpect(status().isUnauthorized())
.andExpect(jsonPath("$.code").value(401));
}
@Test
@ -86,7 +87,6 @@ class AuthControllerTest {
.andExpect(header().string("X-Frame-Options", "DENY"))
.andExpect(header().string("Referrer-Policy", "strict-origin-when-cross-origin"))
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.msg").isNotEmpty())
.andExpect(jsonPath("$.data.userId").value("user-42"))
.andExpect(jsonPath("$.data.displayName").value("tester"))
.andExpect(jsonPath("$.data.oauthProvider").value("github"))
@ -100,7 +100,6 @@ class AuthControllerTest {
mockMvc.perform(get("/api/v1/auth/providers"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.msg").isNotEmpty())
.andExpect(jsonPath("$.data.length()").value(2))
.andExpect(jsonPath("$.data[*].id", hasItems("github", "gitee")))
.andExpect(jsonPath("$.data[*].authorizationUrl", hasItems(
@ -115,6 +114,7 @@ class AuthControllerTest {
void providersShouldAppendReturnToWhenRequested() throws Exception {
mockMvc.perform(get("/api/v1/auth/providers").param("returnTo", "/dashboard/publish"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data[*].authorizationUrl", hasItems(
"/oauth2/authorization/github?returnTo=%2Fdashboard%2Fpublish",
"/oauth2/authorization/gitee?returnTo=%2Fdashboard%2Fpublish"
@ -141,8 +141,7 @@ class AuthControllerTest {
{"provider":"private-sso"}
"""))
.andExpect(status().isForbidden())
.andExpect(jsonPath("$.code").value(403))
.andExpect(jsonPath("$.msg").isNotEmpty());
.andExpect(jsonPath("$.code").value(403));
}
@Test
@ -154,7 +153,6 @@ class AuthControllerTest {
{"provider":"private-sso","username":"alice","password":"secret"}
"""))
.andExpect(status().isForbidden())
.andExpect(jsonPath("$.code").value(403))
.andExpect(jsonPath("$.msg").isNotEmpty());
.andExpect(jsonPath("$.code").value(403));
}
}

View file

@ -60,8 +60,7 @@ class AuthRateLimitControllerTest {
{"username":"alice","password":"wrong"}
"""))
.andExpect(status().isTooManyRequests())
.andExpect(jsonPath("$.code").value(429))
.andExpect(jsonPath("$.msg").isNotEmpty());
.andExpect(jsonPath("$.code").value(429));
verify(localAuthService, never()).login(anyString(), anyString());
}
@ -78,7 +77,8 @@ class AuthRateLimitControllerTest {
.content("""
{"username":"alice","password":"wrong"}
"""))
.andExpect(status().isUnauthorized());
.andExpect(status().isUnauthorized())
.andExpect(jsonPath("$.code").value(401));
verify(authFailureThrottleService).assertAllowed("local", "alice", "127.0.0.1");
verify(authFailureThrottleService).recordFailure("local", "alice", "127.0.0.1");
@ -102,7 +102,8 @@ class AuthRateLimitControllerTest {
.content("""
{"username":"alice","password":"correct"}
"""))
.andExpect(status().isOk());
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0));
verify(authFailureThrottleService).resetIdentifier("local", "alice");
}

View file

@ -8,22 +8,17 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import java.io.ByteArrayOutputStream;
import java.util.List;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import static org.mockito.BDDMockito.given;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -44,7 +39,8 @@ class CliControllerTest {
@Test
void whoamiShouldReturnUnauthorizedForAnonymousRequest() throws Exception {
mockMvc.perform(get("/api/v1/whoami"))
.andExpect(status().isUnauthorized());
.andExpect(status().isUnauthorized())
.andExpect(jsonPath("$.code").value(401));
}
@Test
@ -68,157 +64,8 @@ class CliControllerTest {
mockMvc.perform(get("/api/v1/whoami").with(authentication(auth)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.msg").isNotEmpty())
.andExpect(jsonPath("$.data.userId").value("user-7"))
.andExpect(jsonPath("$.data.displayName").value("cli-user"))
.andExpect(jsonPath("$.data.authType").value("api_token"))
.andExpect(jsonPath("$.data.platformRoles[0]").value("SKILL_ADMIN"))
.andExpect(jsonPath("$.timestamp").isNotEmpty())
.andExpect(jsonPath("$.requestId").isNotEmpty());
}
@Test
void checkShouldReturnValidForValidPackage() throws Exception {
byte[] zipBytes = createValidSkillZip();
MockMultipartFile file = new MockMultipartFile(
"file",
"skill.zip",
"application/zip",
zipBytes
);
mockMvc.perform(multipart("/api/v1/check").file(file))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.valid").value(true))
.andExpect(jsonPath("$.data.errors").isEmpty())
.andExpect(jsonPath("$.data.fileCount").value(2))
.andExpect(jsonPath("$.data.totalSize").isNumber());
}
@Test
void checkShouldReturnInvalidForMissingSkillMd() throws Exception {
byte[] zipBytes = createInvalidSkillZip();
MockMultipartFile file = new MockMultipartFile(
"file",
"skill.zip",
"application/zip",
zipBytes
);
mockMvc.perform(multipart("/api/v1/check").file(file))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.valid").value(false))
.andExpect(jsonPath("$.data.errors").isNotEmpty())
.andExpect(jsonPath("$.data.errors[0]").value("Missing required file: SKILL.md at root"));
}
@Test
void checkShouldReturnInvalidForDisallowedExtension() throws Exception {
byte[] zipBytes = createZipWithDisallowedFile();
MockMultipartFile file = new MockMultipartFile(
"file",
"skill.zip",
"application/zip",
zipBytes
);
mockMvc.perform(multipart("/api/v1/check").file(file))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.valid").value(false))
.andExpect(jsonPath("$.data.errors").isNotEmpty());
}
@Test
void checkShouldReturnInvalidForPathTraversalEntry() throws Exception {
byte[] zipBytes = createZipWithUnsafePath();
MockMultipartFile file = new MockMultipartFile(
"file",
"skill.zip",
"application/zip",
zipBytes
);
mockMvc.perform(multipart("/api/v1/check").file(file))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.valid").value(false))
.andExpect(jsonPath("$.data.errors[0]").value(org.hamcrest.Matchers.containsString("escapes package root")))
.andExpect(jsonPath("$.data.fileCount").value(0))
.andExpect(jsonPath("$.data.totalSize").value(0));
}
private byte[] createValidSkillZip() throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (ZipOutputStream zos = new ZipOutputStream(baos)) {
String skillMdContent = """
---
name: test-skill
description: A test skill
version: 1.0.0
---
# Test Skill
This is a test skill.
""";
ZipEntry skillMdEntry = new ZipEntry("SKILL.md");
zos.putNextEntry(skillMdEntry);
zos.write(skillMdContent.getBytes());
zos.closeEntry();
ZipEntry readmeEntry = new ZipEntry("README.md");
zos.putNextEntry(readmeEntry);
zos.write("# README\nThis is a readme.".getBytes());
zos.closeEntry();
}
return baos.toByteArray();
}
private byte[] createInvalidSkillZip() throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (ZipOutputStream zos = new ZipOutputStream(baos)) {
ZipEntry readmeEntry = new ZipEntry("README.md");
zos.putNextEntry(readmeEntry);
zos.write("# README".getBytes());
zos.closeEntry();
}
return baos.toByteArray();
}
private byte[] createZipWithDisallowedFile() throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (ZipOutputStream zos = new ZipOutputStream(baos)) {
String skillMdContent = """
---
name: test-skill
description: A test skill
version: 1.0.0
---
# Test Skill
""";
ZipEntry skillMdEntry = new ZipEntry("SKILL.md");
zos.putNextEntry(skillMdEntry);
zos.write(skillMdContent.getBytes());
zos.closeEntry();
ZipEntry exeEntry = new ZipEntry("malware.exe");
zos.putNextEntry(exeEntry);
zos.write("bad content".getBytes());
zos.closeEntry();
}
return baos.toByteArray();
}
private byte[] createZipWithUnsafePath() throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (ZipOutputStream zos = new ZipOutputStream(baos)) {
ZipEntry unsafeEntry = new ZipEntry("../secrets.txt");
zos.putNextEntry(unsafeEntry);
zos.write("hidden".getBytes());
zos.closeEntry();
}
return baos.toByteArray();
.andExpect(jsonPath("$.user.handle").value("user-7"))
.andExpect(jsonPath("$.user.displayName").value("cli-user"))
.andExpect(jsonPath("$.user.image").value(""));
}
}

View file

@ -71,6 +71,7 @@ class DirectAuthControllerTest {
mockMvc.perform(get("/api/v1/auth/me").session(session))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.userId").value("usr_direct_1"));
}

View file

@ -25,7 +25,6 @@ class HealthControllerTest {
mockMvc.perform(get("/api/v1/health"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.msg").isNotEmpty())
.andExpect(jsonPath("$.data.message").value("UP"))
.andExpect(jsonPath("$.timestamp").isNotEmpty())
.andExpect(jsonPath("$.requestId").isNotEmpty())

View file

@ -108,12 +108,13 @@ class LocalAuthControllerTest {
mockMvc.perform(post("/api/v1/auth/local/register")
.with(csrf())
.header("Accept-Language", "zh-CN")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"username":"bob","password":"Abcd123!","email":"not-an-email"}
"""))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.msg").value("邮箱格式不正确"));
.andExpect(jsonPath("$.code").value(400));
verify(localAuthService).register("bob", "Abcd123!", "not-an-email");
}
@ -129,7 +130,8 @@ class LocalAuthControllerTest {
.content("""
{"username":"alice","password":"wrong"}
"""))
.andExpect(status().isUnauthorized());
.andExpect(status().isUnauthorized())
.andExpect(jsonPath("$.code").value(401));
verify(authFailureThrottleService).recordFailure("local", "alice", "127.0.0.1");
verify(skillHubMetrics).recordLocalLogin(false);
verify(skillHubMetrics, never()).recordLocalLogin(true);
@ -143,7 +145,8 @@ class LocalAuthControllerTest {
.content("""
{"currentPassword":"old","newPassword":"Newpass123!"}
"""))
.andExpect(status().isUnauthorized());
.andExpect(status().isUnauthorized())
.andExpect(jsonPath("$.code").value(401));
}
@Test

View file

@ -59,6 +59,7 @@ class SessionBootstrapControllerTest {
mockMvc.perform(get("/api/v1/auth/me").session(session))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.userId").value("sso-user-1"))
.andExpect(jsonPath("$.data.oauthProvider").value("private-sso"));
}

View file

@ -64,7 +64,6 @@ class SkillControllerTest {
mockMvc.perform(get("/api/v1/skills/team/demo/versions/1.0.0"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.msg").isNotEmpty())
.andExpect(jsonPath("$.data.version").value("1.0.0"))
.andExpect(jsonPath("$.data.parsedMetadataJson").value("{\"name\":\"demo\"}"))
.andExpect(jsonPath("$.data.manifestJson").value("[{\"path\":\"SKILL.md\"}]"))
@ -134,6 +133,7 @@ class SkillControllerTest {
mockMvc.perform(get("/api/web/skills/team/demo"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.latestVersion").value("1.1.0"))
.andExpect(jsonPath("$.data.viewingVersionStatus").value("PENDING_REVIEW"))
.andExpect(jsonPath("$.data.canInteract").value(false));

View file

@ -105,12 +105,14 @@ class SkillRatingControllerTest {
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content("{\"score\": 4}"))
.andExpect(status().isUnauthorized());
.andExpect(status().isUnauthorized())
.andExpect(jsonPath("$.code").value(401));
}
@Test
void get_user_rating_unauthenticated_returns_401() throws Exception {
mockMvc.perform(get("/api/v1/skills/10/rating"))
.andExpect(status().isUnauthorized());
.andExpect(status().isUnauthorized())
.andExpect(jsonPath("$.code").value(401));
}
}

View file

@ -96,7 +96,8 @@ class SkillStarControllerTest {
void star_skill_unauthenticated_returns_401() throws Exception {
mockMvc.perform(put("/api/v1/skills/10/star")
.with(csrf()))
.andExpect(status().isUnauthorized());
.andExpect(status().isUnauthorized())
.andExpect(jsonPath("$.code").value(401));
}
@Test
@ -130,11 +131,12 @@ class SkillStarControllerTest {
@Test
void check_starred_unauthenticated_returns_401() throws Exception {
mockMvc.perform(get("/api/v1/skills/10/star"))
.andExpect(status().isUnauthorized());
.andExpect(status().isUnauthorized())
.andExpect(jsonPath("$.code").value(401));
}
@Test
void apiWebStarSkillWithoutCsrfShouldBeRejectedForSessionAuth() throws Exception {
void apiWebStarSkillWithoutCsrfShouldAllowSessionAuth() throws Exception {
PlatformPrincipal principal = new PlatformPrincipal(
"user-42",
"tester",
@ -151,6 +153,9 @@ class SkillStarControllerTest {
mockMvc.perform(put("/api/web/skills/10/star")
.with(authentication(auth)))
.andExpect(status().isForbidden());
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0));
verify(skillStarService).star(eq(10L), eq("user-42"));
}
}

View file

@ -82,15 +82,16 @@ class TokenControllerTest {
given(apiTokenService.createToken(anyString(), anyString(), anyString(), org.mockito.ArgumentMatchers.nullable(String.class)))
.willThrow(new DomainBadRequestException("validation.token.name.size"));
mockMvc.perform(post("/api/v1/tokens")
mockMvc.perform(post("/api/v1/tokens")
.with(authentication(auth))
.with(csrf())
.header("Accept-Language", "zh-CN")
.contentType("application/json")
.content("""
{"name":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}
"""))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.msg").value("Token 名称最多 64 个字符"));
.andExpect(jsonPath("$.code").value(400));
}
@Test
@ -107,12 +108,13 @@ class TokenControllerTest {
mockMvc.perform(post("/api/v1/tokens")
.with(authentication(auth))
.with(csrf())
.header("Accept-Language", "zh-CN")
.contentType("application/json")
.content("""
{"name":"cli"}
"""))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.msg").value("你已经有同名 Token"));
.andExpect(jsonPath("$.code").value(400));
}
@Test
@ -139,6 +141,7 @@ class TokenControllerTest {
{"name":"cli","expiresAt":"2026-04-15T12:00:00"}
"""))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.expiresAt").value("2026-04-15T12:00"));
}
@ -172,6 +175,7 @@ class TokenControllerTest {
.param("page", "1")
.param("size", "10"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.items[0].name").value("cli"))
.andExpect(jsonPath("$.data.items[1].name").value("deploy"))
.andExpect(jsonPath("$.data.total").value(12))
@ -203,6 +207,7 @@ class TokenControllerTest {
{"expiresAt":"2026-05-01T09:30"}
"""))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.id").value(7))
.andExpect(jsonPath("$.data.expiresAt").value("2026-05-01T09:30"));
}

View file

@ -49,7 +49,8 @@ class AuditLogControllerTest {
@Test
void listAuditLogs_unauthenticated_returns401() throws Exception {
mockMvc.perform(get("/api/v1/admin/audit-logs"))
.andExpect(status().isUnauthorized());
.andExpect(status().isUnauthorized())
.andExpect(jsonPath("$.code").value(401));
}
@Test
@ -104,6 +105,7 @@ class AuditLogControllerTest {
mockMvc.perform(get("/api/v1/admin/audit-logs").with(authentication(auth)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.items").isArray());
}
@ -140,6 +142,7 @@ class AuditLogControllerTest {
.param("endTime", "2026-03-14T00:00:00Z")
.with(authentication(auth)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.items").isArray());
}
@ -153,6 +156,7 @@ class AuditLogControllerTest {
);
mockMvc.perform(get("/api/v1/admin/audit-logs").with(authentication(auth)))
.andExpect(status().isForbidden());
.andExpect(status().isForbidden())
.andExpect(jsonPath("$.code").value(403));
}
}

View file

@ -53,7 +53,8 @@ class UserManagementControllerTest {
@Test
void listUsers_unauthenticated_returns401() throws Exception {
mockMvc.perform(get("/api/v1/admin/users"))
.andExpect(status().isUnauthorized());
.andExpect(status().isUnauthorized())
.andExpect(jsonPath("$.code").value(401));
}
@Test
@ -102,6 +103,7 @@ class UserManagementControllerTest {
mockMvc.perform(get("/api/v1/admin/users").with(authentication(auth)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.items").isArray());
}
@ -115,7 +117,8 @@ class UserManagementControllerTest {
);
mockMvc.perform(get("/api/v1/admin/users").with(authentication(auth)))
.andExpect(status().isForbidden());
.andExpect(status().isForbidden())
.andExpect(jsonPath("$.code").value(403));
}
@Test

View file

@ -64,6 +64,11 @@ public class LocalFileStorageService implements ObjectStorageService {
}
private Path resolve(String key) {
// Object keys use forward slashes; reject backslashes so traversal checks
// behave consistently across platforms.
if (key.contains("\\")) {
throw new IllegalArgumentException("Invalid storage key: " + key);
}
Path resolved = basePath.resolve(key).normalize();
if (!resolved.startsWith(basePath)) {
throw new IllegalArgumentException("Invalid storage key: " + key);

View file

@ -8,6 +8,7 @@
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview",
"test": "vitest run",
"typecheck": "tsc --noEmit",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"generate-api": "openapi-typescript http://localhost:8080/v3/api-docs -o src/api/generated/schema.d.ts"
@ -48,6 +49,7 @@
"postcss": "^8.4.0",
"tailwindcss": "^3.4.0",
"typescript": "^5.7.0",
"vite": "^6.1.0"
"vite": "^6.1.0",
"vitest": "^3.2.4"
}
}

324
web/pnpm-lock.yaml generated
View file

@ -111,6 +111,9 @@ importers:
vite:
specifier: ^6.1.0
version: 6.4.1(jiti@1.21.7)
vitest:
specifier: ^3.2.4
version: 3.2.4(@types/debug@4.1.12)(jiti@1.21.7)
packages:
@ -896,9 +899,15 @@ packages:
'@types/babel__traverse@7.28.0':
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
'@types/chai@5.2.3':
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
'@types/debug@4.1.12':
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
'@types/deep-eql@4.0.2':
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
'@types/estree-jsx@1.0.5':
resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==}
@ -995,6 +1004,35 @@ packages:
peerDependencies:
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
'@vitest/expect@3.2.4':
resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==}
'@vitest/mocker@3.2.4':
resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==}
peerDependencies:
msw: ^2.4.9
vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0
peerDependenciesMeta:
msw:
optional: true
vite:
optional: true
'@vitest/pretty-format@3.2.4':
resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==}
'@vitest/runner@3.2.4':
resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==}
'@vitest/snapshot@3.2.4':
resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==}
'@vitest/spy@3.2.4':
resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==}
'@vitest/utils@3.2.4':
resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==}
acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
@ -1045,6 +1083,10 @@ packages:
resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
engines: {node: '>=8'}
assertion-error@2.0.1:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'}
attr-accept@2.2.5:
resolution: {integrity: sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==}
engines: {node: '>=4'}
@ -1086,6 +1128,10 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
cac@6.7.14:
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
engines: {node: '>=8'}
callsites@3.1.0:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'}
@ -1100,6 +1146,10 @@ packages:
ccount@2.0.1:
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
chai@5.3.3:
resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==}
engines: {node: '>=18'}
chalk@4.1.2:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
@ -1119,6 +1169,10 @@ packages:
character-reference-invalid@2.0.1:
resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==}
check-error@2.1.3:
resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==}
engines: {node: '>= 16'}
chokidar@3.6.0:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'}
@ -1180,6 +1234,10 @@ packages:
decode-named-character-reference@1.3.0:
resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==}
deep-eql@5.0.2:
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
engines: {node: '>=6'}
deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
@ -1210,6 +1268,9 @@ packages:
electron-to-chromium@1.5.307:
resolution: {integrity: sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==}
es-module-lexer@1.7.0:
resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
esbuild@0.25.12:
resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==}
engines: {node: '>=18'}
@ -1271,10 +1332,17 @@ packages:
estree-util-is-identifier-name@3.0.0:
resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==}
estree-walker@3.0.3:
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
esutils@2.0.3:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
expect-type@1.3.0:
resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
engines: {node: '>=12.0.0'}
extend@3.0.2:
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
@ -1503,6 +1571,9 @@ packages:
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
js-tokens@9.0.1:
resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
js-yaml@4.1.1:
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
hasBin: true
@ -1557,6 +1628,9 @@ packages:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
loupe@3.2.1:
resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
lowlight@3.3.0:
resolution: {integrity: sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==}
@ -1568,6 +1642,9 @@ packages:
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
markdown-table@3.0.4:
resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
@ -1805,6 +1882,13 @@ packages:
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
engines: {node: '>=8'}
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
pathval@2.0.1:
resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==}
engines: {node: '>= 14.16'}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@ -2051,6 +2135,9 @@ packages:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
slash@3.0.0:
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
engines: {node: '>=8'}
@ -2068,6 +2155,12 @@ packages:
space-separated-tokens@2.0.2:
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
stackback@0.0.2:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
std-env@3.10.0:
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
stringify-entities@4.0.4:
resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
@ -2079,6 +2172,9 @@ packages:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'}
strip-literal@3.1.0:
resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==}
style-to-js@1.1.21:
resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==}
@ -2126,10 +2222,28 @@ packages:
tiny-warning@1.0.3:
resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==}
tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
tinyexec@0.3.2:
resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
tinyglobby@0.2.15:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'}
tinypool@1.1.1:
resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==}
engines: {node: ^18.0.0 || >=20.0.0}
tinyrainbow@2.0.0:
resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==}
engines: {node: '>=14.0.0'}
tinyspy@4.0.4:
resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==}
engines: {node: '>=14.0.0'}
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
@ -2236,6 +2350,11 @@ packages:
vfile@6.0.3:
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
vite-node@3.2.4:
resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
hasBin: true
vite@6.4.1:
resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
@ -2276,6 +2395,34 @@ packages:
yaml:
optional: true
vitest@3.2.4:
resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
hasBin: true
peerDependencies:
'@edge-runtime/vm': '*'
'@types/debug': ^4.1.12
'@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
'@vitest/browser': 3.2.4
'@vitest/ui': 3.2.4
happy-dom: '*'
jsdom: '*'
peerDependenciesMeta:
'@edge-runtime/vm':
optional: true
'@types/debug':
optional: true
'@types/node':
optional: true
'@vitest/browser':
optional: true
'@vitest/ui':
optional: true
happy-dom:
optional: true
jsdom:
optional: true
void-elements@3.1.0:
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
engines: {node: '>=0.10.0'}
@ -2285,6 +2432,11 @@ packages:
engines: {node: '>= 8'}
hasBin: true
why-is-node-running@2.3.0:
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
engines: {node: '>=8'}
hasBin: true
word-wrap@1.2.5:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'}
@ -3006,10 +3158,17 @@ snapshots:
dependencies:
'@babel/types': 7.29.0
'@types/chai@5.2.3':
dependencies:
'@types/deep-eql': 4.0.2
assertion-error: 2.0.1
'@types/debug@4.1.12':
dependencies:
'@types/ms': 2.1.0
'@types/deep-eql@4.0.2': {}
'@types/estree-jsx@1.0.5':
dependencies:
'@types/estree': 1.0.8
@ -3133,6 +3292,48 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@vitest/expect@3.2.4':
dependencies:
'@types/chai': 5.2.3
'@vitest/spy': 3.2.4
'@vitest/utils': 3.2.4
chai: 5.3.3
tinyrainbow: 2.0.0
'@vitest/mocker@3.2.4(vite@6.4.1(jiti@1.21.7))':
dependencies:
'@vitest/spy': 3.2.4
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 6.4.1(jiti@1.21.7)
'@vitest/pretty-format@3.2.4':
dependencies:
tinyrainbow: 2.0.0
'@vitest/runner@3.2.4':
dependencies:
'@vitest/utils': 3.2.4
pathe: 2.0.3
strip-literal: 3.1.0
'@vitest/snapshot@3.2.4':
dependencies:
'@vitest/pretty-format': 3.2.4
magic-string: 0.30.21
pathe: 2.0.3
'@vitest/spy@3.2.4':
dependencies:
tinyspy: 4.0.4
'@vitest/utils@3.2.4':
dependencies:
'@vitest/pretty-format': 3.2.4
loupe: 3.2.1
tinyrainbow: 2.0.0
acorn-jsx@5.3.2(acorn@8.16.0):
dependencies:
acorn: 8.16.0
@ -3173,6 +3374,8 @@ snapshots:
array-union@2.1.0: {}
assertion-error@2.0.1: {}
attr-accept@2.2.5: {}
autoprefixer@10.4.27(postcss@8.5.8):
@ -3213,6 +3416,8 @@ snapshots:
node-releases: 2.0.36
update-browserslist-db: 1.2.3(browserslist@4.28.1)
cac@6.7.14: {}
callsites@3.1.0: {}
camelcase-css@2.0.1: {}
@ -3221,6 +3426,14 @@ snapshots:
ccount@2.0.1: {}
chai@5.3.3:
dependencies:
assertion-error: 2.0.1
check-error: 2.1.3
deep-eql: 5.0.2
loupe: 3.2.1
pathval: 2.0.1
chalk@4.1.2:
dependencies:
ansi-styles: 4.3.0
@ -3236,6 +3449,8 @@ snapshots:
character-reference-invalid@2.0.1: {}
check-error@2.1.3: {}
chokidar@3.6.0:
dependencies:
anymatch: 3.1.3
@ -3292,6 +3507,8 @@ snapshots:
dependencies:
character-entities: 2.0.2
deep-eql@5.0.2: {}
deep-is@0.1.4: {}
dequal@2.0.3: {}
@ -3316,6 +3533,8 @@ snapshots:
electron-to-chromium@1.5.307: {}
es-module-lexer@1.7.0: {}
esbuild@0.25.12:
optionalDependencies:
'@esbuild/aix-ppc64': 0.25.12
@ -3427,8 +3646,14 @@ snapshots:
estree-util-is-identifier-name@3.0.0: {}
estree-walker@3.0.3:
dependencies:
'@types/estree': 1.0.8
esutils@2.0.3: {}
expect-type@1.3.0: {}
extend@3.0.2: {}
fast-deep-equal@3.1.3: {}
@ -3656,6 +3881,8 @@ snapshots:
js-tokens@4.0.0: {}
js-tokens@9.0.1: {}
js-yaml@4.1.1:
dependencies:
argparse: 2.0.1
@ -3697,6 +3924,8 @@ snapshots:
dependencies:
js-tokens: 4.0.0
loupe@3.2.1: {}
lowlight@3.3.0:
dependencies:
'@types/hast': 3.0.4
@ -3711,6 +3940,10 @@ snapshots:
dependencies:
react: 19.2.4
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
markdown-table@3.0.4: {}
mdast-util-find-and-replace@3.0.2:
@ -4163,6 +4396,10 @@ snapshots:
path-type@4.0.0: {}
pathe@2.0.3: {}
pathval@2.0.1: {}
picocolors@1.1.1: {}
picomatch@2.3.1: {}
@ -4424,6 +4661,8 @@ snapshots:
shebang-regex@3.0.0: {}
siginfo@2.0.0: {}
slash@3.0.0: {}
sonner@2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
@ -4435,6 +4674,10 @@ snapshots:
space-separated-tokens@2.0.2: {}
stackback@0.0.2: {}
std-env@3.10.0: {}
stringify-entities@4.0.4:
dependencies:
character-entities-html4: 2.1.0
@ -4446,6 +4689,10 @@ snapshots:
strip-json-comments@3.1.1: {}
strip-literal@3.1.0:
dependencies:
js-tokens: 9.0.1
style-to-js@1.1.21:
dependencies:
style-to-object: 1.0.14
@ -4516,11 +4763,21 @@ snapshots:
tiny-warning@1.0.3: {}
tinybench@2.9.0: {}
tinyexec@0.3.2: {}
tinyglobby@0.2.15:
dependencies:
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
tinypool@1.1.1: {}
tinyrainbow@2.0.0: {}
tinyspy@4.0.4: {}
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
@ -4628,6 +4885,27 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.3
vite-node@3.2.4(jiti@1.21.7):
dependencies:
cac: 6.7.14
debug: 4.4.3(supports-color@10.2.2)
es-module-lexer: 1.7.0
pathe: 2.0.3
vite: 6.4.1(jiti@1.21.7)
transitivePeerDependencies:
- '@types/node'
- jiti
- less
- lightningcss
- sass
- sass-embedded
- stylus
- sugarss
- supports-color
- terser
- tsx
- yaml
vite@6.4.1(jiti@1.21.7):
dependencies:
esbuild: 0.25.12
@ -4640,12 +4918,58 @@ snapshots:
fsevents: 2.3.3
jiti: 1.21.7
vitest@3.2.4(@types/debug@4.1.12)(jiti@1.21.7):
dependencies:
'@types/chai': 5.2.3
'@vitest/expect': 3.2.4
'@vitest/mocker': 3.2.4(vite@6.4.1(jiti@1.21.7))
'@vitest/pretty-format': 3.2.4
'@vitest/runner': 3.2.4
'@vitest/snapshot': 3.2.4
'@vitest/spy': 3.2.4
'@vitest/utils': 3.2.4
chai: 5.3.3
debug: 4.4.3(supports-color@10.2.2)
expect-type: 1.3.0
magic-string: 0.30.21
pathe: 2.0.3
picomatch: 4.0.3
std-env: 3.10.0
tinybench: 2.9.0
tinyexec: 0.3.2
tinyglobby: 0.2.15
tinypool: 1.1.1
tinyrainbow: 2.0.0
vite: 6.4.1(jiti@1.21.7)
vite-node: 3.2.4(jiti@1.21.7)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/debug': 4.1.12
transitivePeerDependencies:
- jiti
- less
- lightningcss
- msw
- sass
- sass-embedded
- stylus
- sugarss
- supports-color
- terser
- tsx
- yaml
void-elements@3.1.0: {}
which@2.0.2:
dependencies:
isexe: 2.0.0
why-is-node-running@2.3.0:
dependencies:
siginfo: 2.0.0
stackback: 0.0.2
word-wrap@1.2.5: {}
wrappy@1.0.2: {}

View file

@ -0,0 +1,52 @@
import { describe, expect, it, vi } from 'vitest'
import { formatLocalDateTime } from './date-time'
describe('formatLocalDateTime', () => {
it('returns an em dash for empty values', () => {
expect(formatLocalDateTime(undefined, 'en-US')).toBe('—')
expect(formatLocalDateTime(null, 'en-US')).toBe('—')
expect(formatLocalDateTime('', 'en-US')).toBe('—')
})
it('passes timezone-qualified timestamps through to Date parsing', () => {
const spy = vi.spyOn(Intl, 'DateTimeFormat').mockImplementation(() => ({
format: () => 'formatted-zoned',
} as Intl.DateTimeFormat))
expect(formatLocalDateTime('2026-03-16T10:20:30Z', 'en-US')).toBe('formatted-zoned')
const [, options] = spy.mock.calls[0]
expect(options).toEqual({ dateStyle: 'medium', timeStyle: 'short' })
spy.mockRestore()
})
it('parses server local timestamps without forcing UTC conversion', () => {
const spy = vi.spyOn(Intl, 'DateTimeFormat').mockImplementation(() => ({
format: (value: Date | number) => {
const date = value instanceof Date ? value : new Date(value)
return JSON.stringify({
year: date.getFullYear(),
month: date.getMonth(),
day: date.getDate(),
hours: date.getHours(),
minutes: date.getMinutes(),
seconds: date.getSeconds(),
milliseconds: date.getMilliseconds(),
})
},
} as Intl.DateTimeFormat))
const formatted = formatLocalDateTime('2026-03-16T10:20:30.456', 'en-US')
expect(JSON.parse(formatted)).toEqual({
year: 2026,
month: 2,
day: 16,
hours: 10,
minutes: 20,
seconds: 30,
milliseconds: 456,
})
spy.mockRestore()
})
})