diff --git a/backend/internal/config/config_test.go b/backend/internal/config/config_test.go new file mode 100644 index 0000000..5958734 --- /dev/null +++ b/backend/internal/config/config_test.go @@ -0,0 +1,167 @@ +package config + +import ( + "os" + "testing" + "time" +) + +func TestLoadDefaults(t *testing.T) { + // Clear any environment variables that might interfere + envVars := []string{ + "APP_NAME", "PORT", "DATABASE_URL", "DB_MAX_OPEN_CONNS", + "DB_MAX_IDLE_CONNS", "DB_CONN_MAX_IDLE", "MERCADOPAGO_BASE_URL", + "MARKETPLACE_COMMISSION", "JWT_SECRET", "JWT_EXPIRES_IN", + "PASSWORD_PEPPER", "CORS_ORIGINS", + } + origEnvs := make(map[string]string) + for _, key := range envVars { + origEnvs[key] = os.Getenv(key) + os.Unsetenv(key) + } + defer func() { + for key, val := range origEnvs { + if val != "" { + os.Setenv(key, val) + } + } + }() + + cfg := Load() + + if cfg.AppName != "saveinmed-performance-core" { + t.Errorf("expected AppName 'saveinmed-performance-core', got '%s'", cfg.AppName) + } + if cfg.Port != "8214" { + t.Errorf("expected Port '8214', got '%s'", cfg.Port) + } + if cfg.MaxOpenConns != 15 { + t.Errorf("expected MaxOpenConns 15, got %d", cfg.MaxOpenConns) + } + if cfg.MaxIdleConns != 5 { + t.Errorf("expected MaxIdleConns 5, got %d", cfg.MaxIdleConns) + } + if cfg.ConnMaxIdle != 5*time.Minute { + t.Errorf("expected ConnMaxIdle 5m, got %v", cfg.ConnMaxIdle) + } + if cfg.JWTSecret != "dev-secret" { + t.Errorf("expected JWTSecret 'dev-secret', got '%s'", cfg.JWTSecret) + } + if cfg.JWTExpiresIn != 24*time.Hour { + t.Errorf("expected JWTExpiresIn 24h, got %v", cfg.JWTExpiresIn) + } + if cfg.MarketplaceCommission != 2.5 { + t.Errorf("expected MarketplaceCommission 2.5, got %f", cfg.MarketplaceCommission) + } + if len(cfg.CORSOrigins) != 1 || cfg.CORSOrigins[0] != "*" { + t.Errorf("expected CORSOrigins ['*'], got %v", cfg.CORSOrigins) + } +} + +func TestLoadFromEnv(t *testing.T) { + os.Setenv("APP_NAME", "test-app") + os.Setenv("PORT", "9999") + os.Setenv("DATABASE_URL", "postgres://test:test@localhost:5432/test") + os.Setenv("DB_MAX_OPEN_CONNS", "100") + os.Setenv("DB_MAX_IDLE_CONNS", "50") + os.Setenv("DB_CONN_MAX_IDLE", "10m") + os.Setenv("MARKETPLACE_COMMISSION", "5.0") + os.Setenv("JWT_SECRET", "super-secret") + os.Setenv("JWT_EXPIRES_IN", "12h") + os.Setenv("PASSWORD_PEPPER", "pepper123") + os.Setenv("CORS_ORIGINS", "https://example.com,https://app.example.com") + + defer func() { + os.Unsetenv("APP_NAME") + os.Unsetenv("PORT") + os.Unsetenv("DATABASE_URL") + os.Unsetenv("DB_MAX_OPEN_CONNS") + os.Unsetenv("DB_MAX_IDLE_CONNS") + os.Unsetenv("DB_CONN_MAX_IDLE") + os.Unsetenv("MARKETPLACE_COMMISSION") + os.Unsetenv("JWT_SECRET") + os.Unsetenv("JWT_EXPIRES_IN") + os.Unsetenv("PASSWORD_PEPPER") + os.Unsetenv("CORS_ORIGINS") + }() + + cfg := Load() + + if cfg.AppName != "test-app" { + t.Errorf("expected AppName 'test-app', got '%s'", cfg.AppName) + } + if cfg.Port != "9999" { + t.Errorf("expected Port '9999', got '%s'", cfg.Port) + } + if cfg.DatabaseURL != "postgres://test:test@localhost:5432/test" { + t.Errorf("expected custom DatabaseURL, got '%s'", cfg.DatabaseURL) + } + if cfg.MaxOpenConns != 100 { + t.Errorf("expected MaxOpenConns 100, got %d", cfg.MaxOpenConns) + } + if cfg.MaxIdleConns != 50 { + t.Errorf("expected MaxIdleConns 50, got %d", cfg.MaxIdleConns) + } + if cfg.ConnMaxIdle != 10*time.Minute { + t.Errorf("expected ConnMaxIdle 10m, got %v", cfg.ConnMaxIdle) + } + if cfg.MarketplaceCommission != 5.0 { + t.Errorf("expected MarketplaceCommission 5.0, got %f", cfg.MarketplaceCommission) + } + if cfg.JWTSecret != "super-secret" { + t.Errorf("expected JWTSecret 'super-secret', got '%s'", cfg.JWTSecret) + } + if cfg.JWTExpiresIn != 12*time.Hour { + t.Errorf("expected JWTExpiresIn 12h, got %v", cfg.JWTExpiresIn) + } + if cfg.PasswordPepper != "pepper123" { + t.Errorf("expected PasswordPepper 'pepper123', got '%s'", cfg.PasswordPepper) + } + if len(cfg.CORSOrigins) != 2 { + t.Errorf("expected 2 CORS origins, got %d", len(cfg.CORSOrigins)) + } +} + +func TestAddr(t *testing.T) { + cfg := Config{Port: "3000"} + expected := ":3000" + if cfg.Addr() != expected { + t.Errorf("expected Addr '%s', got '%s'", expected, cfg.Addr()) + } +} + +func TestInvalidEnvValues(t *testing.T) { + os.Setenv("DB_MAX_OPEN_CONNS", "not-a-number") + os.Setenv("MARKETPLACE_COMMISSION", "invalid") + os.Setenv("DB_CONN_MAX_IDLE", "bad-duration") + + defer func() { + os.Unsetenv("DB_MAX_OPEN_CONNS") + os.Unsetenv("MARKETPLACE_COMMISSION") + os.Unsetenv("DB_CONN_MAX_IDLE") + }() + + cfg := Load() + + // Should use defaults when values are invalid + if cfg.MaxOpenConns != 15 { + t.Errorf("expected fallback MaxOpenConns 15, got %d", cfg.MaxOpenConns) + } + if cfg.MarketplaceCommission != 2.5 { + t.Errorf("expected fallback MarketplaceCommission 2.5, got %f", cfg.MarketplaceCommission) + } + if cfg.ConnMaxIdle != 5*time.Minute { + t.Errorf("expected fallback ConnMaxIdle 5m, got %v", cfg.ConnMaxIdle) + } +} + +func TestEmptyCORSOrigins(t *testing.T) { + os.Setenv("CORS_ORIGINS", "") + defer os.Unsetenv("CORS_ORIGINS") + + cfg := Load() + + if len(cfg.CORSOrigins) != 1 || cfg.CORSOrigins[0] != "*" { + t.Errorf("expected fallback CORSOrigins ['*'], got %v", cfg.CORSOrigins) + } +} diff --git a/backend/internal/http/middleware/middleware_test.go b/backend/internal/http/middleware/middleware_test.go new file mode 100644 index 0000000..95ece34 --- /dev/null +++ b/backend/internal/http/middleware/middleware_test.go @@ -0,0 +1,342 @@ +package middleware + +import ( + "compress/gzip" + "context" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/gofrs/uuid/v5" + "github.com/golang-jwt/jwt/v5" +) + +// --- CORS Tests --- + +func TestCORSWithConfigAllowAll(t *testing.T) { + cfg := CORSConfig{AllowedOrigins: []string{"*"}} + handler := CORSWithConfig(cfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Header.Set("Origin", "https://example.com") + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + if rec.Header().Get("Access-Control-Allow-Origin") != "*" { + t.Errorf("expected Access-Control-Allow-Origin '*', got '%s'", rec.Header().Get("Access-Control-Allow-Origin")) + } +} + +func TestCORSWithConfigSpecificOrigins(t *testing.T) { + cfg := CORSConfig{AllowedOrigins: []string{"https://allowed.com", "https://another.com"}} + handler := CORSWithConfig(cfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + + // Test allowed origin + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Header.Set("Origin", "https://allowed.com") + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, req) + + if rec.Header().Get("Access-Control-Allow-Origin") != "https://allowed.com" { + t.Errorf("expected origin 'https://allowed.com', got '%s'", rec.Header().Get("Access-Control-Allow-Origin")) + } + + // Test blocked origin + req2 := httptest.NewRequest(http.MethodGet, "/", nil) + req2.Header.Set("Origin", "https://blocked.com") + rec2 := httptest.NewRecorder() + handler.ServeHTTP(rec2, req2) + + if rec2.Header().Get("Access-Control-Allow-Origin") != "" { + t.Errorf("expected empty Access-Control-Allow-Origin, got '%s'", rec2.Header().Get("Access-Control-Allow-Origin")) + } +} + +func TestCORSPreflight(t *testing.T) { + cfg := CORSConfig{AllowedOrigins: []string{"*"}} + handler := CORSWithConfig(cfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusTeapot) // Should not reach here + })) + + req := httptest.NewRequest(http.MethodOptions, "/", nil) + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Errorf("expected status 200 for preflight, got %d", rec.Code) + } +} + +func TestCORSHeaders(t *testing.T) { + cfg := CORSConfig{AllowedOrigins: []string{"*"}} + handler := CORSWithConfig(cfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + + req := httptest.NewRequest(http.MethodGet, "/", nil) + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + if !strings.Contains(rec.Header().Get("Access-Control-Allow-Methods"), "GET") { + t.Error("expected Access-Control-Allow-Methods to include GET") + } + if !strings.Contains(rec.Header().Get("Access-Control-Allow-Headers"), "Authorization") { + t.Error("expected Access-Control-Allow-Headers to include Authorization") + } +} + +// --- Auth Tests --- + +func createTestToken(secret string, userID uuid.UUID, role string, companyID *uuid.UUID) string { + claims := jwt.MapClaims{ + "sub": userID.String(), + "role": role, + "exp": time.Now().Add(time.Hour).Unix(), + } + if companyID != nil { + claims["company_id"] = companyID.String() + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenStr, _ := token.SignedString([]byte(secret)) + return tokenStr +} + +func TestRequireAuthValidToken(t *testing.T) { + secret := "test-secret" + userID, _ := uuid.NewV4() + companyID, _ := uuid.NewV4() + tokenStr := createTestToken(secret, userID, "Admin", &companyID) + + var receivedClaims Claims + handler := RequireAuth([]byte(secret))(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + receivedClaims, _ = GetClaims(r.Context()) + w.WriteHeader(http.StatusOK) + })) + + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Header.Set("Authorization", "Bearer "+tokenStr) + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", rec.Code) + } + if receivedClaims.UserID != userID { + t.Errorf("expected userID %s, got %s", userID, receivedClaims.UserID) + } + if receivedClaims.Role != "Admin" { + t.Errorf("expected role 'Admin', got '%s'", receivedClaims.Role) + } +} + +func TestRequireAuthMissingToken(t *testing.T) { + handler := RequireAuth([]byte("secret"))(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + + req := httptest.NewRequest(http.MethodGet, "/", nil) + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusUnauthorized { + t.Errorf("expected status 401, got %d", rec.Code) + } +} + +func TestRequireAuthInvalidToken(t *testing.T) { + handler := RequireAuth([]byte("secret"))(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Header.Set("Authorization", "Bearer invalid-token") + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusUnauthorized { + t.Errorf("expected status 401, got %d", rec.Code) + } +} + +func TestRequireAuthWrongSecret(t *testing.T) { + userID, _ := uuid.NewV4() + tokenStr := createTestToken("correct-secret", userID, "User", nil) + + handler := RequireAuth([]byte("wrong-secret"))(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Header.Set("Authorization", "Bearer "+tokenStr) + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusUnauthorized { + t.Errorf("expected status 401, got %d", rec.Code) + } +} + +func TestRequireAuthRoleRestriction(t *testing.T) { + secret := "secret" + userID, _ := uuid.NewV4() + tokenStr := createTestToken(secret, userID, "User", nil) + + handler := RequireAuth([]byte(secret), "Admin")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Header.Set("Authorization", "Bearer "+tokenStr) + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusForbidden { + t.Errorf("expected status 403, got %d", rec.Code) + } +} + +func TestRequireAuthRoleAllowed(t *testing.T) { + secret := "secret" + userID, _ := uuid.NewV4() + tokenStr := createTestToken(secret, userID, "Admin", nil) + + handler := RequireAuth([]byte(secret), "Admin")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Header.Set("Authorization", "Bearer "+tokenStr) + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", rec.Code) + } +} + +func TestGetClaimsFromContext(t *testing.T) { + claims := Claims{ + UserID: uuid.Must(uuid.NewV4()), + Role: "Admin", + } + ctx := context.WithValue(context.Background(), claimsKey, claims) + + retrieved, ok := GetClaims(ctx) + if !ok { + t.Error("expected to retrieve claims from context") + } + if retrieved.UserID != claims.UserID { + t.Errorf("expected userID %s, got %s", claims.UserID, retrieved.UserID) + } +} + +func TestGetClaimsNotInContext(t *testing.T) { + _, ok := GetClaims(context.Background()) + if ok { + t.Error("expected claims to not be in context") + } +} + +// --- Gzip Tests --- + +func TestGzipCompression(t *testing.T) { + handler := Gzip(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("Hello, World!")) + })) + + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Header.Set("Accept-Encoding", "gzip") + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + if rec.Header().Get("Content-Encoding") != "gzip" { + t.Error("expected Content-Encoding 'gzip'") + } + + // Decompress and verify + reader, err := gzip.NewReader(rec.Body) + if err != nil { + t.Fatalf("failed to create gzip reader: %v", err) + } + defer reader.Close() + + body, err := io.ReadAll(reader) + if err != nil { + t.Fatalf("failed to read gzip body: %v", err) + } + + if string(body) != "Hello, World!" { + t.Errorf("expected 'Hello, World!', got '%s'", string(body)) + } +} + +func TestGzipNoCompression(t *testing.T) { + handler := Gzip(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("Hello, World!")) + })) + + req := httptest.NewRequest(http.MethodGet, "/", nil) + // No Accept-Encoding header + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + if rec.Header().Get("Content-Encoding") == "gzip" { + t.Error("should not use gzip when not requested") + } + if rec.Body.String() != "Hello, World!" { + t.Errorf("expected 'Hello, World!', got '%s'", rec.Body.String()) + } +} + +// --- Logger Tests --- + +func TestLoggerMiddleware(t *testing.T) { + handler := Logger(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + + req := httptest.NewRequest(http.MethodGet, "/test-path", nil) + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", rec.Code) + } +} + +// --- CORS Legacy Wrapper Test --- + +func TestCORSLegacyWrapper(t *testing.T) { + handler := CORS(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Header.Set("Origin", "https://example.com") + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + if rec.Header().Get("Access-Control-Allow-Origin") != "*" { + t.Errorf("expected '*', got '%s'", rec.Header().Get("Access-Control-Allow-Origin")) + } +} diff --git a/backend/internal/payments/payments_test.go b/backend/internal/payments/payments_test.go new file mode 100644 index 0000000..e1c8af6 --- /dev/null +++ b/backend/internal/payments/payments_test.go @@ -0,0 +1,162 @@ +package payments + +import ( + "context" + "testing" + "time" + + "github.com/gofrs/uuid/v5" + "github.com/saveinmed/backend-go/internal/domain" +) + +func TestNewMercadoPagoGateway(t *testing.T) { + gateway := NewMercadoPagoGateway("https://api.mercadopago.com", 2.5) + + if gateway.BaseURL != "https://api.mercadopago.com" { + t.Errorf("expected BaseURL 'https://api.mercadopago.com', got '%s'", gateway.BaseURL) + } + if gateway.MarketplaceCommission != 2.5 { + t.Errorf("expected commission 2.5, got %f", gateway.MarketplaceCommission) + } +} + +func TestCreatePreference(t *testing.T) { + gateway := NewMercadoPagoGateway("https://api.mercadopago.com", 2.5) + + order := &domain.Order{ + ID: uuid.Must(uuid.NewV4()), + BuyerID: uuid.Must(uuid.NewV4()), + SellerID: uuid.Must(uuid.NewV4()), + TotalCents: 10000, // R$100 + } + + ctx := context.Background() + pref, err := gateway.CreatePreference(ctx, order) + if err != nil { + t.Fatalf("failed to create preference: %v", err) + } + + if pref.OrderID != order.ID { + t.Errorf("expected order ID %s, got %s", order.ID, pref.OrderID) + } + if pref.Gateway != "mercadopago" { + t.Errorf("expected gateway 'mercadopago', got '%s'", pref.Gateway) + } + if pref.CommissionPct != 2.5 { + t.Errorf("expected commission 2.5, got %f", pref.CommissionPct) + } + + // Test marketplace fee calculation (2.5% of 10000 = 250) + expectedFee := int64(250) + if pref.MarketplaceFee != expectedFee { + t.Errorf("expected marketplace fee %d, got %d", expectedFee, pref.MarketplaceFee) + } + + // Test seller receivable calculation (10000 - 250 = 9750) + expectedReceivable := int64(9750) + if pref.SellerReceivable != expectedReceivable { + t.Errorf("expected seller receivable %d, got %d", expectedReceivable, pref.SellerReceivable) + } + + // Test payment URL format + expectedURL := "https://api.mercadopago.com/checkout/v1/redirect?order_id=" + order.ID.String() + if pref.PaymentURL != expectedURL { + t.Errorf("expected URL '%s', got '%s'", expectedURL, pref.PaymentURL) + } +} + +func TestCreatePreferenceWithDifferentCommissions(t *testing.T) { + testCases := []struct { + name string + commission float64 + totalCents int64 + expectedFee int64 + expectedSeller int64 + }{ + {"5% commission", 5.0, 10000, 500, 9500}, + {"10% commission", 10.0, 10000, 1000, 9000}, + {"0% commission", 0.0, 10000, 0, 10000}, + {"2.5% on large order", 2.5, 100000, 2500, 97500}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + gateway := NewMercadoPagoGateway("https://test.com", tc.commission) + order := &domain.Order{ + ID: uuid.Must(uuid.NewV4()), + TotalCents: tc.totalCents, + } + + pref, err := gateway.CreatePreference(context.Background(), order) + if err != nil { + t.Fatalf("failed to create preference: %v", err) + } + + if pref.MarketplaceFee != tc.expectedFee { + t.Errorf("expected fee %d, got %d", tc.expectedFee, pref.MarketplaceFee) + } + if pref.SellerReceivable != tc.expectedSeller { + t.Errorf("expected seller receivable %d, got %d", tc.expectedSeller, pref.SellerReceivable) + } + }) + } +} + +func TestCreatePreferenceWithCancelledContext(t *testing.T) { + gateway := NewMercadoPagoGateway("https://api.mercadopago.com", 2.5) + + order := &domain.Order{ + ID: uuid.Must(uuid.NewV4()), + TotalCents: 10000, + } + + ctx, cancel := context.WithCancel(context.Background()) + cancel() // Cancel immediately + + _, err := gateway.CreatePreference(ctx, order) + if err == nil { + t.Error("expected error for cancelled context") + } +} + +func TestCreatePreferenceWithTimeout(t *testing.T) { + gateway := NewMercadoPagoGateway("https://api.mercadopago.com", 2.5) + + order := &domain.Order{ + ID: uuid.Must(uuid.NewV4()), + TotalCents: 10000, + } + + // Create a context that will timeout after a very short duration + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond) + defer cancel() + + // Wait for context to expire + time.Sleep(10 * time.Millisecond) + + _, err := gateway.CreatePreference(ctx, order) + if err == nil { + t.Error("expected error for timed out context") + } +} + +func TestCreatePreferenceWithZeroTotal(t *testing.T) { + gateway := NewMercadoPagoGateway("https://api.mercadopago.com", 2.5) + + order := &domain.Order{ + ID: uuid.Must(uuid.NewV4()), + TotalCents: 0, + } + + pref, err := gateway.CreatePreference(context.Background(), order) + if err != nil { + t.Fatalf("failed to create preference: %v", err) + } + + if pref.MarketplaceFee != 0 { + t.Errorf("expected fee 0, got %d", pref.MarketplaceFee) + } + if pref.SellerReceivable != 0 { + t.Errorf("expected seller receivable 0, got %d", pref.SellerReceivable) + } +} diff --git a/backend/internal/usecase/usecase_test.go b/backend/internal/usecase/usecase_test.go new file mode 100644 index 0000000..d93ba3c --- /dev/null +++ b/backend/internal/usecase/usecase_test.go @@ -0,0 +1,784 @@ +package usecase + +import ( + "context" + "testing" + "time" + + "github.com/gofrs/uuid/v5" + "github.com/saveinmed/backend-go/internal/domain" +) + +// MockRepository implements Repository interface for testing +type MockRepository struct { + companies []domain.Company + products []domain.Product + users []domain.User + orders []domain.Order + cartItems []domain.CartItem + reviews []domain.Review +} + +func NewMockRepository() *MockRepository { + return &MockRepository{ + companies: make([]domain.Company, 0), + products: make([]domain.Product, 0), + users: make([]domain.User, 0), + orders: make([]domain.Order, 0), + cartItems: make([]domain.CartItem, 0), + reviews: make([]domain.Review, 0), + } +} + +// Company methods +func (m *MockRepository) CreateCompany(ctx context.Context, company *domain.Company) error { + company.CreatedAt = time.Now() + company.UpdatedAt = time.Now() + m.companies = append(m.companies, *company) + return nil +} + +func (m *MockRepository) ListCompanies(ctx context.Context) ([]domain.Company, error) { + return m.companies, nil +} + +func (m *MockRepository) GetCompany(ctx context.Context, id uuid.UUID) (*domain.Company, error) { + for _, c := range m.companies { + if c.ID == id { + return &c, nil + } + } + return nil, nil +} + +func (m *MockRepository) UpdateCompany(ctx context.Context, company *domain.Company) error { + for i, c := range m.companies { + if c.ID == company.ID { + m.companies[i] = *company + return nil + } + } + return nil +} + +func (m *MockRepository) DeleteCompany(ctx context.Context, id uuid.UUID) error { + for i, c := range m.companies { + if c.ID == id { + m.companies = append(m.companies[:i], m.companies[i+1:]...) + return nil + } + } + return nil +} + +// Product methods +func (m *MockRepository) CreateProduct(ctx context.Context, product *domain.Product) error { + product.CreatedAt = time.Now() + product.UpdatedAt = time.Now() + m.products = append(m.products, *product) + return nil +} + +func (m *MockRepository) ListProducts(ctx context.Context) ([]domain.Product, error) { + return m.products, nil +} + +func (m *MockRepository) GetProduct(ctx context.Context, id uuid.UUID) (*domain.Product, error) { + for _, p := range m.products { + if p.ID == id { + return &p, nil + } + } + return nil, nil +} + +func (m *MockRepository) UpdateProduct(ctx context.Context, product *domain.Product) error { + for i, p := range m.products { + if p.ID == product.ID { + m.products[i] = *product + return nil + } + } + return nil +} + +func (m *MockRepository) DeleteProduct(ctx context.Context, id uuid.UUID) error { + for i, p := range m.products { + if p.ID == id { + m.products = append(m.products[:i], m.products[i+1:]...) + return nil + } + } + return nil +} + +func (m *MockRepository) AdjustInventory(ctx context.Context, productID uuid.UUID, delta int64, reason string) (*domain.InventoryItem, error) { + return &domain.InventoryItem{ProductID: productID, Quantity: delta}, nil +} + +func (m *MockRepository) ListInventory(ctx context.Context, filter domain.InventoryFilter) ([]domain.InventoryItem, error) { + return []domain.InventoryItem{}, nil +} + +// Order methods +func (m *MockRepository) CreateOrder(ctx context.Context, order *domain.Order) error { + m.orders = append(m.orders, *order) + return nil +} + +func (m *MockRepository) ListOrders(ctx context.Context) ([]domain.Order, error) { + return m.orders, nil +} + +func (m *MockRepository) GetOrder(ctx context.Context, id uuid.UUID) (*domain.Order, error) { + for _, o := range m.orders { + if o.ID == id { + return &o, nil + } + } + return nil, nil +} + +func (m *MockRepository) UpdateOrderStatus(ctx context.Context, id uuid.UUID, status domain.OrderStatus) error { + for i, o := range m.orders { + if o.ID == id { + m.orders[i].Status = status + return nil + } + } + return nil +} + +func (m *MockRepository) DeleteOrder(ctx context.Context, id uuid.UUID) error { + for i, o := range m.orders { + if o.ID == id { + m.orders = append(m.orders[:i], m.orders[i+1:]...) + return nil + } + } + return nil +} + +func (m *MockRepository) CreateShipment(ctx context.Context, shipment *domain.Shipment) error { + return nil +} + +func (m *MockRepository) GetShipmentByOrderID(ctx context.Context, orderID uuid.UUID) (*domain.Shipment, error) { + return nil, nil +} + +// User methods +func (m *MockRepository) CreateUser(ctx context.Context, user *domain.User) error { + m.users = append(m.users, *user) + return nil +} + +func (m *MockRepository) ListUsers(ctx context.Context, filter domain.UserFilter) ([]domain.User, int64, error) { + return m.users, int64(len(m.users)), nil +} + +func (m *MockRepository) GetUser(ctx context.Context, id uuid.UUID) (*domain.User, error) { + for _, u := range m.users { + if u.ID == id { + return &u, nil + } + } + return nil, nil +} + +func (m *MockRepository) GetUserByEmail(ctx context.Context, email string) (*domain.User, error) { + for _, u := range m.users { + if u.Email == email { + return &u, nil + } + } + return nil, nil +} + +func (m *MockRepository) UpdateUser(ctx context.Context, user *domain.User) error { + return nil +} + +func (m *MockRepository) DeleteUser(ctx context.Context, id uuid.UUID) error { + return nil +} + +// Cart methods +func (m *MockRepository) AddCartItem(ctx context.Context, item *domain.CartItem) (*domain.CartItem, error) { + m.cartItems = append(m.cartItems, *item) + return item, nil +} + +func (m *MockRepository) ListCartItems(ctx context.Context, buyerID uuid.UUID) ([]domain.CartItem, error) { + var items []domain.CartItem + for _, c := range m.cartItems { + if c.BuyerID == buyerID { + items = append(items, c) + } + } + return items, nil +} + +func (m *MockRepository) DeleteCartItem(ctx context.Context, id uuid.UUID, buyerID uuid.UUID) error { + for i, c := range m.cartItems { + if c.ID == id && c.BuyerID == buyerID { + m.cartItems = append(m.cartItems[:i], m.cartItems[i+1:]...) + return nil + } + } + return nil +} + +// Review methods +func (m *MockRepository) CreateReview(ctx context.Context, review *domain.Review) error { + m.reviews = append(m.reviews, *review) + return nil +} + +func (m *MockRepository) GetCompanyRating(ctx context.Context, companyID uuid.UUID) (*domain.CompanyRating, error) { + return &domain.CompanyRating{CompanyID: companyID, AverageScore: 4.5, TotalReviews: 10}, nil +} + +func (m *MockRepository) SellerDashboard(ctx context.Context, sellerID uuid.UUID) (*domain.SellerDashboard, error) { + return &domain.SellerDashboard{SellerID: sellerID}, nil +} + +func (m *MockRepository) AdminDashboard(ctx context.Context, since time.Time) (*domain.AdminDashboard, error) { + return &domain.AdminDashboard{GMVCents: 1000000}, nil +} + +// MockPaymentGateway for testing +type MockPaymentGateway struct{} + +func (m *MockPaymentGateway) CreatePreference(ctx context.Context, order *domain.Order) (*domain.PaymentPreference, error) { + return &domain.PaymentPreference{ + OrderID: order.ID, + Gateway: "mock", + CommissionPct: 2.5, + MarketplaceFee: int64(float64(order.TotalCents) * 0.025), + SellerReceivable: order.TotalCents - int64(float64(order.TotalCents)*0.025), + PaymentURL: "https://mock.payment.url", + }, nil +} + +// Helper to create a test service +func newTestService() (*Service, *MockRepository) { + repo := NewMockRepository() + gateway := &MockPaymentGateway{} + svc := NewService(repo, gateway, 2.5, "test-secret", time.Hour, "test-pepper") + return svc, repo +} + +// --- Company Tests --- + +func TestRegisterCompany(t *testing.T) { + svc, _ := newTestService() + ctx := context.Background() + + company := &domain.Company{ + Role: "pharmacy", + CNPJ: "12345678901234", + CorporateName: "Test Pharmacy", + LicenseNumber: "LIC-001", + } + + err := svc.RegisterCompany(ctx, company) + if err != nil { + t.Fatalf("failed to register company: %v", err) + } + + if company.ID == uuid.Nil { + t.Error("expected company ID to be set") + } +} + +func TestListCompanies(t *testing.T) { + svc, _ := newTestService() + ctx := context.Background() + + companies, err := svc.ListCompanies(ctx) + if err != nil { + t.Fatalf("failed to list companies: %v", err) + } + + if len(companies) != 0 { + t.Errorf("expected 0 companies, got %d", len(companies)) + } +} + +func TestVerifyCompany(t *testing.T) { + svc, repo := newTestService() + ctx := context.Background() + + company := &domain.Company{ + ID: uuid.Must(uuid.NewV4()), + Role: "pharmacy", + CNPJ: "12345678901234", + CorporateName: "Test Pharmacy", + IsVerified: false, + } + repo.companies = append(repo.companies, *company) + + verified, err := svc.VerifyCompany(ctx, company.ID) + if err != nil { + t.Fatalf("failed to verify company: %v", err) + } + + if !verified.IsVerified { + t.Error("expected company to be verified") + } +} + +// --- Product Tests --- + +func TestRegisterProduct(t *testing.T) { + svc, _ := newTestService() + ctx := context.Background() + + product := &domain.Product{ + SellerID: uuid.Must(uuid.NewV4()), + Name: "Test Product", + Description: "A test product", + Batch: "BATCH-001", + ExpiresAt: time.Now().AddDate(1, 0, 0), + PriceCents: 1000, + Stock: 100, + } + + err := svc.RegisterProduct(ctx, product) + if err != nil { + t.Fatalf("failed to register product: %v", err) + } + + if product.ID == uuid.Nil { + t.Error("expected product ID to be set") + } +} + +func TestListProducts(t *testing.T) { + svc, _ := newTestService() + ctx := context.Background() + + products, err := svc.ListProducts(ctx) + if err != nil { + t.Fatalf("failed to list products: %v", err) + } + + if len(products) != 0 { + t.Errorf("expected 0 products, got %d", len(products)) + } +} + +// --- Order Tests --- + +func TestCreateOrder(t *testing.T) { + svc, _ := newTestService() + ctx := context.Background() + + order := &domain.Order{ + BuyerID: uuid.Must(uuid.NewV4()), + SellerID: uuid.Must(uuid.NewV4()), + TotalCents: 10000, + } + + err := svc.CreateOrder(ctx, order) + if err != nil { + t.Fatalf("failed to create order: %v", err) + } + + if order.ID == uuid.Nil { + t.Error("expected order ID to be set") + } + if order.Status != domain.OrderStatusPending { + t.Errorf("expected status 'Pendente', got '%s'", order.Status) + } +} + +func TestUpdateOrderStatus(t *testing.T) { + svc, repo := newTestService() + ctx := context.Background() + + order := &domain.Order{ + ID: uuid.Must(uuid.NewV4()), + BuyerID: uuid.Must(uuid.NewV4()), + SellerID: uuid.Must(uuid.NewV4()), + Status: domain.OrderStatusPending, + TotalCents: 10000, + } + repo.orders = append(repo.orders, *order) + + err := svc.UpdateOrderStatus(ctx, order.ID, domain.OrderStatusPaid) + if err != nil { + t.Fatalf("failed to update order status: %v", err) + } +} + +// --- User Tests --- + +func TestCreateUser(t *testing.T) { + svc, _ := newTestService() + ctx := context.Background() + + user := &domain.User{ + CompanyID: uuid.Must(uuid.NewV4()), + Role: "admin", + Name: "Test User", + Email: "test@example.com", + } + + err := svc.CreateUser(ctx, user, "password123") + if err != nil { + t.Fatalf("failed to create user: %v", err) + } + + if user.ID == uuid.Nil { + t.Error("expected user ID to be set") + } + if user.PasswordHash == "" { + t.Error("expected password to be hashed") + } +} + +func TestListUsers(t *testing.T) { + svc, _ := newTestService() + ctx := context.Background() + + page, err := svc.ListUsers(ctx, domain.UserFilter{}, 1, 20) + if err != nil { + t.Fatalf("failed to list users: %v", err) + } + + if page.Page != 1 { + t.Errorf("expected page 1, got %d", page.Page) + } + if page.PageSize != 20 { + t.Errorf("expected pageSize 20, got %d", page.PageSize) + } +} + +func TestListUsersPagination(t *testing.T) { + svc, _ := newTestService() + ctx := context.Background() + + // Test with page < 1 (should default to 1) + page, err := svc.ListUsers(ctx, domain.UserFilter{}, 0, 10) + if err != nil { + t.Fatalf("failed to list users: %v", err) + } + if page.Page != 1 { + t.Errorf("expected page 1, got %d", page.Page) + } + + // Test with pageSize <= 0 (should default to 20) + page2, err := svc.ListUsers(ctx, domain.UserFilter{}, 1, 0) + if err != nil { + t.Fatalf("failed to list users: %v", err) + } + if page2.PageSize != 20 { + t.Errorf("expected pageSize 20, got %d", page2.PageSize) + } +} + +// --- Authentication Tests --- + +func TestAuthenticate(t *testing.T) { + svc, repo := newTestService() + ctx := context.Background() + + // First create a user + user := &domain.User{ + CompanyID: uuid.Must(uuid.NewV4()), + Role: "admin", + Name: "Test User", + Email: "auth@example.com", + } + err := svc.CreateUser(ctx, user, "testpass123") + if err != nil { + t.Fatalf("failed to create user: %v", err) + } + + // Update the mock with the hashed password + repo.users[0] = *user + + // Test authentication + token, expiresAt, err := svc.Authenticate(ctx, "auth@example.com", "testpass123") + if err != nil { + t.Fatalf("failed to authenticate: %v", err) + } + + if token == "" { + t.Error("expected token to be returned") + } + if expiresAt.Before(time.Now()) { + t.Error("expected expiration to be in the future") + } +} + +func TestAuthenticateInvalidPassword(t *testing.T) { + svc, repo := newTestService() + ctx := context.Background() + + user := &domain.User{ + CompanyID: uuid.Must(uuid.NewV4()), + Role: "admin", + Name: "Test User", + Email: "fail@example.com", + } + svc.CreateUser(ctx, user, "correctpass") + repo.users[0] = *user + + _, _, err := svc.Authenticate(ctx, "fail@example.com", "wrongpass") + if err == nil { + t.Error("expected authentication to fail") + } +} + +// --- Cart Tests --- + +func TestAddItemToCart(t *testing.T) { + svc, repo := newTestService() + ctx := context.Background() + + buyerID := uuid.Must(uuid.NewV4()) + product := &domain.Product{ + ID: uuid.Must(uuid.NewV4()), + SellerID: uuid.Must(uuid.NewV4()), + Name: "Test Product", + PriceCents: 1000, + Stock: 100, + Batch: "BATCH-001", + ExpiresAt: time.Now().AddDate(1, 0, 0), + } + repo.products = append(repo.products, *product) + + summary, err := svc.AddItemToCart(ctx, buyerID, product.ID, 5) + if err != nil { + t.Fatalf("failed to add item to cart: %v", err) + } + + if summary.SubtotalCents != 5000 { + t.Errorf("expected subtotal 5000, got %d", summary.SubtotalCents) + } +} + +func TestAddItemToCartInvalidQuantity(t *testing.T) { + svc, _ := newTestService() + ctx := context.Background() + + buyerID := uuid.Must(uuid.NewV4()) + productID := uuid.Must(uuid.NewV4()) + + _, err := svc.AddItemToCart(ctx, buyerID, productID, 0) + if err == nil { + t.Error("expected error for zero quantity") + } +} + +func TestCartB2BDiscount(t *testing.T) { + svc, repo := newTestService() + ctx := context.Background() + + buyerID := uuid.Must(uuid.NewV4()) + product := &domain.Product{ + ID: uuid.Must(uuid.NewV4()), + SellerID: uuid.Must(uuid.NewV4()), + Name: "Expensive Product", + PriceCents: 50000, // R$500 per unit + Stock: 1000, + Batch: "BATCH-001", + ExpiresAt: time.Now().AddDate(1, 0, 0), + } + repo.products = append(repo.products, *product) + + // Add enough to trigger B2B discount (>R$1000) + summary, err := svc.AddItemToCart(ctx, buyerID, product.ID, 3) // R$1500 + if err != nil { + t.Fatalf("failed to add item to cart: %v", err) + } + + if summary.DiscountCents == 0 { + t.Error("expected B2B discount to be applied") + } + if summary.DiscountReason == "" { + t.Error("expected discount reason to be set") + } +} + +// --- Review Tests --- + +func TestCreateReview(t *testing.T) { + svc, repo := newTestService() + ctx := context.Background() + + buyerID := uuid.Must(uuid.NewV4()) + sellerID := uuid.Must(uuid.NewV4()) + + order := &domain.Order{ + ID: uuid.Must(uuid.NewV4()), + BuyerID: buyerID, + SellerID: sellerID, + Status: domain.OrderStatusDelivered, + TotalCents: 10000, + } + repo.orders = append(repo.orders, *order) + + review, err := svc.CreateReview(ctx, buyerID, order.ID, 5, "Great service!") + if err != nil { + t.Fatalf("failed to create review: %v", err) + } + + if review.Rating != 5 { + t.Errorf("expected rating 5, got %d", review.Rating) + } +} + +func TestCreateReviewInvalidRating(t *testing.T) { + svc, _ := newTestService() + ctx := context.Background() + + _, err := svc.CreateReview(ctx, uuid.Must(uuid.NewV4()), uuid.Must(uuid.NewV4()), 6, "Invalid") + if err == nil { + t.Error("expected error for invalid rating") + } +} + +func TestCreateReviewNotDelivered(t *testing.T) { + svc, repo := newTestService() + ctx := context.Background() + + buyerID := uuid.Must(uuid.NewV4()) + order := &domain.Order{ + ID: uuid.Must(uuid.NewV4()), + BuyerID: buyerID, + Status: domain.OrderStatusPending, // Not delivered + TotalCents: 10000, + } + repo.orders = append(repo.orders, *order) + + _, err := svc.CreateReview(ctx, buyerID, order.ID, 5, "Great!") + if err == nil { + t.Error("expected error for non-delivered order") + } +} + +// --- Dashboard Tests --- + +func TestGetSellerDashboard(t *testing.T) { + svc, _ := newTestService() + ctx := context.Background() + + sellerID := uuid.Must(uuid.NewV4()) + dashboard, err := svc.GetSellerDashboard(ctx, sellerID) + if err != nil { + t.Fatalf("failed to get seller dashboard: %v", err) + } + + if dashboard.SellerID != sellerID { + t.Errorf("expected seller ID %s, got %s", sellerID, dashboard.SellerID) + } +} + +func TestGetAdminDashboard(t *testing.T) { + svc, _ := newTestService() + ctx := context.Background() + + dashboard, err := svc.GetAdminDashboard(ctx) + if err != nil { + t.Fatalf("failed to get admin dashboard: %v", err) + } + + if dashboard.GMVCents != 1000000 { + t.Errorf("expected GMV 1000000, got %d", dashboard.GMVCents) + } +} + +// --- Payment Tests --- + +func TestCreatePaymentPreference(t *testing.T) { + svc, repo := newTestService() + ctx := context.Background() + + order := &domain.Order{ + ID: uuid.Must(uuid.NewV4()), + BuyerID: uuid.Must(uuid.NewV4()), + SellerID: uuid.Must(uuid.NewV4()), + TotalCents: 10000, + } + repo.orders = append(repo.orders, *order) + + pref, err := svc.CreatePaymentPreference(ctx, order.ID) + if err != nil { + t.Fatalf("failed to create payment preference: %v", err) + } + + if pref.PaymentURL == "" { + t.Error("expected payment URL to be set") + } + if pref.MarketplaceFee == 0 { + t.Error("expected marketplace fee to be calculated") + } +} + +func TestHandlePaymentWebhook(t *testing.T) { + svc, repo := newTestService() + ctx := context.Background() + + order := &domain.Order{ + ID: uuid.Must(uuid.NewV4()), + BuyerID: uuid.Must(uuid.NewV4()), + SellerID: uuid.Must(uuid.NewV4()), + Status: domain.OrderStatusPending, + TotalCents: 10000, + } + repo.orders = append(repo.orders, *order) + + event := domain.PaymentWebhookEvent{ + PaymentID: "PAY-123", + OrderID: order.ID, + Status: "approved", + TotalPaidAmount: 10000, + } + + result, err := svc.HandlePaymentWebhook(ctx, event) + if err != nil { + t.Fatalf("failed to handle webhook: %v", err) + } + + if result.Status != "approved" { + t.Errorf("expected status 'approved', got '%s'", result.Status) + } + if result.MarketplaceFee == 0 { + t.Error("expected marketplace fee to be calculated") + } +} + +// --- Register Account Tests --- + +func TestRegisterAccount(t *testing.T) { + svc, _ := newTestService() + ctx := context.Background() + + company := &domain.Company{ + Role: "pharmacy", + CNPJ: "12345678901234", + CorporateName: "Test Pharmacy", + } + + user := &domain.User{ + Role: "admin", + Name: "Admin User", + Email: "admin@example.com", + } + + err := svc.RegisterAccount(ctx, company, user, "password123") + if err != nil { + t.Fatalf("failed to register account: %v", err) + } + + if company.ID == uuid.Nil { + t.Error("expected company ID to be set") + } + if user.CompanyID != company.ID { + t.Error("expected user to be linked to company") + } +} diff --git a/marketplace/package-lock.json b/marketplace/package-lock.json index dc76220..a9216f8 100644 --- a/marketplace/package-lock.json +++ b/marketplace/package-lock.json @@ -17,17 +17,36 @@ "zustand": "^4.5.5" }, "devDependencies": { + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.1", + "@testing-library/user-event": "^14.6.1", "@types/react": "^18.3.7", "@types/react-dom": "^18.3.0", "@types/react-window": "^1.8.8", - "@vitejs/plugin-react": "^4.3.2", + "@vitejs/plugin-react": "^4.7.0", "autoprefixer": "^10.4.20", + "jsdom": "^27.3.0", "postcss": "^8.4.47", "tailwindcss": "^3.4.10", "typescript": "^5.6.2", - "vite": "^5.4.3" + "vite": "^5.4.3", + "vitest": "^4.0.16" } }, + "node_modules/@acemir/cssom": { + "version": "0.9.29", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.29.tgz", + "integrity": "sha512-G90x0VW+9nW4dFajtjCoT+NM0scAfH9Mb08IcjgFHYbfiL/lU04dTF9JuVOi3/OH+DJCQdcIseSXkdCB9Ky6JA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -41,6 +60,61 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.1.tgz", + "integrity": "sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "lru-cache": "^11.2.4" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.7.6", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.6.tgz", + "integrity": "sha512-hBaJER6A9MpdG3WgdlOolHmbOYvSk46y7IQN/1+iqiCuUu6iWdQrs9DGKF8ocqsEqWujWf/V7b7vaDgiUmIvUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.4" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -333,6 +407,143 @@ "node": ">=6.9.0" } }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.0.21", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.21.tgz", + "integrity": "sha512-plP8N8zKfEZ26figX4Nvajx8DuzfuRpLTqglQ5d0chfnt35Qt3X+m6ASZ+rG0D0kxe/upDVNwSIVJP5n4FuNfw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -622,6 +833,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/netbsd-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", @@ -639,6 +867,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/openbsd-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", @@ -656,6 +901,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", @@ -1155,6 +1417,110 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.1.tgz", + "integrity": "sha512-gr4KtAWqIOQoucWYD/f6ki+j5chXfcPc74Col/6poTyqTmn7zRmodWahWRCp8tYd+GMqBonw6hstNzqjbs6gjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1200,6 +1566,24 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1232,6 +1616,7 @@ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -1267,6 +1652,123 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@vitest/expect": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.16.tgz", + "integrity": "sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.16", + "@vitest/utils": "4.0.16", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.16.tgz", + "integrity": "sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.16.tgz", + "integrity": "sha512-VWEDm5Wv9xEo80ctjORcTQRJ539EGPB3Pb9ApvVRAY1U/WkHXmmYISqU5E79uCwcW7xYUV38gwZD+RV755fu3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.16", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.16.tgz", + "integrity": "sha512-sf6NcrYhYBsSYefxnry+DR8n3UV4xWZwWxYbCJUt2YdvtqzSPR7VfGrY0zsv090DAbjFZsi7ZaMi1KnSRyK1XA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.16", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.16.tgz", + "integrity": "sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.16.tgz", + "integrity": "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.16", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", @@ -1295,6 +1797,26 @@ "dev": true, "license": "MIT" }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1359,6 +1881,16 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -1464,6 +1996,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.1.tgz", + "integrity": "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -1531,6 +2073,27 @@ "dev": true, "license": "MIT" }, + "node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -1544,6 +2107,21 @@ "node": ">=4" } }, + "node_modules/cssstyle": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.5.tgz", + "integrity": "sha512-GlsEptulso7Jg0VaOZ8BXQi3AkYM5BOJKEO/rjMidSCq70FkIC5y0eawrCXeYzxgt3OCf4Ls+eoxN+/05vN0Ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^4.1.1", + "@csstools/css-syntax-patches-for-csstree": "^1.0.21", + "css-tree": "^3.1.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -1551,6 +2129,20 @@ "devOptional": true, "license": "MIT" }, + "node_modules/data-urls": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz", + "integrity": "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1569,6 +2161,13 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -1578,6 +2177,16 @@ "node": ">=0.4.0" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -1592,6 +2201,13 @@ "dev": true, "license": "MIT" }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -1613,6 +2229,19 @@ "dev": true, "license": "ISC" }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -1631,6 +2260,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -1707,6 +2343,26 @@ "node": ">=6" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -1945,6 +2601,70 @@ "node": ">= 0.4" } }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -2007,6 +2727,13 @@ "node": ">=0.12.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/jiti": { "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", @@ -2024,6 +2751,47 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, + "node_modules/jsdom": { + "version": "27.3.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.3.0.tgz", + "integrity": "sha512-GtldT42B8+jefDUC4yUKAvsaOrH7PDHmZxZXNgF2xMmymjUbRYJvpAybZAKEmXDGTM0mCsz8duOa4vTm5AY2Kg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@acemir/cssom": "^0.9.28", + "@asamuzakjp/dom-selector": "^6.7.6", + "cssstyle": "^5.3.4", + "data-urls": "^6.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.1.0", + "ws": "^8.18.3", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -2092,6 +2860,26 @@ "yallist": "^3.0.2" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -2101,6 +2889,13 @@ "node": ">= 0.4" } }, + "node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/memoize-one": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", @@ -2152,6 +2947,16 @@ "node": ">= 0.6" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2227,6 +3032,30 @@ "node": ">= 6" } }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -2234,6 +3063,13 @@ "dev": true, "license": "MIT" }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2438,12 +3274,37 @@ "dev": true, "license": "MIT" }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -2492,6 +3353,13 @@ "react": "^18.3.1" } }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -2574,6 +3442,30 @@ "node": ">=8.10.0" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -2672,6 +3564,26 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -2691,6 +3603,13 @@ "semver": "bin/semver.js" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2701,6 +3620,33 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/sucrase": { "version": "3.35.1", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", @@ -2737,6 +3683,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/tailwindcss": { "version": "3.4.19", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", @@ -2798,6 +3751,23 @@ "node": ">=0.8" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -2847,6 +3817,36 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.0.19", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.19.tgz", + "integrity": "sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.19" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.19", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.19.tgz", + "integrity": "sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==", + "dev": true, + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -2860,6 +3860,32 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -2989,6 +4015,768 @@ } } }, + "node_modules/vitest": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.16.tgz", + "integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.16", + "@vitest/mocker": "4.0.16", + "@vitest/pretty-format": "4.0.16", + "@vitest/runner": "4.0.16", + "@vitest/snapshot": "4.0.16", + "@vitest/spy": "4.0.16", + "@vitest/utils": "4.0.16", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.16", + "@vitest/browser-preview": "4.0.16", + "@vitest/browser-webdriverio": "4.0.16", + "@vitest/ui": "4.0.16", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.16.tgz", + "integrity": "sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.16", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/vitest/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest/node_modules/vite": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", + "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz", + "integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/marketplace/package.json b/marketplace/package.json index cbcee8c..af0f9da 100644 --- a/marketplace/package.json +++ b/marketplace/package.json @@ -6,7 +6,9 @@ "scripts": { "dev": "vite", "build": "tsc -b && vite build", - "preview": "vite preview" + "preview": "vite preview", + "test": "vitest", + "test:coverage": "vitest run --coverage" }, "dependencies": { "@mercadopago/sdk-react": "^1.0.6", @@ -18,14 +20,19 @@ "zustand": "^4.5.5" }, "devDependencies": { + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.1", + "@testing-library/user-event": "^14.6.1", "@types/react": "^18.3.7", "@types/react-dom": "^18.3.0", "@types/react-window": "^1.8.8", - "@vitejs/plugin-react": "^4.3.2", + "@vitejs/plugin-react": "^4.7.0", "autoprefixer": "^10.4.20", + "jsdom": "^27.3.0", "postcss": "^8.4.47", "tailwindcss": "^3.4.10", "typescript": "^5.6.2", - "vite": "^5.4.3" + "vite": "^5.4.3", + "vitest": "^4.0.16" } } diff --git a/marketplace/src/App.tsx b/marketplace/src/App.tsx index 1b8c379..cc0af48 100644 --- a/marketplace/src/App.tsx +++ b/marketplace/src/App.tsx @@ -4,6 +4,10 @@ import { DashboardPage } from './pages/Dashboard' import { CartPage } from './pages/Cart' import { CheckoutPage } from './pages/Checkout' import { ProfilePage } from './pages/Profile' +import { OrdersPage } from './pages/Orders' +import { InventoryPage } from './pages/Inventory' +import { CompanyPage } from './pages/Company' +import { SellerDashboardPage } from './pages/SellerDashboard' import { ProtectedRoute } from './components/ProtectedRoute' function App() { @@ -34,6 +38,38 @@ function App() { } /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> { + let store: Record = {} + return { + getItem: vi.fn((key: string) => store[key] || null), + setItem: vi.fn((key: string, value: string) => { + store[key] = value + }), + removeItem: vi.fn((key: string) => { + delete store[key] + }), + clear: vi.fn(() => { + store = {} + }) + } +})() + +Object.defineProperty(window, 'localStorage', { value: localStorageMock }) + +describe('usePersistentFilters', () => { + beforeEach(() => { + localStorageMock.clear() + vi.clearAllMocks() + }) + + it('should return default filters when localStorage is empty', () => { + const { result } = renderHook(() => usePersistentFilters()) + + expect(result.current.filters).toEqual(defaultFilters) + }) + + it('should load persisted filters from localStorage', () => { + const persistedFilters: FilterState = { + activeIngredient: 'Amoxicilina', + category: 'Antibiótico', + lab: 'EMS' + } + localStorageMock.getItem.mockReturnValueOnce(JSON.stringify(persistedFilters)) + + const { result } = renderHook(() => usePersistentFilters()) + + expect(result.current.filters).toEqual(persistedFilters) + }) + + it('should update filters and persist to localStorage', () => { + const { result } = renderHook(() => usePersistentFilters()) + + act(() => { + result.current.setFilters({ + ...defaultFilters, + activeIngredient: 'Dipirona' + }) + }) + + expect(result.current.filters.activeIngredient).toBe('Dipirona') + expect(localStorageMock.setItem).toHaveBeenCalledWith( + 'mp-quick-filters', + expect.stringContaining('Dipirona') + ) + }) + + it('should update individual filter fields', () => { + const { result } = renderHook(() => usePersistentFilters()) + + act(() => { + result.current.setFilters((prev) => ({ ...prev, lab: 'Eurofarma' })) + }) + + expect(result.current.filters.lab).toBe('Eurofarma') + expect(result.current.filters.activeIngredient).toBe('') + expect(result.current.filters.category).toBe('') + }) + + it('should reset filters to defaults', () => { + const { result } = renderHook(() => usePersistentFilters()) + + act(() => { + result.current.setFilters({ + activeIngredient: 'Test', + category: 'Test', + lab: 'Test' + }) + }) + + act(() => { + result.current.setFilters(defaultFilters) + }) + + expect(result.current.filters).toEqual(defaultFilters) + }) +}) diff --git a/marketplace/src/pages/Company.tsx b/marketplace/src/pages/Company.tsx new file mode 100644 index 0000000..5a356bf --- /dev/null +++ b/marketplace/src/pages/Company.tsx @@ -0,0 +1,173 @@ +import { useEffect, useState } from 'react' +import { Shell } from '../layouts/Shell' +import { apiClient } from '../services/apiClient' + +interface Company { + id: string + role: string + cnpj: string + corporate_name: string + license_number: string + is_verified: boolean + created_at: string +} + +export function CompanyPage() { + const [company, setCompany] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [editing, setEditing] = useState(false) + const [form, setForm] = useState({ + corporate_name: '', + license_number: '' + }) + + useEffect(() => { + loadCompany() + }, []) + + const loadCompany = async () => { + try { + setLoading(true) + const response = await apiClient.get('/v1/companies/me') + setCompany(response.data) + setForm({ + corporate_name: response.data.corporate_name, + license_number: response.data.license_number + }) + setError(null) + } catch (err) { + setError('Erro ao carregar dados da empresa') + console.error(err) + } finally { + setLoading(false) + } + } + + const saveChanges = async () => { + if (!company) return + try { + await apiClient.put(`/v1/companies/${company.id}`, form) + await loadCompany() + setEditing(false) + } catch (err) { + console.error('Erro ao salvar:', err) + } + } + + const roleLabels: Record = { + pharmacy: 'Farmácia', + distributor: 'Distribuidora', + admin: 'Administrador' + } + + return ( + +
+
+

Minha Empresa

+ {company && !editing && ( + + )} +
+ + {loading && ( +
+
+
+ )} + + {error && ( +
{error}
+ )} + + {company && !editing && ( +
+
+ Status + {company.is_verified ? ( + + ✓ Verificada + + ) : ( + + Pendente verificação + + )} +
+
+
+

Razão Social

+

{company.corporate_name}

+
+
+

CNPJ

+

{company.cnpj}

+
+
+

Tipo

+

{roleLabels[company.role] || company.role}

+
+
+

Licença Sanitária

+

{company.license_number}

+
+
+

Cadastro

+

+ {new Date(company.created_at).toLocaleDateString('pt-BR')} +

+
+
+
+ )} + + {company && editing && ( +
+
+ + setForm({ ...form, corporate_name: e.target.value })} + className="w-full rounded border border-gray-200 px-3 py-2" + /> +
+
+ + setForm({ ...form, license_number: e.target.value })} + className="w-full rounded border border-gray-200 px-3 py-2" + /> +
+
+ + +
+
+ )} +
+
+ ) +} diff --git a/marketplace/src/pages/Inventory.tsx b/marketplace/src/pages/Inventory.tsx new file mode 100644 index 0000000..d55eb0a --- /dev/null +++ b/marketplace/src/pages/Inventory.tsx @@ -0,0 +1,153 @@ +import { useEffect, useState } from 'react' +import { Shell } from '../layouts/Shell' +import { apiClient } from '../services/apiClient' + +interface InventoryItem { + product_id: string + seller_id: string + name: string + batch: string + expires_at: string + quantity: number + price_cents: number +} + +export function InventoryPage() { + const [inventory, setInventory] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [expiringDays, setExpiringDays] = useState('') + + useEffect(() => { + loadInventory() + }, [expiringDays]) + + const loadInventory = async () => { + try { + setLoading(true) + const params = expiringDays ? `?expires_in_days=${expiringDays}` : '' + const response = await apiClient.get(`/v1/inventory${params}`) + setInventory(response.data || []) + setError(null) + } catch (err) { + setError('Erro ao carregar estoque') + console.error(err) + } finally { + setLoading(false) + } + } + + const adjustStock = async (productId: string, delta: number, reason: string) => { + try { + await apiClient.post('/v1/inventory/adjust', { + product_id: productId, + delta, + reason + }) + await loadInventory() + } catch (err) { + console.error('Erro ao ajustar estoque:', err) + } + } + + const isExpiringSoon = (date: string) => { + const expires = new Date(date) + const now = new Date() + const diffDays = Math.ceil((expires.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)) + return diffDays <= 30 + } + + return ( + +
+
+
+

Estoque

+

Controle de inventário e validades

+
+
+ + +
+
+ + {loading && ( +
+
+
+ )} + + {error && ( +
{error}
+ )} + + {!loading && inventory.length === 0 && ( +
+ Nenhum item no estoque +
+ )} + +
+ + + + + + + + + + + + + {inventory.map((item) => ( + + + + + + + + + ))} + +
ProdutoLoteValidadeQuantidadePreçoAções
{item.name}{item.batch} + {new Date(item.expires_at).toLocaleDateString('pt-BR')} + {isExpiringSoon(item.expires_at) && ⚠️} + {item.quantity} + R$ {(item.price_cents / 100).toFixed(2)} + +
+ + +
+
+
+
+
+ ) +} diff --git a/marketplace/src/pages/Orders.tsx b/marketplace/src/pages/Orders.tsx new file mode 100644 index 0000000..90f7ac3 --- /dev/null +++ b/marketplace/src/pages/Orders.tsx @@ -0,0 +1,138 @@ +import { useEffect, useState } from 'react' +import { Shell } from '../layouts/Shell' +import { apiClient } from '../services/apiClient' + +interface Order { + id: string + buyer_id: string + seller_id: string + status: string + total_cents: number + created_at: string +} + +const statusColors: Record = { + Pendente: 'bg-yellow-100 text-yellow-800', + Pago: 'bg-blue-100 text-blue-800', + Faturado: 'bg-purple-100 text-purple-800', + Entregue: 'bg-green-100 text-green-800' +} + +export function OrdersPage() { + const [orders, setOrders] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + loadOrders() + }, []) + + const loadOrders = async () => { + try { + setLoading(true) + const response = await apiClient.get('/v1/orders') + setOrders(response.data || []) + setError(null) + } catch (err) { + setError('Erro ao carregar pedidos') + console.error(err) + } finally { + setLoading(false) + } + } + + const updateStatus = async (orderId: string, newStatus: string) => { + try { + await apiClient.post(`/v1/orders/${orderId}/status`, { status: newStatus }) + await loadOrders() + } catch (err) { + console.error('Erro ao atualizar status:', err) + } + } + + const formatCurrency = (cents: number) => `R$ ${(cents / 100).toFixed(2)}` + + return ( + +
+
+
+

Pedidos

+

Gerenciamento de pedidos e status

+
+ +
+ + {loading && ( +
+
+
+ )} + + {error && ( +
{error}
+ )} + + {!loading && orders.length === 0 && ( +
+ Nenhum pedido encontrado +
+ )} + +
+ {orders.map((order) => ( +
+
+
+

Pedido #{order.id.slice(0, 8)}

+

+ {new Date(order.created_at).toLocaleDateString('pt-BR')} +

+
+
+

+ {formatCurrency(order.total_cents)} +

+ + {order.status} + +
+
+
+ {order.status === 'Pendente' && ( + + )} + {order.status === 'Pago' && ( + + )} + {order.status === 'Faturado' && ( + + )} +
+
+ ))} +
+
+
+ ) +} diff --git a/marketplace/src/pages/SellerDashboard.tsx b/marketplace/src/pages/SellerDashboard.tsx new file mode 100644 index 0000000..1c9a492 --- /dev/null +++ b/marketplace/src/pages/SellerDashboard.tsx @@ -0,0 +1,152 @@ +import { useEffect, useState } from 'react' +import { Shell } from '../layouts/Shell' +import { apiClient } from '../services/apiClient' + +interface SellerDashboardData { + seller_id: string + total_sales_cents: number + orders_count: number + top_products: Array<{ + product_id: string + name: string + total_quantity: number + revenue_cents: number + }> + low_stock_alerts: Array<{ + id: string + name: string + stock: number + }> +} + +export function SellerDashboardPage() { + const [data, setData] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + loadDashboard() + }, []) + + const loadDashboard = async () => { + try { + setLoading(true) + const response = await apiClient.get('/v1/dashboard/seller') + setData(response.data) + setError(null) + } catch (err) { + setError('Erro ao carregar dashboard') + console.error(err) + } finally { + setLoading(false) + } + } + + const formatCurrency = (cents: number) => `R$ ${(cents / 100).toFixed(2)}` + + return ( + +
+
+
+

Dashboard do Vendedor

+

Métricas e indicadores de performance

+
+ +
+ + {loading && ( +
+
+
+ )} + + {error && ( +
{error}
+ )} + + {data && ( + <> + {/* KPI Cards */} +
+
+

Total de Vendas

+

{formatCurrency(data.total_sales_cents)}

+
+
+

Pedidos

+

{data.orders_count}

+
+
+

Ticket Médio

+

+ {data.orders_count > 0 + ? formatCurrency(data.total_sales_cents / data.orders_count) + : 'R$ 0,00'} +

+
+
+ +
+ {/* Top Products */} +
+

Top Produtos

+ {data.top_products.length === 0 ? ( +

Nenhum produto vendido ainda

+ ) : ( +
+ {data.top_products.map((product, idx) => ( +
+
+ + {idx + 1} + +
+

{product.name}

+

{product.total_quantity} unidades

+
+
+ + {formatCurrency(product.revenue_cents)} + +
+ ))} +
+ )} +
+ + {/* Low Stock Alerts */} +
+

+ ⚠️ Alertas de Estoque Baixo +

+ {data.low_stock_alerts.length === 0 ? ( +

✓ Todos os produtos com estoque adequado

+ ) : ( +
+ {data.low_stock_alerts.map((product) => ( +
+ {product.name} + + {product.stock} restantes + +
+ ))} +
+ )} +
+
+ + )} +
+
+ ) +} diff --git a/marketplace/src/services/apiClient.test.ts b/marketplace/src/services/apiClient.test.ts new file mode 100644 index 0000000..7757ce2 --- /dev/null +++ b/marketplace/src/services/apiClient.test.ts @@ -0,0 +1,49 @@ +import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest' +import { apiClient } from './apiClient' + +describe('apiClient', () => { + beforeEach(() => { + // Reset token before each test + apiClient.setToken(null) + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + describe('setToken', () => { + it('should set token to null initially', () => { + // Token starts as null - we just verify setToken doesn't throw + expect(() => apiClient.setToken(null)).not.toThrow() + }) + + it('should accept a token string', () => { + expect(() => apiClient.setToken('test-token-123')).not.toThrow() + }) + + it('should clear token when set to null', () => { + apiClient.setToken('some-token') + apiClient.setToken(null) + // No error means success + expect(true).toBe(true) + }) + }) + + describe('api methods', () => { + it('should expose get method', () => { + expect(typeof apiClient.get).toBe('function') + }) + + it('should expose post method', () => { + expect(typeof apiClient.post).toBe('function') + }) + + it('should expose put method', () => { + expect(typeof apiClient.put).toBe('function') + }) + + it('should expose delete method', () => { + expect(typeof apiClient.delete).toBe('function') + }) + }) +}) diff --git a/marketplace/src/stores/cartStore.test.ts b/marketplace/src/stores/cartStore.test.ts new file mode 100644 index 0000000..7161cef --- /dev/null +++ b/marketplace/src/stores/cartStore.test.ts @@ -0,0 +1,200 @@ +import { describe, it, expect, beforeEach } from 'vitest' +import { useCartStore, selectGroupedCart, selectCartSummary, CartItem } from './cartStore' + +const mockItem: Omit = { + id: 'prod-1', + name: 'Produto Teste', + activeIngredient: 'Amoxicilina', + lab: 'EMS', + batch: 'L1001', + expiry: '2025-12', + vendorId: 'vendor-1', + vendorName: 'Distribuidora Norte', + unitPrice: 25.00 +} + +const mockItem2: Omit = { + id: 'prod-2', + name: 'Produto Teste 2', + activeIngredient: 'Dipirona', + lab: 'Eurofarma', + batch: 'L2002', + expiry: '2026-01', + vendorId: 'vendor-2', + vendorName: 'Distribuidora Sul', + unitPrice: 15.00 +} + +describe('cartStore', () => { + beforeEach(() => { + // Reset the store before each test + useCartStore.setState({ items: [] }) + }) + + describe('addItem', () => { + it('should add an item to the cart', () => { + const store = useCartStore.getState() + store.addItem(mockItem) + + const state = useCartStore.getState() + expect(state.items).toHaveLength(1) + expect(state.items[0]).toMatchObject({ ...mockItem, quantity: 1 }) + }) + + it('should add item with custom quantity', () => { + const store = useCartStore.getState() + store.addItem(mockItem, 5) + + const state = useCartStore.getState() + expect(state.items[0].quantity).toBe(5) + }) + + it('should increment quantity if item already exists', () => { + const store = useCartStore.getState() + store.addItem(mockItem, 2) + store.addItem(mockItem, 3) + + const state = useCartStore.getState() + expect(state.items).toHaveLength(1) + expect(state.items[0].quantity).toBe(5) + }) + }) + + describe('updateQuantity', () => { + it('should update quantity of existing item', () => { + useCartStore.getState().addItem(mockItem, 2) + useCartStore.getState().updateQuantity('prod-1', 10) + + const state = useCartStore.getState() + expect(state.items[0].quantity).toBe(10) + }) + + it('should not affect other items', () => { + useCartStore.getState().addItem(mockItem, 2) + useCartStore.getState().addItem(mockItem2, 3) + useCartStore.getState().updateQuantity('prod-1', 10) + + const state = useCartStore.getState() + expect(state.items[0].quantity).toBe(10) + expect(state.items[1].quantity).toBe(3) + }) + }) + + describe('removeItem', () => { + it('should remove item from cart', () => { + useCartStore.getState().addItem(mockItem) + useCartStore.getState().addItem(mockItem2) + useCartStore.getState().removeItem('prod-1') + + const state = useCartStore.getState() + expect(state.items).toHaveLength(1) + expect(state.items[0].id).toBe('prod-2') + }) + + it('should handle removing non-existent item', () => { + useCartStore.getState().addItem(mockItem) + useCartStore.getState().removeItem('non-existent') + + const state = useCartStore.getState() + expect(state.items).toHaveLength(1) + }) + }) + + describe('clearVendor', () => { + it('should remove all items from a specific vendor', () => { + const mockItemSameVendor: Omit = { + ...mockItem, + id: 'prod-3', + name: 'Outro Produto' + } + + useCartStore.getState().addItem(mockItem) + useCartStore.getState().addItem(mockItemSameVendor) + useCartStore.getState().addItem(mockItem2) + useCartStore.getState().clearVendor('vendor-1') + + const state = useCartStore.getState() + expect(state.items).toHaveLength(1) + expect(state.items[0].vendorId).toBe('vendor-2') + }) + }) + + describe('clearAll', () => { + it('should remove all items from cart', () => { + useCartStore.getState().addItem(mockItem) + useCartStore.getState().addItem(mockItem2) + useCartStore.getState().clearAll() + + const state = useCartStore.getState() + expect(state.items).toHaveLength(0) + }) + }) +}) + +describe('selectors', () => { + beforeEach(() => { + useCartStore.setState({ items: [] }) + }) + + describe('selectGroupedCart', () => { + it('should group items by vendor', () => { + useCartStore.getState().addItem(mockItem, 2) + useCartStore.getState().addItem(mockItem2, 3) + + const state = useCartStore.getState() + const groups = selectGroupedCart(state) + + expect(Object.keys(groups)).toHaveLength(2) + expect(groups['vendor-1'].items).toHaveLength(1) + expect(groups['vendor-2'].items).toHaveLength(1) + }) + + it('should calculate vendor totals correctly', () => { + useCartStore.getState().addItem(mockItem, 2) // 2 * 25 = 50 + useCartStore.getState().addItem(mockItem2, 3) // 3 * 15 = 45 + + const state = useCartStore.getState() + const groups = selectGroupedCart(state) + + expect(groups['vendor-1'].total).toBe(50) + expect(groups['vendor-2'].total).toBe(45) + }) + + it('should return empty object for empty cart', () => { + const state = useCartStore.getState() + const groups = selectGroupedCart(state) + + expect(Object.keys(groups)).toHaveLength(0) + }) + }) + + describe('selectCartSummary', () => { + it('should calculate total items correctly', () => { + useCartStore.getState().addItem(mockItem, 2) + useCartStore.getState().addItem(mockItem2, 3) + + const state = useCartStore.getState() + const summary = selectCartSummary(state) + + expect(summary.totalItems).toBe(5) + }) + + it('should calculate total value correctly', () => { + useCartStore.getState().addItem(mockItem, 2) // 2 * 25 = 50 + useCartStore.getState().addItem(mockItem2, 3) // 3 * 15 = 45 + + const state = useCartStore.getState() + const summary = selectCartSummary(state) + + expect(summary.totalValue).toBe(95) + }) + + it('should return zeros for empty cart', () => { + const state = useCartStore.getState() + const summary = selectCartSummary(state) + + expect(summary.totalItems).toBe(0) + expect(summary.totalValue).toBe(0) + }) + }) +}) diff --git a/marketplace/src/test/setup.ts b/marketplace/src/test/setup.ts new file mode 100644 index 0000000..c44951a --- /dev/null +++ b/marketplace/src/test/setup.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom' diff --git a/marketplace/vite.config.ts b/marketplace/vite.config.ts index 6ddd140..5a33944 100644 --- a/marketplace/vite.config.ts +++ b/marketplace/vite.config.ts @@ -1,9 +1,7 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' +// https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], - server: { - port: 5173 - } }) diff --git a/marketplace/vitest.config.ts b/marketplace/vitest.config.ts new file mode 100644 index 0000000..22cf471 --- /dev/null +++ b/marketplace/vitest.config.ts @@ -0,0 +1,24 @@ +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + test: { + globals: true, + environment: 'jsdom', + setupFiles: ['./src/test/setup.ts'], + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html'], + include: ['src/**/*.{ts,tsx}'], + exclude: [ + 'src/test/**', + 'src/**/*.test.{ts,tsx}', + 'src/**/*.spec.{ts,tsx}', + 'src/main.tsx', + 'src/vite-env.d.ts' + ] + } + } +})